Basic Layout

The starter files generated from choosing the sqlalchemy backend option in the cookiecutter are very basic, but they provide a good orientation for the high-level patterns common to most URL dispatch-based Pyramid projects.

Application configuration with __init__.py

A directory on disk can be turned into a Python package by containing an __init__.py file. Even if empty, this marks a directory as a Python package. We use __init__.py both as a marker, indicating the directory in which it's contained is a package, and to contain application configuration code.

Open tutorial/__init__.py. It should already contain the following:

 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('.models')
10        config.include('.routes')
11        config.scan()
12    return config.make_wsgi_app()

Let's go over this piece-by-piece. First we need some imports to support later code:

1from pyramid.config import Configurator
2
3

__init__.py defines a function named main. Here is the entirety of the main function we've defined in our __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('.models')
10        config.include('.routes')
11        config.scan()
12    return config.make_wsgi_app()

When you invoke the pserve development.ini command, the main function above is executed. It accepts some settings and returns a WSGI application. (See Startup for more about pserve.)

Next in main, construct a Configurator object using a context manager:

7    with Configurator(settings=settings) as config:

settings is passed to the Configurator as a keyword argument with the dictionary values passed as the **settings argument. This will be a dictionary of settings parsed from the .ini file, which contains deployment-related values, such as pyramid.reload_templates, sqlalchemy.url, and so on.

Next include Jinja2 templating bindings so that we can use renderers with the .jinja2 extension within our project.

8        config.include('pyramid_jinja2')

Next include the package models using a dotted Python path. The exact setup of the models will be covered later.

9        config.include('.models')

Next include the routes module using a dotted Python path. This module will be explained in the next section.

10        config.include('.routes')

Note

Pyramid's pyramid.config.Configurator.include() method is the primary mechanism for extending the configurator and breaking your code into feature-focused modules.

main next calls the scan method of the configurator (pyramid.config.Configurator.scan()), which will recursively scan our tutorial package, looking for @view_config and other special decorators. When it finds a @view_config decorator, a view configuration will be registered, allowing one of our application URLs to be mapped to some code.

11        config.scan()

Finally main is finished configuring things, so it uses the pyramid.config.Configurator.make_wsgi_app() method to return a WSGI application:

12    return config.make_wsgi_app()

Route declarations

Open the tutorial/routes.py file. It should already contain the following:

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

On line 2, we call pyramid.config.Configurator.add_static_view() with three arguments: static (the name), static (the path), and cache_max_age (a keyword argument).

This registers a static resource view which will match any URL that starts with the prefix /static (by virtue of the first argument to add_static_view). This will serve up static resources for us from within the static directory of our tutorial package, in this case via http://localhost:6543/static/ and below (by virtue of the second argument to add_static_view). With this declaration, we're saying that any URL that starts with /static should go to the static view; any remainder of its path (e.g., the /foo in /static/foo) will be used to compose a path to a static file resource, such as a CSS file.

On line 3, the module registers a route configuration via the pyramid.config.Configurator.add_route() method that will be used when the URL is /. Since this route has a pattern equaling /, it is the route that will be matched when the URL / is visited, e.g., http://localhost:6543/.

View declarations via the views package

The main function of a web framework is mapping each URL pattern to code (a view callable) that is executed when the requested URL matches the corresponding route. Our application uses the pyramid.view.view_config() decorator to perform this mapping.

Open tutorial/views/default.py in the views package. It should already contain the following:

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

The important part here is that the @view_config decorator associates the function it decorates (my_view) with a view configuration, consisting of:

  • a route_name (home)

  • a renderer, which is a template from the templates subdirectory of the package.

When the pattern associated with the home view is matched during a request, my_view() will be executed. my_view() returns a dictionary; the renderer will use the templates/mytemplate.jinja2 template to create a response based on the values in the dictionary.

Note that my_view() accepts a single argument named request. This is the standard call signature for a Pyramid view callable.

Remember in our __init__.py when we executed the pyramid.config.Configurator.scan() method config.scan()? The purpose of calling the scan method was to find and process this @view_config decorator in order to create a view configuration within our application. Without being processed by scan, the decorator effectively does nothing. @view_config is inert without being detected via a scan.

The sample my_view() created by the cookiecutter uses a try: and except: clause to detect if there is a problem accessing the project database and provide an alternate error response. That response will include the text shown at the end of the file, which will be displayed in the browser to inform the user about possible actions to take to solve the problem.

Content models with the models package

In an SQLAlchemy-based application, a model object is an object composed by querying the SQL database. The models package is where the alchemy cookiecutter put the classes that implement our models.

First, open tutorial/models/meta.py, which should already contain the following:

 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: http://alembic.zzzcomputing.com/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 contains imports and support code for defining the models. We create a dictionary NAMING_CONVENTION as well for consistent naming of support objects like indices and constraints.

 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: http://alembic.zzzcomputing.com/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

Next we create a metadata object from the class sqlalchemy.schema.MetaData, using NAMING_CONVENTION as the value for the naming_convention argument.

