Source code for sqlsoup

"""

"""

from sqlalchemy import Table, MetaData, join
from sqlalchemy import schema, sql, util
from sqlalchemy.engine.base import Engine
from sqlalchemy.orm import scoped_session, sessionmaker, mapper, \
                            class_mapper, relationship, session,\
                            object_session, attributes
from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE
from sqlalchemy.sql import expression

__version__ = '0.9.0'
__all__ = ['SQLSoupError', 'SQLSoup', 'SelectableClassType', 'TableClassType', 'Session']

Session = scoped_session(sessionmaker())
"""SQLSoup's default session registry.

This is an instance of :class:`sqlalchemy.orm.scoping.ScopedSession`,
and provides a new :class:`sqlalchemy.orm.session.Session`
object for each application thread which refers to it.

"""

class AutoAdd(MapperExtension):
    def __init__(self, scoped_session):
        self.scoped_session = scoped_session

    def instrument_class(self, mapper, class_):
        class_.__init__ = self._default__init__(mapper)

    def _default__init__(ext, mapper):
        def __init__(self, **kwargs):
            for key, value in kwargs.iteritems():
                setattr(self, key, value)
        return __init__

    def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
        session = self.scoped_session()
        state = attributes.instance_state(instance)
        session._save_impl(state)
        return EXT_CONTINUE

    def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
        sess = object_session(instance)
        if sess:
            sess.expunge(instance)
        return EXT_CONTINUE

class SQLSoupError(Exception):
    pass

class ArgumentError(SQLSoupError):
    pass

