Source code for score.sa.orm._init
# Copyright © 2015-2017 STRG.AT GmbH, Vienna, Austria
#
# This file is part of the The SCORE Framework.
#
# The SCORE Framework and all its parts are free software: you can redistribute
# them and/or modify them under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation which is in the
# file named COPYING.LESSER.txt.
#
# The SCORE Framework and all its parts are distributed without any WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. For more details see the GNU Lesser General Public
# License.
#
# If you have not received a copy of the GNU Lesser General Public License see
# http://www.gnu.org/licenses/.
#
# The License-Agreement realised between you as Licensee and STRG.AT GmbH as
# Licenser including the issue of its valid conclusion and its pre- and
# post-contractual effects is governed by the laws of Austria. Any disputes
# concerning this License-Agreement including the issue of its valid conclusion
# and its pre- and post-contractual effects are exclusively decided by the
# competent court, in whose district STRG.AT GmbH has its registered seat, at
# the discretion of STRG.AT GmbH also the competent court, in whose district the
# Licensee has his registered seat, an establishment or assets.
from score.init import (
ConfiguredModule, ConfigurationError, parse_dotted_path, parse_bool)
from ._session import sessionmaker, QueryIdsMixin
from .base import BaseMeta
from .triggers import CreateInheritanceTrigger, DropInheritanceTrigger
from .views import (
generate_drop_inheritance_view_statement,
generate_create_inheritance_view_statement,
)
import weakref
import sqlalchemy as sa
defaults = {
'ctx.member': 'orm',
'zope_transactions': False,
}
[docs]def init(confdict, db, ctx=None):
"""
Initializes this module acoording to :ref:`our module initialization
guidelines <module_initialization>` with the following configuration keys:
:confkey:`base`
The dotted python path to the :ref:`base class <sa_orm_base_class>` to
configure, as interpreted by :func:`score.init.parse_dotted_path`.
:confkey:`ctx.member` :confdefault:`db`
The name of the :term:`context member`, that should be registered with
the configured :mod:`score.ctx` module (if there is one). The default
value allows you to always access a valid session within a
:class:`score.ctx.Context` like this:
>>> ctx.orm.query(User).first()
:confkey:`zope_transactions` :confdefault:`False`
Whether the :attr:`Session` should include the `zope transaction
extension`_ outside of :class:`score.ctx.Context` objects. Note that
sessions created as :term:`context members <context member>` always
include this extension, since the :mod:`score.ctx` module makes use of
zope transactions.
.. _zope transaction extension:
https://pypi.python.org/pypi/zope.sqlalchemy
"""
conf = defaults.copy()
conf.update(confdict)
if not conf['base']:
raise ConfigurationError('score.sa.orm', 'No base class configured')
Base = parse_dotted_path(conf['base'])
if not issubclass(type(Base), BaseMeta):
raise ConfigurationError(
'score.sa.orm',
'Configured base class not created via create_base()')
if Base.metadata.bind:
raise ConfigurationError(
'score.sa.orm', 'Base class already bound to another engine')
Base.metadata.bind = db.engine
ctx_member = None
if conf['ctx.member'] and conf['ctx.member'] != 'None':
ctx_member = conf['ctx.member']
if db.engine.dialect.name == 'sqlite':
@sa.event.listens_for(db.engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
zope_transactions = parse_bool(conf['zope_transactions'])
return ConfiguredSaOrmModule(db, ctx, Base, ctx_member, zope_transactions)
[docs]class ConfiguredSaOrmModule(ConfiguredModule):
"""
This module's :class:`configuration class
<score.init.ConfiguredModule>`.
"""
def __init__(self, db, ctx, Base, ctx_member, zope_transactions):
super().__init__('score.sa.orm')
self.db = db
self.Base = Base
self.ctx_member = ctx_member
self.zope_transactions = zope_transactions
self.Session = None
self.session_mixins = {QueryIdsMixin}
self.__ctx_sessions = weakref.WeakKeyDictionary()
if ctx and ctx_member:
ctx.register(ctx_member, self.get_session)
[docs] def add_session_mixin(self, mixin):
"""
Adds a mixin class to the Session obejct.
You can use this function to add arbitrary features to your database
sessions:
.. code-block:: python
class RestaurantSketch:
def __init__(self, *args, **kwargs):
# this function will receive the same arguments as the
# base Session class (sqlalchemy.orm.session.Session)
pass
def dirty_fork(self):
try:
raise Exception('The waiter has commited suicide!')
except Exception as e:
raise BadPunchLineException() from e
After registering this mixin through this function, you can access its
functions in every session instance:
.. code-block:: python
try:
# prepare for a bad punch line
ctx.orm.dirty_fork()
except BadPunchLineException:
pass # out
This function must be called before this object is finalized.
"""
assert not self._finalized
self.session_mixins.add(mixin)
def _finalize(self):
kwargs = {
'bind': self.db.engine,
}
if self.zope_transactions:
from zope.sqlalchemy import ZopeTransactionExtension
kwargs['extension'] = ZopeTransactionExtension()
self.Session = sessionmaker(self, **kwargs)
def get_session(self, ctx):
"""
Provides a session instance, which is bound to the life-cycle of given
context object. Will always return the same session object for the same
input value.
"""
try:
return self.__ctx_sessions[ctx]
except KeyError:
from zope.sqlalchemy import ZopeTransactionExtension
zope_tx = ZopeTransactionExtension(
transaction_manager=ctx.tx_manager)
session = self.Session(extension=zope_tx,
bind=self.db.get_connection(ctx))
self.__ctx_sessions[ctx] = session
return session
[docs] def create(self):
"""
Generates all necessary tables, views, triggers, sequences, etc.
"""
# create all tables
self.Base.metadata.create_all()
session = self.Session(extension=[])
# generate inheritance views and triggers: we do this starting with the
# base class and working our way down the inheritance hierarchy
classes = [cls for cls in self.Base.__subclasses__()
if cls.__score_sa_orm__['parent'] is None]
while classes:
for cls in classes:
self._create_inheritance_trigger(session, cls)
self._create_inheritance_view(session, cls)
classes = [sub for cls in classes for sub in cls.__subclasses__()]
session.commit()
def _create_inheritance_trigger(self, session, class_):
"""
Creates the inheritance trigger for given *class_*. This trigger will
delete entries from parent tables, whenever a row in the given table is
deleted.
Example: assuming the given class ``Administrator`` is a sub-class of
``User``, this will create an sqlite trigger like the following:
CREATE TRIGGER autodel_administrator
AFTER DELETE ON _administrator
FOR EACH ROW BEGIN
DELETE FROM _user WHERE id = OLD.id;
END
"""
parent_tables = []
parent = class_.__score_sa_orm__['parent']
while parent:
parent_tables.append(parent.__table__)
parent = parent.__score_sa_orm__['parent']
session.execute(DropInheritanceTrigger(class_.__table__))
if parent_tables:
session.execute(CreateInheritanceTrigger(
class_.__table__, parent_tables[0]))
def _create_inheritance_view(self, session, class_):
"""
Creates the inheritance view for given *class_*. The view combines all
fields in the given class, as well as those in parent classes.
Example: assuming the following table structure:
CREATE TABLE _file (
id INTEGER NOT NULL,
name VARCHAR(100)
);
CREATE TABLE _image (
id INTEGER NOT NULL,
format VARCHAR(10),
FOREIGN KEY(id) REFERENCES _file (id)
);
The inheritance view for the ``Image`` class would look like the
following:
CREATE VIEW image AS
SELECT f.id, f.name, i.format
FROM _file f INNER JOIN _image i ON f.id = i.id
"""
dropview = generate_drop_inheritance_view_statement(class_)
session.execute(dropview)
if class_.__score_sa_orm__['inheritance'] is not None:
createview = generate_create_inheritance_view_statement(class_)
session.execute(createview)