.. _wiki2_basic_layout:
|
|
============
|
Basic Layout
|
============
|
|
The starter files generated by the ``alchemy`` cookiecutter are very basic, but
|
they provide a good orientation for the high-level patterns common to most
|
:term:`URL dispatch`-based :app:`Pyramid` projects.
|
|
|
Application configuration with ``__init__.py``
|
----------------------------------------------
|
|
A directory on disk can be turned into a Python :term:`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:
|
|
.. literalinclude:: src/basiclayout/tutorial/__init__.py
|
:linenos:
|
:language: py
|
|
Let's go over this piece-by-piece. First we need some imports to support later
|
code:
|
|
.. literalinclude:: src/basiclayout/tutorial/__init__.py
|
:end-before: main
|
:lineno-match:
|
:language: py
|
|
``__init__.py`` defines a function named ``main``. Here is the entirety of
|
the ``main`` function we've defined in our ``__init__.py``:
|
|
.. literalinclude:: src/basiclayout/tutorial/__init__.py
|
:pyobject: main
|
:lineno-match:
|
:language: py
|
|
When you invoke the ``pserve development.ini`` command, the ``main`` function
|
above is executed. It accepts some settings and returns a :term:`WSGI`
|
application. (See :ref:`startup_chapter` for more about ``pserve``.)
|
|
Next in ``main``, construct a :term:`Configurator` object:
|
|
.. literalinclude:: src/basiclayout/tutorial/__init__.py
|
:lines: 7
|
:lineno-match:
|
:language: py
|
|
``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 :term:`Jinja2` templating bindings so that we can use renderers
|
with the ``.jinja2`` extension within our project.
|
|
.. literalinclude:: src/basiclayout/tutorial/__init__.py
|
:lines: 8
|
:lineno-match:
|
:language: py
|
|
Next include the package ``models`` using a dotted Python path. The exact
|
setup of the models will be covered later.
|
|
.. literalinclude:: src/basiclayout/tutorial/__init__.py
|
:lines: 9
|
:lineno-match:
|
:language: py
|
|
Next include the ``routes`` module using a dotted Python path. This module will
|
be explained in the next section.
|
|
.. literalinclude:: src/basiclayout/tutorial/__init__.py
|
:lines: 10
|
:lineno-match:
|
:language: py
|
|
.. note::
|
|
Pyramid's :meth:`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
|
(:meth:`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.
|
|
.. literalinclude:: src/basiclayout/tutorial/__init__.py
|
:lines: 11
|
:lineno-match:
|
:language: py
|
|
Finally ``main`` is finished configuring things, so it uses the
|
:meth:`pyramid.config.Configurator.make_wsgi_app` method to return a
|
:term:`WSGI` application:
|
|
.. literalinclude:: src/basiclayout/tutorial/__init__.py
|
:lines: 12
|
:lineno-match:
|
:language: py
|
|
|
Route declarations
|
------------------
|
|
Open the ``tutorial/routes.py`` file. It should already contain the following:
|
|
.. literalinclude:: src/basiclayout/tutorial/routes.py
|
:linenos:
|
:language: py
|
|
On line 2, we call :meth:`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 :term:`route configuration` via the
|
:meth:`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
|
:term:`view callable`) that is executed when the requested URL matches the
|
corresponding :term:`route`. Our application uses the
|
:meth:`pyramid.view.view_config` decorator to perform this mapping.
|
|
Open ``tutorial/views/default.py`` in the ``views`` package. It should already
|
contain the following:
|
|
.. literalinclude:: src/basiclayout/tutorial/views/default.py
|
:linenos:
|
:language: py
|
|
The important part here is that the ``@view_config`` decorator associates the
|
function it decorates (``my_view``) with a :term:`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 :term:`view callable`.
|
|
Remember in our ``__init__.py`` when we executed the
|
:meth:`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 :term:`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:
|
|
.. literalinclude:: src/basiclayout/tutorial/models/meta.py
|
:linenos:
|
:language: py
|
|
``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.
|
|
.. literalinclude:: src/basiclayout/tutorial/models/meta.py
|
:end-before: metadata
|
:linenos:
|
:language: py
|
|
Next we create a ``metadata`` object from the class
|
: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.
|
|
.. literalinclude:: src/basiclayout/tutorial/models/meta.py
|
:lines: 15-16
|
:lineno-match:
|
:language: py
|
|
Next open ``tutorial/models/mymodel.py``, which should already contain the
|
following:
|
|
.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py
|
:linenos:
|
:language: py
|
|
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``:
|
|
.. literalinclude:: src/basiclayout/tutorial/models/mymodel.py
|
:pyobject: MyModel
|
:lineno-match:
|
:language: py
|
|
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:
|
|
.. code-block:: python
|
|
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:
|
|
.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
|
:linenos:
|
:language: py
|
|
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
|
:meth:`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
|
:func:`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 :term:`SQLAlchemy`
|
database engine using :func:`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://``).
|
|
.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
|
:pyobject: get_engine
|
:lineno-match:
|
:language: py
|
|
The function ``get_session_factory`` accepts an :term:`SQLAlchemy` database
|
engine, and creates a ``session_factory`` from the :term:`SQLAlchemy` class
|
:class:`sqlalchemy.orm.session.sessionmaker`. This ``session_factory`` is then
|
used for creating sessions bound to the database engine.
|
|
.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
|
:pyobject: get_session_factory
|
:lineno-match:
|
:language: py
|
|
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.
|
|
.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
|
:pyobject: get_tm_session
|
:lineno-match:
|
:language: py
|
|
Finally, we define an ``includeme`` function, which is a hook for use with
|
:meth:`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.
|
|
.. literalinclude:: src/basiclayout/tutorial/models/__init__.py
|
:pyobject: includeme
|
:lineno-match:
|
:language: py
|
|
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.
|