A MetaData object represents the table and other schema definitions for a single database. We also need to create a declarative Base object to use as a base class for our models. Our models will inherit from this Base, which will attach the tables to the metadata we created, and define our application's database schema.

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

Next open tutorial/models/mymodel.py, which should already contain the following:

 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)

Notice we've defined the models as a package to make it straightforward for defining models in separate modules. To give a simple example of a model class, we have defined one named MyModel in mymodel.py:

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

Our example model does not require an __init__ method because SQLAlchemy supplies for us a default constructor, if one is not already present, which accepts keyword arguments of the same name as that of the mapped attributes.

Note

Example usage of MyModel:

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

The MyModel class has a __tablename__ attribute. This informs SQLAlchemy which table to use to store the data representing instances of this class.

Finally, open tutorial/models/__init__.py, which should already contain the following:

 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):
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          import transaction
39
40          engine = get_engine(settings)
41          session_factory = get_session_factory(engine)
42          with transaction.manager:
43              dbsession = get_tm_session(session_factory, transaction.manager)
44
45    """
46    dbsession = session_factory()
47    zope.sqlalchemy.register(
48        dbsession, transaction_manager=transaction_manager)
49    return dbsession
50
51
52def includeme(config):
53    """
54    Initialize the model for a Pyramid app.
55
56    Activate this setup using ``config.include('tutorial.models')``.
57
58    """
59    settings = config.get_settings()
60    settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
61
62    # use pyramid_tm to hook the transaction lifecycle to the request
63    config.include('pyramid_tm')
64
65    # use pyramid_retry to retry a request when transient exceptions occur
66    config.include('pyramid_retry')
67
68    session_factory = get_session_factory(get_engine(settings))
69    config.registry['dbsession_factory'] = session_factory
70
71    # make request.dbsession available for use in Pyramid
72    config.add_request_method(
73        # r.tm is the transaction manager used by pyramid_tm
74        lambda r: get_tm_session(session_factory, r.tm),
75        'dbsession',
76        reify=True
77    )

Our models/__init__.py module defines the primary API we will use for configuring the database connections within our application, and it contains several functions we will cover below.

As we mentioned above, the purpose of the models.meta.metadata object is to describe the schema of the database. This is done by defining models that inherit from the Base object attached to that metadata object. In Python, code is only executed if it is imported, and so to attach the models table defined in mymodel.py to the metadata, we must import it. If we skip this step, then later, when we run sqlalchemy.schema.MetaData.create_all(), the table will not be created because the metadata object does not know about it!

Another important reason to import all of the models is that, when defining relationships between models, they must all exist in order for SQLAlchemy to find and build those internal mappings. This is why, after importing all the models, we explicitly execute the function sqlalchemy.orm.configure_mappers(), once we are sure all the models have been defined and before we start creating connections.

Next we define several functions for connecting to our database. The first and lowest level is the get_engine function. This creates an SQLAlchemy database engine using sqlalchemy.engine_from_config() from the sqlalchemy.-prefixed settings in the development.ini file's [app:main] section. This setting is a URI (something like sqlite://).

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

The function get_session_factory accepts an SQLAlchemy database engine, and creates a session_factory from the SQLAlchemy class sqlalchemy.orm.session.sessionmaker. This session_factory is then used for creating sessions bound to the database engine.

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

The function get_tm_session registers a database session with a transaction manager, and returns a dbsession object. With the transaction manager, our application will automatically issue a transaction commit after every request, unless an exception is raised, in which case the transaction will be aborted.

25def get_tm_session(session_factory, transaction_manager):
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          import transaction
39
40          engine = get_engine(settings)
41          session_factory = get_session_factory(engine)
42          with transaction.manager:
43              dbsession = get_tm_session(session_factory, transaction.manager)
44
45    """
46    dbsession = session_factory()
47    zope.sqlalchemy.register(
48        dbsession, transaction_manager=transaction_manager)
49    return dbsession

Finally, we define an includeme function, which is a hook for use with pyramid.config.Configurator.include() to activate code in a Pyramid application add-on. It is the code that is executed above when we ran config.include('.models') in our application's main function. This function will take the settings from the application, create an engine, and define a request.dbsession property, which we can use to do work on behalf of an incoming request to our application.

52def includeme(config):
53    """
54    Initialize the model for a Pyramid app.
55
56    Activate this setup using ``config.include('tutorial.models')``.
57
58    """
59    settings = config.get_settings()
60    settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
61
62    # use pyramid_tm to hook the transaction lifecycle to the request
63    config.include('pyramid_tm')
64
65    # use pyramid_retry to retry a request when transient exceptions occur
66    config.include('pyramid_retry')
67
68    session_factory = get_session_factory(get_engine(settings))
69    config.registry['dbsession_factory'] = session_factory
70
71    # make request.dbsession available for use in Pyramid
72    config.add_request_method(
73        # r.tm is the transaction manager used by pyramid_tm
74        lambda r: get_tm_session(session_factory, r.tm),
75        'dbsession',
76        reify=True
77    )

That's about all there is to it regarding models, views, and initialization code in our stock application.

The Index import and the Index object creation in mymodel.py is not required for this tutorial, and will be removed in the next step.