# metaclass is necessary to expose class methods with getattr, e.g.
# we want to pass db.users.select through to users._mapper.select
[docs]class SelectableClassType(type): """Represent a SQLSoup mapping to a :class:`sqlalchemy.sql.expression.Selectable` construct, such as a table or SELECT statement. """ def insert(cls, **kwargs): raise SQLSoupError( 'SQLSoup can only modify mapped Tables (found: %s)' \ % cls._table.__class__.__name__ ) def __clause_element__(cls): return cls._table def __getattr__(cls, attr): if attr == '_query': # called during mapper init raise AttributeError() return getattr(cls._query, attr)
[docs]class TableClassType(SelectableClassType): """Represent a SQLSoup mapping to a :class:`sqlalchemy.schema.Table` construct. This object is produced automatically when a table-name attribute is accessed from a :class:`.SQLSoup` instance. """ def insert(cls, **kwargs): o = cls() o.__dict__.update(kwargs) return o
[docs] def relate(cls, propname, *args, **kwargs): """Produce a relationship between this mapped table and another one. This makes usage of SQLAlchemy's :func:`sqlalchemy.orm.relationship` construct. """ class_mapper(cls)._configure_property(propname, relationship(*args, **kwargs))
def _is_outer_join(selectable): if not isinstance(selectable, sql.Join): return False if selectable.isouter: return True return _is_outer_join(selectable.left) or _is_outer_join(selectable.right) def _selectable_name(selectable): if isinstance(selectable, sql.Alias): return _selectable_name(selectable.element) elif isinstance(selectable, sql.Select): return ''.join(_selectable_name(s) for s in selectable.froms) elif isinstance(selectable, schema.Table): return selectable.name.capitalize() else: x = selectable.__class__.__name__ if x[0] == '_': x = x[1:] return x def _class_for_table(session, engine, selectable, base_cls, mapper_kwargs): selectable = expression._clause_element_as_expr(selectable) mapname = 'Mapped' + _selectable_name(selectable) # Py2K if isinstance(mapname, unicode): engine_encoding = engine.dialect.encoding mapname = mapname.encode(engine_encoding) # end Py2K if isinstance(selectable, Table): klass = TableClassType(mapname, (base_cls,), {}) else: klass = SelectableClassType(mapname, (base_cls,), {}) def _compare(self, o): L = list(self.__class__.c.keys()) L.sort() t1 = [getattr(self, k) for k in L] try: t2 = [getattr(o, k) for k in L] except AttributeError: raise TypeError('unable to compare with %s' % o.__class__) return t1, t2 # python2/python3 compatible system of # __cmp__ - __lt__ + __eq__ def __lt__(self, o): t1, t2 = _compare(self, o) return t1 < t2 def __eq__(self, o): t1, t2 = _compare(self, o) return t1 == t2 def __repr__(self): L = ["%s=%r" % (key, getattr(self, key, '')) for key in self.__class__.c.keys()] return '%s(%s)' % (self.__class__.__name__, ','.join(L)) for m in ['__eq__', '__repr__', '__lt__']: setattr(klass, m, eval(m)) klass._table = selectable klass.c = expression.ColumnCollection() mappr = mapper(klass, selectable, extension=AutoAdd(session), **mapper_kwargs) for k in mappr.iterate_properties: klass.c[k.key] = k.columns[0] klass._query = session.query_property() return klass
[docs]class SQLSoup(object): """Represent an ORM-wrapped database resource.""" def __init__(self, engine_or_metadata, base=object, session=None): """Initialize a new :class:`.SQLSoup`. :param engine_or_metadata: a string database URL, :class:`.Engine` or :class:`.MetaData` object to associate with. If the argument is a :class:`.MetaData`, it should be *bound* to an :class:`.Engine`. :param base: a class which will serve as the default class for returned mapped classes. Defaults to ``object``. :param session: a :class:`.ScopedSession` or :class:`.Session` with which to associate ORM operations for this :class:`.SQLSoup` instance. If ``None``, a :class:`.ScopedSession` that's local to this module is used. """ self.session = session or Session self.base=base if isinstance(engine_or_metadata, MetaData): self._metadata = engine_or_metadata elif isinstance(engine_or_metadata, (basestring, Engine)): self._metadata = MetaData(engine_or_metadata) else: raise ArgumentError("invalid engine or metadata argument %r" % engine_or_metadata) self._cache = {} self.schema = None @property
[docs] def bind(self): """The :class:`sqlalchemy.engine.base.Engine` associated with this :class:`.SQLSoup`.""" return self._metadata.bind
engine = bind
[docs] def delete(self, instance): """Mark an instance as deleted.""" self.session.delete(instance)
[docs] def execute(self, stmt, **params): """Execute a SQL statement. The statement may be a string SQL string, an :func:`sqlalchemy.sql.expression.select` construct, or a :func:`sqlalchemy.sql.expression.text` construct. """ return self.session.execute(sql.text(stmt, bind=self.bind), **params)
@property def _underlying_session(self): if isinstance(self.session, session.Session): return self.session else: return self.session()
[docs] def connection(self): """Return the current :class:`sqlalchemy.engine.base.Connection` in use by the current transaction.""" return self._underlying_session._connection_for_bind(self.bind)
[docs] def flush(self): """Flush pending changes to the database. See :meth:`sqlalchemy.orm.session.Session.flush`. """ self.session.flush()
[docs] def rollback(self): """Rollback the current transction. See :meth:`sqlalchemy.orm.session.Session.rollback`. """ self.session.rollback()
[docs] def commit(self): """Commit the current transaction. See :meth:`sqlalchemy.orm.session.Session.commit`. """ self.session.commit()
[docs] def expunge(self, instance): """Remove an instance from the :class:`.Session`. See :meth:`sqlalchemy.orm.session.Session.expunge`. """ self.session.expunge(instance)
[docs] def expunge_all(self): """Clear all objects from the current :class:`.Session`. See :meth:`.Session.expunge_all`. """ self.session.expunge_all()
[docs] def map_to(self, attrname, tablename=None, selectable=None, schema=None, base=None, mapper_args=util.immutabledict()): """Configure a mapping to the given attrname. This is the "master" method that can be used to create any configuration. :param attrname: String attribute name which will be established as an attribute on this :class:.`.SQLSoup` instance. :param base: a Python class which will be used as the base for the mapped class. If ``None``, the "base" argument specified by this :class:`.SQLSoup` instance's constructor will be used, which defaults to ``object``. :param mapper_args: Dictionary of arguments which will be passed directly to :func:`.orm.mapper`. :param tablename: String name of a :class:`.Table` to be reflected. If a :class:`.Table` is already available, use the ``selectable`` argument. This argument is mutually exclusive versus the ``selectable`` argument. :param selectable: a :class:`.Table`, :class:`.Join`, or :class:`.Select` object which will be mapped. This argument is mutually exclusive versus the ``tablename`` argument. :param schema: String schema name to use if the ``tablename`` argument is present. """ if attrname in self._cache: raise SQLSoupError( "Attribute '%s' is already mapped to '%s'" % ( attrname, class_mapper(self._cache[attrname]).mapped_table )) if tablename is not None: if not isinstance(tablename, basestring): raise ArgumentError("'tablename' argument must be a string." ) if selectable is not None: raise ArgumentError("'tablename' and 'selectable' " "arguments are mutually exclusive") selectable = Table(tablename, self._metadata, autoload=True, autoload_with=self.bind, schema=schema or self.schema) elif schema: raise ArgumentError("'tablename' argument is required when " "using 'schema'.") elif selectable is not None: if not isinstance(selectable, expression.FromClause): raise ArgumentError("'selectable' argument must be a " "table, select, join, or other " "selectable construct.") else: raise ArgumentError("'tablename' or 'selectable' argument is " "required.") if not selectable.primary_key.columns: if tablename: raise SQLSoupError( "table '%s' does not have a primary " "key defined" % tablename) else: raise SQLSoupError( "selectable '%s' does not have a primary " "key defined" % selectable) mapped_cls = _class_for_table( self.session, self.engine, selectable, base or self.base, mapper_args ) self._cache[attrname] = mapped_cls return mapped_cls
[docs] def map(self, selectable, base=None, **mapper_args): """Map a selectable directly. The class and its mapping are not cached and will be discarded once dereferenced (as of 0.6.6). :param selectable: an :func:`.expression.select` construct. :param base: a Python class which will be used as the base for the mapped class. If ``None``, the "base" argument specified by this :class:`.SQLSoup` instance's constructor will be used, which defaults to ``object``. :param mapper_args: Dictionary of arguments which will be passed directly to :func:`.orm.mapper`. """ return _class_for_table( self.session, self.engine, selectable, base or self.base, mapper_args )
[docs] def with_labels(self, selectable, base=None, **mapper_args): """Map a selectable directly, wrapping the selectable in a subquery with labels. The class and its mapping are not cached and will be discarded once dereferenced (as of 0.6.6). :param selectable: an :func:`.expression.select` construct. :param base: a Python class which will be used as the base for the mapped class. If ``None``, the "base" argument specified by this :class:`.SQLSoup` instance's constructor will be used, which defaults to ``object``. :param mapper_args: Dictionary of arguments which will be passed directly to :func:`.orm.mapper`. """ # TODO give meaningful aliases return self.map( expression._clause_element_as_expr(selectable). select(use_labels=True). alias('foo'), base=base, **mapper_args)
[docs] def join(self, left, right, onclause=None, isouter=False, base=None, **mapper_args): """Create an :func:`.expression.join` and map to it. The class and its mapping are not cached and will be discarded once dereferenced (as of 0.6.6). :param left: a mapped class or table object. :param right: a mapped class or table object. :param onclause: optional "ON" clause construct.. :param isouter: if True, the join will be an OUTER join. :param base: a Python class which will be used as the base for the mapped class. If ``None``, the "base" argument specified by this :class:`.SQLSoup` instance's constructor will be used, which defaults to ``object``. :param mapper_args: Dictionary of arguments which will be passed directly to :func:`.orm.mapper`. """ j = join(left, right, onclause=onclause, isouter=isouter) return self.map(j, base=base, **mapper_args)
[docs] def entity(self, attr, schema=None): """Return the named entity from this :class:`.SQLSoup`, or create if not present. For more generalized mapping, see :meth:`.map_to`. """ try: return self._cache[attr] except KeyError, ke: return self.map_to(attr, tablename=attr, schema=schema)
def __getattr__(self, attr): return self.entity(attr) def __repr__(self): return 'SQLSoup(%r)' % self._metadata