Source code for score.sa.orm.base

# 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.

import re
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.declarative.api import DeclarativeMeta


IdType = sa.BigInteger()
IdType = IdType.with_variant(sa.Integer, 'sqlite')


# taken from stackoverflow:
# http://stackoverflow.com/a/1176023/44562
_first_cap_re = re.compile('(.)([A-Z][a-z]+)')
_all_cap_re = re.compile('([a-z0-9])([A-Z])')


[docs]def cls2tbl(cls): """ Converts a class (or a class name) to a table name. The class name is expected to be in *CamelCase*. The return value will be *seperated_by_underscores* and prefixed with an underscore. Omitting the underscore will yield the name of the class's :ref:`view <sa_orm_view>`. """ if isinstance(cls, type): cls = cls.__name__ s1 = _first_cap_re.sub(r'\1_\2', cls) return '_' + _all_cap_re.sub(r'\1_\2', s1).lower()
[docs]def tbl2cls(tbl): """ Inverse of :func:`.cls2tbl`. Returns the name of a class. """ if tbl[0] == '_': tbl = tbl[1:] parts = tbl.split('_') return ''.join(map(lambda s: s.capitalize(), parts))
def clsname(cls): return '%s.%s' % (cls.__module__, cls.__name__) class ConfigurationError(Exception): pass class BaseMeta(DeclarativeMeta): """ Metaclass for the created :class:`.Base` class. """ def __init__(cls, classname, bases, attrs): """ Normalizes configuration values of new database classes. """ if hasattr(cls, '__score_sa_orm__'): BaseMeta.set_config(cls, classname, bases, attrs) BaseMeta.set_tablename(cls, classname, bases, attrs) BaseMeta.configure_inheritance(cls, classname, bases, attrs) BaseMeta.set_id(cls, classname, bases, attrs) DeclarativeMeta.__init__(cls, classname, bases, attrs) def set_config(cls, classname, bases, attrs): """ Sets the class' __score_sa_orm__ value with the computed configuration. This dict will contain the following values at the end of this function: - base: the :term:`base class` of this class. - parent: the parent class in the inheritance hierarchy towards Base. - inheritance: the inheritance type - type_name: name of this type in the database, as stored in the type_column. - type_column: name of the column containing the type_name """ if '__score_sa_orm__' not in attrs: cls.__score_sa_orm__ = attrs['__score_sa_orm__'] = {} BaseMeta.set_base_config(cls, classname, bases, attrs) BaseMeta.set_parent_config(cls, classname, bases, attrs) BaseMeta.set_inheritance_config(cls, classname, bases, attrs) BaseMeta.set_type_name_config(cls, classname, bases, attrs) BaseMeta.set_type_column_config(cls, classname, bases, attrs) def set_base_config(cls, classname, bases, attrs): base_classes = dict() for base in bases: if hasattr(base, '__score_sa_orm__'): base_classes[base.__score_sa_orm__['base']] = base if len(base_classes) > 1: raise ConfigurationError( 'Multiple base class parents for class %s:\n- %s' % ( clsname(cls), '\n- '.join(base_classes.values()))) cls.__score_sa_orm__['base'] = next(iter(base_classes)) def set_parent_config(cls, classname, bases, attrs): cfg = cls.__score_sa_orm__ cfg['parent'] = None for base in bases: if base != cfg['base'] and issubclass(base, cfg['base']): if cfg['parent'] is not None: raise ConfigurationError( 'Diamond inheritance from Base class in %s' % clsname(cls)) cfg['parent'] = base def set_inheritance_config(cls, classname, bases, attrs): cfg = cls.__score_sa_orm__ parent = cfg['parent'] if parent is not None: # this is a sub-class of another class that should # already have the 'polymorphic_on' configuration. inheritance = parent.__score_sa_orm__['inheritance'] if inheritance is None: raise ConfigurationError( 'Parent table of %s does not support inheritance' % clsname(cls)) if 'inheritance' not in cfg: cfg['inheritance'] = inheritance elif cfg['inheritance'] != inheritance: raise ConfigurationError( 'Cannot change inheritance type of %s in subclass %s' % (parent.__name__, clsname(cls))) elif 'inheritance' not in cfg: cfg['inheritance'] = 'joined-table' else: if cfg['inheritance'] not in ('single-table', 'joined-table', None): raise ConfigurationError( 'Invalid inheritance configuration "%s" in class %s' % cfg['inheritance'], clsname(cls)) def set_type_name_config(cls, classname, bases, attrs): cfg = cls.__score_sa_orm__ if 'type_name' not in cfg: if 'polymorphic_identity' in attrs.get('__mapper_args__', {}): type_name = attrs['__mapper_args__']['polymorphic_identity'] cfg['type_name'] = type_name else: cfg['type_name'] = cls2tbl(classname)[1:] elif 'polymorphic_identity' in attrs.get('__mapper_args__', {}): raise ConfigurationError( 'Both sqlalchemy and score.sa.orm configured with a ' 'polymorphic identity,\n' 'please remove one of the two configurations in %s:\n' ' - __mapper_args__[polymorphic_identity]\n' ' - __score_sa_orm__[type_name]' % (clsname(cls),)) def set_type_column_config(cls, classname, bases, attrs): cfg = cls.__score_sa_orm__ if 'type_column' not in cfg: if 'polymorphic_on' in attrs.get('__mapper_args__', {}): cfg['type_column'] = attrs['__mapper_args__']['polymorphic_on'] else: cfg['type_column'] = '_type' if 'polymorphic_on' in attrs.get('__mapper_args__', {}): raise ConfigurationError( 'Both sqlalchemy and score.sa.orm configured with a type ' 'column,\n' 'please remove one of the two configurations in %s:\n' ' - __mapper_args__[polymorphic_on]\n' ' - __score_sa_orm__[type_column]' % (clsname(cls),)) def set_tablename(cls, classname, bases, attrs): """ Sets the ``__tablename__`` member for sqlalchemy. """ if cls.__score_sa_orm__['inheritance'] == 'single-table' and \ cls.__score_sa_orm__['parent'] is not None: # this is a sub-class of another class that should # already have a __tablename__ attribute. return cls.__tablename__ = attrs['__tablename__'] = cls2tbl(classname) def configure_inheritance(cls, classname, bases, attrs): """ Sets all necessary members to make the desired inheritance configuration work. Will set any/all of the following attributes, depending on the *inheritance* configuration: - cls.__mapper_args__['polymorphic_identity'] - cls.__mapper_args__['polymorphic_on'] - cls._type (or equivalent) """ cfg = cls.__score_sa_orm__ if cfg['inheritance'] is None: return if '__mapper_args__' not in attrs: cls.__mapper_args__ = attrs['__mapper_args__'] = {} if 'polymorphic_identity' not in cls.__mapper_args__: cls.__mapper_args__['polymorphic_identity'] = cfg['type_name'] if cfg['parent'] is not None: # this is a sub-class of another class that should # already have the 'polymorphic_on' configuration. return type_column = cfg['type_column'] cls.__mapper_args__['polymorphic_on'] = type_column if type_column not in attrs: attrs[type_column] = sa.Column(sa.String(100), nullable=False) setattr(cls, type_column, attrs[type_column]) def set_id(cls, classname, bases, attrs): """ Generates the ``id`` column. The column will contain a foreign key constraint to parent class' table, if it is not a direct descendant of the :ref:`base class <sa_orm_base_class>`. """ if cls.__score_sa_orm__['inheritance'] == 'single-table' and \ cls.__score_sa_orm__['parent'] is not None: return try: cls.__mapper_args__['primary_key'] # primary key already configured via mapper, nothing to do here return except (AttributeError, KeyError): pass Base = cls.__score_sa_orm__['base'] args = [IdType] kwargs = { 'primary_key': True, 'nullable': False, } for base in bases: if base != Base and issubclass(base, Base): args.append(sa.ForeignKey('%s.id' % base.__tablename__)) break attrs['id'] = sa.Column(*args, **kwargs) cls.id = attrs['id']
[docs]def create_base(): """ Returns a :ref:`base class <sa_orm_base_class>` for database access objects. """ Base = declarative_base(metaclass=BaseMeta) Base.__score_sa_orm__ = { 'base': Base, } return Base