Documentation¶
This package ties together SQLAlchemy and the component architecture to make it easy to develop projects using SQLAlchemy through their complete lifecycle.
It also includes some common models and mixins that are useful in a variety of projects.
Installation Instructions¶
The best way to install mortar_rdb is with pip:
pip install mortar_rdb
Of course, once it’s installed, make sure it’s in your requirements.txt
!
Python version requirements
This package has been tested with Python 2.7, 3.4+, MySQL, Postgres and SQLite on Linux, and is also expected to work on Mac OS X and Windows with any database supported by SQLAlchemy.
Basic Usage¶
This narrative documentation covers the use case of developing an
application from scratch that uses mortar_rdb
to interact with a
relational database through development and testing.
Development¶
For this narrative, we’ll assume we’re developing our application in a
python package called sample
that uses the following model:
sample/model.py
from mortar_rdb import declarative_base
from mortar_rdb.controlled import Config, scan
from sqlalchemy import Table, Column, Integer, String
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(20))
source = scan('sample')
config = Config(source)
There’s nothing particularly special about this model other than that
we’ve used mortar_rdb.declarative_base()
to obtain a declarative base rather
than calling sqlalchemy.ext.declarative.declarative_base()
. This
means that multiple python packages can all use the same declarative
base, without having to worry about which package first defines the
base.
This also means that all tables and models used in our application, regardless of the package they are defined in, can refer to each other.
To allow us to take advantage of the schema controls provided by
mortar_rdb
, we have also defined a Config
with a Source
returned from a
scan()
. The source is defined seperately to
the configuration for two reasons:
- it allows a configuration in another package to use the source defined here, which encapsulates all the tables managed by this package.
- it makes it easier to write tests for migration scripts for the tables managed by this package.
To use the above model, we have the following view code:
sample/views.py
from mortar_rdb import get_session
from sample.model import User
def add_user(name):
session = get_session()
session.add(User(name=name))
def edit_user(id,name):
session = get_session()
user = session.query(User).filter(User.id == id).one()
user.name = name
When using mortar_rdb
, the session is obtained by calling
mortar_rdb.get_session()
. This allows the provision of the session to
be independent of its use, which makes testing and deploying to
different environments easier.
It is also advised that application code does not manage committing or rollback of database transactions via the session unless absolutely necessary. These actions should be the responsibility of the framework running the application.
For the purposes of this narrative, we will use the following micro framework:
sample/run.py
from mortar_rdb import register_session
from sample import views
from sample.config import db_url
from sample.model import config
import sys
import transaction
def main():
register_session(db_url)
name = sys.argv[1]
args = sys.argv[2:]
with transaction.manager:
getattr(views, name)(*args)
print("Ran %r with %r" % (name, args))
if __name__=='__main__':
main()
Although there’s not much to it, the above framework shows the elements you will need to plug in to whatever framework you choose to use.
The main one of these is the call to register_session()
which sets up the components necessary for get_session()
to
return a Session
object.
The example framework is also shown to manage these sessions using the
transaction
package. Should your framework not use this
package, you are strongly suggested to read the documentation for
register_session()
in detail to make sure you pass the
correct parameters to get the behaviour required by your framework.
Testing¶
It’s alway a good idea to write automated tests, preferably before
writing the code under test. mortar_rdb
aids this by providing the
mortar_rdb.testing
module.
The following example shows how to provides minimal coverage using
mortar_rdb.testing.register_session()
and illustrates how the
abstraction of configuring a session from obtaining a session in
mortar_rdb
makes testing easier:
sample/tests.py
from mortar_rdb import get_session
from mortar_rdb.testing import register_session
from sample.model import User, config
from sample.views import add_user, edit_user
from unittest import TestCase
class Tests(TestCase):
def setUp(self):
self.session = register_session(config=config)
def tearDown(self):
self.session.rollback()
def test_add_user(self):
# code under test
add_user('Mr Test')
# checks
user = self.session.query(User).one()
self.assertEqual('Mr Test', user.name)
def test_edit_user(self):
# setup
self.session.add(User(id=1, name='Mr Chips'))
# code under test
edit_user('1', 'Mr Fish')
# checks
user = self.session.query(User).one()
self.assertEqual('Mr Fish', user.name)
If you wish to run these tests against a particular database, rather
than using the default in-memory SQLite database, then set the
DB_URL
enviroment variable to the SQLAlchemy url of the database
you’d like to use. For example, if you run your tests with pytest
and are developing in a unix-like environment against a MySQL
database, you could do:
$ DB_URL=mysql://scott:tiger@localhost/test pytest
Release¶
With the application developed and tested, it is now time to release
and deploy it. Users of mortar_rdb
are encouraged to create a small
database management script making use of
mortar_rdb.controlled.Scripts
.
Here’s is an example for the above model:
sample/db.py
from mortar_rdb.controlled import Scripts
from sample.config import db_url, is_production
from sample.model import config
scripts = Scripts(db_url, config, not is_production)
if __name__=='__main__':
scripts()
This script can be used to create all tables required by the
applications Config
as follows:
$ bin/db create
For database at sqlite:////test.db:
Creating the following tables:
user
Other commands are are provided by Scripts
and both the command line help, obtained with the --help
option to
either the script or any of its commands, and documentation are well
worth a read.
So, the view code, database model, tests and framework are all now ready and the database has been created. The framework is now ready to use:
$ bin/run add_user test
Ran 'add_user' with ['test']
Sequences¶
Many applications require non-repeating sequences of integer numbers. While some database engines provide structures for this purpose, others, such as MySQL and SQLite do not.
Alternatively, you may wish to have a more global source of unique integer identifiers that you share across several database servers.
For either of these cases, you can use the sequence support provided
by mortar_rdb
.
Usage is very simple. During application configuration, register sequences at the same time as you register the session to use for a database:
from mortar_rdb import register_session, get_session
from mortar_rdb.sequence import register_sequence
import transaction
register_session(db_url)
with transaction.manager:
session = get_session()
register_sequence('seq1', session)
register_sequence('seq2', session)
You’ll notice that you need to manage a database transaction when you
call register_sequence()
, either using the
transaction
package or by manually calling
commit()
after the call. This is
because registering a sequence, certainly in the default
SequenceImplementation
, may require
creating a row in a table.
Once registered, whenever you need some unique integers in application
code, get hold of the sequence and call its
next()
method:
>>> from mortar_rdb import get_session
>>> from mortar_rdb.sequence import get_sequence
>>> seq1 = get_sequence('seq1')
>>> seq2 = get_sequence('seq2')
>>> session = get_session()
>>> seq1.next(session)
1
>>> seq1.next(session)
2
>>> seq1.next(session)
3
>>> seq2.next(session)
1
Note
The default implementation used by
register_sequence()
is
mortar_rdb.sequence.generic.SequenceImplementation
.
This uses a table called sequences
in the database which needs
to be created before
you first call register_sequence()
. If you
are using mortar_rdb.controlled
then you can use the source from
mortar_rdb.sequence.generic.source
as part of your
Config
to take care of table creation.
Also, please note that this implementation may cause contention on the table in question. If your database engine provides native sequences, an implementation that used those would be gratefully received!
API Reference¶
mortar_rdb¶
-
mortar_rdb.
declarative_base
(**kw)¶ Return a
Base
as would be returned bydeclarative_base()
.Only one
Base
will exist for each combination of parameters that this function is called with. If it is called with the same combination of parameters more than once, subsequent calls will return the existingBase
.This method should be used so that even if more than one package used by a project defines models, they will all end up in the same
MetaData
instance and all have the same declarative registry.
-
mortar_rdb.
drop_tables
(engine)¶ Drop all the tables in the database attached to by the supplied engine.
As many foreign key constraints as possible will be dropped first making this quite brutal!
-
mortar_rdb.
get_session
(name='')¶ Return a
Session
instance from the current registry as registered with the supplied name.
-
mortar_rdb.
register_session
(url=None, name='', engine=None, echo=None, transactional=True, scoped=True, twophase=True)¶ Create a
Session
class and register it for later use.Generally, you’ll only need to pass in a
SQLAlchemy
connection URL. If you want to register multiple sessions for a particular application, then you should name them. If you want to provide specific engine configuration, then you can pass in anEngine
instance. In that case, you must not pass in a URL.Parameters: - echo – If True, then all SQL will be echoed to the python logging framework. This option cannot be specified if you pass in an engine.
- scoped – If True, then
get_session()
will return a distinct session for each thread that it is called from but, within that thread, it will always return the same session. If it is False, every call toget_session()
will return a new session. - transactional –
If True, a
SQLAlchemy
extension will be used that that enables thetransaction
package to manage the lifecycle of the SQLAlchemy session (eg:begin()
/commit()
/rollback()
). This can only be done when scoped sessions are used.If False, you will need to make sure you call
begin()
/commit()
/rollback()
, as appropriate, yourself. - twophase – By default two-phase transactions are used where supported by the underlying database. Where this causes problems, single-phase transactions can be used for all engines by passing this parameter as False.
mortar_rdb.controlled¶
When a database is used, it’s essential that code using the database only interacts with a database that is of the form it expects. A corollary of that is that it is important to be able to update the structure of a database from the form expected by one version of the code to that expected by another version of the code.
mortar_rdb.controlled
aims to facilitate this along with providing
a command line harness for creating necessary tables within a
database, emptying out a non-production database and upgrading a
database to a new structure where SQLAlchemy
is
used.
Packages, Models and Tables¶
SQLAlchemy
uses Table
objects that
are mapped to one or more model classes. These objects are defined
within python packages.
Configurations¶
A single database may contain tables that are defined in more that one package. For example, an authentication package may contain some table definitions for storing users and their permissions. That package may be used by an application which also contains a package that defines its own tables.
A Config
is a way of expressing
which tables should be expected in a database.
In general, it is recommended that a
Config
is defined once, in whatever
package ‘owns’ a particular database. For example, an application may
define a configuration for its own tables and those of any packages on
which it relies, such as the hypothetical authentication package
described above. If another application wants to use this
application’s database, it can import the configuration and check that
the database structure matches that expected by the code it is
currently using.
-
class
mortar_rdb.controlled.
Config
(*sources)¶ A configuration for a particular database to allow control of the schema of that database.
Parameters: sources – The Source
instances from which to create this configuration.
-
class
mortar_rdb.controlled.
Scripts
(url, config, failsafe)¶ A command-line harness for performing schema control functions on a database. You should instantiate this in a small python script and call it when the script is run as a command, eg:
from mortar_rdb.controlled import Scripts from sample.model import config scripts = Scripts('sqlite://', config, True) if __name__=='__main__': script()
Writing the script in this style also allows
scripts
to be used as a :mod:setuptools entry point.Parameters: - url – The
SQLAlchemy
url to connect to the database to be managed. If this isn’t known at the time when this class is instantiated, then useNone
. - config – A
Config
instance describing the schema of the database to be managed. - failsafe – A boolean value that should be
True
if it’s okay for the database being managed to have all its tables dropped. For obvious reasons, this should beFalse
when managing your production database.
-
create
()¶ Create all the tables in the configuration in the database
-
drop
()¶ Drop all tables in the database
- url – The
-
class
mortar_rdb.controlled.
Source
(*tables)¶ A collection of tables that should have their versioning managed together. This usually means they originate from one package.
Parameters: tables – A sequence of Table
objects that contain all the tables that will be managed by the repository in this Source.
-
mortar_rdb.controlled.
scan
(package, tables=())¶ Scan a package or module and return a
Source
containing the tables from any declaratively mapped models found, anyTable
objects explicitly passed in and thesqlalchemy-migrate
repository contained within the package.Note
While the package parameter is passed as a string, this will be resolved into a module or package object. It is not a distribution name, although the two are often very similar.
Parameters: - package – A dotted path to the package to be scanned for
Table
objects. - tables – A sequence of
Table
objects to be added to the returnedSource
. Any tables not created as part of declaratively mapping a class will need to be passed in using this sequence asscan()
cannot sensibly scan for these objects.
- package – A dotted path to the package to be scanned for
mortar_rdb.testing¶
Helpers for unit testing when using mortar_rdb
-
class
mortar_rdb.testing.
TestingBase
¶ This is a helper class that can either be used to make
declarative_base()
return a new, emptyBase
for testing purposes.If writing a suite of unit tests, this can be done as follows:
from mortar_rdb.testing import TestingBase from unittest import TestCase class YourTestCase(TestCase): def setUp(self): self.tb = TestingBase() def tearDown(self): self.tb.restore()
If you need a fresh
Base
for a short section of Python code,TestingBase
can also be used as a context manager:with TestingBase(): base = declarative_base() # your test code here
-
mortar_rdb.testing.
register_session
(url=None, name='', engine=None, echo=False, transactional=True, scoped=True, config=None, metadata=None)¶ This will create a
Session
class for testing purposes and register it for later use.The calling parameters mirror those of
mortar_rdb.register_session()
but if neither url nor engine is specified then:- The environment will be consulted for a variable called
DB_URL
. If found, that will be used for the url parameter. - If url is still None, an implicit url of
sqlite://
will be used.
If a
Config
is passed in then, once any existing content in the database has been removed, any tables controlled by that config will be created.If a
MetaData
instance is passed in, then all tables within it will be created.Unlike the non-testing
register_session
, this will also return an instance of the registered session.Warning
No matter where the url or engine come from, the entire contents of the database they point at will be destroyed!
- The environment will be consulted for a variable called
mortar_rdb.sequence¶
Database independent provision of non-repeating, always-incrementing sequences of integers.
-
mortar_rdb.sequence.
get_sequence
(name)¶ Obtain a previously registered sequence. Once obtained, the
next()
method should be called as many times as necessary.Each call will return one system-wide unique integer that will be greater than any integers previously returned.
-
mortar_rdb.sequence.
register_sequence
(name, session, impl=<class 'mortar_rdb.sequence.generic.SequenceImplementation'>)¶ Register a sequence for later user.
Parameters: - name – A string containing the name of the sequence.
- session – A
Session
instance that will be used to set up anything needed in the database for the sequence to be functional. It will not be retained and may be closed and discarded once this function has returned. - impl – A class whose instances implement
ISequence
. Defaults tomortar_rdb.sequence.generic.SequenceImplementation
.
-
class
mortar_rdb.sequence.generic.
SequenceImplementation
(name, session)¶ A sequence implementation that uses a table in the database with one row for each named sequence.
-
next
(session)¶ Return the next integer in the sequence using the
Session
provided.Warning
The current implementation will lock the row (or table, depending on which database you use) for the sequence in question. This could conceivably cause contention problems if more than one connection is trying to generate integers from the sequence at one time.
-
mortar_rdb.interfaces¶
Internal interface definitions. Unless you’re doing something pretty special, you don’t need to know about these.
-
interface
mortar_rdb.interfaces.
ISession
¶ A marker interface for SQLAlchemy Sessions. This is so that we can register factories that return them.
-
interface
mortar_rdb.interfaces.
ISequence
¶ An interface for sequence utility impementations. A sequence is a non-repeating, always-incrementing sequence of integers.
Implementations of this interface will be instantiated once and then have their
next()
method called often.
Development¶
This package is developed using continuous integration which can be found here:
https://travis-ci.org/Mortar/mortar_rdb
The latest development version of the documentation can be found here:
http://mortar_rdb.readthedocs.org/en/latest/
If you wish to contribute to this project, then you should fork the repository found here:
https://github.com/Mortar/mortar_rdb
Once that has been done and you have a checkout, you can follow these instructions to perform various development tasks:
Setting up a virtualenv¶
The recommended way to set up a development environment is to turn your checkout into a virtualenv and then install the package in editable form as follows:
$ virtualenv .
$ bin/pip install -U -e .[test,build]
Running the tests¶
Once you’ve set up a virtualenv, the tests can be run as follows:
$ bin/pytest
Some of the tests can be run against a specific database to check
compatibility with specific database back ends. To do this, set the
DB_URL
environment variable to the SQLAlchemy url of the database
you’d like to use. For example, if you are testing in a unix-like
environment and want to test against a MySQL database, you could do:
$ DB_URL=mysql://scott:tiger@localhost/mortar_rdb_tests bin/test
Building the documentation¶
The Sphinx documentation is built by doing the following from the directory containing setup.py:
$ source bin/activate
$ cd docs
$ make html
To check that the description that will be used on PyPI renders properly, do the following:
$ python setup.py --long-description | rst2html.py > desc.html
The resulting desc.html
should be checked by opening in a browser.
Making a release¶
To make a release, just update the version in setup.py
,
update the change log, tag it
and push to https://github.com/Mortar/mortar_rdb
and Travis CI should take care of the rest.
Once Travis CI is done, make sure to go to https://readthedocs.org/projects/mortar_rdb/versions/ and make sure the new release is marked as an Active Version.
Changes¶
3.0.0 (7 Mar 2019)¶
- Drop support for the ancient SQLAlchemy extension mechanism.
2.2.0 (29 Oct 2015)¶
- More careful password masking.
- Better support for using
controlled.Scripts
as part of another script framework.
2.1.1 (4 Oct 2015)¶
- Deploy to PyPI using Travis.
2.1.0 (4 Oct 2015)¶
- Drop support for Python 2.6.
- Add support for Python 3.4+.
- Move to Read The Docs for documentation.
- Move to virtualenv and nose for development.
- Move to Travis CI and Coveralls for automated continuous testing.
2.0.0 (29 Oct 2013)¶
- Remove use of
sqlalchemy-migrate
,alembic
is a better bet but not yet introduced. - Much work to better adhere to PEP 8, including renaming the major functions.
1.2.1 (30 Jun 2011)¶
- Add
setuptools_git
to the build chain so thatsetuptools
include_package_data works once more.
1.2.0 (30 Jun 2011)¶
- Pass None as the default for echo rather than False on the advice of Daniel Holth.
- When using
register_session()
, allow explicit disabling of two-phase commit. - No longer log passwords during session registration.
- Specify
sqlalchemy
0.6 as a requirement, untilzope.sqlalchemy
is ported,mortar_rdb
shouldn’t be used withsqlalchemy
0.7.
1.1.0 (27 Feb 2011)¶
- Allow passing in
SessionExtension
instances to bothregisterSession()
functions. - Fixed a bug that resulted in an exception when passing
echo=True
tomortar_rdb.testing.registerSession()
but not passing aurl
.
1.0.1 (19 Feb 2011)¶
- Fixed a missing declaration of dependency on
zope.dottedname
.
1.0.0 (18 Feb 2011)¶
- Initial Release.
License¶
Copyright (c) 2011-2015 Simplistix Ltd, 2015-2018 Chris Withers
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.