基本布局

通过选择 sqlalchemy cookiecutter中的后端选项是非常基本的,但它们为大多数常见的高级模式提供了良好的方向。 URL dispatch 基于 Pyramid 项目。

应用程序配置 __init__.py

磁盘上的目录可以转换为python package 通过包含 __init__.py 文件。即使是空的,也会将目录标记为python包。我们使用 __init__.py 两者都作为标记,指示包含它的目录是一个包,并包含应用程序配置代码。

正常开放 tutorial/__init__.py . 它应该已经包含以下内容:

 1from pyramid.config import Configurator
 2
 3
 4def main(global_config, **settings):
 5    """ This function returns a Pyramid WSGI application.
 6    """
 7    with Configurator(settings=settings) as config:
 8        config.include('pyramid_jinja2')
 9        config.include('.routes')
10        config.include('.models')
11        config.scan()
12    return config.make_wsgi_app()

让我们一块一块地来看看。首先,我们需要一些导入来支持后面的代码:

1from pyramid.config import Configurator
2
3

__init__.py 定义名为的函数 main . 以下是整个 main 我们定义的函数 __init__.py

 4def main(global_config, **settings):
 5    """ This function returns a Pyramid WSGI application.
 6    """
 7    with Configurator(settings=settings) as config:
 8        config.include('pyramid_jinja2')
 9        config.include('.routes')
10        config.include('.models')
11        config.scan()
12    return config.make_wsgi_app()

