morro
This commit is contained in:
1
sqlalchemy/ext/__init__.py
Normal file
1
sqlalchemy/ext/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
878
sqlalchemy/ext/associationproxy.py
Normal file
878
sqlalchemy/ext/associationproxy.py
Normal file
@@ -0,0 +1,878 @@
|
||||
"""Contain the ``AssociationProxy`` class.
|
||||
|
||||
The ``AssociationProxy`` is a Python property object which provides
|
||||
transparent proxied access to the endpoint of an association object.
|
||||
|
||||
See the example ``examples/association/proxied_association.py``.
|
||||
|
||||
"""
|
||||
import itertools
|
||||
import operator
|
||||
import weakref
|
||||
from sqlalchemy import exceptions
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.orm import collections
|
||||
from sqlalchemy.sql import not_
|
||||
|
||||
|
||||
def association_proxy(target_collection, attr, **kw):
|
||||
"""Return a Python property implementing a view of *attr* over a collection.
|
||||
|
||||
Implements a read/write view over an instance's *target_collection*,
|
||||
extracting *attr* from each member of the collection. The property acts
|
||||
somewhat like this list comprehension::
|
||||
|
||||
[getattr(member, *attr*)
|
||||
for member in getattr(instance, *target_collection*)]
|
||||
|
||||
Unlike the list comprehension, the collection returned by the property is
|
||||
always in sync with *target_collection*, and mutations made to either
|
||||
collection will be reflected in both.
|
||||
|
||||
Implements a Python property representing a relationship as a collection of
|
||||
simpler values. The proxied property will mimic the collection type of
|
||||
the target (list, dict or set), or, in the case of a one to one relationship,
|
||||
a simple scalar value.
|
||||
|
||||
:param target_collection: Name of the relationship attribute we'll proxy to,
|
||||
usually created with :func:`~sqlalchemy.orm.relationship`.
|
||||
|
||||
:param attr: Attribute on the associated instances we'll proxy for.
|
||||
|
||||
For example, given a target collection of [obj1, obj2], a list created
|
||||
by this proxy property would look like [getattr(obj1, *attr*),
|
||||
getattr(obj2, *attr*)]
|
||||
|
||||
If the relationship is one-to-one or otherwise uselist=False, then simply:
|
||||
getattr(obj, *attr*)
|
||||
|
||||
:param creator: optional.
|
||||
|
||||
When new items are added to this proxied collection, new instances of
|
||||
the class collected by the target collection will be created. For list
|
||||
and set collections, the target class constructor will be called with
|
||||
the 'value' for the new instance. For dict types, two arguments are
|
||||
passed: key and value.
|
||||
|
||||
If you want to construct instances differently, supply a *creator*
|
||||
function that takes arguments as above and returns instances.
|
||||
|
||||
For scalar relationships, creator() will be called if the target is None.
|
||||
If the target is present, set operations are proxied to setattr() on the
|
||||
associated object.
|
||||
|
||||
If you have an associated object with multiple attributes, you may set
|
||||
up multiple association proxies mapping to different attributes. See
|
||||
the unit tests for examples, and for examples of how creator() functions
|
||||
can be used to construct the scalar relationship on-demand in this
|
||||
situation.
|
||||
|
||||
:param \*\*kw: Passes along any other keyword arguments to
|
||||
:class:`AssociationProxy`.
|
||||
|
||||
"""
|
||||
return AssociationProxy(target_collection, attr, **kw)
|
||||
|
||||
|
||||
class AssociationProxy(object):
|
||||
"""A descriptor that presents a read/write view of an object attribute."""
|
||||
|
||||
def __init__(self, target_collection, attr, creator=None,
|
||||
getset_factory=None, proxy_factory=None, proxy_bulk_set=None):
|
||||
"""Arguments are:
|
||||
|
||||
target_collection
|
||||
Name of the collection we'll proxy to, usually created with
|
||||
'relationship()' in a mapper setup.
|
||||
|
||||
attr
|
||||
Attribute on the collected instances we'll proxy for. For example,
|
||||
given a target collection of [obj1, obj2], a list created by this
|
||||
proxy property would look like [getattr(obj1, attr), getattr(obj2,
|
||||
attr)]
|
||||
|
||||
creator
|
||||
Optional. When new items are added to this proxied collection, new
|
||||
instances of the class collected by the target collection will be
|
||||
created. For list and set collections, the target class constructor
|
||||
will be called with the 'value' for the new instance. For dict
|
||||
types, two arguments are passed: key and value.
|
||||
|
||||
If you want to construct instances differently, supply a 'creator'
|
||||
function that takes arguments as above and returns instances.
|
||||
|
||||
getset_factory
|
||||
Optional. Proxied attribute access is automatically handled by
|
||||
routines that get and set values based on the `attr` argument for
|
||||
this proxy.
|
||||
|
||||
If you would like to customize this behavior, you may supply a
|
||||
`getset_factory` callable that produces a tuple of `getter` and
|
||||
`setter` functions. The factory is called with two arguments, the
|
||||
abstract type of the underlying collection and this proxy instance.
|
||||
|
||||
proxy_factory
|
||||
Optional. The type of collection to emulate is determined by
|
||||
sniffing the target collection. If your collection type can't be
|
||||
determined by duck typing or you'd like to use a different
|
||||
collection implementation, you may supply a factory function to
|
||||
produce those collections. Only applicable to non-scalar relationships.
|
||||
|
||||
proxy_bulk_set
|
||||
Optional, use with proxy_factory. See the _set() method for
|
||||
details.
|
||||
|
||||
"""
|
||||
self.target_collection = target_collection
|
||||
self.value_attr = attr
|
||||
self.creator = creator
|
||||
self.getset_factory = getset_factory
|
||||
self.proxy_factory = proxy_factory
|
||||
self.proxy_bulk_set = proxy_bulk_set
|
||||
|
||||
self.scalar = None
|
||||
self.owning_class = None
|
||||
self.key = '_%s_%s_%s' % (
|
||||
type(self).__name__, target_collection, id(self))
|
||||
self.collection_class = None
|
||||
|
||||
def _get_property(self):
|
||||
return (orm.class_mapper(self.owning_class).
|
||||
get_property(self.target_collection))
|
||||
|
||||
@property
|
||||
def target_class(self):
|
||||
"""The class the proxy is attached to."""
|
||||
return self._get_property().mapper.class_
|
||||
|
||||
def _target_is_scalar(self):
|
||||
return not self._get_property().uselist
|
||||
|
||||
def __get__(self, obj, class_):
|
||||
if self.owning_class is None:
|
||||
self.owning_class = class_ and class_ or type(obj)
|
||||
if obj is None:
|
||||
return self
|
||||
elif self.scalar is None:
|
||||
self.scalar = self._target_is_scalar()
|
||||
if self.scalar:
|
||||
self._initialize_scalar_accessors()
|
||||
|
||||
if self.scalar:
|
||||
return self._scalar_get(getattr(obj, self.target_collection))
|
||||
else:
|
||||
try:
|
||||
# If the owning instance is reborn (orm session resurrect,
|
||||
# etc.), refresh the proxy cache.
|
||||
creator_id, proxy = getattr(obj, self.key)
|
||||
if id(obj) == creator_id:
|
||||
return proxy
|
||||
except AttributeError:
|
||||
pass
|
||||
proxy = self._new(_lazy_collection(obj, self.target_collection))
|
||||
setattr(obj, self.key, (id(obj), proxy))
|
||||
return proxy
|
||||
|
||||
def __set__(self, obj, values):
|
||||
if self.owning_class is None:
|
||||
self.owning_class = type(obj)
|
||||
if self.scalar is None:
|
||||
self.scalar = self._target_is_scalar()
|
||||
if self.scalar:
|
||||
self._initialize_scalar_accessors()
|
||||
|
||||
if self.scalar:
|
||||
creator = self.creator and self.creator or self.target_class
|
||||
target = getattr(obj, self.target_collection)
|
||||
if target is None:
|
||||
setattr(obj, self.target_collection, creator(values))
|
||||
else:
|
||||
self._scalar_set(target, values)
|
||||
else:
|
||||
proxy = self.__get__(obj, None)
|
||||
if proxy is not values:
|
||||
proxy.clear()
|
||||
self._set(proxy, values)
|
||||
|
||||
def __delete__(self, obj):
|
||||
if self.owning_class is None:
|
||||
self.owning_class = type(obj)
|
||||
delattr(obj, self.key)
|
||||
|
||||
def _initialize_scalar_accessors(self):
|
||||
if self.getset_factory:
|
||||
get, set = self.getset_factory(None, self)
|
||||
else:
|
||||
get, set = self._default_getset(None)
|
||||
self._scalar_get, self._scalar_set = get, set
|
||||
|
||||
def _default_getset(self, collection_class):
|
||||
attr = self.value_attr
|
||||
getter = operator.attrgetter(attr)
|
||||
if collection_class is dict:
|
||||
setter = lambda o, k, v: setattr(o, attr, v)
|
||||
else:
|
||||
setter = lambda o, v: setattr(o, attr, v)
|
||||
return getter, setter
|
||||
|
||||
def _new(self, lazy_collection):
|
||||
creator = self.creator and self.creator or self.target_class
|
||||
self.collection_class = util.duck_type_collection(lazy_collection())
|
||||
|
||||
if self.proxy_factory:
|
||||
return self.proxy_factory(lazy_collection, creator, self.value_attr, self)
|
||||
|
||||
if self.getset_factory:
|
||||
getter, setter = self.getset_factory(self.collection_class, self)
|
||||
else:
|
||||
getter, setter = self._default_getset(self.collection_class)
|
||||
|
||||
if self.collection_class is list:
|
||||
return _AssociationList(lazy_collection, creator, getter, setter, self)
|
||||
elif self.collection_class is dict:
|
||||
return _AssociationDict(lazy_collection, creator, getter, setter, self)
|
||||
elif self.collection_class is set:
|
||||
return _AssociationSet(lazy_collection, creator, getter, setter, self)
|
||||
else:
|
||||
raise exceptions.ArgumentError(
|
||||
'could not guess which interface to use for '
|
||||
'collection_class "%s" backing "%s"; specify a '
|
||||
'proxy_factory and proxy_bulk_set manually' %
|
||||
(self.collection_class.__name__, self.target_collection))
|
||||
|
||||
def _inflate(self, proxy):
|
||||
creator = self.creator and self.creator or self.target_class
|
||||
|
||||
if self.getset_factory:
|
||||
getter, setter = self.getset_factory(self.collection_class, self)
|
||||
else:
|
||||
getter, setter = self._default_getset(self.collection_class)
|
||||
|
||||
proxy.creator = creator
|
||||
proxy.getter = getter
|
||||
proxy.setter = setter
|
||||
|
||||
def _set(self, proxy, values):
|
||||
if self.proxy_bulk_set:
|
||||
self.proxy_bulk_set(proxy, values)
|
||||
elif self.collection_class is list:
|
||||
proxy.extend(values)
|
||||
elif self.collection_class is dict:
|
||||
proxy.update(values)
|
||||
elif self.collection_class is set:
|
||||
proxy.update(values)
|
||||
else:
|
||||
raise exceptions.ArgumentError(
|
||||
'no proxy_bulk_set supplied for custom '
|
||||
'collection_class implementation')
|
||||
|
||||
@property
|
||||
def _comparator(self):
|
||||
return self._get_property().comparator
|
||||
|
||||
def any(self, criterion=None, **kwargs):
|
||||
return self._comparator.any(getattr(self.target_class, self.value_attr).has(criterion, **kwargs))
|
||||
|
||||
def has(self, criterion=None, **kwargs):
|
||||
return self._comparator.has(getattr(self.target_class, self.value_attr).has(criterion, **kwargs))
|
||||
|
||||
def contains(self, obj):
|
||||
return self._comparator.any(**{self.value_attr: obj})
|
||||
|
||||
def __eq__(self, obj):
|
||||
return self._comparator.has(**{self.value_attr: obj})
|
||||
|
||||
def __ne__(self, obj):
|
||||
return not_(self.__eq__(obj))
|
||||
|
||||
|
||||
class _lazy_collection(object):
|
||||
def __init__(self, obj, target):
|
||||
self.ref = weakref.ref(obj)
|
||||
self.target = target
|
||||
|
||||
def __call__(self):
|
||||
obj = self.ref()
|
||||
if obj is None:
|
||||
raise exceptions.InvalidRequestError(
|
||||
"stale association proxy, parent object has gone out of "
|
||||
"scope")
|
||||
return getattr(obj, self.target)
|
||||
|
||||
def __getstate__(self):
|
||||
return {'obj':self.ref(), 'target':self.target}
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.ref = weakref.ref(state['obj'])
|
||||
self.target = state['target']
|
||||
|
||||
class _AssociationCollection(object):
|
||||
def __init__(self, lazy_collection, creator, getter, setter, parent):
|
||||
"""Constructs an _AssociationCollection.
|
||||
|
||||
This will always be a subclass of either _AssociationList,
|
||||
_AssociationSet, or _AssociationDict.
|
||||
|
||||
lazy_collection
|
||||
A callable returning a list-based collection of entities (usually an
|
||||
object attribute managed by a SQLAlchemy relationship())
|
||||
|
||||
creator
|
||||
A function that creates new target entities. Given one parameter:
|
||||
value. This assertion is assumed::
|
||||
|
||||
obj = creator(somevalue)
|
||||
assert getter(obj) == somevalue
|
||||
|
||||
getter
|
||||
A function. Given an associated object, return the 'value'.
|
||||
|
||||
setter
|
||||
A function. Given an associated object and a value, store that
|
||||
value on the object.
|
||||
|
||||
"""
|
||||
self.lazy_collection = lazy_collection
|
||||
self.creator = creator
|
||||
self.getter = getter
|
||||
self.setter = setter
|
||||
self.parent = parent
|
||||
|
||||
col = property(lambda self: self.lazy_collection())
|
||||
|
||||
def __len__(self):
|
||||
return len(self.col)
|
||||
|
||||
def __nonzero__(self):
|
||||
return bool(self.col)
|
||||
|
||||
def __getstate__(self):
|
||||
return {'parent':self.parent, 'lazy_collection':self.lazy_collection}
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.parent = state['parent']
|
||||
self.lazy_collection = state['lazy_collection']
|
||||
self.parent._inflate(self)
|
||||
|
||||
class _AssociationList(_AssociationCollection):
|
||||
"""Generic, converting, list-to-list proxy."""
|
||||
|
||||
def _create(self, value):
|
||||
return self.creator(value)
|
||||
|
||||
def _get(self, object):
|
||||
return self.getter(object)
|
||||
|
||||
def _set(self, object, value):
|
||||
return self.setter(object, value)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self._get(self.col[index])
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
if not isinstance(index, slice):
|
||||
self._set(self.col[index], value)
|
||||
else:
|
||||
if index.stop is None:
|
||||
stop = len(self)
|
||||
elif index.stop < 0:
|
||||
stop = len(self) + index.stop
|
||||
else:
|
||||
stop = index.stop
|
||||
step = index.step or 1
|
||||
|
||||
rng = range(index.start or 0, stop, step)
|
||||
if step == 1:
|
||||
for i in rng:
|
||||
del self[index.start]
|
||||
i = index.start
|
||||
for item in value:
|
||||
self.insert(i, item)
|
||||
i += 1
|
||||
else:
|
||||
if len(value) != len(rng):
|
||||
raise ValueError(
|
||||
"attempt to assign sequence of size %s to "
|
||||
"extended slice of size %s" % (len(value),
|
||||
len(rng)))
|
||||
for i, item in zip(rng, value):
|
||||
self._set(self.col[i], item)
|
||||
|
||||
def __delitem__(self, index):
|
||||
del self.col[index]
|
||||
|
||||
def __contains__(self, value):
|
||||
for member in self.col:
|
||||
# testlib.pragma exempt:__eq__
|
||||
if self._get(member) == value:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __getslice__(self, start, end):
|
||||
return [self._get(member) for member in self.col[start:end]]
|
||||
|
||||
def __setslice__(self, start, end, values):
|
||||
members = [self._create(v) for v in values]
|
||||
self.col[start:end] = members
|
||||
|
||||
def __delslice__(self, start, end):
|
||||
del self.col[start:end]
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over proxied values.
|
||||
|
||||
For the actual domain objects, iterate over .col instead or
|
||||
just use the underlying collection directly from its property
|
||||
on the parent.
|
||||
"""
|
||||
|
||||
for member in self.col:
|
||||
yield self._get(member)
|
||||
raise StopIteration
|
||||
|
||||
def append(self, value):
|
||||
item = self._create(value)
|
||||
self.col.append(item)
|
||||
|
||||
def count(self, value):
|
||||
return sum([1 for _ in
|
||||
itertools.ifilter(lambda v: v == value, iter(self))])
|
||||
|
||||
def extend(self, values):
|
||||
for v in values:
|
||||
self.append(v)
|
||||
|
||||
def insert(self, index, value):
|
||||
self.col[index:index] = [self._create(value)]
|
||||
|
||||
def pop(self, index=-1):
|
||||
return self.getter(self.col.pop(index))
|
||||
|
||||
def remove(self, value):
|
||||
for i, val in enumerate(self):
|
||||
if val == value:
|
||||
del self.col[i]
|
||||
return
|
||||
raise ValueError("value not in list")
|
||||
|
||||
def reverse(self):
|
||||
"""Not supported, use reversed(mylist)"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def sort(self):
|
||||
"""Not supported, use sorted(mylist)"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def clear(self):
|
||||
del self.col[0:len(self.col)]
|
||||
|
||||
def __eq__(self, other):
|
||||
return list(self) == other
|
||||
|
||||
def __ne__(self, other):
|
||||
return list(self) != other
|
||||
|
||||
def __lt__(self, other):
|
||||
return list(self) < other
|
||||
|
||||
def __le__(self, other):
|
||||
return list(self) <= other
|
||||
|
||||
def __gt__(self, other):
|
||||
return list(self) > other
|
||||
|
||||
def __ge__(self, other):
|
||||
return list(self) >= other
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(list(self), other)
|
||||
|
||||
def __add__(self, iterable):
|
||||
try:
|
||||
other = list(iterable)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
return list(self) + other
|
||||
|
||||
def __radd__(self, iterable):
|
||||
try:
|
||||
other = list(iterable)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
return other + list(self)
|
||||
|
||||
def __mul__(self, n):
|
||||
if not isinstance(n, int):
|
||||
return NotImplemented
|
||||
return list(self) * n
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __iadd__(self, iterable):
|
||||
self.extend(iterable)
|
||||
return self
|
||||
|
||||
def __imul__(self, n):
|
||||
# unlike a regular list *=, proxied __imul__ will generate unique
|
||||
# backing objects for each copy. *= on proxied lists is a bit of
|
||||
# a stretch anyhow, and this interpretation of the __imul__ contract
|
||||
# is more plausibly useful than copying the backing objects.
|
||||
if not isinstance(n, int):
|
||||
return NotImplemented
|
||||
if n == 0:
|
||||
self.clear()
|
||||
elif n > 1:
|
||||
self.extend(list(self) * (n - 1))
|
||||
return self
|
||||
|
||||
def copy(self):
|
||||
return list(self)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(list(self))
|
||||
|
||||
def __hash__(self):
|
||||
raise TypeError("%s objects are unhashable" % type(self).__name__)
|
||||
|
||||
for func_name, func in locals().items():
|
||||
if (util.callable(func) and func.func_name == func_name and
|
||||
not func.__doc__ and hasattr(list, func_name)):
|
||||
func.__doc__ = getattr(list, func_name).__doc__
|
||||
del func_name, func
|
||||
|
||||
|
||||
_NotProvided = util.symbol('_NotProvided')
|
||||
class _AssociationDict(_AssociationCollection):
|
||||
"""Generic, converting, dict-to-dict proxy."""
|
||||
|
||||
def _create(self, key, value):
|
||||
return self.creator(key, value)
|
||||
|
||||
def _get(self, object):
|
||||
return self.getter(object)
|
||||
|
||||
def _set(self, object, key, value):
|
||||
return self.setter(object, key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._get(self.col[key])
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in self.col:
|
||||
self._set(self.col[key], key, value)
|
||||
else:
|
||||
self.col[key] = self._create(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.col[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
# testlib.pragma exempt:__hash__
|
||||
return key in self.col
|
||||
|
||||
def has_key(self, key):
|
||||
# testlib.pragma exempt:__hash__
|
||||
return key in self.col
|
||||
|
||||
def __iter__(self):
|
||||
return self.col.iterkeys()
|
||||
|
||||
def clear(self):
|
||||
self.col.clear()
|
||||
|
||||
def __eq__(self, other):
|
||||
return dict(self) == other
|
||||
|
||||
def __ne__(self, other):
|
||||
return dict(self) != other
|
||||
|
||||
def __lt__(self, other):
|
||||
return dict(self) < other
|
||||
|
||||
def __le__(self, other):
|
||||
return dict(self) <= other
|
||||
|
||||
def __gt__(self, other):
|
||||
return dict(self) > other
|
||||
|
||||
def __ge__(self, other):
|
||||
return dict(self) >= other
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(dict(self), other)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(dict(self.items()))
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
if key not in self.col:
|
||||
self.col[key] = self._create(key, default)
|
||||
return default
|
||||
else:
|
||||
return self[key]
|
||||
|
||||
def keys(self):
|
||||
return self.col.keys()
|
||||
|
||||
def iterkeys(self):
|
||||
return self.col.iterkeys()
|
||||
|
||||
def values(self):
|
||||
return [ self._get(member) for member in self.col.values() ]
|
||||
|
||||
def itervalues(self):
|
||||
for key in self.col:
|
||||
yield self._get(self.col[key])
|
||||
raise StopIteration
|
||||
|
||||
def items(self):
|
||||
return [(k, self._get(self.col[k])) for k in self]
|
||||
|
||||
def iteritems(self):
|
||||
for key in self.col:
|
||||
yield (key, self._get(self.col[key]))
|
||||
raise StopIteration
|
||||
|
||||
def pop(self, key, default=_NotProvided):
|
||||
if default is _NotProvided:
|
||||
member = self.col.pop(key)
|
||||
else:
|
||||
member = self.col.pop(key, default)
|
||||
return self._get(member)
|
||||
|
||||
def popitem(self):
|
||||
item = self.col.popitem()
|
||||
return (item[0], self._get(item[1]))
|
||||
|
||||
def update(self, *a, **kw):
|
||||
if len(a) > 1:
|
||||
raise TypeError('update expected at most 1 arguments, got %i' %
|
||||
len(a))
|
||||
elif len(a) == 1:
|
||||
seq_or_map = a[0]
|
||||
for item in seq_or_map:
|
||||
if isinstance(item, tuple):
|
||||
self[item[0]] = item[1]
|
||||
else:
|
||||
self[item] = seq_or_map[item]
|
||||
|
||||
for key, value in kw:
|
||||
self[key] = value
|
||||
|
||||
def copy(self):
|
||||
return dict(self.items())
|
||||
|
||||
def __hash__(self):
|
||||
raise TypeError("%s objects are unhashable" % type(self).__name__)
|
||||
|
||||
for func_name, func in locals().items():
|
||||
if (util.callable(func) and func.func_name == func_name and
|
||||
not func.__doc__ and hasattr(dict, func_name)):
|
||||
func.__doc__ = getattr(dict, func_name).__doc__
|
||||
del func_name, func
|
||||
|
||||
|
||||
class _AssociationSet(_AssociationCollection):
|
||||
"""Generic, converting, set-to-set proxy."""
|
||||
|
||||
def _create(self, value):
|
||||
return self.creator(value)
|
||||
|
||||
def _get(self, object):
|
||||
return self.getter(object)
|
||||
|
||||
def _set(self, object, value):
|
||||
return self.setter(object, value)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.col)
|
||||
|
||||
def __nonzero__(self):
|
||||
if self.col:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def __contains__(self, value):
|
||||
for member in self.col:
|
||||
# testlib.pragma exempt:__eq__
|
||||
if self._get(member) == value:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over proxied values.
|
||||
|
||||
For the actual domain objects, iterate over .col instead or just use
|
||||
the underlying collection directly from its property on the parent.
|
||||
|
||||
"""
|
||||
for member in self.col:
|
||||
yield self._get(member)
|
||||
raise StopIteration
|
||||
|
||||
def add(self, value):
|
||||
if value not in self:
|
||||
self.col.add(self._create(value))
|
||||
|
||||
# for discard and remove, choosing a more expensive check strategy rather
|
||||
# than call self.creator()
|
||||
def discard(self, value):
|
||||
for member in self.col:
|
||||
if self._get(member) == value:
|
||||
self.col.discard(member)
|
||||
break
|
||||
|
||||
def remove(self, value):
|
||||
for member in self.col:
|
||||
if self._get(member) == value:
|
||||
self.col.discard(member)
|
||||
return
|
||||
raise KeyError(value)
|
||||
|
||||
def pop(self):
|
||||
if not self.col:
|
||||
raise KeyError('pop from an empty set')
|
||||
member = self.col.pop()
|
||||
return self._get(member)
|
||||
|
||||
def update(self, other):
|
||||
for value in other:
|
||||
self.add(value)
|
||||
|
||||
def __ior__(self, other):
|
||||
if not collections._set_binops_check_strict(self, other):
|
||||
return NotImplemented
|
||||
for value in other:
|
||||
self.add(value)
|
||||
return self
|
||||
|
||||
def _set(self):
|
||||
return set(iter(self))
|
||||
|
||||
def union(self, other):
|
||||
return set(self).union(other)
|
||||
|
||||
__or__ = union
|
||||
|
||||
def difference(self, other):
|
||||
return set(self).difference(other)
|
||||
|
||||
__sub__ = difference
|
||||
|
||||
def difference_update(self, other):
|
||||
for value in other:
|
||||
self.discard(value)
|
||||
|
||||
def __isub__(self, other):
|
||||
if not collections._set_binops_check_strict(self, other):
|
||||
return NotImplemented
|
||||
for value in other:
|
||||
self.discard(value)
|
||||
return self
|
||||
|
||||
def intersection(self, other):
|
||||
return set(self).intersection(other)
|
||||
|
||||
__and__ = intersection
|
||||
|
||||
def intersection_update(self, other):
|
||||
want, have = self.intersection(other), set(self)
|
||||
|
||||
remove, add = have - want, want - have
|
||||
|
||||
for value in remove:
|
||||
self.remove(value)
|
||||
for value in add:
|
||||
self.add(value)
|
||||
|
||||
def __iand__(self, other):
|
||||
if not collections._set_binops_check_strict(self, other):
|
||||
return NotImplemented
|
||||
want, have = self.intersection(other), set(self)
|
||||
|
||||
remove, add = have - want, want - have
|
||||
|
||||
for value in remove:
|
||||
self.remove(value)
|
||||
for value in add:
|
||||
self.add(value)
|
||||
return self
|
||||
|
||||
def symmetric_difference(self, other):
|
||||
return set(self).symmetric_difference(other)
|
||||
|
||||
__xor__ = symmetric_difference
|
||||
|
||||
def symmetric_difference_update(self, other):
|
||||
want, have = self.symmetric_difference(other), set(self)
|
||||
|
||||
remove, add = have - want, want - have
|
||||
|
||||
for value in remove:
|
||||
self.remove(value)
|
||||
for value in add:
|
||||
self.add(value)
|
||||
|
||||
def __ixor__(self, other):
|
||||
if not collections._set_binops_check_strict(self, other):
|
||||
return NotImplemented
|
||||
want, have = self.symmetric_difference(other), set(self)
|
||||
|
||||
remove, add = have - want, want - have
|
||||
|
||||
for value in remove:
|
||||
self.remove(value)
|
||||
for value in add:
|
||||
self.add(value)
|
||||
return self
|
||||
|
||||
def issubset(self, other):
|
||||
return set(self).issubset(other)
|
||||
|
||||
def issuperset(self, other):
|
||||
return set(self).issuperset(other)
|
||||
|
||||
def clear(self):
|
||||
self.col.clear()
|
||||
|
||||
def copy(self):
|
||||
return set(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
return set(self) == other
|
||||
|
||||
def __ne__(self, other):
|
||||
return set(self) != other
|
||||
|
||||
def __lt__(self, other):
|
||||
return set(self) < other
|
||||
|
||||
def __le__(self, other):
|
||||
return set(self) <= other
|
||||
|
||||
def __gt__(self, other):
|
||||
return set(self) > other
|
||||
|
||||
def __ge__(self, other):
|
||||
return set(self) >= other
|
||||
|
||||
def __repr__(self):
|
||||
return repr(set(self))
|
||||
|
||||
def __hash__(self):
|
||||
raise TypeError("%s objects are unhashable" % type(self).__name__)
|
||||
|
||||
for func_name, func in locals().items():
|
||||
if (util.callable(func) and func.func_name == func_name and
|
||||
not func.__doc__ and hasattr(set, func_name)):
|
||||
func.__doc__ = getattr(set, func_name).__doc__
|
||||
del func_name, func
|
194
sqlalchemy/ext/compiler.py
Normal file
194
sqlalchemy/ext/compiler.py
Normal file
@@ -0,0 +1,194 @@
|
||||
"""Provides an API for creation of custom ClauseElements and compilers.
|
||||
|
||||
Synopsis
|
||||
========
|
||||
|
||||
Usage involves the creation of one or more :class:`~sqlalchemy.sql.expression.ClauseElement`
|
||||
subclasses and one or more callables defining its compilation::
|
||||
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.sql.expression import ColumnClause
|
||||
|
||||
class MyColumn(ColumnClause):
|
||||
pass
|
||||
|
||||
@compiles(MyColumn)
|
||||
def compile_mycolumn(element, compiler, **kw):
|
||||
return "[%s]" % element.name
|
||||
|
||||
Above, ``MyColumn`` extends :class:`~sqlalchemy.sql.expression.ColumnClause`,
|
||||
the base expression element for named column objects. The ``compiles``
|
||||
decorator registers itself with the ``MyColumn`` class so that it is invoked
|
||||
when the object is compiled to a string::
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
s = select([MyColumn('x'), MyColumn('y')])
|
||||
print str(s)
|
||||
|
||||
Produces::
|
||||
|
||||
SELECT [x], [y]
|
||||
|
||||
Dialect-specific compilation rules
|
||||
==================================
|
||||
|
||||
Compilers can also be made dialect-specific. The appropriate compiler will be
|
||||
invoked for the dialect in use::
|
||||
|
||||
from sqlalchemy.schema import DDLElement
|
||||
|
||||
class AlterColumn(DDLElement):
|
||||
|
||||
def __init__(self, column, cmd):
|
||||
self.column = column
|
||||
self.cmd = cmd
|
||||
|
||||
@compiles(AlterColumn)
|
||||
def visit_alter_column(element, compiler, **kw):
|
||||
return "ALTER COLUMN %s ..." % element.column.name
|
||||
|
||||
@compiles(AlterColumn, 'postgresql')
|
||||
def visit_alter_column(element, compiler, **kw):
|
||||
return "ALTER TABLE %s ALTER COLUMN %s ..." % (element.table.name, element.column.name)
|
||||
|
||||
The second ``visit_alter_table`` will be invoked when any ``postgresql`` dialect is used.
|
||||
|
||||
Compiling sub-elements of a custom expression construct
|
||||
=======================================================
|
||||
|
||||
The ``compiler`` argument is the :class:`~sqlalchemy.engine.base.Compiled`
|
||||
object in use. This object can be inspected for any information about the
|
||||
in-progress compilation, including ``compiler.dialect``,
|
||||
``compiler.statement`` etc. The :class:`~sqlalchemy.sql.compiler.SQLCompiler`
|
||||
and :class:`~sqlalchemy.sql.compiler.DDLCompiler` both include a ``process()``
|
||||
method which can be used for compilation of embedded attributes::
|
||||
|
||||
from sqlalchemy.sql.expression import Executable, ClauseElement
|
||||
|
||||
class InsertFromSelect(Executable, ClauseElement):
|
||||
def __init__(self, table, select):
|
||||
self.table = table
|
||||
self.select = select
|
||||
|
||||
@compiles(InsertFromSelect)
|
||||
def visit_insert_from_select(element, compiler, **kw):
|
||||
return "INSERT INTO %s (%s)" % (
|
||||
compiler.process(element.table, asfrom=True),
|
||||
compiler.process(element.select)
|
||||
)
|
||||
|
||||
insert = InsertFromSelect(t1, select([t1]).where(t1.c.x>5))
|
||||
print insert
|
||||
|
||||
Produces::
|
||||
|
||||
"INSERT INTO mytable (SELECT mytable.x, mytable.y, mytable.z FROM mytable WHERE mytable.x > :x_1)"
|
||||
|
||||
Cross Compiling between SQL and DDL compilers
|
||||
---------------------------------------------
|
||||
|
||||
SQL and DDL constructs are each compiled using different base compilers - ``SQLCompiler``
|
||||
and ``DDLCompiler``. A common need is to access the compilation rules of SQL expressions
|
||||
from within a DDL expression. The ``DDLCompiler`` includes an accessor ``sql_compiler`` for this reason, such as below where we generate a CHECK
|
||||
constraint that embeds a SQL expression::
|
||||
|
||||
@compiles(MyConstraint)
|
||||
def compile_my_constraint(constraint, ddlcompiler, **kw):
|
||||
return "CONSTRAINT %s CHECK (%s)" % (
|
||||
constraint.name,
|
||||
ddlcompiler.sql_compiler.process(constraint.expression)
|
||||
)
|
||||
|
||||
Changing the default compilation of existing constructs
|
||||
=======================================================
|
||||
|
||||
The compiler extension applies just as well to the existing constructs. When overriding
|
||||
the compilation of a built in SQL construct, the @compiles decorator is invoked upon
|
||||
the appropriate class (be sure to use the class, i.e. ``Insert`` or ``Select``, instead of the creation function such as ``insert()`` or ``select()``).
|
||||
|
||||
Within the new compilation function, to get at the "original" compilation routine,
|
||||
use the appropriate visit_XXX method - this because compiler.process() will call upon the
|
||||
overriding routine and cause an endless loop. Such as, to add "prefix" to all insert statements::
|
||||
|
||||
from sqlalchemy.sql.expression import Insert
|
||||
|
||||
@compiles(Insert)
|
||||
def prefix_inserts(insert, compiler, **kw):
|
||||
return compiler.visit_insert(insert.prefix_with("some prefix"), **kw)
|
||||
|
||||
The above compiler will prefix all INSERT statements with "some prefix" when compiled.
|
||||
|
||||
Subclassing Guidelines
|
||||
======================
|
||||
|
||||
A big part of using the compiler extension is subclassing SQLAlchemy expression constructs. To make this easier, the expression and schema packages feature a set of "bases" intended for common tasks. A synopsis is as follows:
|
||||
|
||||
* :class:`~sqlalchemy.sql.expression.ClauseElement` - This is the root
|
||||
expression class. Any SQL expression can be derived from this base, and is
|
||||
probably the best choice for longer constructs such as specialized INSERT
|
||||
statements.
|
||||
|
||||
* :class:`~sqlalchemy.sql.expression.ColumnElement` - The root of all
|
||||
"column-like" elements. Anything that you'd place in the "columns" clause of
|
||||
a SELECT statement (as well as order by and group by) can derive from this -
|
||||
the object will automatically have Python "comparison" behavior.
|
||||
|
||||
:class:`~sqlalchemy.sql.expression.ColumnElement` classes want to have a
|
||||
``type`` member which is expression's return type. This can be established
|
||||
at the instance level in the constructor, or at the class level if its
|
||||
generally constant::
|
||||
|
||||
class timestamp(ColumnElement):
|
||||
type = TIMESTAMP()
|
||||
|
||||
* :class:`~sqlalchemy.sql.expression.FunctionElement` - This is a hybrid of a
|
||||
``ColumnElement`` and a "from clause" like object, and represents a SQL
|
||||
function or stored procedure type of call. Since most databases support
|
||||
statements along the line of "SELECT FROM <some function>"
|
||||
``FunctionElement`` adds in the ability to be used in the FROM clause of a
|
||||
``select()`` construct.
|
||||
|
||||
* :class:`~sqlalchemy.schema.DDLElement` - The root of all DDL expressions,
|
||||
like CREATE TABLE, ALTER TABLE, etc. Compilation of ``DDLElement``
|
||||
subclasses is issued by a ``DDLCompiler`` instead of a ``SQLCompiler``.
|
||||
``DDLElement`` also features ``Table`` and ``MetaData`` event hooks via the
|
||||
``execute_at()`` method, allowing the construct to be invoked during CREATE
|
||||
TABLE and DROP TABLE sequences.
|
||||
|
||||
* :class:`~sqlalchemy.sql.expression.Executable` - This is a mixin which should be
|
||||
used with any expression class that represents a "standalone" SQL statement that
|
||||
can be passed directly to an ``execute()`` method. It is already implicit
|
||||
within ``DDLElement`` and ``FunctionElement``.
|
||||
|
||||
"""
|
||||
|
||||
def compiles(class_, *specs):
|
||||
def decorate(fn):
|
||||
existing = getattr(class_, '_compiler_dispatcher', None)
|
||||
if not existing:
|
||||
existing = _dispatcher()
|
||||
|
||||
# TODO: why is the lambda needed ?
|
||||
setattr(class_, '_compiler_dispatch', lambda *arg, **kw: existing(*arg, **kw))
|
||||
setattr(class_, '_compiler_dispatcher', existing)
|
||||
|
||||
if specs:
|
||||
for s in specs:
|
||||
existing.specs[s] = fn
|
||||
else:
|
||||
existing.specs['default'] = fn
|
||||
return fn
|
||||
return decorate
|
||||
|
||||
class _dispatcher(object):
|
||||
def __init__(self):
|
||||
self.specs = {}
|
||||
|
||||
def __call__(self, element, compiler, **kw):
|
||||
# TODO: yes, this could also switch off of DBAPI in use.
|
||||
fn = self.specs.get(compiler.dialect.name, None)
|
||||
if not fn:
|
||||
fn = self.specs['default']
|
||||
return fn(element, compiler, **kw)
|
||||
|
940
sqlalchemy/ext/declarative.py
Normal file
940
sqlalchemy/ext/declarative.py
Normal file
@@ -0,0 +1,940 @@
|
||||
"""
|
||||
Synopsis
|
||||
========
|
||||
|
||||
SQLAlchemy object-relational configuration involves the use of
|
||||
:class:`~sqlalchemy.schema.Table`, :func:`~sqlalchemy.orm.mapper`, and
|
||||
class objects to define the three areas of configuration.
|
||||
:mod:`~sqlalchemy.ext.declarative` allows all three types of
|
||||
configuration to be expressed declaratively on an individual
|
||||
mapped class. Regular SQLAlchemy schema elements and ORM constructs
|
||||
are used in most cases.
|
||||
|
||||
As a simple example::
|
||||
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class SomeClass(Base):
|
||||
__tablename__ = 'some_table'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(50))
|
||||
|
||||
Above, the :func:`declarative_base` callable returns a new base class from which
|
||||
all mapped classes should inherit. When the class definition is completed, a
|
||||
new :class:`~sqlalchemy.schema.Table` and
|
||||
:class:`~sqlalchemy.orm.mapper` will have been generated, accessible
|
||||
via the ``__table__`` and ``__mapper__`` attributes on the ``SomeClass`` class.
|
||||
|
||||
Defining Attributes
|
||||
===================
|
||||
|
||||
In the above example, the :class:`~sqlalchemy.schema.Column` objects are
|
||||
automatically named with the name of the attribute to which they are
|
||||
assigned.
|
||||
|
||||
They can also be explicitly named, and that name does not have to be
|
||||
the same as name assigned on the class.
|
||||
The column will be assigned to the :class:`~sqlalchemy.schema.Table` using the
|
||||
given name, and mapped to the class using the attribute name::
|
||||
|
||||
class SomeClass(Base):
|
||||
__tablename__ = 'some_table'
|
||||
id = Column("some_table_id", Integer, primary_key=True)
|
||||
name = Column("name", String(50))
|
||||
|
||||
Attributes may be added to the class after its construction, and they will be
|
||||
added to the underlying :class:`~sqlalchemy.schema.Table` and
|
||||
:func:`~sqlalchemy.orm.mapper()` definitions as appropriate::
|
||||
|
||||
SomeClass.data = Column('data', Unicode)
|
||||
SomeClass.related = relationship(RelatedInfo)
|
||||
|
||||
Classes which are mapped explicitly using
|
||||
:func:`~sqlalchemy.orm.mapper()` can interact freely with declarative
|
||||
classes.
|
||||
|
||||
It is recommended, though not required, that all tables
|
||||
share the same underlying :class:`~sqlalchemy.schema.MetaData` object,
|
||||
so that string-configured :class:`~sqlalchemy.schema.ForeignKey`
|
||||
references can be resolved without issue.
|
||||
|
||||
Association of Metadata and Engine
|
||||
==================================
|
||||
|
||||
The :func:`declarative_base` base class contains a
|
||||
:class:`~sqlalchemy.schema.MetaData` object where newly
|
||||
defined :class:`~sqlalchemy.schema.Table` objects are collected. This
|
||||
is accessed via the :class:`~sqlalchemy.schema.MetaData` class level
|
||||
accessor, so to create tables we can say::
|
||||
|
||||
engine = create_engine('sqlite://')
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
The :class:`~sqlalchemy.engine.base.Engine` created above may also be
|
||||
directly associated with the declarative base class using the ``bind``
|
||||
keyword argument, where it will be associated with the underlying
|
||||
:class:`~sqlalchemy.schema.MetaData` object and allow SQL operations
|
||||
involving that metadata and its tables to make use of that engine
|
||||
automatically::
|
||||
|
||||
Base = declarative_base(bind=create_engine('sqlite://'))
|
||||
|
||||
Alternatively, by way of the normal
|
||||
:class:`~sqlalchemy.schema.MetaData` behaviour, the ``bind`` attribute
|
||||
of the class level accessor can be assigned at any time as follows::
|
||||
|
||||
Base.metadata.bind = create_engine('sqlite://')
|
||||
|
||||
The :func:`declarative_base` can also receive a pre-created
|
||||
:class:`~sqlalchemy.schema.MetaData` object, which allows a
|
||||
declarative setup to be associated with an already
|
||||
existing traditional collection of :class:`~sqlalchemy.schema.Table`
|
||||
objects::
|
||||
|
||||
mymetadata = MetaData()
|
||||
Base = declarative_base(metadata=mymetadata)
|
||||
|
||||
Configuring Relationships
|
||||
=========================
|
||||
|
||||
Relationships to other classes are done in the usual way, with the added
|
||||
feature that the class specified to :func:`~sqlalchemy.orm.relationship`
|
||||
may be a string name (note that :func:`~sqlalchemy.orm.relationship` is
|
||||
only available as of SQLAlchemy 0.6beta2, and in all prior versions is known
|
||||
as :func:`~sqlalchemy.orm.relation`,
|
||||
including 0.5 and 0.4). The "class registry" associated with ``Base``
|
||||
is used at mapper compilation time to resolve the name into the actual
|
||||
class object, which is expected to have been defined once the mapper
|
||||
configuration is used::
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'users'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(50))
|
||||
addresses = relationship("Address", backref="user")
|
||||
|
||||
class Address(Base):
|
||||
__tablename__ = 'addresses'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
email = Column(String(50))
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
|
||||
Column constructs, since they are just that, are immediately usable,
|
||||
as below where we define a primary join condition on the ``Address``
|
||||
class using them::
|
||||
|
||||
class Address(Base):
|
||||
__tablename__ = 'addresses'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
email = Column(String(50))
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
user = relationship(User, primaryjoin=user_id == User.id)
|
||||
|
||||
In addition to the main argument for :func:`~sqlalchemy.orm.relationship`,
|
||||
other arguments which depend upon the columns present on an as-yet
|
||||
undefined class may also be specified as strings. These strings are
|
||||
evaluated as Python expressions. The full namespace available within
|
||||
this evaluation includes all classes mapped for this declarative base,
|
||||
as well as the contents of the ``sqlalchemy`` package, including
|
||||
expression functions like :func:`~sqlalchemy.sql.expression.desc` and
|
||||
:attr:`~sqlalchemy.sql.expression.func`::
|
||||
|
||||
class User(Base):
|
||||
# ....
|
||||
addresses = relationship("Address",
|
||||
order_by="desc(Address.email)",
|
||||
primaryjoin="Address.user_id==User.id")
|
||||
|
||||
As an alternative to string-based attributes, attributes may also be
|
||||
defined after all classes have been created. Just add them to the target
|
||||
class after the fact::
|
||||
|
||||
User.addresses = relationship(Address,
|
||||
primaryjoin=Address.user_id==User.id)
|
||||
|
||||
Configuring Many-to-Many Relationships
|
||||
======================================
|
||||
|
||||
There's nothing special about many-to-many with declarative. The
|
||||
``secondary`` argument to :func:`~sqlalchemy.orm.relationship` still
|
||||
requires a :class:`~sqlalchemy.schema.Table` object, not a declarative
|
||||
class. The :class:`~sqlalchemy.schema.Table` should share the same
|
||||
:class:`~sqlalchemy.schema.MetaData` object used by the declarative
|
||||
base::
|
||||
|
||||
keywords = Table(
|
||||
'keywords', Base.metadata,
|
||||
Column('author_id', Integer, ForeignKey('authors.id')),
|
||||
Column('keyword_id', Integer, ForeignKey('keywords.id'))
|
||||
)
|
||||
|
||||
class Author(Base):
|
||||
__tablename__ = 'authors'
|
||||
id = Column(Integer, primary_key=True)
|
||||
keywords = relationship("Keyword", secondary=keywords)
|
||||
|
||||
You should generally **not** map a class and also specify its table in
|
||||
a many-to-many relationship, since the ORM may issue duplicate INSERT and
|
||||
DELETE statements.
|
||||
|
||||
|
||||
Defining Synonyms
|
||||
=================
|
||||
|
||||
Synonyms are introduced in :ref:`synonyms`. To define a getter/setter
|
||||
which proxies to an underlying attribute, use
|
||||
:func:`~sqlalchemy.orm.synonym` with the ``descriptor`` argument::
|
||||
|
||||
class MyClass(Base):
|
||||
__tablename__ = 'sometable'
|
||||
|
||||
_attr = Column('attr', String)
|
||||
|
||||
def _get_attr(self):
|
||||
return self._some_attr
|
||||
def _set_attr(self, attr):
|
||||
self._some_attr = attr
|
||||
attr = synonym('_attr', descriptor=property(_get_attr, _set_attr))
|
||||
|
||||
The above synonym is then usable as an instance attribute as well as a
|
||||
class-level expression construct::
|
||||
|
||||
x = MyClass()
|
||||
x.attr = "some value"
|
||||
session.query(MyClass).filter(MyClass.attr == 'some other value').all()
|
||||
|
||||
For simple getters, the :func:`synonym_for` decorator can be used in
|
||||
conjunction with ``@property``::
|
||||
|
||||
class MyClass(Base):
|
||||
__tablename__ = 'sometable'
|
||||
|
||||
_attr = Column('attr', String)
|
||||
|
||||
@synonym_for('_attr')
|
||||
@property
|
||||
def attr(self):
|
||||
return self._some_attr
|
||||
|
||||
Similarly, :func:`comparable_using` is a front end for the
|
||||
:func:`~sqlalchemy.orm.comparable_property` ORM function::
|
||||
|
||||
class MyClass(Base):
|
||||
__tablename__ = 'sometable'
|
||||
|
||||
name = Column('name', String)
|
||||
|
||||
@comparable_using(MyUpperCaseComparator)
|
||||
@property
|
||||
def uc_name(self):
|
||||
return self.name.upper()
|
||||
|
||||
Table Configuration
|
||||
===================
|
||||
|
||||
Table arguments other than the name, metadata, and mapped Column
|
||||
arguments are specified using the ``__table_args__`` class attribute.
|
||||
This attribute accommodates both positional as well as keyword
|
||||
arguments that are normally sent to the
|
||||
:class:`~sqlalchemy.schema.Table` constructor.
|
||||
The attribute can be specified in one of two forms. One is as a
|
||||
dictionary::
|
||||
|
||||
class MyClass(Base):
|
||||
__tablename__ = 'sometable'
|
||||
__table_args__ = {'mysql_engine':'InnoDB'}
|
||||
|
||||
The other, a tuple of the form
|
||||
``(arg1, arg2, ..., {kwarg1:value, ...})``, which allows positional
|
||||
arguments to be specified as well (usually constraints)::
|
||||
|
||||
class MyClass(Base):
|
||||
__tablename__ = 'sometable'
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(['id'], ['remote_table.id']),
|
||||
UniqueConstraint('foo'),
|
||||
{'autoload':True}
|
||||
)
|
||||
|
||||
Note that the keyword parameters dictionary is required in the tuple
|
||||
form even if empty.
|
||||
|
||||
As an alternative to ``__tablename__``, a direct
|
||||
:class:`~sqlalchemy.schema.Table` construct may be used. The
|
||||
:class:`~sqlalchemy.schema.Column` objects, which in this case require
|
||||
their names, will be added to the mapping just like a regular mapping
|
||||
to a table::
|
||||
|
||||
class MyClass(Base):
|
||||
__table__ = Table('my_table', Base.metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('name', String(50))
|
||||
)
|
||||
|
||||
Mapper Configuration
|
||||
====================
|
||||
|
||||
Configuration of mappers is done with the
|
||||
:func:`~sqlalchemy.orm.mapper` function and all the possible mapper
|
||||
configuration parameters can be found in the documentation for that
|
||||
function.
|
||||
|
||||
:func:`~sqlalchemy.orm.mapper` is still used by declaratively mapped
|
||||
classes and keyword parameters to the function can be passed by
|
||||
placing them in the ``__mapper_args__`` class variable::
|
||||
|
||||
class Widget(Base):
|
||||
__tablename__ = 'widgets'
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
__mapper_args__ = {'extension': MyWidgetExtension()}
|
||||
|
||||
Inheritance Configuration
|
||||
=========================
|
||||
|
||||
Declarative supports all three forms of inheritance as intuitively
|
||||
as possible. The ``inherits`` mapper keyword argument is not needed
|
||||
as declarative will determine this from the class itself. The various
|
||||
"polymorphic" keyword arguments are specified using ``__mapper_args__``.
|
||||
|
||||
Joined Table Inheritance
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Joined table inheritance is defined as a subclass that defines its own
|
||||
table::
|
||||
|
||||
class Person(Base):
|
||||
__tablename__ = 'people'
|
||||
id = Column(Integer, primary_key=True)
|
||||
discriminator = Column('type', String(50))
|
||||
__mapper_args__ = {'polymorphic_on': discriminator}
|
||||
|
||||
class Engineer(Person):
|
||||
__tablename__ = 'engineers'
|
||||
__mapper_args__ = {'polymorphic_identity': 'engineer'}
|
||||
id = Column(Integer, ForeignKey('people.id'), primary_key=True)
|
||||
primary_language = Column(String(50))
|
||||
|
||||
Note that above, the ``Engineer.id`` attribute, since it shares the
|
||||
same attribute name as the ``Person.id`` attribute, will in fact
|
||||
represent the ``people.id`` and ``engineers.id`` columns together, and
|
||||
will render inside a query as ``"people.id"``.
|
||||
To provide the ``Engineer`` class with an attribute that represents
|
||||
only the ``engineers.id`` column, give it a different attribute name::
|
||||
|
||||
class Engineer(Person):
|
||||
__tablename__ = 'engineers'
|
||||
__mapper_args__ = {'polymorphic_identity': 'engineer'}
|
||||
engineer_id = Column('id', Integer, ForeignKey('people.id'), primary_key=True)
|
||||
primary_language = Column(String(50))
|
||||
|
||||
Single Table Inheritance
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Single table inheritance is defined as a subclass that does not have
|
||||
its own table; you just leave out the ``__table__`` and ``__tablename__``
|
||||
attributes::
|
||||
|
||||
class Person(Base):
|
||||
__tablename__ = 'people'
|
||||
id = Column(Integer, primary_key=True)
|
||||
discriminator = Column('type', String(50))
|
||||
__mapper_args__ = {'polymorphic_on': discriminator}
|
||||
|
||||
class Engineer(Person):
|
||||
__mapper_args__ = {'polymorphic_identity': 'engineer'}
|
||||
primary_language = Column(String(50))
|
||||
|
||||
When the above mappers are configured, the ``Person`` class is mapped
|
||||
to the ``people`` table *before* the ``primary_language`` column is
|
||||
defined, and this column will not be included in its own mapping.
|
||||
When ``Engineer`` then defines the ``primary_language`` column, the
|
||||
column is added to the ``people`` table so that it is included in the
|
||||
mapping for ``Engineer`` and is also part of the table's full set of
|
||||
columns. Columns which are not mapped to ``Person`` are also excluded
|
||||
from any other single or joined inheriting classes using the
|
||||
``exclude_properties`` mapper argument. Below, ``Manager`` will have
|
||||
all the attributes of ``Person`` and ``Manager`` but *not* the
|
||||
``primary_language`` attribute of ``Engineer``::
|
||||
|
||||
class Manager(Person):
|
||||
__mapper_args__ = {'polymorphic_identity': 'manager'}
|
||||
golf_swing = Column(String(50))
|
||||
|
||||
The attribute exclusion logic is provided by the
|
||||
``exclude_properties`` mapper argument, and declarative's default
|
||||
behavior can be disabled by passing an explicit ``exclude_properties``
|
||||
collection (empty or otherwise) to the ``__mapper_args__``.
|
||||
|
||||
Concrete Table Inheritance
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Concrete is defined as a subclass which has its own table and sets the
|
||||
``concrete`` keyword argument to ``True``::
|
||||
|
||||
class Person(Base):
|
||||
__tablename__ = 'people'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(50))
|
||||
|
||||
class Engineer(Person):
|
||||
__tablename__ = 'engineers'
|
||||
__mapper_args__ = {'concrete':True}
|
||||
id = Column(Integer, primary_key=True)
|
||||
primary_language = Column(String(50))
|
||||
name = Column(String(50))
|
||||
|
||||
Usage of an abstract base class is a little less straightforward as it
|
||||
requires usage of :func:`~sqlalchemy.orm.util.polymorphic_union`::
|
||||
|
||||
engineers = Table('engineers', Base.metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('name', String(50)),
|
||||
Column('primary_language', String(50))
|
||||
)
|
||||
managers = Table('managers', Base.metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('name', String(50)),
|
||||
Column('golf_swing', String(50))
|
||||
)
|
||||
|
||||
punion = polymorphic_union({
|
||||
'engineer':engineers,
|
||||
'manager':managers
|
||||
}, 'type', 'punion')
|
||||
|
||||
class Person(Base):
|
||||
__table__ = punion
|
||||
__mapper_args__ = {'polymorphic_on':punion.c.type}
|
||||
|
||||
class Engineer(Person):
|
||||
__table__ = engineers
|
||||
__mapper_args__ = {'polymorphic_identity':'engineer', 'concrete':True}
|
||||
|
||||
class Manager(Person):
|
||||
__table__ = managers
|
||||
__mapper_args__ = {'polymorphic_identity':'manager', 'concrete':True}
|
||||
|
||||
|
||||
Mix-in Classes
|
||||
==============
|
||||
|
||||
A common need when using :mod:`~sqlalchemy.ext.declarative` is to
|
||||
share some functionality, often a set of columns, across many
|
||||
classes. The normal python idiom would be to put this common code into
|
||||
a base class and have all the other classes subclass this class.
|
||||
|
||||
When using :mod:`~sqlalchemy.ext.declarative`, this need is met by
|
||||
using a "mix-in class". A mix-in class is one that isn't mapped to a
|
||||
table and doesn't subclass the declarative :class:`Base`. For example::
|
||||
|
||||
class MyMixin(object):
|
||||
|
||||
__table_args__ = {'mysql_engine':'InnoDB'}
|
||||
__mapper_args__=dict(always_refresh=True)
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
def foo(self):
|
||||
return 'bar'+str(self.id)
|
||||
|
||||
class MyModel(Base,MyMixin):
|
||||
__tablename__='test'
|
||||
name = Column(String(1000), nullable=False, index=True)
|
||||
|
||||
As the above example shows, ``__table_args__`` and ``__mapper_args__``
|
||||
can both be abstracted out into a mix-in if you use common values for
|
||||
these across many classes.
|
||||
|
||||
However, particularly in the case of ``__table_args__``, you may want
|
||||
to combine some parameters from several mix-ins with those you wish to
|
||||
define on the class iteself. To help with this, a
|
||||
:func:`~sqlalchemy.util.classproperty` decorator is provided that lets
|
||||
you implement a class property with a function. For example::
|
||||
|
||||
from sqlalchemy.util import classproperty
|
||||
|
||||
class MySQLSettings:
|
||||
__table_args__ = {'mysql_engine':'InnoDB'}
|
||||
|
||||
class MyOtherMixin:
|
||||
__table_args__ = {'info':'foo'}
|
||||
|
||||
class MyModel(Base,MySQLSettings,MyOtherMixin):
|
||||
__tablename__='my_model'
|
||||
|
||||
@classproperty
|
||||
def __table_args__(self):
|
||||
args = dict()
|
||||
args.update(MySQLSettings.__table_args__)
|
||||
args.update(MyOtherMixin.__table_args__)
|
||||
return args
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
|
||||
Class Constructor
|
||||
=================
|
||||
|
||||
As a convenience feature, the :func:`declarative_base` sets a default
|
||||
constructor on classes which takes keyword arguments, and assigns them
|
||||
to the named attributes::
|
||||
|
||||
e = Engineer(primary_language='python')
|
||||
|
||||
Sessions
|
||||
========
|
||||
|
||||
Note that ``declarative`` does nothing special with sessions, and is
|
||||
only intended as an easier way to configure mappers and
|
||||
:class:`~sqlalchemy.schema.Table` objects. A typical application
|
||||
setup using :func:`~sqlalchemy.orm.scoped_session` might look like::
|
||||
|
||||
engine = create_engine('postgresql://scott:tiger@localhost/test')
|
||||
Session = scoped_session(sessionmaker(autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine))
|
||||
Base = declarative_base()
|
||||
|
||||
Mapped instances then make usage of
|
||||
:class:`~sqlalchemy.orm.session.Session` in the usual way.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.schema import Table, Column, MetaData
|
||||
from sqlalchemy.orm import synonym as _orm_synonym, mapper, comparable_property, class_mapper
|
||||
from sqlalchemy.orm.interfaces import MapperProperty
|
||||
from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty
|
||||
from sqlalchemy.orm.util import _is_mapped_class
|
||||
from sqlalchemy import util, exceptions
|
||||
from sqlalchemy.sql import util as sql_util
|
||||
|
||||
|
||||
__all__ = 'declarative_base', 'synonym_for', 'comparable_using', 'instrument_declarative'
|
||||
|
||||
def instrument_declarative(cls, registry, metadata):
|
||||
"""Given a class, configure the class declaratively,
|
||||
using the given registry, which can be any dictionary, and
|
||||
MetaData object.
|
||||
|
||||
"""
|
||||
if '_decl_class_registry' in cls.__dict__:
|
||||
raise exceptions.InvalidRequestError(
|
||||
"Class %r already has been "
|
||||
"instrumented declaratively" % cls)
|
||||
cls._decl_class_registry = registry
|
||||
cls.metadata = metadata
|
||||
_as_declarative(cls, cls.__name__, cls.__dict__)
|
||||
|
||||
def _as_declarative(cls, classname, dict_):
|
||||
|
||||
# dict_ will be a dictproxy, which we can't write to, and we need to!
|
||||
dict_ = dict(dict_)
|
||||
|
||||
column_copies = dict()
|
||||
unmapped_mixins = False
|
||||
for base in cls.__bases__:
|
||||
names = dir(base)
|
||||
if not _is_mapped_class(base):
|
||||
unmapped_mixins = True
|
||||
for name in names:
|
||||
obj = getattr(base,name, None)
|
||||
if isinstance(obj, Column):
|
||||
if obj.foreign_keys:
|
||||
raise exceptions.InvalidRequestError(
|
||||
"Columns with foreign keys to other columns "
|
||||
"are not allowed on declarative mixins at this time."
|
||||
)
|
||||
dict_[name]=column_copies[obj]=obj.copy()
|
||||
elif isinstance(obj, RelationshipProperty):
|
||||
raise exceptions.InvalidRequestError(
|
||||
"relationships are not allowed on "
|
||||
"declarative mixins at this time.")
|
||||
|
||||
# doing it this way enables these attributes to be descriptors
|
||||
get_mapper_args = '__mapper_args__' in dict_
|
||||
get_table_args = '__table_args__' in dict_
|
||||
if unmapped_mixins:
|
||||
get_mapper_args = get_mapper_args or getattr(cls,'__mapper_args__',None)
|
||||
get_table_args = get_table_args or getattr(cls,'__table_args__',None)
|
||||
tablename = getattr(cls,'__tablename__',None)
|
||||
if tablename:
|
||||
# subtle: if tablename is a descriptor here, we actually
|
||||
# put the wrong value in, but it serves as a marker to get
|
||||
# the right value value...
|
||||
dict_['__tablename__']=tablename
|
||||
|
||||
# now that we know whether or not to get these, get them from the class
|
||||
# if we should, enabling them to be decorators
|
||||
mapper_args = get_mapper_args and cls.__mapper_args__ or {}
|
||||
table_args = get_table_args and cls.__table_args__ or None
|
||||
|
||||
# make sure that column copies are used rather than the original columns
|
||||
# from any mixins
|
||||
for k, v in mapper_args.iteritems():
|
||||
mapper_args[k] = column_copies.get(v,v)
|
||||
|
||||
cls._decl_class_registry[classname] = cls
|
||||
our_stuff = util.OrderedDict()
|
||||
for k in dict_:
|
||||
value = dict_[k]
|
||||
if (isinstance(value, tuple) and len(value) == 1 and
|
||||
isinstance(value[0], (Column, MapperProperty))):
|
||||
util.warn("Ignoring declarative-like tuple value of attribute "
|
||||
"%s: possibly a copy-and-paste error with a comma "
|
||||
"left at the end of the line?" % k)
|
||||
continue
|
||||
if not isinstance(value, (Column, MapperProperty)):
|
||||
continue
|
||||
prop = _deferred_relationship(cls, value)
|
||||
our_stuff[k] = prop
|
||||
|
||||
# set up attributes in the order they were created
|
||||
our_stuff.sort(key=lambda key: our_stuff[key]._creation_order)
|
||||
|
||||
# extract columns from the class dict
|
||||
cols = []
|
||||
for key, c in our_stuff.iteritems():
|
||||
if isinstance(c, ColumnProperty):
|
||||
for col in c.columns:
|
||||
if isinstance(col, Column) and col.table is None:
|
||||
_undefer_column_name(key, col)
|
||||
cols.append(col)
|
||||
elif isinstance(c, Column):
|
||||
_undefer_column_name(key, c)
|
||||
cols.append(c)
|
||||
# if the column is the same name as the key,
|
||||
# remove it from the explicit properties dict.
|
||||
# the normal rules for assigning column-based properties
|
||||
# will take over, including precedence of columns
|
||||
# in multi-column ColumnProperties.
|
||||
if key == c.key:
|
||||
del our_stuff[key]
|
||||
|
||||
table = None
|
||||
if '__table__' not in dict_:
|
||||
if '__tablename__' in dict_:
|
||||
# see above: if __tablename__ is a descriptor, this
|
||||
# means we get the right value used!
|
||||
tablename = cls.__tablename__
|
||||
|
||||
if isinstance(table_args, dict):
|
||||
args, table_kw = (), table_args
|
||||
elif isinstance(table_args, tuple):
|
||||
args = table_args[0:-1]
|
||||
table_kw = table_args[-1]
|
||||
if len(table_args) < 2 or not isinstance(table_kw, dict):
|
||||
raise exceptions.ArgumentError(
|
||||
"Tuple form of __table_args__ is "
|
||||
"(arg1, arg2, arg3, ..., {'kw1':val1, 'kw2':val2, ...})"
|
||||
)
|
||||
else:
|
||||
args, table_kw = (), {}
|
||||
|
||||
autoload = dict_.get('__autoload__')
|
||||
if autoload:
|
||||
table_kw['autoload'] = True
|
||||
|
||||
cls.__table__ = table = Table(tablename, cls.metadata,
|
||||
*(tuple(cols) + tuple(args)), **table_kw)
|
||||
else:
|
||||
table = cls.__table__
|
||||
if cols:
|
||||
for c in cols:
|
||||
if not table.c.contains_column(c):
|
||||
raise exceptions.ArgumentError(
|
||||
"Can't add additional column %r when specifying __table__" % key
|
||||
)
|
||||
|
||||
if 'inherits' not in mapper_args:
|
||||
for c in cls.__bases__:
|
||||
if _is_mapped_class(c):
|
||||
mapper_args['inherits'] = cls._decl_class_registry.get(c.__name__, None)
|
||||
break
|
||||
|
||||
if hasattr(cls, '__mapper_cls__'):
|
||||
mapper_cls = util.unbound_method_to_callable(cls.__mapper_cls__)
|
||||
else:
|
||||
mapper_cls = mapper
|
||||
|
||||
if table is None and 'inherits' not in mapper_args:
|
||||
raise exceptions.InvalidRequestError(
|
||||
"Class %r does not have a __table__ or __tablename__ "
|
||||
"specified and does not inherit from an existing table-mapped class." % cls
|
||||
)
|
||||
|
||||
elif 'inherits' in mapper_args and not mapper_args.get('concrete', False):
|
||||
inherited_mapper = class_mapper(mapper_args['inherits'], compile=False)
|
||||
inherited_table = inherited_mapper.local_table
|
||||
if 'inherit_condition' not in mapper_args and table is not None:
|
||||
# figure out the inherit condition with relaxed rules
|
||||
# about nonexistent tables, to allow for ForeignKeys to
|
||||
# not-yet-defined tables (since we know for sure that our
|
||||
# parent table is defined within the same MetaData)
|
||||
mapper_args['inherit_condition'] = sql_util.join_condition(
|
||||
mapper_args['inherits'].__table__, table,
|
||||
ignore_nonexistent_tables=True)
|
||||
|
||||
if table is None:
|
||||
# single table inheritance.
|
||||
# ensure no table args
|
||||
if table_args is not None:
|
||||
raise exceptions.ArgumentError(
|
||||
"Can't place __table_args__ on an inherited class with no table."
|
||||
)
|
||||
|
||||
# add any columns declared here to the inherited table.
|
||||
for c in cols:
|
||||
if c.primary_key:
|
||||
raise exceptions.ArgumentError(
|
||||
"Can't place primary key columns on an inherited class with no table."
|
||||
)
|
||||
if c.name in inherited_table.c:
|
||||
raise exceptions.ArgumentError(
|
||||
"Column '%s' on class %s conflicts with existing column '%s'" %
|
||||
(c, cls, inherited_table.c[c.name])
|
||||
)
|
||||
inherited_table.append_column(c)
|
||||
|
||||
# single or joined inheritance
|
||||
# exclude any cols on the inherited table which are not mapped on the
|
||||
# parent class, to avoid
|
||||
# mapping columns specific to sibling/nephew classes
|
||||
inherited_mapper = class_mapper(mapper_args['inherits'], compile=False)
|
||||
inherited_table = inherited_mapper.local_table
|
||||
|
||||
if 'exclude_properties' not in mapper_args:
|
||||
mapper_args['exclude_properties'] = exclude_properties = \
|
||||
set([c.key for c in inherited_table.c
|
||||
if c not in inherited_mapper._columntoproperty])
|
||||
exclude_properties.difference_update([c.key for c in cols])
|
||||
|
||||
cls.__mapper__ = mapper_cls(cls, table, properties=our_stuff, **mapper_args)
|
||||
|
||||
class DeclarativeMeta(type):
|
||||
def __init__(cls, classname, bases, dict_):
|
||||
if '_decl_class_registry' in cls.__dict__:
|
||||
return type.__init__(cls, classname, bases, dict_)
|
||||
|
||||
_as_declarative(cls, classname, cls.__dict__)
|
||||
return type.__init__(cls, classname, bases, dict_)
|
||||
|
||||
def __setattr__(cls, key, value):
|
||||
if '__mapper__' in cls.__dict__:
|
||||
if isinstance(value, Column):
|
||||
_undefer_column_name(key, value)
|
||||
cls.__table__.append_column(value)
|
||||
cls.__mapper__.add_property(key, value)
|
||||
elif isinstance(value, ColumnProperty):
|
||||
for col in value.columns:
|
||||
if isinstance(col, Column) and col.table is None:
|
||||
_undefer_column_name(key, col)
|
||||
cls.__table__.append_column(col)
|
||||
cls.__mapper__.add_property(key, value)
|
||||
elif isinstance(value, MapperProperty):
|
||||
cls.__mapper__.add_property(key, _deferred_relationship(cls, value))
|
||||
else:
|
||||
type.__setattr__(cls, key, value)
|
||||
else:
|
||||
type.__setattr__(cls, key, value)
|
||||
|
||||
|
||||
class _GetColumns(object):
|
||||
def __init__(self, cls):
|
||||
self.cls = cls
|
||||
def __getattr__(self, key):
|
||||
|
||||
mapper = class_mapper(self.cls, compile=False)
|
||||
if mapper:
|
||||
prop = mapper.get_property(key)
|
||||
if not isinstance(prop, ColumnProperty):
|
||||
raise exceptions.InvalidRequestError(
|
||||
"Property %r is not an instance of"
|
||||
" ColumnProperty (i.e. does not correspond"
|
||||
" directly to a Column)." % key)
|
||||
return getattr(self.cls, key)
|
||||
|
||||
|
||||
def _deferred_relationship(cls, prop):
|
||||
def resolve_arg(arg):
|
||||
import sqlalchemy
|
||||
|
||||
def access_cls(key):
|
||||
if key in cls._decl_class_registry:
|
||||
return _GetColumns(cls._decl_class_registry[key])
|
||||
elif key in cls.metadata.tables:
|
||||
return cls.metadata.tables[key]
|
||||
else:
|
||||
return sqlalchemy.__dict__[key]
|
||||
|
||||
d = util.PopulateDict(access_cls)
|
||||
def return_cls():
|
||||
try:
|
||||
x = eval(arg, globals(), d)
|
||||
|
||||
if isinstance(x, _GetColumns):
|
||||
return x.cls
|
||||
else:
|
||||
return x
|
||||
except NameError, n:
|
||||
raise exceptions.InvalidRequestError(
|
||||
"When compiling mapper %s, expression %r failed to locate a name (%r). "
|
||||
"If this is a class name, consider adding this relationship() to the %r "
|
||||
"class after both dependent classes have been defined." % (
|
||||
prop.parent, arg, n.args[0], cls))
|
||||
return return_cls
|
||||
|
||||
if isinstance(prop, RelationshipProperty):
|
||||
for attr in ('argument', 'order_by', 'primaryjoin', 'secondaryjoin',
|
||||
'secondary', '_foreign_keys', 'remote_side'):
|
||||
v = getattr(prop, attr)
|
||||
if isinstance(v, basestring):
|
||||
setattr(prop, attr, resolve_arg(v))
|
||||
|
||||
if prop.backref and isinstance(prop.backref, tuple):
|
||||
key, kwargs = prop.backref
|
||||
for attr in ('primaryjoin', 'secondaryjoin', 'secondary',
|
||||
'foreign_keys', 'remote_side', 'order_by'):
|
||||
if attr in kwargs and isinstance(kwargs[attr], basestring):
|
||||
kwargs[attr] = resolve_arg(kwargs[attr])
|
||||
|
||||
|
||||
return prop
|
||||
|
||||
def synonym_for(name, map_column=False):
|
||||
"""Decorator, make a Python @property a query synonym for a column.
|
||||
|
||||
A decorator version of :func:`~sqlalchemy.orm.synonym`. The function being
|
||||
decorated is the 'descriptor', otherwise passes its arguments through
|
||||
to synonym()::
|
||||
|
||||
@synonym_for('col')
|
||||
@property
|
||||
def prop(self):
|
||||
return 'special sauce'
|
||||
|
||||
The regular ``synonym()`` is also usable directly in a declarative setting
|
||||
and may be convenient for read/write properties::
|
||||
|
||||
prop = synonym('col', descriptor=property(_read_prop, _write_prop))
|
||||
|
||||
"""
|
||||
def decorate(fn):
|
||||
return _orm_synonym(name, map_column=map_column, descriptor=fn)
|
||||
return decorate
|
||||
|
||||
def comparable_using(comparator_factory):
|
||||
"""Decorator, allow a Python @property to be used in query criteria.
|
||||
|
||||
This is a decorator front end to
|
||||
:func:`~sqlalchemy.orm.comparable_property` that passes
|
||||
through the comparator_factory and the function being decorated::
|
||||
|
||||
@comparable_using(MyComparatorType)
|
||||
@property
|
||||
def prop(self):
|
||||
return 'special sauce'
|
||||
|
||||
The regular ``comparable_property()`` is also usable directly in a
|
||||
declarative setting and may be convenient for read/write properties::
|
||||
|
||||
prop = comparable_property(MyComparatorType)
|
||||
|
||||
"""
|
||||
def decorate(fn):
|
||||
return comparable_property(comparator_factory, fn)
|
||||
return decorate
|
||||
|
||||
def _declarative_constructor(self, **kwargs):
|
||||
"""A simple constructor that allows initialization from kwargs.
|
||||
|
||||
Sets attributes on the constructed instance using the names and
|
||||
values in ``kwargs``.
|
||||
|
||||
Only keys that are present as
|
||||
attributes of the instance's class are allowed. These could be,
|
||||
for example, any mapped columns or relationships.
|
||||
"""
|
||||
for k in kwargs:
|
||||
if not hasattr(type(self), k):
|
||||
raise TypeError(
|
||||
"%r is an invalid keyword argument for %s" %
|
||||
(k, type(self).__name__))
|
||||
setattr(self, k, kwargs[k])
|
||||
_declarative_constructor.__name__ = '__init__'
|
||||
|
||||
def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
|
||||
name='Base', constructor=_declarative_constructor,
|
||||
metaclass=DeclarativeMeta):
|
||||
"""Construct a base class for declarative class definitions.
|
||||
|
||||
The new base class will be given a metaclass that produces
|
||||
appropriate :class:`~sqlalchemy.schema.Table` objects and makes
|
||||
the appropriate :func:`~sqlalchemy.orm.mapper` calls based on the
|
||||
information provided declaratively in the class and any subclasses
|
||||
of the class.
|
||||
|
||||
:param bind: An optional
|
||||
:class:`~sqlalchemy.engine.base.Connectable`, will be assigned
|
||||
the ``bind`` attribute on the :class:`~sqlalchemy.MetaData`
|
||||
instance.
|
||||
|
||||
|
||||
:param metadata:
|
||||
An optional :class:`~sqlalchemy.MetaData` instance. All
|
||||
:class:`~sqlalchemy.schema.Table` objects implicitly declared by
|
||||
subclasses of the base will share this MetaData. A MetaData instance
|
||||
will be created if none is provided. The
|
||||
:class:`~sqlalchemy.MetaData` instance will be available via the
|
||||
`metadata` attribute of the generated declarative base class.
|
||||
|
||||
:param mapper:
|
||||
An optional callable, defaults to :func:`~sqlalchemy.orm.mapper`. Will be
|
||||
used to map subclasses to their Tables.
|
||||
|
||||
:param cls:
|
||||
Defaults to :class:`object`. A type to use as the base for the generated
|
||||
declarative base class. May be a class or tuple of classes.
|
||||
|
||||
:param name:
|
||||
Defaults to ``Base``. The display name for the generated
|
||||
class. Customizing this is not required, but can improve clarity in
|
||||
tracebacks and debugging.
|
||||
|
||||
:param constructor:
|
||||
Defaults to
|
||||
:func:`~sqlalchemy.ext.declarative._declarative_constructor`, an
|
||||
__init__ implementation that assigns \**kwargs for declared
|
||||
fields and relationships to an instance. If ``None`` is supplied,
|
||||
no __init__ will be provided and construction will fall back to
|
||||
cls.__init__ by way of the normal Python semantics.
|
||||
|
||||
:param metaclass:
|
||||
Defaults to :class:`DeclarativeMeta`. A metaclass or __metaclass__
|
||||
compatible callable to use as the meta type of the generated
|
||||
declarative base class.
|
||||
|
||||
"""
|
||||
lcl_metadata = metadata or MetaData()
|
||||
if bind:
|
||||
lcl_metadata.bind = bind
|
||||
|
||||
bases = not isinstance(cls, tuple) and (cls,) or cls
|
||||
class_dict = dict(_decl_class_registry=dict(),
|
||||
metadata=lcl_metadata)
|
||||
|
||||
if constructor:
|
||||
class_dict['__init__'] = constructor
|
||||
if mapper:
|
||||
class_dict['__mapper_cls__'] = mapper
|
||||
|
||||
return metaclass(name, bases, class_dict)
|
||||
|
||||
def _undefer_column_name(key, column):
|
||||
if column.key is None:
|
||||
column.key = key
|
||||
if column.name is None:
|
||||
column.name = key
|
125
sqlalchemy/ext/horizontal_shard.py
Normal file
125
sqlalchemy/ext/horizontal_shard.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# horizontal_shard.py
|
||||
# Copyright (C) the SQLAlchemy authors and contributors
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Horizontal sharding support.
|
||||
|
||||
Defines a rudimental 'horizontal sharding' system which allows a Session to
|
||||
distribute queries and persistence operations across multiple databases.
|
||||
|
||||
For a usage example, see the :ref:`examples_sharding` example included in
|
||||
the source distrbution.
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.orm.query import Query
|
||||
|
||||
__all__ = ['ShardedSession', 'ShardedQuery']
|
||||
|
||||
|
||||
class ShardedSession(Session):
|
||||
def __init__(self, shard_chooser, id_chooser, query_chooser, shards=None, **kwargs):
|
||||
"""Construct a ShardedSession.
|
||||
|
||||
:param shard_chooser: A callable which, passed a Mapper, a mapped instance, and possibly a
|
||||
SQL clause, returns a shard ID. This id may be based off of the
|
||||
attributes present within the object, or on some round-robin
|
||||
scheme. If the scheme is based on a selection, it should set
|
||||
whatever state on the instance to mark it in the future as
|
||||
participating in that shard.
|
||||
|
||||
:param id_chooser: A callable, passed a query and a tuple of identity values, which
|
||||
should return a list of shard ids where the ID might reside. The
|
||||
databases will be queried in the order of this listing.
|
||||
|
||||
:param query_chooser: For a given Query, returns the list of shard_ids where the query
|
||||
should be issued. Results from all shards returned will be combined
|
||||
together into a single listing.
|
||||
|
||||
:param shards: A dictionary of string shard names to :class:`~sqlalchemy.engine.base.Engine`
|
||||
objects.
|
||||
|
||||
"""
|
||||
super(ShardedSession, self).__init__(**kwargs)
|
||||
self.shard_chooser = shard_chooser
|
||||
self.id_chooser = id_chooser
|
||||
self.query_chooser = query_chooser
|
||||
self.__binds = {}
|
||||
self._mapper_flush_opts = {'connection_callable':self.connection}
|
||||
self._query_cls = ShardedQuery
|
||||
if shards is not None:
|
||||
for k in shards:
|
||||
self.bind_shard(k, shards[k])
|
||||
|
||||
def connection(self, mapper=None, instance=None, shard_id=None, **kwargs):
|
||||
if shard_id is None:
|
||||
shard_id = self.shard_chooser(mapper, instance)
|
||||
|
||||
if self.transaction is not None:
|
||||
return self.transaction.connection(mapper, shard_id=shard_id)
|
||||
else:
|
||||
return self.get_bind(mapper,
|
||||
shard_id=shard_id,
|
||||
instance=instance).contextual_connect(**kwargs)
|
||||
|
||||
def get_bind(self, mapper, shard_id=None, instance=None, clause=None, **kw):
|
||||
if shard_id is None:
|
||||
shard_id = self.shard_chooser(mapper, instance, clause=clause)
|
||||
return self.__binds[shard_id]
|
||||
|
||||
def bind_shard(self, shard_id, bind):
|
||||
self.__binds[shard_id] = bind
|
||||
|
||||
class ShardedQuery(Query):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ShardedQuery, self).__init__(*args, **kwargs)
|
||||
self.id_chooser = self.session.id_chooser
|
||||
self.query_chooser = self.session.query_chooser
|
||||
self._shard_id = None
|
||||
|
||||
def set_shard(self, shard_id):
|
||||
"""return a new query, limited to a single shard ID.
|
||||
|
||||
all subsequent operations with the returned query will
|
||||
be against the single shard regardless of other state.
|
||||
"""
|
||||
|
||||
q = self._clone()
|
||||
q._shard_id = shard_id
|
||||
return q
|
||||
|
||||
def _execute_and_instances(self, context):
|
||||
if self._shard_id is not None:
|
||||
result = self.session.connection(
|
||||
mapper=self._mapper_zero(),
|
||||
shard_id=self._shard_id).execute(context.statement, self._params)
|
||||
return self.instances(result, context)
|
||||
else:
|
||||
partial = []
|
||||
for shard_id in self.query_chooser(self):
|
||||
result = self.session.connection(
|
||||
mapper=self._mapper_zero(),
|
||||
shard_id=shard_id).execute(context.statement, self._params)
|
||||
partial = partial + list(self.instances(result, context))
|
||||
|
||||
# if some kind of in memory 'sorting'
|
||||
# were done, this is where it would happen
|
||||
return iter(partial)
|
||||
|
||||
def get(self, ident, **kwargs):
|
||||
if self._shard_id is not None:
|
||||
return super(ShardedQuery, self).get(ident)
|
||||
else:
|
||||
ident = util.to_list(ident)
|
||||
for shard_id in self.id_chooser(self, ident):
|
||||
o = self.set_shard(shard_id).get(ident, **kwargs)
|
||||
if o is not None:
|
||||
return o
|
||||
else:
|
||||
return None
|
||||
|
315
sqlalchemy/ext/orderinglist.py
Normal file
315
sqlalchemy/ext/orderinglist.py
Normal file
@@ -0,0 +1,315 @@
|
||||
"""A custom list that manages index/position information for its children.
|
||||
|
||||
:author: Jason Kirtland
|
||||
|
||||
``orderinglist`` is a helper for mutable ordered relationships. It will intercept
|
||||
list operations performed on a relationship collection and automatically
|
||||
synchronize changes in list position with an attribute on the related objects.
|
||||
(See :ref:`advdatamapping_entitycollections` for more information on the general pattern.)
|
||||
|
||||
Example: Two tables that store slides in a presentation. Each slide
|
||||
has a number of bullet points, displayed in order by the 'position'
|
||||
column on the bullets table. These bullets can be inserted and re-ordered
|
||||
by your end users, and you need to update the 'position' column of all
|
||||
affected rows when changes are made.
|
||||
|
||||
.. sourcecode:: python+sql
|
||||
|
||||
slides_table = Table('Slides', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('name', String))
|
||||
|
||||
bullets_table = Table('Bullets', metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('slide_id', Integer, ForeignKey('Slides.id')),
|
||||
Column('position', Integer),
|
||||
Column('text', String))
|
||||
|
||||
class Slide(object):
|
||||
pass
|
||||
class Bullet(object):
|
||||
pass
|
||||
|
||||
mapper(Slide, slides_table, properties={
|
||||
'bullets': relationship(Bullet, order_by=[bullets_table.c.position])
|
||||
})
|
||||
mapper(Bullet, bullets_table)
|
||||
|
||||
The standard relationship mapping will produce a list-like attribute on each Slide
|
||||
containing all related Bullets, but coping with changes in ordering is totally
|
||||
your responsibility. If you insert a Bullet into that list, there is no
|
||||
magic- it won't have a position attribute unless you assign it it one, and
|
||||
you'll need to manually renumber all the subsequent Bullets in the list to
|
||||
accommodate the insert.
|
||||
|
||||
An ``orderinglist`` can automate this and manage the 'position' attribute on all
|
||||
related bullets for you.
|
||||
|
||||
.. sourcecode:: python+sql
|
||||
|
||||
mapper(Slide, slides_table, properties={
|
||||
'bullets': relationship(Bullet,
|
||||
collection_class=ordering_list('position'),
|
||||
order_by=[bullets_table.c.position])
|
||||
})
|
||||
mapper(Bullet, bullets_table)
|
||||
|
||||
s = Slide()
|
||||
s.bullets.append(Bullet())
|
||||
s.bullets.append(Bullet())
|
||||
s.bullets[1].position
|
||||
>>> 1
|
||||
s.bullets.insert(1, Bullet())
|
||||
s.bullets[2].position
|
||||
>>> 2
|
||||
|
||||
Use the ``ordering_list`` function to set up the ``collection_class`` on relationships
|
||||
(as in the mapper example above). This implementation depends on the list
|
||||
starting in the proper order, so be SURE to put an order_by on your relationship.
|
||||
|
||||
.. warning:: ``ordering_list`` only provides limited functionality when a primary
|
||||
key column or unique column is the target of the sort. Since changing the order of
|
||||
entries often means that two rows must trade values, this is not possible when
|
||||
the value is constrained by a primary key or unique constraint, since one of the rows
|
||||
would temporarily have to point to a third available value so that the other row
|
||||
could take its old value. ``ordering_list`` doesn't do any of this for you,
|
||||
nor does SQLAlchemy itself.
|
||||
|
||||
``ordering_list`` takes the name of the related object's ordering attribute as
|
||||
an argument. By default, the zero-based integer index of the object's
|
||||
position in the ``ordering_list`` is synchronized with the ordering attribute:
|
||||
index 0 will get position 0, index 1 position 1, etc. To start numbering at 1
|
||||
or some other integer, provide ``count_from=1``.
|
||||
|
||||
Ordering values are not limited to incrementing integers. Almost any scheme
|
||||
can implemented by supplying a custom ``ordering_func`` that maps a Python list
|
||||
index to any value you require.
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
from sqlalchemy.orm.collections import collection
|
||||
from sqlalchemy import util
|
||||
|
||||
__all__ = [ 'ordering_list' ]
|
||||
|
||||
|
||||
def ordering_list(attr, count_from=None, **kw):
|
||||
"""Prepares an OrderingList factory for use in mapper definitions.
|
||||
|
||||
Returns an object suitable for use as an argument to a Mapper relationship's
|
||||
``collection_class`` option. Arguments are:
|
||||
|
||||
attr
|
||||
Name of the mapped attribute to use for storage and retrieval of
|
||||
ordering information
|
||||
|
||||
count_from (optional)
|
||||
Set up an integer-based ordering, starting at ``count_from``. For
|
||||
example, ``ordering_list('pos', count_from=1)`` would create a 1-based
|
||||
list in SQL, storing the value in the 'pos' column. Ignored if
|
||||
``ordering_func`` is supplied.
|
||||
|
||||
Passes along any keyword arguments to ``OrderingList`` constructor.
|
||||
"""
|
||||
|
||||
kw = _unsugar_count_from(count_from=count_from, **kw)
|
||||
return lambda: OrderingList(attr, **kw)
|
||||
|
||||
# Ordering utility functions
|
||||
def count_from_0(index, collection):
|
||||
"""Numbering function: consecutive integers starting at 0."""
|
||||
|
||||
return index
|
||||
|
||||
def count_from_1(index, collection):
|
||||
"""Numbering function: consecutive integers starting at 1."""
|
||||
|
||||
return index + 1
|
||||
|
||||
def count_from_n_factory(start):
|
||||
"""Numbering function: consecutive integers starting at arbitrary start."""
|
||||
|
||||
def f(index, collection):
|
||||
return index + start
|
||||
try:
|
||||
f.__name__ = 'count_from_%i' % start
|
||||
except TypeError:
|
||||
pass
|
||||
return f
|
||||
|
||||
def _unsugar_count_from(**kw):
|
||||
"""Builds counting functions from keywrod arguments.
|
||||
|
||||
Keyword argument filter, prepares a simple ``ordering_func`` from a
|
||||
``count_from`` argument, otherwise passes ``ordering_func`` on unchanged.
|
||||
"""
|
||||
|
||||
count_from = kw.pop('count_from', None)
|
||||
if kw.get('ordering_func', None) is None and count_from is not None:
|
||||
if count_from == 0:
|
||||
kw['ordering_func'] = count_from_0
|
||||
elif count_from == 1:
|
||||
kw['ordering_func'] = count_from_1
|
||||
else:
|
||||
kw['ordering_func'] = count_from_n_factory(count_from)
|
||||
return kw
|
||||
|
||||
class OrderingList(list):
|
||||
"""A custom list that manages position information for its children.
|
||||
|
||||
See the module and __init__ documentation for more details. The
|
||||
``ordering_list`` factory function is used to configure ``OrderingList``
|
||||
collections in ``mapper`` relationship definitions.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, ordering_attr=None, ordering_func=None,
|
||||
reorder_on_append=False):
|
||||
"""A custom list that manages position information for its children.
|
||||
|
||||
``OrderingList`` is a ``collection_class`` list implementation that
|
||||
syncs position in a Python list with a position attribute on the
|
||||
mapped objects.
|
||||
|
||||
This implementation relies on the list starting in the proper order,
|
||||
so be **sure** to put an ``order_by`` on your relationship.
|
||||
|
||||
ordering_attr
|
||||
Name of the attribute that stores the object's order in the
|
||||
relationship.
|
||||
|
||||
ordering_func
|
||||
Optional. A function that maps the position in the Python list to a
|
||||
value to store in the ``ordering_attr``. Values returned are
|
||||
usually (but need not be!) integers.
|
||||
|
||||
An ``ordering_func`` is called with two positional parameters: the
|
||||
index of the element in the list, and the list itself.
|
||||
|
||||
If omitted, Python list indexes are used for the attribute values.
|
||||
Two basic pre-built numbering functions are provided in this module:
|
||||
``count_from_0`` and ``count_from_1``. For more exotic examples
|
||||
like stepped numbering, alphabetical and Fibonacci numbering, see
|
||||
the unit tests.
|
||||
|
||||
reorder_on_append
|
||||
Default False. When appending an object with an existing (non-None)
|
||||
ordering value, that value will be left untouched unless
|
||||
``reorder_on_append`` is true. This is an optimization to avoid a
|
||||
variety of dangerous unexpected database writes.
|
||||
|
||||
SQLAlchemy will add instances to the list via append() when your
|
||||
object loads. If for some reason the result set from the database
|
||||
skips a step in the ordering (say, row '1' is missing but you get
|
||||
'2', '3', and '4'), reorder_on_append=True would immediately
|
||||
renumber the items to '1', '2', '3'. If you have multiple sessions
|
||||
making changes, any of whom happen to load this collection even in
|
||||
passing, all of the sessions would try to "clean up" the numbering
|
||||
in their commits, possibly causing all but one to fail with a
|
||||
concurrent modification error. Spooky action at a distance.
|
||||
|
||||
Recommend leaving this with the default of False, and just call
|
||||
``reorder()`` if you're doing ``append()`` operations with
|
||||
previously ordered instances or when doing some housekeeping after
|
||||
manual sql operations.
|
||||
|
||||
"""
|
||||
self.ordering_attr = ordering_attr
|
||||
if ordering_func is None:
|
||||
ordering_func = count_from_0
|
||||
self.ordering_func = ordering_func
|
||||
self.reorder_on_append = reorder_on_append
|
||||
|
||||
# More complex serialization schemes (multi column, e.g.) are possible by
|
||||
# subclassing and reimplementing these two methods.
|
||||
def _get_order_value(self, entity):
|
||||
return getattr(entity, self.ordering_attr)
|
||||
|
||||
def _set_order_value(self, entity, value):
|
||||
setattr(entity, self.ordering_attr, value)
|
||||
|
||||
def reorder(self):
|
||||
"""Synchronize ordering for the entire collection.
|
||||
|
||||
Sweeps through the list and ensures that each object has accurate
|
||||
ordering information set.
|
||||
|
||||
"""
|
||||
for index, entity in enumerate(self):
|
||||
self._order_entity(index, entity, True)
|
||||
|
||||
# As of 0.5, _reorder is no longer semi-private
|
||||
_reorder = reorder
|
||||
|
||||
def _order_entity(self, index, entity, reorder=True):
|
||||
have = self._get_order_value(entity)
|
||||
|
||||
# Don't disturb existing ordering if reorder is False
|
||||
if have is not None and not reorder:
|
||||
return
|
||||
|
||||
should_be = self.ordering_func(index, self)
|
||||
if have != should_be:
|
||||
self._set_order_value(entity, should_be)
|
||||
|
||||
def append(self, entity):
|
||||
super(OrderingList, self).append(entity)
|
||||
self._order_entity(len(self) - 1, entity, self.reorder_on_append)
|
||||
|
||||
def _raw_append(self, entity):
|
||||
"""Append without any ordering behavior."""
|
||||
|
||||
super(OrderingList, self).append(entity)
|
||||
_raw_append = collection.adds(1)(_raw_append)
|
||||
|
||||
def insert(self, index, entity):
|
||||
super(OrderingList, self).insert(index, entity)
|
||||
self._reorder()
|
||||
|
||||
def remove(self, entity):
|
||||
super(OrderingList, self).remove(entity)
|
||||
self._reorder()
|
||||
|
||||
def pop(self, index=-1):
|
||||
entity = super(OrderingList, self).pop(index)
|
||||
self._reorder()
|
||||
return entity
|
||||
|
||||
def __setitem__(self, index, entity):
|
||||
if isinstance(index, slice):
|
||||
step = index.step or 1
|
||||
start = index.start or 0
|
||||
if start < 0:
|
||||
start += len(self)
|
||||
stop = index.stop or len(self)
|
||||
if stop < 0:
|
||||
stop += len(self)
|
||||
|
||||
for i in xrange(start, stop, step):
|
||||
self.__setitem__(i, entity[i])
|
||||
else:
|
||||
self._order_entity(index, entity, True)
|
||||
super(OrderingList, self).__setitem__(index, entity)
|
||||
|
||||
def __delitem__(self, index):
|
||||
super(OrderingList, self).__delitem__(index)
|
||||
self._reorder()
|
||||
|
||||
# Py2K
|
||||
def __setslice__(self, start, end, values):
|
||||
super(OrderingList, self).__setslice__(start, end, values)
|
||||
self._reorder()
|
||||
|
||||
def __delslice__(self, start, end):
|
||||
super(OrderingList, self).__delslice__(start, end)
|
||||
self._reorder()
|
||||
# end Py2K
|
||||
|
||||
for func_name, func in locals().items():
|
||||
if (util.callable(func) and func.func_name == func_name and
|
||||
not func.__doc__ and hasattr(list, func_name)):
|
||||
func.__doc__ = getattr(list, func_name).__doc__
|
||||
del func_name, func
|
||||
|
155
sqlalchemy/ext/serializer.py
Normal file
155
sqlalchemy/ext/serializer.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""Serializer/Deserializer objects for usage with SQLAlchemy query structures,
|
||||
allowing "contextual" deserialization.
|
||||
|
||||
Any SQLAlchemy query structure, either based on sqlalchemy.sql.*
|
||||
or sqlalchemy.orm.* can be used. The mappers, Tables, Columns, Session
|
||||
etc. which are referenced by the structure are not persisted in serialized
|
||||
form, but are instead re-associated with the query structure
|
||||
when it is deserialized.
|
||||
|
||||
Usage is nearly the same as that of the standard Python pickle module::
|
||||
|
||||
from sqlalchemy.ext.serializer import loads, dumps
|
||||
metadata = MetaData(bind=some_engine)
|
||||
Session = scoped_session(sessionmaker())
|
||||
|
||||
# ... define mappers
|
||||
|
||||
query = Session.query(MyClass).filter(MyClass.somedata=='foo').order_by(MyClass.sortkey)
|
||||
|
||||
# pickle the query
|
||||
serialized = dumps(query)
|
||||
|
||||
# unpickle. Pass in metadata + scoped_session
|
||||
query2 = loads(serialized, metadata, Session)
|
||||
|
||||
print query2.all()
|
||||
|
||||
Similar restrictions as when using raw pickle apply; mapped classes must be
|
||||
themselves be pickleable, meaning they are importable from a module-level
|
||||
namespace.
|
||||
|
||||
The serializer module is only appropriate for query structures. It is not
|
||||
needed for:
|
||||
|
||||
* instances of user-defined classes. These contain no references to engines,
|
||||
sessions or expression constructs in the typical case and can be serialized directly.
|
||||
|
||||
* Table metadata that is to be loaded entirely from the serialized structure (i.e. is
|
||||
not already declared in the application). Regular pickle.loads()/dumps() can
|
||||
be used to fully dump any ``MetaData`` object, typically one which was reflected
|
||||
from an existing database at some previous point in time. The serializer module
|
||||
is specifically for the opposite case, where the Table metadata is already present
|
||||
in memory.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import class_mapper, Query
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.orm.mapper import Mapper
|
||||
from sqlalchemy.orm.attributes import QueryableAttribute
|
||||
from sqlalchemy import Table, Column
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.util import pickle
|
||||
import re
|
||||
import base64
|
||||
# Py3K
|
||||
#from io import BytesIO as byte_buffer
|
||||
# Py2K
|
||||
from cStringIO import StringIO as byte_buffer
|
||||
# end Py2K
|
||||
|
||||
# Py3K
|
||||
#def b64encode(x):
|
||||
# return base64.b64encode(x).decode('ascii')
|
||||
#def b64decode(x):
|
||||
# return base64.b64decode(x.encode('ascii'))
|
||||
# Py2K
|
||||
b64encode = base64.b64encode
|
||||
b64decode = base64.b64decode
|
||||
# end Py2K
|
||||
|
||||
__all__ = ['Serializer', 'Deserializer', 'dumps', 'loads']
|
||||
|
||||
|
||||
|
||||
def Serializer(*args, **kw):
|
||||
pickler = pickle.Pickler(*args, **kw)
|
||||
|
||||
def persistent_id(obj):
|
||||
#print "serializing:", repr(obj)
|
||||
if isinstance(obj, QueryableAttribute):
|
||||
cls = obj.impl.class_
|
||||
key = obj.impl.key
|
||||
id = "attribute:" + key + ":" + b64encode(pickle.dumps(cls))
|
||||
elif isinstance(obj, Mapper) and not obj.non_primary:
|
||||
id = "mapper:" + b64encode(pickle.dumps(obj.class_))
|
||||
elif isinstance(obj, Table):
|
||||
id = "table:" + str(obj)
|
||||
elif isinstance(obj, Column) and isinstance(obj.table, Table):
|
||||
id = "column:" + str(obj.table) + ":" + obj.key
|
||||
elif isinstance(obj, Session):
|
||||
id = "session:"
|
||||
elif isinstance(obj, Engine):
|
||||
id = "engine:"
|
||||
else:
|
||||
return None
|
||||
return id
|
||||
|
||||
pickler.persistent_id = persistent_id
|
||||
return pickler
|
||||
|
||||
our_ids = re.compile(r'(mapper|table|column|session|attribute|engine):(.*)')
|
||||
|
||||
def Deserializer(file, metadata=None, scoped_session=None, engine=None):
|
||||
unpickler = pickle.Unpickler(file)
|
||||
|
||||
def get_engine():
|
||||
if engine:
|
||||
return engine
|
||||
elif scoped_session and scoped_session().bind:
|
||||
return scoped_session().bind
|
||||
elif metadata and metadata.bind:
|
||||
return metadata.bind
|
||||
else:
|
||||
return None
|
||||
|
||||
def persistent_load(id):
|
||||
m = our_ids.match(id)
|
||||
if not m:
|
||||
return None
|
||||
else:
|
||||
type_, args = m.group(1, 2)
|
||||
if type_ == 'attribute':
|
||||
key, clsarg = args.split(":")
|
||||
cls = pickle.loads(b64decode(clsarg))
|
||||
return getattr(cls, key)
|
||||
elif type_ == "mapper":
|
||||
cls = pickle.loads(b64decode(args))
|
||||
return class_mapper(cls)
|
||||
elif type_ == "table":
|
||||
return metadata.tables[args]
|
||||
elif type_ == "column":
|
||||
table, colname = args.split(':')
|
||||
return metadata.tables[table].c[colname]
|
||||
elif type_ == "session":
|
||||
return scoped_session()
|
||||
elif type_ == "engine":
|
||||
return get_engine()
|
||||
else:
|
||||
raise Exception("Unknown token: %s" % type_)
|
||||
unpickler.persistent_load = persistent_load
|
||||
return unpickler
|
||||
|
||||
def dumps(obj, protocol=0):
|
||||
buf = byte_buffer()
|
||||
pickler = Serializer(buf, protocol)
|
||||
pickler.dump(obj)
|
||||
return buf.getvalue()
|
||||
|
||||
def loads(data, metadata=None, scoped_session=None, engine=None):
|
||||
buf = byte_buffer(data)
|
||||
unpickler = Deserializer(buf, metadata, scoped_session, engine)
|
||||
return unpickler.load()
|
||||
|
||||
|
551
sqlalchemy/ext/sqlsoup.py
Normal file
551
sqlalchemy/ext/sqlsoup.py
Normal file
@@ -0,0 +1,551 @@
|
||||
"""
|
||||
Introduction
|
||||
============
|
||||
|
||||
SqlSoup provides a convenient way to access existing database tables without
|
||||
having to declare table or mapper classes ahead of time. It is built on top of the SQLAlchemy ORM and provides a super-minimalistic interface to an existing database.
|
||||
|
||||
Suppose we have a database with users, books, and loans tables
|
||||
(corresponding to the PyWebOff dataset, if you're curious).
|
||||
|
||||
Creating a SqlSoup gateway is just like creating an SQLAlchemy
|
||||
engine::
|
||||
|
||||
>>> from sqlalchemy.ext.sqlsoup import SqlSoup
|
||||
>>> db = SqlSoup('sqlite:///:memory:')
|
||||
|
||||
or, you can re-use an existing engine::
|
||||
|
||||
>>> db = SqlSoup(engine)
|
||||
|
||||
You can optionally specify a schema within the database for your
|
||||
SqlSoup::
|
||||
|
||||
>>> db.schema = myschemaname
|
||||
|
||||
Loading objects
|
||||
===============
|
||||
|
||||
Loading objects is as easy as this::
|
||||
|
||||
>>> users = db.users.all()
|
||||
>>> users.sort()
|
||||
>>> users
|
||||
[MappedUsers(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0), MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)]
|
||||
|
||||
Of course, letting the database do the sort is better::
|
||||
|
||||
>>> db.users.order_by(db.users.name).all()
|
||||
[MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1), MappedUsers(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0)]
|
||||
|
||||
Field access is intuitive::
|
||||
|
||||
>>> users[0].email
|
||||
u'student@example.edu'
|
||||
|
||||
Of course, you don't want to load all users very often. Let's add a
|
||||
WHERE clause. Let's also switch the order_by to DESC while we're at
|
||||
it::
|
||||
|
||||
>>> from sqlalchemy import or_, and_, desc
|
||||
>>> where = or_(db.users.name=='Bhargan Basepair', db.users.email=='student@example.edu')
|
||||
>>> db.users.filter(where).order_by(desc(db.users.name)).all()
|
||||
[MappedUsers(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0), MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)]
|
||||
|
||||
You can also use .first() (to retrieve only the first object from a query) or
|
||||
.one() (like .first when you expect exactly one user -- it will raise an
|
||||
exception if more were returned)::
|
||||
|
||||
>>> db.users.filter(db.users.name=='Bhargan Basepair').one()
|
||||
MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)
|
||||
|
||||
Since name is the primary key, this is equivalent to
|
||||
|
||||
>>> db.users.get('Bhargan Basepair')
|
||||
MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)
|
||||
|
||||
This is also equivalent to
|
||||
|
||||
>>> db.users.filter_by(name='Bhargan Basepair').one()
|
||||
MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)
|
||||
|
||||
filter_by is like filter, but takes kwargs instead of full clause expressions.
|
||||
This makes it more concise for simple queries like this, but you can't do
|
||||
complex queries like the or\_ above or non-equality based comparisons this way.
|
||||
|
||||
Full query documentation
|
||||
------------------------
|
||||
|
||||
Get, filter, filter_by, order_by, limit, and the rest of the
|
||||
query methods are explained in detail in :ref:`ormtutorial_querying`.
|
||||
|
||||
Modifying objects
|
||||
=================
|
||||
|
||||
Modifying objects is intuitive::
|
||||
|
||||
>>> user = _
|
||||
>>> user.email = 'basepair+nospam@example.edu'
|
||||
>>> db.commit()
|
||||
|
||||
(SqlSoup leverages the sophisticated SQLAlchemy unit-of-work code, so
|
||||
multiple updates to a single object will be turned into a single
|
||||
``UPDATE`` statement when you commit.)
|
||||
|
||||
To finish covering the basics, let's insert a new loan, then delete
|
||||
it::
|
||||
|
||||
>>> book_id = db.books.filter_by(title='Regional Variation in Moss').first().id
|
||||
>>> db.loans.insert(book_id=book_id, user_name=user.name)
|
||||
MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None)
|
||||
|
||||
>>> loan = db.loans.filter_by(book_id=2, user_name='Bhargan Basepair').one()
|
||||
>>> db.delete(loan)
|
||||
>>> db.commit()
|
||||
|
||||
You can also delete rows that have not been loaded as objects. Let's
|
||||
do our insert/delete cycle once more, this time using the loans
|
||||
table's delete method. (For SQLAlchemy experts: note that no flush()
|
||||
call is required since this delete acts at the SQL level, not at the
|
||||
Mapper level.) The same where-clause construction rules apply here as
|
||||
to the select methods.
|
||||
|
||||
::
|
||||
|
||||
>>> db.loans.insert(book_id=book_id, user_name=user.name)
|
||||
MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None)
|
||||
>>> db.loans.delete(db.loans.book_id==2)
|
||||
|
||||
You can similarly update multiple rows at once. This will change the
|
||||
book_id to 1 in all loans whose book_id is 2::
|
||||
|
||||
>>> db.loans.update(db.loans.book_id==2, book_id=1)
|
||||
>>> db.loans.filter_by(book_id=1).all()
|
||||
[MappedLoans(book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
|
||||
|
||||
|
||||
Joins
|
||||
=====
|
||||
|
||||
Occasionally, you will want to pull out a lot of data from related
|
||||
tables all at once. In this situation, it is far more efficient to
|
||||
have the database perform the necessary join. (Here we do not have *a
|
||||
lot of data* but hopefully the concept is still clear.) SQLAlchemy is
|
||||
smart enough to recognize that loans has a foreign key to users, and
|
||||
uses that as the join condition automatically.
|
||||
|
||||
::
|
||||
|
||||
>>> join1 = db.join(db.users, db.loans, isouter=True)
|
||||
>>> join1.filter_by(name='Joe Student').all()
|
||||
[MappedJoin(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0,book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
|
||||
|
||||
If you're unfortunate enough to be using MySQL with the default MyISAM
|
||||
storage engine, you'll have to specify the join condition manually,
|
||||
since MyISAM does not store foreign keys. Here's the same join again,
|
||||
with the join condition explicitly specified::
|
||||
|
||||
>>> db.join(db.users, db.loans, db.users.name==db.loans.user_name, isouter=True)
|
||||
<class 'sqlalchemy.ext.sqlsoup.MappedJoin'>
|
||||
|
||||
You can compose arbitrarily complex joins by combining Join objects
|
||||
with tables or other joins. Here we combine our first join with the
|
||||
books table::
|
||||
|
||||
>>> join2 = db.join(join1, db.books)
|
||||
>>> join2.all()
|
||||
[MappedJoin(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0,book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0),id=1,title=u'Mustards I Have Known',published_year=u'1989',authors=u'Jones')]
|
||||
|
||||
If you join tables that have an identical column name, wrap your join
|
||||
with `with_labels`, to disambiguate columns with their table name
|
||||
(.c is short for .columns)::
|
||||
|
||||
>>> db.with_labels(join1).c.keys()
|
||||
[u'users_name', u'users_email', u'users_password', u'users_classname', u'users_admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date']
|
||||
|
||||
You can also join directly to a labeled object::
|
||||
|
||||
>>> labeled_loans = db.with_labels(db.loans)
|
||||
>>> db.join(db.users, labeled_loans, isouter=True).c.keys()
|
||||
[u'name', u'email', u'password', u'classname', u'admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date']
|
||||
|
||||
|
||||
Relationships
|
||||
=============
|
||||
|
||||
You can define relationships on SqlSoup classes:
|
||||
|
||||
>>> db.users.relate('loans', db.loans)
|
||||
|
||||
These can then be used like a normal SA property:
|
||||
|
||||
>>> db.users.get('Joe Student').loans
|
||||
[MappedLoans(book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
|
||||
|
||||
>>> db.users.filter(~db.users.loans.any()).all()
|
||||
[MappedUsers(name=u'Bhargan Basepair',email='basepair+nospam@example.edu',password=u'basepair',classname=None,admin=1)]
|
||||
|
||||
|
||||
relate can take any options that the relationship function accepts in normal mapper definition:
|
||||
|
||||
>>> del db._cache['users']
|
||||
>>> db.users.relate('loans', db.loans, order_by=db.loans.loan_date, cascade='all, delete-orphan')
|
||||
|
||||
Advanced Use
|
||||
============
|
||||
|
||||
Sessions, Transations and Application Integration
|
||||
-------------------------------------------------
|
||||
|
||||
**Note:** please read and understand this section thoroughly before using SqlSoup in any web application.
|
||||
|
||||
SqlSoup uses a ScopedSession to provide thread-local sessions. You
|
||||
can get a reference to the current one like this::
|
||||
|
||||
>>> session = db.session
|
||||
|
||||
The default session is available at the module level in SQLSoup, via::
|
||||
|
||||
>>> from sqlalchemy.ext.sqlsoup import Session
|
||||
|
||||
The configuration of this session is ``autoflush=True``, ``autocommit=False``.
|
||||
This means when you work with the SqlSoup object, you need to call ``db.commit()``
|
||||
in order to have changes persisted. You may also call ``db.rollback()`` to
|
||||
roll things back.
|
||||
|
||||
Since the SqlSoup object's Session automatically enters into a transaction as soon
|
||||
as it's used, it is *essential* that you call ``commit()`` or ``rollback()``
|
||||
on it when the work within a thread completes. This means all the guidelines
|
||||
for web application integration at :ref:`session_lifespan` must be followed.
|
||||
|
||||
The SqlSoup object can have any session or scoped session configured onto it.
|
||||
This is of key importance when integrating with existing code or frameworks
|
||||
such as Pylons. If your application already has a ``Session`` configured,
|
||||
pass it to your SqlSoup object::
|
||||
|
||||
>>> from myapplication import Session
|
||||
>>> db = SqlSoup(session=Session)
|
||||
|
||||
If the ``Session`` is configured with ``autocommit=True``, use ``flush()``
|
||||
instead of ``commit()`` to persist changes - in this case, the ``Session``
|
||||
closes out its transaction immediately and no external management is needed. ``rollback()`` is also not available. Configuring a new SQLSoup object in "autocommit" mode looks like::
|
||||
|
||||
>>> from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
>>> db = SqlSoup('sqlite://', session=scoped_session(sessionmaker(autoflush=False, expire_on_commit=False, autocommit=True)))
|
||||
|
||||
|
||||
Mapping arbitrary Selectables
|
||||
-----------------------------
|
||||
|
||||
SqlSoup can map any SQLAlchemy ``Selectable`` with the map
|
||||
method. Let's map a ``Select`` object that uses an aggregate function;
|
||||
we'll use the SQLAlchemy ``Table`` that SqlSoup introspected as the
|
||||
basis. (Since we're not mapping to a simple table or join, we need to
|
||||
tell SQLAlchemy how to find the *primary key* which just needs to be
|
||||
unique within the select, and not necessarily correspond to a *real*
|
||||
PK in the database.)
|
||||
|
||||
::
|
||||
|
||||
>>> from sqlalchemy import select, func
|
||||
>>> b = db.books._table
|
||||
>>> s = select([b.c.published_year, func.count('*').label('n')], from_obj=[b], group_by=[b.c.published_year])
|
||||
>>> s = s.alias('years_with_count')
|
||||
>>> years_with_count = db.map(s, primary_key=[s.c.published_year])
|
||||
>>> years_with_count.filter_by(published_year='1989').all()
|
||||
[MappedBooks(published_year=u'1989',n=1)]
|
||||
|
||||
Obviously if we just wanted to get a list of counts associated with
|
||||
book years once, raw SQL is going to be less work. The advantage of
|
||||
mapping a Select is reusability, both standalone and in Joins. (And if
|
||||
you go to full SQLAlchemy, you can perform mappings like this directly
|
||||
to your object models.)
|
||||
|
||||
An easy way to save mapped selectables like this is to just hang them on
|
||||
your db object::
|
||||
|
||||
>>> db.years_with_count = years_with_count
|
||||
|
||||
Python is flexible like that!
|
||||
|
||||
|
||||
Raw SQL
|
||||
-------
|
||||
|
||||
SqlSoup works fine with SQLAlchemy's text construct, described in :ref:`sqlexpression_text`.
|
||||
You can also execute textual SQL directly using the `execute()` method,
|
||||
which corresponds to the `execute()` method on the underlying `Session`.
|
||||
Expressions here are expressed like ``text()`` constructs, using named parameters
|
||||
with colons::
|
||||
|
||||
>>> rp = db.execute('select name, email from users where name like :name order by name', name='%Bhargan%')
|
||||
>>> for name, email in rp.fetchall(): print name, email
|
||||
Bhargan Basepair basepair+nospam@example.edu
|
||||
|
||||
Or you can get at the current transaction's connection using `connection()`. This is the
|
||||
raw connection object which can accept any sort of SQL expression or raw SQL string passed to the database::
|
||||
|
||||
>>> conn = db.connection()
|
||||
>>> conn.execute("'select name, email from users where name like ? order by name'", '%Bhargan%')
|
||||
|
||||
|
||||
Dynamic table names
|
||||
-------------------
|
||||
|
||||
You can load a table whose name is specified at runtime with the entity() method:
|
||||
|
||||
>>> tablename = 'loans'
|
||||
>>> db.entity(tablename) == db.loans
|
||||
True
|
||||
|
||||
entity() also takes an optional schema argument. If none is specified, the
|
||||
default schema is used.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy import Table, MetaData, join
|
||||
from sqlalchemy import schema, sql
|
||||
from sqlalchemy.engine.base import Engine
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker, mapper, \
|
||||
class_mapper, relationship, session,\
|
||||
object_session
|
||||
from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE
|
||||
from sqlalchemy.exceptions import SQLAlchemyError, InvalidRequestError, ArgumentError
|
||||
from sqlalchemy.sql import expression
|
||||
|
||||
|
||||
__all__ = ['PKNotFoundError', 'SqlSoup']
|
||||
|
||||
Session = scoped_session(sessionmaker(autoflush=True, autocommit=False))
|
||||
|
||||
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()
|
||||
session._save_without_cascade(instance)
|
||||
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 PKNotFoundError(SQLAlchemyError):
|
||||
pass
|
||||
|
||||
def _ddl_error(cls):
|
||||
msg = 'SQLSoup can only modify mapped Tables (found: %s)' \
|
||||
% cls._table.__class__.__name__
|
||||
raise InvalidRequestError(msg)
|
||||
|
||||
# metaclass is necessary to expose class methods with getattr, e.g.
|
||||
# we want to pass db.users.select through to users._mapper.select
|
||||
class SelectableClassType(type):
|
||||
def insert(cls, **kwargs):
|
||||
_ddl_error(cls)
|
||||
|
||||
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)
|
||||
|
||||
class TableClassType(SelectableClassType):
|
||||
def insert(cls, **kwargs):
|
||||
o = cls()
|
||||
o.__dict__.update(kwargs)
|
||||
return o
|
||||
|
||||
def relate(cls, propname, *args, **kwargs):
|
||||
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, **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, (object,), {})
|
||||
else:
|
||||
klass = SelectableClassType(mapname, (object,), {})
|
||||
|
||||
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
|
||||
|
||||
class SqlSoup(object):
|
||||
def __init__(self, engine_or_metadata, **kw):
|
||||
"""Initialize a new ``SqlSoup``.
|
||||
|
||||
`args` may either be an ``SQLEngine`` or a set of arguments
|
||||
suitable for passing to ``create_engine``.
|
||||
"""
|
||||
|
||||
self.session = kw.pop('session', Session)
|
||||
|
||||
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
|
||||
def engine(self):
|
||||
return self._metadata.bind
|
||||
|
||||
bind = engine
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.session.delete(*args, **kwargs)
|
||||
|
||||
def execute(self, stmt, **params):
|
||||
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()
|
||||
|
||||
def connection(self):
|
||||
return self._underlying_session._connection_for_bind(self.bind)
|
||||
|
||||
def flush(self):
|
||||
self.session.flush()
|
||||
|
||||
def rollback(self):
|
||||
self.session.rollback()
|
||||
|
||||
def commit(self):
|
||||
self.session.commit()
|
||||
|
||||
def clear(self):
|
||||
self.session.expunge_all()
|
||||
|
||||
def expunge(self, *args, **kw):
|
||||
self.session.expunge(*args, **kw)
|
||||
|
||||
def expunge_all(self):
|
||||
self.session.expunge_all()
|
||||
|
||||
def map(self, selectable, **kwargs):
|
||||
try:
|
||||
t = self._cache[selectable]
|
||||
except KeyError:
|
||||
t = _class_for_table(self.session, self.engine, selectable, **kwargs)
|
||||
self._cache[selectable] = t
|
||||
return t
|
||||
|
||||
def with_labels(self, item):
|
||||
# TODO give meaningful aliases
|
||||
return self.map(
|
||||
expression._clause_element_as_expr(item).
|
||||
select(use_labels=True).
|
||||
alias('foo'))
|
||||
|
||||
def join(self, *args, **kwargs):
|
||||
j = join(*args, **kwargs)
|
||||
return self.map(j)
|
||||
|
||||
def entity(self, attr, schema=None):
|
||||
try:
|
||||
t = self._cache[attr]
|
||||
except KeyError, ke:
|
||||
table = Table(attr, self._metadata, autoload=True, autoload_with=self.bind, schema=schema or self.schema)
|
||||
if not table.primary_key.columns:
|
||||
raise PKNotFoundError('table %r does not have a primary key defined [columns: %s]' % (attr, ','.join(table.c.keys())))
|
||||
if table.columns:
|
||||
t = _class_for_table(self.session, self.engine, table)
|
||||
else:
|
||||
t = None
|
||||
self._cache[attr] = t
|
||||
return t
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return self.entity(attr)
|
||||
|
||||
def __repr__(self):
|
||||
return 'SqlSoup(%r)' % self._metadata
|
||||
|
Reference in New Issue
Block a user