跳转到主要内容

SQLAlchemy的测试/原型工具

项目描述

Build Status Codecov PyPI version

目的

Fast-alchemy是一个易于使用的原型/测试工具,可以根据yaml输入文件动态创建SQLAlchemy模型和实例。它可以在运行时安全地加载和卸载模型,从而实现灵活的工作流程。

使用案例包括但不限于

  • 原型一个ORM模型可能会更改的应用程序
  • 构建多个基于模型的测试用例,而无需在测试文件中填充您将仅使用一次的SQLA模型

总体思路是,对于简单的构建用例,工具应该简单易用,同时仍然允许复杂场景的可能性。这就是为什么代码是以不会侵入现有代码的方式构建的。

快速入门

Yaml ant_colonies.yaml

AntColony:
  ref: name
  definition:
    name: String
    latin_name: String
    queen_size: Float
    worker_size: Float
    color: String
  instances:
    - name: Argentine Ant
      latin_name: Linepithema humile
      queen_size: 1.6
      worker_size: 1.6
      color: brown
    - name: Black House Ant
      latin_name: Ochetellus
      queen_size: 2.5
      worker_size: 2.5
      color: black

Python代码

import pytest
import sqlalchemy as sa


@pytest.fixture
def fa()
    engine = sa.create_engine('sqlite:///:memory:')
    Base = sa.ext.declarative.declarative_base()
    Base.metadata.bind = engine
    Session = sa.orm.sessionmaker(autocommit=False, autoflush=False, bind=engine)
    session = sa.orm.scoped_session(Session)

    fa = FastAlchemy(Base, session)
    fa.load('ant_colonies.yaml')
    return fa


def test_it_can_test_my_test(fa):
    ants = fa.session.query(fa.AntColony).all()
    assert len(ants) == 2

该库的特点

使用yaml和SQLA填充数据库并不罕见,但到目前为止,fast-alchemy似乎仍然是少数能够提供运行时模型创建之上数据库填充功能的库之一。

定义模型

在我们可以开始填充我们的数据库之前,我们需要一个数据库结构。所以让我们先定义一个模型来开始。

请注意,模型是在我们进行过程中被解释的,这意味着你引用的其他实例或类必须在引用行之前已经定义。一个例外是反向引用,因为它们是一个鸡和蛋的难题。

Formicarium:
  ref: name
  definition:
    name: String
    width: Integer
    collection: relationship|AntCollection
    colonies: Backref|AntColony

看起来很简单,但让我们逐行过一遍。

定义模型名称

Formicarium:

所有根键都将被视为模型,在这种情况下,我们表示我们想要一个名为Formicarium的SQLA模型。

为了简单起见,所有fast-alchemy SQLA模型都将有一个id列作为主键。

在yaml文件中定义引用实例的方法

  ref: name

在某个时候,如果我们能建立模型之间的关系那会很好。由于我们无法控制或查询yaml定义内的ID,因此选择允许将列标记为引用单个实例的方式,以防我们想通过关系链接两个实例。

在这里,我们选择name列作为我们的引用值。

定义列和列类型

  definition:
    name: String