当你调用 pserve development.ini 命令 main 执行上述功能。它接受一些设置并返回 WSGI 应用。(见 启动 更多关于 pserve

下一个 main 构建一个 Configurator 使用上下文管理器的对象:

7    with Configurator(settings=settings) as config:

settings 传递给 Configurator 作为关键字参数,字典值作为 **settings 参数。这将是从 .ini 文件,其中包含与部署相关的值,例如 pyramid.reload_templatessqlalchemy.url 等等。

下一个包括 Jinja2 将绑定模板化,以便我们可以在 .jinja2 在我们的项目中进行扩展。

8        config.include('pyramid_jinja2')

下一个包括 routes 使用点式python路径的模块。下一节将解释此模块。

9        config.include('.routes')

下一个包括包 models 使用点式python路径。稍后将介绍模型的确切设置。

10        config.include('.models')

备注

Pyramid pyramid.config.Configurator.include() 方法是扩展配置器并将代码分解为以功能为中心的模块的主要机制。

main 下一个调用 scan 配置器的方法 (pyramid.config.Configurator.scan() ,它将递归地扫描 tutorial 包,寻找 @view_config 以及其他特殊的装饰师。当它找到一个 @view_config decorator,一个视图配置将被注册,允许我们的一个应用程序URL映射到一些代码。

11        config.scan()

终于 main 已完成配置,因此它使用 pyramid.config.Configurator.make_wsgi_app() 返回的方法 WSGI 应用:

12    return config.make_wsgi_app()

路由声明

打开 tutorial/routes.py 文件。它应该已经包含以下内容:

1def includeme(config):
2    config.add_static_view('static', 'static', cache_max_age=3600)
3    config.add_route('home', '/')

在2号线,我们打电话 pyramid.config.Configurator.add_static_view() 有三个论点: static (名字) static (路径) cache_max_age (关键字参数)。

这将注册一个静态资源视图,该视图将匹配以前缀开头的任何URL。 /static (由于 add_static_view )这将从内部为我们提供静态资源 static 我们的目录 tutorial 包装,在这种情况下,通过 http://localhost:6543/static/ 以下(根据第二个论点 add_static_view )在这个声明中,我们说的是任何以 /static 应转到静态视图;其路径的任何剩余部分(例如 /foo 在里面 /static/foo )将用于组成指向静态文件资源(如CSS文件)的路径。

在第3行,模块寄存器A route configuration 通过 pyramid.config.Configurator.add_route() 当URL为 /. Since this route has a pattern equaling /, it is the route that will be matched when the URL `` /被访问,例如, ``http://localhost:6543/ .

通过查看声明 views

Web框架的主要功能是将每个URL模式映射到代码(A view callable )当请求的URL与相应的 route . 我们的应用程序使用 pyramid.view.view_config() 要执行此映射的Decorator。

正常开放 tutorial/views/default.pyviews 包。它应该已经包含以下内容:

 1from pyramid.view import view_config
 2from pyramid.response import Response
 3from sqlalchemy.exc import SQLAlchemyError
 4
 5from .. import models
 6
 7
 8@view_config(route_name='home', renderer='tutorial:templates/mytemplate.jinja2')
 9def my_view(request):
10    try:
11        query = request.dbsession.query(models.MyModel)
12        one = query.filter(models.MyModel.name == 'one').one()
13    except SQLAlchemyError:
14        return Response(db_err_msg, content_type='text/plain', status=500)
15    return {'one': one, 'project': 'myproj'}
16
17
18db_err_msg = """\
19Pyramid is having a problem using your SQL database.  The problem
20might be caused by one of the following things:
21
221.  You may need to initialize your database tables with `alembic`.
23    Check your README.txt for descriptions and try to run it.
24
252.  Your database server may not be running.  Check that the
26    database server referred to by the "sqlalchemy.url" setting in
27    your "development.ini" file is running.
28
29After you fix the problem, please restart the Pyramid application to
30try it again.
31"""

重要的是, @view_config decorator关联它所修饰的函数 (my_viewview configuration ,包括:

  • route_name (home

  • renderer ,它是来自 templates 包的子目录。

当模式与 home 在请求期间匹配视图, my_view() 将被执行。 my_view() 返回字典;渲染器将使用 templates/mytemplate.jinja2 根据字典中的值创建响应的模板。

注意 my_view() 接受一个名为 request . 这是Pyramid的标准呼叫签名 view callable .

记得我们 __init__.py 当我们执行 pyramid.config.Configurator.scan() 方法 config.scan() ?调用scan方法的目的是查找和处理 @view_config 为了在我们的应用程序中创建一个视图配置。未经处理 scan 装饰师实际上什么也不做。 @view_config 是惰性的,没有通过 scan .

样品 my_view() 由cookiecutter创建,使用 try:except: 子句来检测访问项目数据库是否有问题,并提供备用错误响应。该响应将包括显示在文件末尾的文本,该文本将显示在浏览器中,以通知用户为解决问题而采取的可能措施。

正常开放 tutorial/views/notfound.pyviews 包以查看第二个视图。

1from pyramid.view import notfound_view_config
2
3
4@notfound_view_config(renderer='tutorial:templates/404.jinja2')
5def notfound_view(request):
6    request.response.status = 404
7    return {}

不必重复,我们将指出这一观点与前一观点之间的差异。

  1. 第4行 . 这个 notfound_view 功能装饰有 @notfound_view_config . 此装饰器注册 Not Found View 使用 pyramid.config.Configurator.add_notfound_view() .

    这个 renderer 参数名为 asset specification 属于 tutorial:templates/404.jinja2 .

  2. Lines 5-7 . 一 view callable 已命名 notfound_view 定义,在上面的步骤中对其进行了修饰。它将HTTP响应状态代码设置为 404 . 函数将空字典返回给模板 404.jinja2 ,它不接受任何参数。

内容模型 models

在基于SQLAlchemy的应用程序中,一个 模型 对象是由查询SQL数据库组成的对象。这个 models 包裹在哪里 alchemy CookiCutter放置实现模型的类。

首先,开放 tutorial/models/meta.py ,应该已经包含以下内容:

 1from sqlalchemy.ext.declarative import declarative_base
 2from sqlalchemy.schema import MetaData
 3
 4# Recommended naming convention used by Alembic, as various different database
 5# providers will autogenerate vastly different names making migrations more
 6# difficult. See: https://alembic.sqlalchemy.org/en/latest/naming.html
 7NAMING_CONVENTION = {
 8    "ix": "ix_%(column_0_label)s",
 9    "uq": "uq_%(table_name)s_%(column_0_name)s",
10    "ck": "ck_%(table_name)s_%(constraint_name)s",
11    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
12    "pk": "pk_%(table_name)s"
13}
14
15metadata = MetaData(naming_convention=NAMING_CONVENTION)
16Base = declarative_base(metadata=metadata)

meta.py 包含用于定义模型的导入和支持代码。我们创建一本字典 NAMING_CONVENTION 以及支持对象(如索引和约束)的一致命名。

 1from sqlalchemy.ext.declarative import declarative_base
 2from sqlalchemy.schema import MetaData
 3
 4# Recommended naming convention used by Alembic, as various different database
 5# providers will autogenerate vastly different names making migrations more
 6# difficult. See: https://alembic.sqlalchemy.org/en/latest/naming.html
 7NAMING_CONVENTION = {
 8    "ix": "ix_%(column_0_label)s",
 9    "uq": "uq_%(table_name)s_%(column_0_name)s",
10    "ck": "ck_%(table_name)s_%(constraint_name)s",
11    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
12    "pk": "pk_%(table_name)s"
13}
14

接下来我们创建一个 metadata 类中的对象 sqlalchemy.schema.MetaData 使用 NAMING_CONVENTION 作为 naming_convention 参数。

A MetaData 对象表示单个数据库的表和其他架构定义。我们还需要创建一个声明性的 Base 对象用作模型的基类。我们的模型将从此继承 Base 将表附加到 metadata 我们创建并定义了应用程序的数据库模式。

15metadata = MetaData(naming_convention=NAMING_CONVENTION)
16Base = declarative_base(metadata=metadata)

下一个开放 tutorial/models/mymodel.py ,应该已经包含以下内容:

 1from sqlalchemy import (
 2    Column,
 3    Index,
 4    Integer,
 5    Text,
 6)
 7
 8from .meta import Base
 9
10
11class MyModel(Base):
12    __tablename__ = 'models'
13    id = Column(Integer, primary_key=True)
14    name = Column(Text)
15    value = Column(Integer)
16
17
18Index('my_index', MyModel.name, unique=True, mysql_length=255)

注意,我们已经定义了 models 作为一个包,使在单独的模块中定义模型变得简单。为了给出一个模型类的简单示例,我们定义了一个名为 MyModel 在里面 mymodel.py

11class MyModel(Base):
12    __tablename__ = 'models'
13    id = Column(Integer, primary_key=True)
14    name = Column(Text)
15    value = Column(Integer)

我们的示例模型不需要 __init__ 方法,因为sqlAlchemy为我们提供了一个默认的构造函数(如果还没有),它接受与映射属性同名的关键字参数。

备注

MyModel的示例用法:

johnny = MyModel(name="John Doe", value=10)

这个 MyModel 类有 __tablename__ 属性。这将通知SQLAlchemy使用哪个表来存储表示此类实例的数据。

最后,打开 tutorial/models/__init__.py ,应该已经包含以下内容:

  1from sqlalchemy import engine_from_config
  2from sqlalchemy.orm import sessionmaker
  3from sqlalchemy.orm import configure_mappers
  4import zope.sqlalchemy
  5
  6# Import or define all models here to ensure they are attached to the
  7# ``Base.metadata`` prior to any initialization routines.
  8from .mymodel import MyModel  # flake8: noqa
  9
 10# Run ``configure_mappers`` after defining all of the models to ensure
 11# all relationships can be setup.
 12configure_mappers()
 13
 14
 15def get_engine(settings, prefix='sqlalchemy.'):
 16    return engine_from_config(settings, prefix)
 17
 18
 19def get_session_factory(engine):
 20    factory = sessionmaker()
 21    factory.configure(bind=engine)
 22    return factory
 23
 24
 25def get_tm_session(session_factory, transaction_manager, request=None):
 26    """
 27    Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
 28
 29    This function will hook the session to the transaction manager which
 30    will take care of committing any changes.
 31
 32    - When using pyramid_tm it will automatically be committed or aborted
 33      depending on whether an exception is raised.
 34
 35    - When using scripts you should wrap the session in a manager yourself.
 36      For example:
 37
 38      .. code-block:: python
 39
 40          import transaction
 41
 42          engine = get_engine(settings)
 43          session_factory = get_session_factory(engine)
 44          with transaction.manager:
 45              dbsession = get_tm_session(session_factory, transaction.manager)
 46
 47    This function may be invoked with a ``request`` kwarg, such as when invoked
 48    by the reified ``.dbsession`` Pyramid request attribute which is configured
 49    via the ``includeme`` function below. The default value, for backwards
 50    compatibility, is ``None``.
 51
 52    The ``request`` kwarg is used to populate the ``sqlalchemy.orm.Session``'s
 53    "info" dict.  The "info" dict is the official namespace for developers to
 54    stash session-specific information.  For more information, please see the
 55    SQLAlchemy docs:
 56    https://docs.sqlalchemy.org/en/stable/orm/session_api.html#sqlalchemy.orm.session.Session.params.info
 57
 58    By placing the active ``request`` in the "info" dict, developers will be
 59    able to access the active Pyramid request from an instance of an SQLAlchemy
 60    object in one of two ways:
 61
 62    - Classic SQLAlchemy. This uses the ``Session``'s utility class method:
 63
 64      .. code-block:: python
 65
 66          from sqlalchemy.orm.session import Session as sa_Session
 67
 68          dbsession = sa_Session.object_session(dbObject)
 69          request = dbsession.info["request"]
 70
 71    - Modern SQLAlchemy. This uses the "Runtime Inspection API":
 72
 73      .. code-block:: python
 74
 75          from sqlalchemy import inspect as sa_inspect
 76
 77          dbsession = sa_inspect(dbObject).session
 78          request = dbsession.info["request"]
 79    """
 80    dbsession = session_factory(info={"request": request})
 81    zope.sqlalchemy.register(
 82        dbsession, transaction_manager=transaction_manager
 83    )
 84    return dbsession
 85
 86
 87def includeme(config):
 88    """
 89    Initialize the model for a Pyramid app.
 90
 91    Activate this setup using ``config.include('tutorial.models')``.
 92
 93    """
 94    settings = config.get_settings()
 95    settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
 96
 97    # Use ``pyramid_tm`` to hook the transaction lifecycle to the request.
 98    # Note: the packages ``pyramid_tm`` and ``transaction`` work together to
 99    # automatically close the active database session after every request.
100    # If your project migrates away from ``pyramid_tm``, you may need to use a
101    # Pyramid callback function to close the database session after each
102    # request.
103    config.include('pyramid_tm')
104
105    # use pyramid_retry to retry a request when transient exceptions occur
106    config.include('pyramid_retry')
107
108    # hook to share the dbengine fixture in testing
109    dbengine = settings.get('dbengine')
110    if not dbengine:
111        dbengine = get_engine(settings)
112
113    session_factory = get_session_factory(dbengine)
114    config.registry['dbsession_factory'] = session_factory
115
116    # make request.dbsession available for use in Pyramid
117    def dbsession(request):
118        # hook to share the dbsession fixture in testing
119        dbsession = request.environ.get('app.dbsession')
120        if dbsession is None:
121            # request.tm is the transaction manager used by pyramid_tm
122            dbsession = get_tm_session(
123                session_factory, request.tm, request=request
124            )
125        return dbsession
126
127    config.add_request_method(dbsession, reify=True)

我们的 models/__init__.py 模块定义了我们将用于在应用程序中配置数据库连接的主要API,它包含我们将在下面介绍的几个函数。

正如我们上面提到的,目的是 models.meta.metadata 对象是描述数据库的架构。这是通过定义继承自 Base 附属于那个的对象 metadata 对象。在Python中,只有在导入代码时才执行代码,因此要附加 models 表定义于 mymodel.pymetadata 我们必须进口。如果我们跳过这一步,那么稍后,当我们运行 sqlalchemy.schema.MetaData.create_all() ,将不会创建表,因为 metadata 对象不知道!

导入所有模型的另一个重要原因是,在定义模型之间的关系时,它们必须都存在,以便SQLAlchemy查找和构建这些内部映射。这就是为什么在导入所有模型之后,我们显式地执行函数 sqlalchemy.orm.configure_mappers() ,在确定所有模型都已定义并且开始创建连接之前。

接下来,我们定义了几个连接到数据库的函数。第一级和最低一级是 get_engine 功能。这创造了一个 SQLAlchemy 数据库引擎使用 sqlalchemy.engine_from_config()sqlalchemy. -中的前缀设置 development.ini 文件的 [app:main] 部分。此设置是一个URI(类似于 sqlite://

15def get_engine(settings, prefix='sqlalchemy.'):
16    return engine_from_config(settings, prefix)

函数 get_session_factory 接受一个 SQLAlchemy 数据库引擎,并创建 session_factorySQLAlchemysqlalchemy.orm.session.sessionmaker . 这个 session_factory 然后用于创建绑定到数据库引擎的会话。

19def get_session_factory(engine):
20    factory = sessionmaker()
21    factory.configure(bind=engine)
22    return factory

函数 get_tm_session 向事务管理器注册数据库会话,并返回 dbsession 对象。使用事务管理器,我们的应用程序将在每次请求之后自动发出事务提交,除非出现异常,在这种情况下,事务将中止。

25def get_tm_session(session_factory, transaction_manager, request=None):
26    """
27    Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
28
29    This function will hook the session to the transaction manager which
30    will take care of committing any changes.
31
32    - When using pyramid_tm it will automatically be committed or aborted
33      depending on whether an exception is raised.
34
35    - When using scripts you should wrap the session in a manager yourself.
36      For example:
37
38      .. code-block:: python
39
40          import transaction
41
42          engine = get_engine(settings)
43          session_factory = get_session_factory(engine)
44          with transaction.manager:
45              dbsession = get_tm_session(session_factory, transaction.manager)
46
47    This function may be invoked with a ``request`` kwarg, such as when invoked
48    by the reified ``.dbsession`` Pyramid request attribute which is configured
49    via the ``includeme`` function below. The default value, for backwards
50    compatibility, is ``None``.
51
52    The ``request`` kwarg is used to populate the ``sqlalchemy.orm.Session``'s
53    "info" dict.  The "info" dict is the official namespace for developers to
54    stash session-specific information.  For more information, please see the
55    SQLAlchemy docs:
56    https://docs.sqlalchemy.org/en/stable/orm/session_api.html#sqlalchemy.orm.session.Session.params.info
57
58    By placing the active ``request`` in the "info" dict, developers will be
59    able to access the active Pyramid request from an instance of an SQLAlchemy
60    object in one of two ways:
61
62    - Classic SQLAlchemy. This uses the ``Session``'s utility class method:
63
64      .. code-block:: python
65
66          from sqlalchemy.orm.session import Session as sa_Session
67
68          dbsession = sa_Session.object_session(dbObject)
69          request = dbsession.info["request"]
70
71    - Modern SQLAlchemy. This uses the "Runtime Inspection API":
72
73      .. code-block:: python
74
75          from sqlalchemy import inspect as sa_inspect
76
77          dbsession = sa_inspect(dbObject).session
78          request = dbsession.info["request"]
79    """
80    dbsession = session_factory(info={"request": request})
81    zope.sqlalchemy.register(
82        dbsession, transaction_manager=transaction_manager
83    )
84    return dbsession

最后,我们定义了 includeme 函数,它是用于 pyramid.config.Configurator.include() 激活Pyramid应用程序加载项中的代码。当我们运行时,上面执行的代码 config.include('.models') 在我们的应用程序中 main 功能。此函数将从应用程序获取设置,创建引擎,并定义 request.dbsession 属性,我们可以使用它来代表应用程序的传入请求进行工作。

 87def includeme(config):
 88    """
 89    Initialize the model for a Pyramid app.
 90
 91    Activate this setup using ``config.include('tutorial.models')``.
 92
 93    """
 94    settings = config.get_settings()
 95    settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
 96
 97    # Use ``pyramid_tm`` to hook the transaction lifecycle to the request.
 98    # Note: the packages ``pyramid_tm`` and ``transaction`` work together to
 99    # automatically close the active database session after every request.
100    # If your project migrates away from ``pyramid_tm``, you may need to use a
101    # Pyramid callback function to close the database session after each
102    # request.
103    config.include('pyramid_tm')
104
105    # use pyramid_retry to retry a request when transient exceptions occur
106    config.include('pyramid_retry')
107
108    # hook to share the dbengine fixture in testing
109    dbengine = settings.get('dbengine')
110    if not dbengine:
111        dbengine = get_engine(settings)
112
113    session_factory = get_session_factory(dbengine)
114    config.registry['dbsession_factory'] = session_factory
115
116    # make request.dbsession available for use in Pyramid
117    def dbsession(request):
118        # hook to share the dbsession fixture in testing
119        dbsession = request.environ.get('app.dbsession')
120        if dbsession is None:
121            # request.tm is the transaction manager used by pyramid_tm
122            dbsession = get_tm_session(
123                session_factory, request.tm, request=request
124            )
125        return dbsession
126
127    config.add_request_method(dbsession, reify=True)

这就是我们的stock应用程序中关于模型、视图和初始化代码的全部内容。

这个 Index 进口与 Index 对象创建位置 mymodel.py 本教程不需要,将在下一步中删除。

测验

项目包含一个测试套件的基本结构,使用 pytest . 稍后将介绍结构 添加测试 .