definition关键字表示我们要定义实际模型的块。这里的规则非常简单:你定义一个列的名称,并给它一个类型。类型是SQLAlchemy的通用类型(https://docs.sqlalchemy.org.cn/en/13/core/type_basics.html),应该按照定义它们的类来写。在本例中,我们定义了一个名为name的列,并给它分配了String类型。

定义关系

    collection: relationship|AntCollection

好了,我们已经做得这么多了。现在让我们更进一步,定义一个关系。使用与SQLAlchemy相同的术语,我们可以通过使用关键字...嗯...relationship来定义关系。我们通过引用yaml中先前定义的模型来指明我们想要与之建立关系的模型。在底层,我们将创建一个与AntCollection的多对一关系,使用它的id作为外键。

    colonies: Backref|AntColony

我们还可以指示我们感兴趣创建反向引用,这些关系将在yaml定义中进一步定义。如前所述,这是我们能够引用尚未定义的模型的情况,如果我们认为fast-alchemy是解释性基于的。

定义实例

太好了,现在我们知道了如何创建模型,我们也可以开始生成一些实例。

由于这个库的主要目标是快速,模型定义和实例创建都可以在同一个文件中完成,但如果你是个整洁的人,你可以分别定义每个文件并单独运行它们。你会在readme中找到如何做。

AntCollection:
  ref: name
  definition:
    name: String
    location: String
    formicaria: Backref|Formicarium
  instances:
    - name: Antics
      location: My bedroom

Formicarium:
  ref: name
  definition:
    name: String
    width: Integer
    collection: relationship|AntCollection
    colonies: Backref|AntColony
  instances:
    - name: PAnts
      collection: Antics
      width: 3

AntColony:
  ref: name
  definition:
    name: String
    latin_name: String
    queen_size: Float
    worker_size: Float
    color: String
    formicarium: relationship|Formicarium
  instances:
    - name: Argentine Ant
      latin_name: Linepithema humile
      queen_size: 1.6
      worker_size: 1.6
      color: brown
      formicarium: PAnts
    - name: Black House Ant
      latin_name: Ochetellus
      queen_size: 2.5
      worker_size: 2.5
      color: black
      formicarium: PAnts

如你所见,非常直接。instances键包含一个键值对列表,其中你填充你在模型中定义的每个列。你可以通过使用为相关模型定义的引用列来填充关系列。在上面的例子中,所有模型都使用name列作为它们的引用列。

请注意,这里也是,文件是带着解释性的心态读取和评估的。这意味着如果你引用实例,它们需要在文件中先定义。

复合键引用

如果你的任何列都不应该是唯一的,你可以通过用逗号分隔你想要用作引用键的列来组合一个唯一的引用。

  ref: name,width

在实例创建方面,你可以按以下方式引用你的关系

Formicarium:
  ref: name,width
  definition:
    name: String
    width: Integer
  instances:
    - name: PedAntic
      width: 10
    - name: PedAntic
      width: 15

AntColony:
  ref: name
  definition:
    name: String
    formicarium: relationship|Formicarium
  instances:
    - name: Argentine Ant
      formicarium: PedAntic,10

在代码中与加载的模型交互

当fast-alchemy加载模型时,模型类被添加到创建它们的fast-alchemy实例中。加载完成后,你可以将模型类作为fast-alchemy实例的属性访问。

engine = sa.create_engine('sqlite:///:memory:')
Base = sa.ext.declarative.declarative_base()
Base.metadata.bind = engine
Session = sa.orm.sessionmaker(autocommit=False, autoflush=False, bind=engine)
session = sa.orm.scoped_session(Session)

fa = FastAlchemy(Base, session)
fa.load('simple_case.yaml')
session.query(fa.AntColony).all()

多态性

yaml定义

由于子类化是面向对象编程中如此自然的一部分,如果库不支持多态性,那将是一个巨大的缺陷。定义一个多态模型与子类化一样简单。你首先定义你的父模型并指出多态区分器。之后,你可以通过将父模型附加到模型名称定义中来指明子模型继承自父模型。多态身份将根据模型名称自动生成。

Formicarium:
  definition:
    name: String
    formicarium_type: String
    polymorphic:
      "on": formicarium_type

SandwichFormicarium|Formicarium:
  ref: name
  definition:
    height: Integer

加载和卸载模型

作为一个有用的测试工具的一部分,就是能够灵活地处理你那奇妙的大脑所能想到的众多测试用例。这就是为什么fast-alchemy内置了加载模型的功能,并在测试显然通过后卸载它们。这使得你能够为每个测试加载不同的模型和实例。

我已经说服了你,让我们继续吧。

手动清理

import pytest


@pytest.fixture
def fa()
    engine = sa.create_engine('sqlite:///:memory:')
    Base = sa.ext.declarative.declarative_base()
    Base.metadata.bind = engine
    Session = sa.orm.sessionmaker(autocommit=False, autoflush=False, bind=engine)
    session = sa.orm.scoped_session(Session)

    return FastAlchemy(Base, session)


def simple_case(fa):
    fa.load('simple_case.yaml')
    run_my_test(fa)
    fa.drop_models()


def complex_case(fa):
    fa.load('complex_case.yaml')
    run_my_test(fa)
    fa.drop_models()

使用fast-alchemy的上下文管理器

def simple_case(fa):
    with fa:
        fa.load('simple_case.yaml')
        run_my_test(fa)


def complex_case(fa):
    with fa:
        fa.load('complex_case.yaml')
        run_my_test(fa)

组合不同的文件

def simple_case(fa):
    fa.load('case_main_part.yaml')
    with fa:
        fa.load('case_secondary_part.yaml')
        run_my_test(fa)
    # case_secondary_part will have unloaded after the context ends
    # retaining the models of case_main_part

删除特定模型

def simple_case(fa):
    fa.load('simple_case.yaml')
    fa.drop_models(models=['Model1', 'Model2'])
    run_my_test(fa)

分别加载模型和实例

def simple_case(fa):
    fa.load_models('models.yaml')
    fa.load_instances('instances.yaml')
    run_my_test(fa)

加载预定义模型的实例

fast-alchemy能够扫描与已声明的基类相关联的已定义模型,并使用它们来填充你的数据库

def simple_case(fa):
    class AntCollection(Base):
        __tablename__ = 'antcollection'
        id = sa.Column(sa.Integer, primary_key=True)
        name = sa.Column(sa.String())
        location = sa.Column(sa.String())

    fa.load_instances('ant_collections.yaml')
    run_my_test(fa)

将复杂案例与fast-alchemy的简单性相结合

def simple_complex_case(fa):
    class ComplexModel(Base):
        __tablename__ = 'complexmodel'
        id = sa.Column(sa.Integer, primary_key=True)
        # TODO: Add some complexity

    fa.load('simple_case.yaml')
    run_my_test(fa)

原型辅助工具

在某一点上,当你调整和玩耍到满意的程度时,你的原型将开始看起来不错,你可能会希望过渡到一个更健壮的实现。使代码更健壮的部分是,嗯...拥有实际的模型。但将你的yaml文件转换为实际的SQLA模型真是太痛苦了。这就是为什么fast-alchemy能够将你的yaml模型导出为完全可导入的Python文件,包含你所有最先进的模型。

from fast_alchemy.export import FastAlchemyExporter

fa = FastAlchemyExporter()
with open('models.py', 'w') as fh:
    fa.export_to_python('instances.yaml', fh)

导出以下文件

import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base

engine = sa.create_engine('sqlite:///:memory:')
Base = declarative_base()
Base.metadata.bind = engine
Session = sa.orm.sessionmaker(autocommit=False, autoflush=False, bind=engine)
session = sa.orm.scoped_session(Session)


class AntCollection(Base):
    __tablename__ = 'antcollection'

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String())
    location = sa.Column(sa.String())


class Formicarium(Base):
    __tablename__ = 'formicarium'

    __mapper_args__ = {
        'polymorphic_on': 'formicarium_type',
        'polymorphic_identity': 'formicarium'
    }

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String())
    formicarium_type = sa.Column(sa.String())
    width = sa.Column(sa.Integer())
    collection = sa.orm.relationship(AntCollection, backref='formicaria')
    collection_id = sa.Column(sa.Integer, sa.ForeignKey('antcollection.id'))


class SandwichFormicarium(Formicarium):
    __tablename__ = 'sandwichformicarium'

    __mapper_args__ = {
        'polymorphic_identity': 'sandwichformicarium'
    }

    id = sa.Column(sa.Integer, sa.ForeignKey('formicarium.id'), primary_key=True)
    height = sa.Column(sa.Integer())


class FreeStandingFormicarium(Formicarium):
    __tablename__ = 'freestandingformicarium'

    __mapper_args__ = {
        'polymorphic_identity': 'freestandingformicarium'
    }

    id = sa.Column(sa.Integer, sa.ForeignKey('formicarium.id'), primary_key=True)
    depth = sa.Column(sa.Integer())
    anti_escape_barrier = sa.Column(sa.String())


class AntColony(Base):
    __tablename__ = 'antcolony'

    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.String())
    latin_name = sa.Column(sa.String())
    queen_size = sa.Column(sa.Float())
    worker_size = sa.Column(sa.Float())
    color = sa.Column(sa.String())
    formicarium = sa.orm.relationship(Formicarium, backref='colonies')
    formicarium_id = sa.Column(sa.Integer, sa.ForeignKey('formicarium.id'))


Base.metadata.create_all()

Flask-SQLAlchemy集成

不必担心,如果你正在开发Flask应用程序,仍然可以使用fast-alchemy。库的行为完全一样,但你可以导入FlaskFastAlchemy而不是导入FastAlchemy来加载你的模型。

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "sqlite:///:memory:"
db = SQLAlchemy(app)

fa = FlaskFastAlchemy(db)
fa.load(os.path.join(DATA_DIR, 'instances.yaml'))

结论

我花在编写这个readme上的时间比花在编写代码上的时间还多。

值吗?也许...很可能...我们拭目以待。

项目详情


下载文件

下载适合您平台的文件。如果您不确定要选择哪一个,请了解更多关于安装包的信息。

源分布

fast-alchemy-0.1.7.tar.gz (20.5 kB 查看哈希值)

上传时间

构建分布

fast_alchemy-0.1.7-py2.py3-none-any.whl (14.5 kB 查看哈希值)

上传时间 Python 2 Python 3

支持者:

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面