Remove submodule, just put Dependencies in ./libs
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
'''
|
||||
Elixir package
|
||||
|
||||
A declarative layer on top of the `SQLAlchemy library
|
||||
<http://www.sqlalchemy.org/>`_. It is a fairly thin wrapper, which provides
|
||||
the ability to create simple Python classes that map directly to relational
|
||||
database tables (this pattern is often referred to as the Active Record design
|
||||
pattern), providing many of the benefits of traditional databases
|
||||
without losing the convenience of Python objects.
|
||||
|
||||
Elixir is intended to replace the ActiveMapper SQLAlchemy extension, and the
|
||||
TurboEntity project but does not intend to replace SQLAlchemy's core features,
|
||||
and instead focuses on providing a simpler syntax for defining model objects
|
||||
when you do not need the full expressiveness of SQLAlchemy's manual mapper
|
||||
definitions.
|
||||
'''
|
||||
|
||||
try:
|
||||
set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
|
||||
import sqlalchemy
|
||||
from sqlalchemy.types import *
|
||||
|
||||
from elixir.options import using_options, using_table_options, \
|
||||
using_mapper_options, options_defaults, \
|
||||
using_options_defaults
|
||||
from elixir.entity import Entity, EntityBase, EntityMeta, EntityDescriptor, \
|
||||
setup_entities, cleanup_entities
|
||||
from elixir.fields import has_field, Field
|
||||
from elixir.relationships import belongs_to, has_one, has_many, \
|
||||
has_and_belongs_to_many, \
|
||||
ManyToOne, OneToOne, OneToMany, ManyToMany
|
||||
from elixir.properties import has_property, GenericProperty, ColumnProperty, \
|
||||
Synonym
|
||||
from elixir.statements import Statement
|
||||
from elixir.collection import EntityCollection, GlobalEntityCollection
|
||||
|
||||
|
||||
__version__ = '0.7.1'
|
||||
|
||||
__all__ = ['Entity', 'EntityBase', 'EntityMeta', 'EntityCollection',
|
||||
'entities',
|
||||
'Field', 'has_field',
|
||||
'has_property', 'GenericProperty', 'ColumnProperty', 'Synonym',
|
||||
'belongs_to', 'has_one', 'has_many', 'has_and_belongs_to_many',
|
||||
'ManyToOne', 'OneToOne', 'OneToMany', 'ManyToMany',
|
||||
'using_options', 'using_table_options', 'using_mapper_options',
|
||||
'options_defaults', 'using_options_defaults',
|
||||
'metadata', 'session',
|
||||
'create_all', 'drop_all',
|
||||
'setup_all', 'cleanup_all',
|
||||
'setup_entities', 'cleanup_entities'] + \
|
||||
sqlalchemy.types.__all__
|
||||
|
||||
__doc_all__ = ['create_all', 'drop_all',
|
||||
'setup_all', 'cleanup_all',
|
||||
'metadata', 'session']
|
||||
|
||||
# default session
|
||||
session = sqlalchemy.orm.scoped_session(sqlalchemy.orm.sessionmaker())
|
||||
|
||||
# default metadata
|
||||
metadata = sqlalchemy.MetaData()
|
||||
|
||||
metadatas = set()
|
||||
|
||||
# default entity collection
|
||||
entities = GlobalEntityCollection()
|
||||
|
||||
|
||||
def create_all(*args, **kwargs):
|
||||
'''Create the necessary tables for all declared entities'''
|
||||
for md in metadatas:
|
||||
md.create_all(*args, **kwargs)
|
||||
|
||||
|
||||
def drop_all(*args, **kwargs):
|
||||
'''Drop tables for all declared entities'''
|
||||
for md in metadatas:
|
||||
md.drop_all(*args, **kwargs)
|
||||
|
||||
|
||||
def setup_all(create_tables=False, *args, **kwargs):
|
||||
'''Setup the table and mapper of all entities in the default entity
|
||||
collection.
|
||||
|
||||
This is called automatically if any entity of the collection is configured
|
||||
with the `autosetup` option and it is first accessed,
|
||||
instanciated (called) or the create_all method of a metadata containing
|
||||
tables from any of those entities is called.
|
||||
'''
|
||||
setup_entities(entities)
|
||||
|
||||
# issue the "CREATE" SQL statements
|
||||
if create_tables:
|
||||
create_all(*args, **kwargs)
|
||||
|
||||
|
||||
def cleanup_all(drop_tables=False, *args, **kwargs):
|
||||
'''Clear all mappers, clear the session, and clear all metadatas.
|
||||
Optionally drops the tables.
|
||||
'''
|
||||
session.close()
|
||||
|
||||
cleanup_entities(entities)
|
||||
|
||||
sqlalchemy.orm.clear_mappers()
|
||||
entities.clear()
|
||||
|
||||
if drop_tables:
|
||||
drop_all(*args, **kwargs)
|
||||
|
||||
for md in metadatas:
|
||||
md.clear()
|
||||
metadatas.clear()
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
'''
|
||||
Default entity collection implementation
|
||||
'''
|
||||
import sys
|
||||
import re
|
||||
|
||||
from elixir.py23compat import rsplit
|
||||
|
||||
class BaseCollection(list):
|
||||
def __init__(self, entities=None):
|
||||
list.__init__(self)
|
||||
if entities is not None:
|
||||
self.extend(entities)
|
||||
|
||||
def extend(self, entities):
|
||||
for e in entities:
|
||||
self.append(e)
|
||||
|
||||
def clear(self):
|
||||
del self[:]
|
||||
|
||||
def resolve_absolute(self, key, full_path, entity=None, root=None):
|
||||
if root is None:
|
||||
root = entity._descriptor.resolve_root
|
||||
if root:
|
||||
full_path = '%s.%s' % (root, full_path)
|
||||
module_path, classname = rsplit(full_path, '.', 1)
|
||||
module = sys.modules[module_path]
|
||||
res = getattr(module, classname, None)
|
||||
if res is None:
|
||||
if entity is not None:
|
||||
raise Exception("Couldn't resolve target '%s' <%s> in '%s'!"
|
||||
% (key, full_path, entity.__name__))
|
||||
else:
|
||||
raise Exception("Couldn't resolve target '%s' <%s>!"
|
||||
% (key, full_path))
|
||||
return res
|
||||
|
||||
def __getattr__(self, key):
|
||||
return self.resolve(key)
|
||||
|
||||
# default entity collection
|
||||
class GlobalEntityCollection(BaseCollection):
|
||||
def __init__(self, entities=None):
|
||||
# _entities is a dict of entities keyed on their name.
|
||||
self._entities = {}
|
||||
super(GlobalEntityCollection, self).__init__(entities)
|
||||
|
||||
def append(self, entity):
|
||||
'''
|
||||
Add an entity to the collection.
|
||||
'''
|
||||
super(EntityCollection, self).append(entity)
|
||||
|
||||
existing_entities = self._entities.setdefault(entity.__name__, [])
|
||||
existing_entities.append(entity)
|
||||
|
||||
def resolve(self, key, entity=None):
|
||||
'''
|
||||
Resolve a key to an Entity. The optional `entity` argument is the
|
||||
"source" entity when resolving relationship targets.
|
||||
'''
|
||||
# Do we have a fully qualified entity name?
|
||||
if '.' in key:
|
||||
return self.resolve_absolute(key, key, entity)
|
||||
else:
|
||||
# Otherwise we look in the entities of this collection
|
||||
res = self._entities.get(key, None)
|
||||
if res is None:
|
||||
if entity:
|
||||
raise Exception("Couldn't resolve target '%s' in '%s'"
|
||||
% (key, entity.__name__))
|
||||
else:
|
||||
raise Exception("This collection does not contain any "
|
||||
"entity corresponding to the key '%s'!"
|
||||
% key)
|
||||
elif len(res) > 1:
|
||||
raise Exception("'%s' resolves to several entities, you should"
|
||||
" use the full path (including the full module"
|
||||
" name) to that entity." % key)
|
||||
else:
|
||||
return res[0]
|
||||
|
||||
def clear(self):
|
||||
self._entities = {}
|
||||
super(GlobalEntityCollection, self).clear()
|
||||
|
||||
# backward compatible name
|
||||
EntityCollection = GlobalEntityCollection
|
||||
|
||||
_leading_dots = re.compile('^([.]*).*$')
|
||||
|
||||
class RelativeEntityCollection(BaseCollection):
|
||||
# the entity=None does not make any sense with a relative entity collection
|
||||
def resolve(self, key, entity):
|
||||
'''
|
||||
Resolve a key to an Entity. The optional `entity` argument is the
|
||||
"source" entity when resolving relationship targets.
|
||||
'''
|
||||
full_path = key
|
||||
|
||||
if '.' not in key or key.startswith('.'):
|
||||
# relative target
|
||||
|
||||
# any leading dot is stripped and with each dot removed,
|
||||
# the entity_module is stripped of one more chunk (starting with
|
||||
# the last one).
|
||||
num_dots = _leading_dots.match(full_path).end(1)
|
||||
full_path = full_path[num_dots:]
|
||||
chunks = entity.__module__.split('.')
|
||||
chunkstokeep = len(chunks) - num_dots
|
||||
if chunkstokeep < 0:
|
||||
raise Exception("Couldn't resolve relative target "
|
||||
"'%s' relative to '%s'" % (key, entity.__module__))
|
||||
entity_module = '.'.join(chunks[:chunkstokeep])
|
||||
|
||||
if entity_module and entity_module is not '__main__':
|
||||
full_path = '%s.%s' % (entity_module, full_path)
|
||||
|
||||
root = ''
|
||||
else:
|
||||
root = None
|
||||
return self.resolve_absolute(key, full_path, entity, root=root)
|
||||
|
||||
def __getattr__(self, key):
|
||||
raise NotImplementedError
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
__all__ = [
|
||||
'before_insert',
|
||||
'after_insert',
|
||||
'before_update',
|
||||
'after_update',
|
||||
'before_delete',
|
||||
'after_delete',
|
||||
'reconstructor'
|
||||
]
|
||||
|
||||
def create_decorator(event_name):
|
||||
def decorator(func):
|
||||
if not hasattr(func, '_elixir_events'):
|
||||
func._elixir_events = []
|
||||
func._elixir_events.append(event_name)
|
||||
return func
|
||||
return decorator
|
||||
|
||||
before_insert = create_decorator('before_insert')
|
||||
after_insert = create_decorator('after_insert')
|
||||
before_update = create_decorator('before_update')
|
||||
after_update = create_decorator('after_update')
|
||||
before_delete = create_decorator('before_delete')
|
||||
after_delete = create_decorator('after_delete')
|
||||
try:
|
||||
from sqlalchemy.orm import reconstructor
|
||||
except ImportError:
|
||||
def reconstructor(func):
|
||||
raise Exception('The reconstructor method decorator is only '
|
||||
'available with SQLAlchemy 0.5 and later')
|
||||
@@ -0,0 +1,5 @@
|
||||
'''
|
||||
Ext package
|
||||
|
||||
Additional Elixir statements and functionality.
|
||||
'''
|
||||
@@ -0,0 +1,234 @@
|
||||
'''
|
||||
Associable Elixir Statement Generator
|
||||
|
||||
==========
|
||||
Associable
|
||||
==========
|
||||
|
||||
About Polymorphic Associations
|
||||
------------------------------
|
||||
|
||||
A frequent pattern in database schemas is the has_and_belongs_to_many, or a
|
||||
many-to-many table. Quite often multiple tables will refer to a single one
|
||||
creating quite a few many-to-many intermediate tables.
|
||||
|
||||
Polymorphic associations lower the amount of many-to-many tables by setting up
|
||||
a table that allows relations to any other table in the database, and relates
|
||||
it to the associable table. In some implementations, this layout does not
|
||||
enforce referential integrity with database foreign key constraints, this
|
||||
implementation uses an additional many-to-many table with foreign key
|
||||
constraints to avoid this problem.
|
||||
|
||||
.. note:
|
||||
SQLite does not support foreign key constraints, so referential integrity
|
||||
can only be enforced using database backends with such support.
|
||||
|
||||
Elixir Statement Generator for Polymorphic Associations
|
||||
-------------------------------------------------------
|
||||
|
||||
The ``associable`` function generates the intermediary tables for an Elixir
|
||||
entity that should be associable with other Elixir entities and returns an
|
||||
Elixir Statement for use with them. This automates the process of creating the
|
||||
polymorphic association tables and ensuring their referential integrity.
|
||||
|
||||
Matching select_XXX and select_by_XXX are also added to the associated entity
|
||||
which allow queries to be run for the associated objects.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class Tag(Entity):
|
||||
name = Field(Unicode)
|
||||
|
||||
acts_as_taggable = associable(Tag)
|
||||
|
||||
class Entry(Entity):
|
||||
title = Field(Unicode)
|
||||
acts_as_taggable('tags')
|
||||
|
||||
class Article(Entity):
|
||||
title = Field(Unicode)
|
||||
acts_as_taggable('tags')
|
||||
|
||||
Or if one of the entities being associated should only have a single member of
|
||||
the associated table:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class Address(Entity):
|
||||
street = Field(String(130))
|
||||
city = Field(String(100))
|
||||
|
||||
is_addressable = associable(Address, 'addresses')
|
||||
|
||||
class Person(Entity):
|
||||
name = Field(Unicode)
|
||||
orders = OneToMany('Order')
|
||||
is_addressable()
|
||||
|
||||
class Order(Entity):
|
||||
order_num = Field(primary_key=True)
|
||||
item_count = Field(Integer)
|
||||
person = ManyToOne('Person')
|
||||
is_addressable('address', uselist=False)
|
||||
|
||||
home = Address(street='123 Elm St.', city='Spooksville')
|
||||
user = Person(name='Jane Doe')
|
||||
user.addresses.append(home)
|
||||
|
||||
neworder = Order(item_count=4)
|
||||
neworder.address = home
|
||||
user.orders.append(neworder)
|
||||
|
||||
# Queries using the added helpers
|
||||
Person.select_by_addresses(city='Cupertino')
|
||||
Person.select_addresses(and_(Address.c.street=='132 Elm St',
|
||||
Address.c.city=='Smallville'))
|
||||
|
||||
Statement Options
|
||||
-----------------
|
||||
|
||||
The generated Elixir Statement has several options available:
|
||||
|
||||
+---------------+-------------------------------------------------------------+
|
||||
| Option Name | Description |
|
||||
+===============+=============================================================+
|
||||
| ``name`` | Specify a custom name for the Entity attribute. This is |
|
||||
| | used to declare the attribute used to access the associated |
|
||||
| | table values. Otherwise, the name will use the plural_name |
|
||||
| | provided to the associable call. |
|
||||
+---------------+-------------------------------------------------------------+
|
||||
| ``uselist`` | Whether or not the associated table should be represented |
|
||||
| | as a list, or a single property. It should be set to False |
|
||||
| | when the entity should only have a single associated |
|
||||
| | entity. Defaults to True. |
|
||||
+---------------+-------------------------------------------------------------+
|
||||
| ``lazy`` | Determines eager loading of the associated entity objects. |
|
||||
| | Defaults to False, to indicate that they should not be |
|
||||
| | lazily loaded. |
|
||||
+---------------+-------------------------------------------------------------+
|
||||
'''
|
||||
from elixir.statements import Statement
|
||||
import sqlalchemy as sa
|
||||
|
||||
__doc_all__ = ['associable']
|
||||
|
||||
|
||||
def associable(assoc_entity, plural_name=None, lazy=True):
|
||||
'''
|
||||
Generate an associable Elixir Statement
|
||||
'''
|
||||
interface_name = assoc_entity._descriptor.tablename
|
||||
able_name = interface_name + 'able'
|
||||
|
||||
if plural_name:
|
||||
attr_name = "%s_rel" % plural_name
|
||||
else:
|
||||
plural_name = interface_name
|
||||
attr_name = "%s_rel" % interface_name
|
||||
|
||||
class GenericAssoc(object):
|
||||
|
||||
def __init__(self, tablename):
|
||||
self.type = tablename
|
||||
|
||||
#TODO: inherit from entity builder
|
||||
class Associable(object):
|
||||
"""An associable Elixir Statement object"""
|
||||
|
||||
def __init__(self, entity, name=None, uselist=True, lazy=True):
|
||||
self.entity = entity
|
||||
self.lazy = lazy
|
||||
self.uselist = uselist
|
||||
|
||||
if name is None:
|
||||
self.name = plural_name
|
||||
else:
|
||||
self.name = name
|
||||
|
||||
def after_table(self):
|
||||
col = sa.Column('%s_assoc_id' % interface_name, sa.Integer,
|
||||
sa.ForeignKey('%s.id' % able_name))
|
||||
self.entity._descriptor.add_column(col)
|
||||
|
||||
if not hasattr(assoc_entity, '_assoc_table'):
|
||||
metadata = assoc_entity._descriptor.metadata
|
||||
association_table = sa.Table("%s" % able_name, metadata,
|
||||
sa.Column('id', sa.Integer, primary_key=True),
|
||||
sa.Column('type', sa.String(40), nullable=False),
|
||||
)
|
||||
tablename = "%s_to_%s" % (able_name, interface_name)
|
||||
association_to_table = sa.Table(tablename, metadata,
|
||||
sa.Column('assoc_id', sa.Integer,
|
||||
sa.ForeignKey(association_table.c.id,
|
||||
ondelete="CASCADE"),
|
||||
primary_key=True),
|
||||
#FIXME: this assumes a single id col
|
||||
sa.Column('%s_id' % interface_name, sa.Integer,
|
||||
sa.ForeignKey(assoc_entity.table.c.id,
|
||||
ondelete="RESTRICT"),
|
||||
primary_key=True),
|
||||
)
|
||||
|
||||
assoc_entity._assoc_table = association_table
|
||||
assoc_entity._assoc_to_table = association_to_table
|
||||
|
||||
def after_mapper(self):
|
||||
if not hasattr(assoc_entity, '_assoc_mapper'):
|
||||
assoc_entity._assoc_mapper = sa.orm.mapper(
|
||||
GenericAssoc, assoc_entity._assoc_table, properties={
|
||||
'targets': sa.orm.relation(
|
||||
assoc_entity,
|
||||
secondary=assoc_entity._assoc_to_table,
|
||||
lazy=lazy, backref='associations',
|
||||
order_by=assoc_entity.mapper.order_by)
|
||||
})
|
||||
|
||||
entity = self.entity
|
||||
entity.mapper.add_property(
|
||||
attr_name,
|
||||
sa.orm.relation(GenericAssoc, lazy=self.lazy,
|
||||
backref='_backref_%s' % entity.table.name)
|
||||
)
|
||||
|
||||
if self.uselist:
|
||||
def get(self):
|
||||
if getattr(self, attr_name) is None:
|
||||
setattr(self, attr_name,
|
||||
GenericAssoc(entity.table.name))
|
||||
return getattr(self, attr_name).targets
|
||||
setattr(entity, self.name, property(get))
|
||||
else:
|
||||
# scalar based property decorator
|
||||
def get(self):
|
||||
attr = getattr(self, attr_name)
|
||||
if attr is not None:
|
||||
return attr.targets[0]
|
||||
else:
|
||||
return None
|
||||
def set(self, value):
|
||||
if getattr(self, attr_name) is None:
|
||||
setattr(self, attr_name,
|
||||
GenericAssoc(entity.table.name))
|
||||
getattr(self, attr_name).targets = [value]
|
||||
setattr(entity, self.name, property(get, set))
|
||||
|
||||
# self.name is both set via mapper synonym and the python
|
||||
# property, but that's how synonym properties work.
|
||||
# adding synonym property after "real" property otherwise it
|
||||
# breaks when using SQLAlchemy > 0.4.1
|
||||
entity.mapper.add_property(self.name, sa.orm.synonym(attr_name))
|
||||
|
||||
# add helper methods
|
||||
def select_by(cls, **kwargs):
|
||||
return cls.query.join([attr_name, 'targets']) \
|
||||
.filter_by(**kwargs).all()
|
||||
setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
|
||||
|
||||
def select(cls, *args, **kwargs):
|
||||
return cls.query.join([attr_name, 'targets']) \
|
||||
.filter(*args, **kwargs).all()
|
||||
setattr(entity, 'select_%s' % self.name, classmethod(select))
|
||||
|
||||
return Statement(Associable)
|
||||
@@ -0,0 +1,124 @@
|
||||
'''
|
||||
An encryption plugin for Elixir utilizing the excellent PyCrypto library, which
|
||||
can be downloaded here: http://www.amk.ca/python/code/crypto
|
||||
|
||||
Values for columns that are specified to be encrypted will be transparently
|
||||
encrypted and safely encoded for storage in a unicode column using the powerful
|
||||
and secure Blowfish Cipher using a specified "secret" which can be passed into
|
||||
the plugin at class declaration time.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
from elixir import *
|
||||
from elixir.ext.encrypted import acts_as_encrypted
|
||||
|
||||
class Person(Entity):
|
||||
name = Field(Unicode)
|
||||
password = Field(Unicode)
|
||||
ssn = Field(Unicode)
|
||||
acts_as_encrypted(for_fields=['password', 'ssn'],
|
||||
with_secret='secret')
|
||||
|
||||
The above Person entity will automatically encrypt and decrypt the password and
|
||||
ssn columns on save, update, and load. Different secrets can be specified on
|
||||
an entity by entity basis, for added security.
|
||||
|
||||
**Important note**: instance attributes are encrypted in-place. This means that
|
||||
if one of the encrypted attributes of an instance is accessed after the
|
||||
instance has been flushed to the database (and thus encrypted), the value for
|
||||
that attribute will be crypted in the in-memory object in addition to the
|
||||
database row.
|
||||
'''
|
||||
|
||||
from Crypto.Cipher import Blowfish
|
||||
from elixir.statements import Statement
|
||||
from sqlalchemy.orm import MapperExtension, EXT_CONTINUE, EXT_STOP
|
||||
|
||||
try:
|
||||
from sqlalchemy.orm import EXT_PASS
|
||||
SA05orlater = False
|
||||
except ImportError:
|
||||
SA05orlater = True
|
||||
|
||||
__all__ = ['acts_as_encrypted']
|
||||
__doc_all__ = []
|
||||
|
||||
|
||||
#
|
||||
# encryption and decryption functions
|
||||
#
|
||||
|
||||
def encrypt_value(value, secret):
|
||||
return Blowfish.new(secret, Blowfish.MODE_CFB) \
|
||||
.encrypt(value).encode('string_escape')
|
||||
|
||||
def decrypt_value(value, secret):
|
||||
return Blowfish.new(secret, Blowfish.MODE_CFB) \
|
||||
.decrypt(value.decode('string_escape'))
|
||||
|
||||
|
||||
#
|
||||
# acts_as_encrypted statement
|
||||
#
|
||||
|
||||
class ActsAsEncrypted(object):
|
||||
|
||||
def __init__(self, entity, for_fields=[], with_secret='abcdef'):
|
||||
|
||||
def perform_encryption(instance, encrypt=True):
|
||||
encrypted = getattr(instance, '_elixir_encrypted', None)
|
||||
if encrypted is encrypt:
|
||||
# skipping encryption or decryption, as it is already done
|
||||
return
|
||||
else:
|
||||
# marking instance as already encrypted/decrypted
|
||||
instance._elixir_encrypted = encrypt
|
||||
|
||||
if encrypt:
|
||||
func = encrypt_value
|
||||
else:
|
||||
func = decrypt_value
|
||||
|
||||
for column_name in for_fields:
|
||||
current_value = getattr(instance, column_name)
|
||||
if current_value:
|
||||
setattr(instance, column_name,
|
||||
func(current_value, with_secret))
|
||||
|
||||
def perform_decryption(instance):
|
||||
perform_encryption(instance, encrypt=False)
|
||||
|
||||
class EncryptedMapperExtension(MapperExtension):
|
||||
|
||||
def before_insert(self, mapper, connection, instance):
|
||||
perform_encryption(instance)
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_update(self, mapper, connection, instance):
|
||||
perform_encryption(instance)
|
||||
return EXT_CONTINUE
|
||||
|
||||
if SA05orlater:
|
||||
def reconstruct_instance(self, mapper, instance):
|
||||
perform_decryption(instance)
|
||||
# no special return value is required for
|
||||
# reconstruct_instance, but you never know...
|
||||
return EXT_CONTINUE
|
||||
else:
|
||||
def populate_instance(self, mapper, selectcontext, row,
|
||||
instance, *args, **kwargs):
|
||||
mapper.populate_instance(selectcontext, instance, row,
|
||||
*args, **kwargs)
|
||||
perform_decryption(instance)
|
||||
# EXT_STOP because we already did populate the instance and
|
||||
# the normal processing should not happen
|
||||
return EXT_STOP
|
||||
|
||||
# make sure that the entity's mapper has our mapper extension
|
||||
entity._descriptor.add_mapper_extension(EncryptedMapperExtension())
|
||||
|
||||
|
||||
acts_as_encrypted = Statement(ActsAsEncrypted)
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
'''
|
||||
This extension is DEPRECATED. Please use the orderinglist SQLAlchemy
|
||||
extension instead.
|
||||
|
||||
For details:
|
||||
http://www.sqlalchemy.org/docs/05/reference/ext/orderinglist.html
|
||||
|
||||
For an Elixir example:
|
||||
http://elixir.ematia.de/trac/wiki/Recipes/UsingEntityForOrderedList
|
||||
or
|
||||
http://elixir.ematia.de/trac/browser/elixir/0.7.0/tests/test_o2m.py#L155
|
||||
|
||||
|
||||
|
||||
An ordered-list plugin for Elixir to help you make an entity be able to be
|
||||
managed in a list-like way. Much inspiration comes from the Ruby on Rails
|
||||
acts_as_list plugin, which is currently more full-featured than this plugin.
|
||||
|
||||
Once you flag an entity with an `acts_as_list()` statement, a column will be
|
||||
added to the entity called `position` which will be an integer column that is
|
||||
managed for you by the plugin. You can pass an alternative column name to
|
||||
the plugin using the `column_name` keyword argument.
|
||||
|
||||
In addition, your entity will get a series of new methods attached to it,
|
||||
including:
|
||||
|
||||
+----------------------+------------------------------------------------------+
|
||||
| Method Name | Description |
|
||||
+======================+======================================================+
|
||||
| ``move_lower`` | Move the item lower in the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_higher`` | Move the item higher in the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_to_bottom`` | Move the item to the bottom of the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_to_top`` | Move the item to the top of the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
| ``move_to`` | Move the item to a specific position in the list |
|
||||
+----------------------+------------------------------------------------------+
|
||||
|
||||
|
||||
Sometimes, your entities that represent list items will be a part of different
|
||||
lists. To implement this behavior, simply pass the `acts_as_list` statement a
|
||||
callable that returns a "qualifier" SQLAlchemy expression. This expression will
|
||||
be added to the generated WHERE clauses used by the plugin.
|
||||
|
||||
Example model usage:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
from elixir import *
|
||||
from elixir.ext.list import acts_as_list
|
||||
|
||||
class ToDo(Entity):
|
||||
subject = Field(String(128))
|
||||
owner = ManyToOne('Person')
|
||||
|
||||
def qualify(self):
|
||||
return ToDo.owner_id == self.owner_id
|
||||
|
||||
acts_as_list(qualifier=qualify)
|
||||
|
||||
class Person(Entity):
|
||||
name = Field(String(64))
|
||||
todos = OneToMany('ToDo', order_by='position')
|
||||
|
||||
|
||||
The above example can then be used to manage ordered todo lists for people.
|
||||
Note that you must set the `order_by` property on the `Person.todo` relation in
|
||||
order for the relation to respect the ordering. Here is an example of using
|
||||
this model in practice:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
p = Person.query.filter_by(name='Jonathan').one()
|
||||
p.todos.append(ToDo(subject='Three'))
|
||||
p.todos.append(ToDo(subject='Two'))
|
||||
p.todos.append(ToDo(subject='One'))
|
||||
session.commit(); session.clear()
|
||||
|
||||
p = Person.query.filter_by(name='Jonathan').one()
|
||||
p.todos[0].move_to_bottom()
|
||||
p.todos[2].move_to_top()
|
||||
session.commit(); session.clear()
|
||||
|
||||
p = Person.query.filter_by(name='Jonathan').one()
|
||||
assert p.todos[0].subject == 'One'
|
||||
assert p.todos[1].subject == 'Two'
|
||||
assert p.todos[2].subject == 'Three'
|
||||
|
||||
|
||||
For more examples, refer to the unit tests for this plugin.
|
||||
'''
|
||||
|
||||
from elixir.statements import Statement
|
||||
from elixir.events import before_insert, before_delete
|
||||
from sqlalchemy import Column, Integer, select, func, literal, and_
|
||||
import warnings
|
||||
|
||||
__all__ = ['acts_as_list']
|
||||
__doc_all__ = []
|
||||
|
||||
|
||||
def get_entity_where(instance):
|
||||
clauses = []
|
||||
for column in instance.table.primary_key.columns:
|
||||
instance_value = getattr(instance, column.name)
|
||||
clauses.append(column == instance_value)
|
||||
return and_(*clauses)
|
||||
|
||||
|
||||
class ListEntityBuilder(object):
|
||||
|
||||
def __init__(self, entity, qualifier=None, column_name='position'):
|
||||
warnings.warn("The act_as_list extension is deprecated. Please use "
|
||||
"SQLAlchemy's orderinglist extension instead",
|
||||
DeprecationWarning, stacklevel=6)
|
||||
self.entity = entity
|
||||
self.qualifier_method = qualifier
|
||||
self.column_name = column_name
|
||||
|
||||
def create_non_pk_cols(self):
|
||||
if self.entity._descriptor.autoload:
|
||||
for c in self.entity.table.c:
|
||||
if c.name == self.column_name:
|
||||
self.position_column = c
|
||||
if not hasattr(self, 'position_column'):
|
||||
raise Exception(
|
||||
"Could not find column '%s' in autoloaded table '%s', "
|
||||
"needed by entity '%s'." % (self.column_name,
|
||||
self.entity.table.name, self.entity.__name__))
|
||||
else:
|
||||
self.position_column = Column(self.column_name, Integer)
|
||||
self.entity._descriptor.add_column(self.position_column)
|
||||
|
||||
def after_table(self):
|
||||
position_column = self.position_column
|
||||
position_column_name = self.column_name
|
||||
|
||||
qualifier_method = self.qualifier_method
|
||||
if not qualifier_method:
|
||||
qualifier_method = lambda self: None
|
||||
|
||||
def _init_position(self):
|
||||
s = select(
|
||||
[(func.max(position_column)+1).label('value')],
|
||||
qualifier_method(self)
|
||||
).union(
|
||||
select([literal(1).label('value')])
|
||||
)
|
||||
a = s.alias()
|
||||
# we use a second func.max to get the maximum between 1 and the
|
||||
# real max position if any exist
|
||||
setattr(self, position_column_name, select([func.max(a.c.value)]))
|
||||
|
||||
# Note that this method could be rewritten more simply like below,
|
||||
# but because this extension is going to be deprecated anyway,
|
||||
# I don't want to risk breaking something I don't want to maintain.
|
||||
# setattr(self, position_column_name, select(
|
||||
# [func.coalesce(func.max(position_column), 0) + 1],
|
||||
# qualifier_method(self)
|
||||
# ))
|
||||
_init_position = before_insert(_init_position)
|
||||
|
||||
def _shift_items(self):
|
||||
self.table.update(
|
||||
and_(
|
||||
position_column > getattr(self, position_column_name),
|
||||
qualifier_method(self)
|
||||
),
|
||||
values={
|
||||
position_column : position_column - 1
|
||||
}
|
||||
).execute()
|
||||
_shift_items = before_delete(_shift_items)
|
||||
|
||||
def move_to_bottom(self):
|
||||
# move the items that were above this item up one
|
||||
self.table.update(
|
||||
and_(
|
||||
position_column >= getattr(self, position_column_name),
|
||||
qualifier_method(self)
|
||||
),
|
||||
values = {
|
||||
position_column : position_column - 1
|
||||
}
|
||||
).execute()
|
||||
|
||||
# move this item to the max position
|
||||
# MySQL does not support the correlated subquery, so we need to
|
||||
# execute the query (through scalar()). See ticket #34.
|
||||
self.table.update(
|
||||
get_entity_where(self),
|
||||
values={
|
||||
position_column : select(
|
||||
[func.max(position_column) + 1],
|
||||
qualifier_method(self)
|
||||
).scalar()
|
||||
}
|
||||
).execute()
|
||||
|
||||
def move_to_top(self):
|
||||
self.move_to(1)
|
||||
|
||||
def move_to(self, position):
|
||||
current_position = getattr(self, position_column_name)
|
||||
|
||||
# determine which direction we're moving
|
||||
if position < current_position:
|
||||
where = and_(
|
||||
position <= position_column,
|
||||
position_column < current_position,
|
||||
qualifier_method(self)
|
||||
)
|
||||
modifier = 1
|
||||
elif position > current_position:
|
||||
where = and_(
|
||||
current_position < position_column,
|
||||
position_column <= position,
|
||||
qualifier_method(self)
|
||||
)
|
||||
modifier = -1
|
||||
|
||||
# shift the items in between the current and new positions
|
||||
self.table.update(where, values = {
|
||||
position_column : position_column + modifier
|
||||
}).execute()
|
||||
|
||||
# update this item's position to the desired position
|
||||
self.table.update(get_entity_where(self)) \
|
||||
.execute(**{position_column_name: position})
|
||||
|
||||
def move_lower(self):
|
||||
# replace for ex.: p.todos.insert(x + 1, p.todos.pop(x))
|
||||
self.move_to(getattr(self, position_column_name) + 1)
|
||||
|
||||
def move_higher(self):
|
||||
self.move_to(getattr(self, position_column_name) - 1)
|
||||
|
||||
|
||||
# attach new methods to entity
|
||||
self.entity._init_position = _init_position
|
||||
self.entity._shift_items = _shift_items
|
||||
self.entity.move_lower = move_lower
|
||||
self.entity.move_higher = move_higher
|
||||
self.entity.move_to_bottom = move_to_bottom
|
||||
self.entity.move_to_top = move_to_top
|
||||
self.entity.move_to = move_to
|
||||
|
||||
|
||||
acts_as_list = Statement(ListEntityBuilder)
|
||||
@@ -0,0 +1,106 @@
|
||||
'''
|
||||
DDL statements for Elixir.
|
||||
|
||||
Entities having the perform_ddl statement, will automatically execute the
|
||||
given DDL statement, at the given moment: ether before or after the table
|
||||
creation in SQL.
|
||||
|
||||
The 'when' argument can be either 'before-create' or 'after-create'.
|
||||
The 'statement' argument can be one of:
|
||||
|
||||
- a single string statement
|
||||
- a list of string statements, in which case, each of them will be executed
|
||||
in turn.
|
||||
- a callable which should take no argument and return either a single string
|
||||
or a list of strings.
|
||||
|
||||
In each string statement, you may use the special '%(fullname)s' construct,
|
||||
that will be replaced with the real table name including schema, if unknown
|
||||
to you. Also, self explained '%(table)s' and '%(schema)s' may be used here.
|
||||
|
||||
You would use this extension to handle non elixir sql statemts, like triggers
|
||||
etc.
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class Movie(Entity):
|
||||
title = Field(Unicode(30), primary_key=True)
|
||||
year = Field(Integer)
|
||||
|
||||
perform_ddl('after-create',
|
||||
"insert into %(fullname)s values ('Alien', 1979)")
|
||||
|
||||
preload_data is a more specific statement meant to preload data in your
|
||||
entity table from a list of tuples (of fields values for each row).
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class Movie(Entity):
|
||||
title = Field(Unicode(30), primary_key=True)
|
||||
year = Field(Integer)
|
||||
|
||||
preload_data(('title', 'year'),
|
||||
[(u'Alien', 1979), (u'Star Wars', 1977)])
|
||||
preload_data(('year', 'title'),
|
||||
[(1982, u'Blade Runner')])
|
||||
preload_data(data=[(u'Batman', 1966)])
|
||||
'''
|
||||
|
||||
from elixir.statements import Statement
|
||||
from elixir.properties import EntityBuilder
|
||||
from sqlalchemy import DDL
|
||||
|
||||
__all__ = ['perform_ddl', 'preload_data']
|
||||
__doc_all__ = []
|
||||
|
||||
#
|
||||
# the perform_ddl statement
|
||||
#
|
||||
class PerformDDLEntityBuilder(EntityBuilder):
|
||||
|
||||
def __init__(self, entity, when, statement, on=None, context=None):
|
||||
self.entity = entity
|
||||
self.when = when
|
||||
self.statement = statement
|
||||
self.on = on
|
||||
self.context = context
|
||||
|
||||
def after_table(self):
|
||||
statement = self.statement
|
||||
if hasattr(statement, '__call__'):
|
||||
statement = statement()
|
||||
if not isinstance(statement, list):
|
||||
statement = [statement]
|
||||
for s in statement:
|
||||
ddl = DDL(s, self.on, self.context)
|
||||
ddl.execute_at(self.when, self.entity.table)
|
||||
|
||||
perform_ddl = Statement(PerformDDLEntityBuilder)
|
||||
|
||||
#
|
||||
# the preload_data statement
|
||||
#
|
||||
class PreloadDataEntityBuilder(EntityBuilder):
|
||||
|
||||
def __init__(self, entity, columns=None, data=None):
|
||||
self.entity = entity
|
||||
self.columns = columns
|
||||
self.data = data
|
||||
|
||||
def after_table(self):
|
||||
all_columns = [col.name for col in self.entity.table.columns]
|
||||
def onload(event, schema_item, connection):
|
||||
columns = self.columns
|
||||
if columns is None:
|
||||
columns = all_columns
|
||||
data = self.data
|
||||
if hasattr(data, '__call__'):
|
||||
data = data()
|
||||
insert = schema_item.insert()
|
||||
connection.execute(insert,
|
||||
[dict(zip(columns, values)) for values in data])
|
||||
|
||||
self.entity.table.append_ddl_listener('after-create', onload)
|
||||
|
||||
preload_data = Statement(PreloadDataEntityBuilder)
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
'''
|
||||
A versioning plugin for Elixir.
|
||||
|
||||
Entities that are marked as versioned with the `acts_as_versioned` statement
|
||||
will automatically have a history table created and a timestamp and version
|
||||
column added to their tables. In addition, versioned entities are provided
|
||||
with four new methods: revert, revert_to, compare_with and get_as_of, and one
|
||||
new attribute: versions. Entities with compound primary keys are supported.
|
||||
|
||||
The `versions` attribute will contain a list of previous versions of the
|
||||
instance, in increasing version number order.
|
||||
|
||||
The `get_as_of` method will retrieve a previous version of the instance "as of"
|
||||
a specified datetime. If the current version is the most recent, it will be
|
||||
returned.
|
||||
|
||||
The `revert` method will rollback the current instance to its previous version,
|
||||
if possible. Once reverted, the current instance will be expired from the
|
||||
session, and you will need to fetch it again to retrieve the now reverted
|
||||
instance.
|
||||
|
||||
The `revert_to` method will rollback the current instance to the specified
|
||||
version number, if possibe. Once reverted, the current instance will be expired
|
||||
from the session, and you will need to fetch it again to retrieve the now
|
||||
reverted instance.
|
||||
|
||||
The `compare_with` method will compare the instance with a previous version. A
|
||||
dictionary will be returned with each field difference as an element in the
|
||||
dictionary where the key is the field name and the value is a tuple of the
|
||||
format (current_value, version_value). Version instances also have a
|
||||
`compare_with` method so that two versions can be compared.
|
||||
|
||||
Also included in the module is a `after_revert` decorator that can be used to
|
||||
decorate methods on the versioned entity that will be called following that
|
||||
instance being reverted.
|
||||
|
||||
The acts_as_versioned statement also accepts an optional `ignore` argument
|
||||
that consists of a list of strings, specifying names of fields. Changes in
|
||||
those fields will not result in a version increment. In addition, you can
|
||||
pass in an optional `check_concurrent` argument, which will use SQLAlchemy's
|
||||
built-in optimistic concurrency mechanisms.
|
||||
|
||||
Note that relationships that are stored in mapping tables will not be included
|
||||
as part of the versioning process, and will need to be handled manually. Only
|
||||
values within the entity's main table will be versioned into the history table.
|
||||
'''
|
||||
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
|
||||
from sqlalchemy import Table, Column, and_, desc
|
||||
from sqlalchemy.orm import mapper, MapperExtension, EXT_CONTINUE, \
|
||||
object_session
|
||||
|
||||
from elixir import Integer, DateTime
|
||||
from elixir.statements import Statement
|
||||
from elixir.properties import EntityBuilder
|
||||
from elixir.entity import getmembers
|
||||
|
||||
__all__ = ['acts_as_versioned', 'after_revert']
|
||||
__doc_all__ = []
|
||||
|
||||
#
|
||||
# utility functions
|
||||
#
|
||||
|
||||
def get_entity_where(instance):
|
||||
clauses = []
|
||||
for column in instance.table.primary_key.columns:
|
||||
instance_value = getattr(instance, column.name)
|
||||
clauses.append(column==instance_value)
|
||||
return and_(*clauses)
|
||||
|
||||
|
||||
def get_history_where(instance):
|
||||
clauses = []
|
||||
history_columns = instance.__history_table__.primary_key.columns
|
||||
for column in instance.table.primary_key.columns:
|
||||
instance_value = getattr(instance, column.name)
|
||||
history_column = getattr(history_columns, column.name)
|
||||
clauses.append(history_column==instance_value)
|
||||
return and_(*clauses)
|
||||
|
||||
|
||||
#
|
||||
# a mapper extension to track versions on insert, update, and delete
|
||||
#
|
||||
|
||||
class VersionedMapperExtension(MapperExtension):
|
||||
def before_insert(self, mapper, connection, instance):
|
||||
version_colname, timestamp_colname = \
|
||||
instance.__class__.__versioned_column_names__
|
||||
setattr(instance, version_colname, 1)
|
||||
setattr(instance, timestamp_colname, datetime.now())
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_update(self, mapper, connection, instance):
|
||||
old_values = instance.table.select(get_entity_where(instance)) \
|
||||
.execute().fetchone()
|
||||
|
||||
# SA might've flagged this for an update even though it didn't change.
|
||||
# This occurs when a relation is updated, thus marking this instance
|
||||
# for a save/update operation. We check here against the last version
|
||||
# to ensure we really should save this version and update the version
|
||||
# data.
|
||||
ignored = instance.__class__.__ignored_fields__
|
||||
version_colname, timestamp_colname = \
|
||||
instance.__class__.__versioned_column_names__
|
||||
for key in instance.table.c.keys():
|
||||
if key in ignored:
|
||||
continue
|
||||
if getattr(instance, key) != old_values[key]:
|
||||
# the instance was really updated, so we create a new version
|
||||
dict_values = dict(old_values.items())
|
||||
connection.execute(
|
||||
instance.__class__.__history_table__.insert(), dict_values)
|
||||
old_version = getattr(instance, version_colname)
|
||||
setattr(instance, version_colname, old_version + 1)
|
||||
setattr(instance, timestamp_colname, datetime.now())
|
||||
break
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_delete(self, mapper, connection, instance):
|
||||
connection.execute(instance.__history_table__.delete(
|
||||
get_history_where(instance)
|
||||
))
|
||||
return EXT_CONTINUE
|
||||
|
||||
|
||||
versioned_mapper_extension = VersionedMapperExtension()
|
||||
|
||||
|
||||
#
|
||||
# the acts_as_versioned statement
|
||||
#
|
||||
|
||||
class VersionedEntityBuilder(EntityBuilder):
|
||||
|
||||
def __init__(self, entity, ignore=None, check_concurrent=False,
|
||||
column_names=None):
|
||||
self.entity = entity
|
||||
self.add_mapper_extension(versioned_mapper_extension)
|
||||
#TODO: we should rather check that the version_id_col isn't set
|
||||
# externally
|
||||
self.check_concurrent = check_concurrent
|
||||
|
||||
# Changes in these fields will be ignored
|
||||
if column_names is None:
|
||||
column_names = ['version', 'timestamp']
|
||||
entity.__versioned_column_names__ = column_names
|
||||
if ignore is None:
|
||||
ignore = []
|
||||
ignore.extend(column_names)
|
||||
entity.__ignored_fields__ = ignore
|
||||
|
||||
def create_non_pk_cols(self):
|
||||
# add a version column to the entity, along with a timestamp
|
||||
version_colname, timestamp_colname = \
|
||||
self.entity.__versioned_column_names__
|
||||
#XXX: fail in case the columns already exist?
|
||||
#col_names = [col.name for col in self.entity._descriptor.columns]
|
||||
#if version_colname not in col_names:
|
||||
self.add_table_column(Column(version_colname, Integer))
|
||||
#if timestamp_colname not in col_names:
|
||||
self.add_table_column(Column(timestamp_colname, DateTime))
|
||||
|
||||
# add a concurrent_version column to the entity, if required
|
||||
if self.check_concurrent:
|
||||
self.entity._descriptor.version_id_col = 'concurrent_version'
|
||||
|
||||
# we copy columns from the main entity table, so we need it to exist first
|
||||
def after_table(self):
|
||||
entity = self.entity
|
||||
version_colname, timestamp_colname = \
|
||||
entity.__versioned_column_names__
|
||||
|
||||
# look for events
|
||||
after_revert_events = []
|
||||
for name, func in getmembers(entity, inspect.ismethod):
|
||||
if getattr(func, '_elixir_after_revert', False):
|
||||
after_revert_events.append(func)
|
||||
|
||||
# create a history table for the entity
|
||||
skipped_columns = [version_colname]
|
||||
if self.check_concurrent:
|
||||
skipped_columns.append('concurrent_version')
|
||||
|
||||
columns = [
|
||||
column.copy() for column in entity.table.c
|
||||
if column.name not in skipped_columns
|
||||
]
|
||||
columns.append(Column(version_colname, Integer, primary_key=True))
|
||||
table = Table(entity.table.name + '_history', entity.table.metadata,
|
||||
*columns
|
||||
)
|
||||
entity.__history_table__ = table
|
||||
|
||||
# create an object that represents a version of this entity
|
||||
class Version(object):
|
||||
pass
|
||||
|
||||
# map the version class to the history table for this entity
|
||||
Version.__name__ = entity.__name__ + 'Version'
|
||||
Version.__versioned_entity__ = entity
|
||||
mapper(Version, entity.__history_table__)
|
||||
|
||||
version_col = getattr(table.c, version_colname)
|
||||
timestamp_col = getattr(table.c, timestamp_colname)
|
||||
|
||||
# attach utility methods and properties to the entity
|
||||
def get_versions(self):
|
||||
v = object_session(self).query(Version) \
|
||||
.filter(get_history_where(self)) \
|
||||
.order_by(version_col) \
|
||||
.all()
|
||||
# history contains all the previous records.
|
||||
# Add the current one to the list to get all the versions
|
||||
v.append(self)
|
||||
return v
|
||||
|
||||
def get_as_of(self, dt):
|
||||
# if the passed in timestamp is older than our current version's
|
||||
# time stamp, then the most recent version is our current version
|
||||
if getattr(self, timestamp_colname) < dt:
|
||||
return self
|
||||
|
||||
# otherwise, we need to look to the history table to get our
|
||||
# older version
|
||||
sess = object_session(self)
|
||||
query = sess.query(Version) \
|
||||
.filter(and_(get_history_where(self),
|
||||
timestamp_col <= dt)) \
|
||||
.order_by(desc(timestamp_col)).limit(1)
|
||||
return query.first()
|
||||
|
||||
def revert_to(self, to_version):
|
||||
if isinstance(to_version, Version):
|
||||
to_version = getattr(to_version, version_colname)
|
||||
|
||||
old_version = table.select(and_(
|
||||
get_history_where(self),
|
||||
version_col == to_version
|
||||
)).execute().fetchone()
|
||||
|
||||
entity.table.update(get_entity_where(self)).execute(
|
||||
dict(old_version.items())
|
||||
)
|
||||
|
||||
table.delete(and_(get_history_where(self),
|
||||
version_col >= to_version)).execute()
|
||||
self.expire()
|
||||
for event in after_revert_events:
|
||||
event(self)
|
||||
|
||||
def revert(self):
|
||||
assert getattr(self, version_colname) > 1
|
||||
self.revert_to(getattr(self, version_colname) - 1)
|
||||
|
||||
def compare_with(self, version):
|
||||
differences = {}
|
||||
for column in self.table.c:
|
||||
if column.name in (version_colname, 'concurrent_version'):
|
||||
continue
|
||||
this = getattr(self, column.name)
|
||||
that = getattr(version, column.name)
|
||||
if this != that:
|
||||
differences[column.name] = (this, that)
|
||||
return differences
|
||||
|
||||
entity.versions = property(get_versions)
|
||||
entity.get_as_of = get_as_of
|
||||
entity.revert_to = revert_to
|
||||
entity.revert = revert
|
||||
entity.compare_with = compare_with
|
||||
Version.compare_with = compare_with
|
||||
|
||||
acts_as_versioned = Statement(VersionedEntityBuilder)
|
||||
|
||||
|
||||
def after_revert(func):
|
||||
"""
|
||||
Decorator for watching for revert events.
|
||||
"""
|
||||
func._elixir_after_revert = True
|
||||
return func
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
'''
|
||||
This module provides support for defining the fields (columns) of your
|
||||
entities. Elixir currently supports two syntaxes to do so: the default
|
||||
`Attribute-based syntax`_ as well as the has_field_ DSL statement.
|
||||
|
||||
Attribute-based syntax
|
||||
----------------------
|
||||
|
||||
Here is a quick example of how to use the object-oriented syntax.
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class Person(Entity):
|
||||
id = Field(Integer, primary_key=True)
|
||||
name = Field(String(50), required=True)
|
||||
ssn = Field(String(50), unique=True)
|
||||
biography = Field(Text)
|
||||
join_date = Field(DateTime, default=datetime.datetime.now)
|
||||
photo = Field(Binary, deferred=True)
|
||||
_email = Field(String(20), colname='email', synonym='email')
|
||||
|
||||
def _set_email(self, email):
|
||||
self._email = email
|
||||
def _get_email(self):
|
||||
return self._email
|
||||
email = property(_get_email, _set_email)
|
||||
|
||||
|
||||
The Field class takes one mandatory argument, which is its type. Please refer
|
||||
to SQLAlchemy documentation for a list of `types supported by SQLAlchemy
|
||||
<http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/types.html>`_.
|
||||
|
||||
Following that first mandatory argument, fields can take any number of
|
||||
optional keyword arguments. Please note that all the **arguments** that are
|
||||
**not specifically processed by Elixir**, as mentioned in the documentation
|
||||
below **are passed on to the SQLAlchemy ``Column`` object**. Please refer to
|
||||
the `SQLAlchemy Column object's documentation
|
||||
<http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/schema.html
|
||||
#sqlalchemy.schema.Column>`_ for more details about other
|
||||
supported keyword arguments.
|
||||
|
||||
The following Elixir-specific arguments are supported:
|
||||
|
||||
+-------------------+---------------------------------------------------------+
|
||||
| Argument Name | Description |
|
||||
+===================+=========================================================+
|
||||
| ``required`` | Specify whether or not this field can be set to None |
|
||||
| | (left without a value). Defaults to ``False``, unless |
|
||||
| | the field is a primary key. |
|
||||
+-------------------+---------------------------------------------------------+
|
||||
| ``colname`` | Specify a custom name for the column of this field. By |
|
||||
| | default the column will have the same name as the |
|
||||
| | attribute. |
|
||||
+-------------------+---------------------------------------------------------+
|
||||
| ``deferred`` | Specify whether this particular column should be |
|
||||
| | fetched by default (along with the other columns) when |
|
||||
| | an instance of the entity is fetched from the database |
|
||||
| | or rather only later on when this particular column is |
|
||||
| | first referenced. This can be useful when one wants to |
|
||||
| | avoid loading a large text or binary field into memory |
|
||||
| | when its not needed. Individual columns can be lazy |
|
||||
| | loaded by themselves (by using ``deferred=True``) |
|
||||
| | or placed into groups that lazy-load together (by using |
|
||||
| | ``deferred`` = `"group_name"`). |
|
||||
+-------------------+---------------------------------------------------------+
|
||||
| ``synonym`` | Specify a synonym name for this field. The field will |
|
||||
| | also be usable under that name in keyword-based Query |
|
||||
| | functions such as filter_by. The Synonym class (see the |
|
||||
| | `properties` module) provides a similar functionality |
|
||||
| | with an (arguably) nicer syntax, but a limited scope. |
|
||||
+-------------------+---------------------------------------------------------+
|
||||
|
||||
has_field
|
||||
---------
|
||||
|
||||
The `has_field` statement allows you to define fields one at a time.
|
||||
|
||||
The first argument is the name of the field, the second is its type. Following
|
||||
these, any number of keyword arguments can be specified for additional
|
||||
behavior. The following arguments are supported:
|
||||
|
||||
+-------------------+---------------------------------------------------------+
|
||||
| Argument Name | Description |
|
||||
+===================+=========================================================+
|
||||
| ``through`` | Specify a relation name to go through. This field will |
|
||||
| | not exist as a column on the database but will be a |
|
||||
| | property which automatically proxy values to the |
|
||||
| | ``attribute`` attribute of the object pointed to by the |
|
||||
| | relation. If the ``attribute`` argument is not present, |
|
||||
| | the name of the current field will be used. In an |
|
||||
| | has_field statement, you can only proxy through a |
|
||||
| | belongs_to or an has_one relationship. |
|
||||
+-------------------+---------------------------------------------------------+
|
||||
| ``attribute`` | Name of the "endpoint" attribute to proxy to. This |
|
||||
| | should only be used in combination with the ``through`` |
|
||||
| | argument. |
|
||||
+-------------------+---------------------------------------------------------+
|
||||
|
||||
|
||||
Here is a quick example of how to use ``has_field``.
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class Person(Entity):
|
||||
has_field('id', Integer, primary_key=True)
|
||||
has_field('name', String(50))
|
||||
'''
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy.orm import deferred, synonym
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
||||
from elixir.statements import ClassMutator
|
||||
from elixir.properties import Property
|
||||
|
||||
__doc_all__ = ['Field']
|
||||
|
||||
|
||||
class Field(Property):
|
||||
'''
|
||||
Represents the definition of a 'field' on an entity.
|
||||
|
||||
This class represents a column on the table where the entity is stored.
|
||||
'''
|
||||
|
||||
def __init__(self, type, *args, **kwargs):
|
||||
super(Field, self).__init__()
|
||||
|
||||
self.colname = kwargs.pop('colname', None)
|
||||
self.synonym = kwargs.pop('synonym', None)
|
||||
self.deferred = kwargs.pop('deferred', False)
|
||||
if 'required' in kwargs:
|
||||
kwargs['nullable'] = not kwargs.pop('required')
|
||||
self.type = type
|
||||
self.primary_key = kwargs.get('primary_key', False)
|
||||
|
||||
self.column = None
|
||||
self.property = None
|
||||
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def attach(self, entity, name):
|
||||
# If no colname was defined (through the 'colname' kwarg), set
|
||||
# it to the name of the attr.
|
||||
if self.colname is None:
|
||||
self.colname = name
|
||||
super(Field, self).attach(entity, name)
|
||||
|
||||
def create_pk_cols(self):
|
||||
if self.primary_key:
|
||||
self.create_col()
|
||||
|
||||
def create_non_pk_cols(self):
|
||||
if not self.primary_key:
|
||||
self.create_col()
|
||||
|
||||
def create_col(self):
|
||||
self.column = Column(self.colname, self.type,
|
||||
*self.args, **self.kwargs)
|
||||
self.add_table_column(self.column)
|
||||
|
||||
def create_properties(self):
|
||||
if self.deferred:
|
||||
group = None
|
||||
if isinstance(self.deferred, basestring):
|
||||
group = self.deferred
|
||||
self.property = deferred(self.column, group=group)
|
||||
elif self.name != self.colname:
|
||||
# if the property name is different from the column name, we need
|
||||
# to add an explicit property (otherwise nothing is needed as it's
|
||||
# done automatically by SA)
|
||||
self.property = self.column
|
||||
|
||||
if self.property is not None:
|
||||
self.add_mapper_property(self.name, self.property)
|
||||
|
||||
if self.synonym:
|
||||
self.add_mapper_property(self.synonym, synonym(self.name))
|
||||
|
||||
|
||||
def has_field_handler(entity, name, *args, **kwargs):
|
||||
if 'through' in kwargs:
|
||||
setattr(entity, name,
|
||||
association_proxy(kwargs.pop('through'),
|
||||
kwargs.pop('attribute', name),
|
||||
**kwargs))
|
||||
return
|
||||
field = Field(*args, **kwargs)
|
||||
field.attach(entity, name)
|
||||
|
||||
has_field = ClassMutator(has_field_handler)
|
||||
@@ -0,0 +1,285 @@
|
||||
'''
|
||||
This module provides support for defining several options on your Elixir
|
||||
entities. There are three different kinds of options that can be set
|
||||
up, and for this there are three different statements: using_options_,
|
||||
using_table_options_ and using_mapper_options_.
|
||||
|
||||
Alternatively, these options can be set on all Elixir entities by modifying
|
||||
the `options_defaults` dictionary before defining any entity.
|
||||
|
||||
`using_options`
|
||||
---------------
|
||||
The 'using_options' DSL statement allows you to set up some additional
|
||||
behaviors on your model objects, including table names, ordering, and
|
||||
more. To specify an option, simply supply the option as a keyword
|
||||
argument onto the statement, as follows:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class Person(Entity):
|
||||
name = Field(Unicode(64))
|
||||
|
||||
using_options(shortnames=True, order_by='name')
|
||||
|
||||
The list of supported arguments are as follows:
|
||||
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| Option Name | Description |
|
||||
+=====================+=======================================================+
|
||||
| ``inheritance`` | Specify the type of inheritance this entity must use. |
|
||||
| | It can be one of ``single``, ``concrete`` or |
|
||||
| | ``multi``. Defaults to ``single``. |
|
||||
| | Note that polymorphic concrete inheritance is |
|
||||
| | currently not implemented. See: |
|
||||
| | http://www.sqlalchemy.org/docs/05/mappers.html |
|
||||
| | #mapping-class-inheritance-hierarchies for an |
|
||||
| | explanation of the different kinds of inheritances. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``abstract`` | Set 'abstract'=True to declare abstract entity. |
|
||||
| | Abstract base classes are useful when you want to put |
|
||||
| | some common information into a number of other |
|
||||
| | entities. Abstract entity will not be used to create |
|
||||
| | any database table. Instead, when it is used as a base|
|
||||
| | class for other entity, its fields will be added to |
|
||||
| | those of the child class. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``polymorphic`` | Whether the inheritance should be polymorphic or not. |
|
||||
| | Defaults to ``True``. The column used to store the |
|
||||
| | type of each row is named "row_type" by default. You |
|
||||
| | can change this by passing the desired name for the |
|
||||
| | column to this argument. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``identity`` | Specify a custom polymorphic identity. When using |
|
||||
| | polymorphic inheritance, this value (usually a |
|
||||
| | string) will represent this particular entity (class) |
|
||||
| | . It will be used to differentiate it from other |
|
||||
| | entities (classes) in your inheritance hierarchy when |
|
||||
| | loading from the database instances of different |
|
||||
| | entities in that hierarchy at the same time. |
|
||||
| | This value will be stored by default in the |
|
||||
| | "row_type" column of the entity's table (see above). |
|
||||
| | You can either provide a |
|
||||
| | plain string or a callable. The callable will be |
|
||||
| | given the entity (ie class) as argument and must |
|
||||
| | return a value (usually a string) representing the |
|
||||
| | polymorphic identity of that entity. |
|
||||
| | By default, this value is automatically generated: it |
|
||||
| | is the name of the entity lower-cased. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``metadata`` | Specify a custom MetaData for this entity. |
|
||||
| | By default, entities uses the global |
|
||||
| | ``elixir.metadata``. |
|
||||
| | This option can also be set for all entities of a |
|
||||
| | module by setting the ``__metadata__`` attribute of |
|
||||
| | that module. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``autoload`` | Automatically load column definitions from the |
|
||||
| | existing database table. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``tablename`` | Specify a custom tablename. You can either provide a |
|
||||
| | plain string or a callable. The callable will be |
|
||||
| | given the entity (ie class) as argument and must |
|
||||
| | return a string representing the name of the table |
|
||||
| | for that entity. By default, the tablename is |
|
||||
| | automatically generated: it is a concatenation of the |
|
||||
| | full module-path to the entity and the entity (class) |
|
||||
| | name itself. The result is lower-cased and separated |
|
||||
| | by underscores ("_"), eg.: for an entity named |
|
||||
| | "MyEntity" in the module "project1.model", the |
|
||||
| | generated table name will be |
|
||||
| | "project1_model_myentity". |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``shortnames`` | Specify whether or not the automatically generated |
|
||||
| | table names include the full module-path |
|
||||
| | to the entity. If ``shortnames`` is ``True``, only |
|
||||
| | the entity name is used. Defaults to ``False``. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``auto_primarykey`` | If given as string, it will represent the |
|
||||
| | auto-primary-key's column name. If this option |
|
||||
| | is True, it will allow auto-creation of a primary |
|
||||
| | key if there's no primary key defined for the |
|
||||
| | corresponding entity. If this option is False, |
|
||||
| | it will disallow auto-creation of a primary key. |
|
||||
| | Defaults to ``True``. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``version_id_col`` | If this option is True, it will create a version |
|
||||
| | column automatically using the default name. If given |
|
||||
| | as string, it will create the column using that name. |
|
||||
| | This can be used to prevent concurrent modifications |
|
||||
| | to the entity's table rows (i.e. it will raise an |
|
||||
| | exception if it happens). Defaults to ``False``. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``order_by`` | How to order select results. Either a string or a |
|
||||
| | list of strings, composed of the field name, |
|
||||
| | optionally lead by a minus (for descending order). |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``session`` | Specify a custom contextual session for this entity. |
|
||||
| | By default, entities uses the global |
|
||||
| | ``elixir.session``. |
|
||||
| | This option takes a ``ScopedSession`` object or |
|
||||
| | ``None``. In the later case your entity will be |
|
||||
| | mapped using a non-contextual mapper which requires |
|
||||
| | manual session management, as seen in pure SQLAlchemy.|
|
||||
| | This option can also be set for all entities of a |
|
||||
| | module by setting the ``__session__`` attribute of |
|
||||
| | that module. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``autosetup`` | DEPRECATED. Specify whether that entity will contain |
|
||||
| | automatic setup triggers. |
|
||||
| | That is if this entity will be |
|
||||
| | automatically setup (along with all other entities |
|
||||
| | which were already declared) if any of the following |
|
||||
| | condition happen: some of its attributes are accessed |
|
||||
| | ('c', 'table', 'mapper' or 'query'), instanciated |
|
||||
| | (called) or the create_all method of this entity's |
|
||||
| | metadata is called. Defaults to ``False``. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
| ``allowcoloverride``| Specify whether it is allowed to override columns. |
|
||||
| | By default, Elixir forbids you to add a column to an |
|
||||
| | entity's table which already exist in that table. If |
|
||||
| | you set this option to ``True`` it will skip that |
|
||||
| | check. Use with care as it is easy to shoot oneself |
|
||||
| | in the foot when overriding columns. |
|
||||
+---------------------+-------------------------------------------------------+
|
||||
|
||||
For examples, please refer to the examples and unit tests.
|
||||
|
||||
`using_table_options`
|
||||
---------------------
|
||||
The 'using_table_options' DSL statement allows you to set up some
|
||||
additional options on your entity table. It is meant only to handle the
|
||||
options which are not supported directly by the 'using_options' statement.
|
||||
By opposition to the 'using_options' statement, these options are passed
|
||||
directly to the underlying SQLAlchemy Table object (both non-keyword arguments
|
||||
and keyword arguments) without any processing.
|
||||
|
||||
For further information, please refer to the `SQLAlchemy table's documentation
|
||||
<http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/schema.html
|
||||
#sqlalchemy.schema.Table>`_.
|
||||
|
||||
You might also be interested in the section about `constraints
|
||||
<http://www.sqlalchemy.org/docs/05/metadata.html
|
||||
#defining-constraints-and-indexes>`_.
|
||||
|
||||
`using_mapper_options`
|
||||
----------------------
|
||||
The 'using_mapper_options' DSL statement allows you to set up some
|
||||
additional options on your entity mapper. It is meant only to handle the
|
||||
options which are not supported directly by the 'using_options' statement.
|
||||
By opposition to the 'using_options' statement, these options are passed
|
||||
directly to the underlying SQLAlchemy mapper (as keyword arguments)
|
||||
without any processing.
|
||||
|
||||
For further information, please refer to the `SQLAlchemy mapper
|
||||
function's documentation
|
||||
<http://www.sqlalchemy.org/docs/05/reference/orm/mapping.html
|
||||
#sqlalchemy.orm.mapper>`_.
|
||||
|
||||
`using_options_defaults`
|
||||
------------------------
|
||||
The 'using_options_defaults' DSL statement allows you to set up some
|
||||
default options on a custom base class. These will be used as the default value
|
||||
for options of all its subclasses. Note that any option not set within the
|
||||
using_options_defaults (nor specifically on a particular Entity) will use the
|
||||
global defaults, so you don't have to provide a default value for all options,
|
||||
but only those you want to change. Please also note that this statement does
|
||||
not work on normal entities, and the normal using_options statement does not
|
||||
work on base classes (because normal options do not and should not propagate to
|
||||
the children classes).
|
||||
'''
|
||||
|
||||
from sqlalchemy import Integer, String
|
||||
|
||||
from elixir.statements import ClassMutator
|
||||
|
||||
__doc_all__ = ['options_defaults']
|
||||
|
||||
OLD_M2MCOL_NAMEFORMAT = "%(tablename)s_%(key)s%(numifself)s"
|
||||
ALTERNATE_M2MCOL_NAMEFORMAT = "%(inversename)s_%(key)s"
|
||||
|
||||
def default_m2m_column_formatter(data):
|
||||
if data['selfref']:
|
||||
return ALTERNATE_M2MCOL_NAMEFORMAT % data
|
||||
else:
|
||||
return OLD_M2MCOL_NAMEFORMAT % data
|
||||
|
||||
NEW_M2MCOL_NAMEFORMAT = default_m2m_column_formatter
|
||||
|
||||
# format constants
|
||||
FKCOL_NAMEFORMAT = "%(relname)s_%(key)s"
|
||||
M2MCOL_NAMEFORMAT = NEW_M2MCOL_NAMEFORMAT
|
||||
CONSTRAINT_NAMEFORMAT = "%(tablename)s_%(colnames)s_fk"
|
||||
MULTIINHERITANCECOL_NAMEFORMAT = "%(entity)s_%(key)s"
|
||||
|
||||
# other global constants
|
||||
DEFAULT_AUTO_PRIMARYKEY_NAME = "id"
|
||||
DEFAULT_AUTO_PRIMARYKEY_TYPE = Integer
|
||||
DEFAULT_VERSION_ID_COL_NAME = "row_version"
|
||||
DEFAULT_POLYMORPHIC_COL_NAME = "row_type"
|
||||
POLYMORPHIC_COL_SIZE = 40
|
||||
POLYMORPHIC_COL_TYPE = String(POLYMORPHIC_COL_SIZE)
|
||||
|
||||
# debugging/migration help
|
||||
MIGRATION_TO_07_AID = False
|
||||
|
||||
#
|
||||
options_defaults = dict(
|
||||
abstract=False,
|
||||
autosetup=False,
|
||||
inheritance='single',
|
||||
polymorphic=True,
|
||||
identity=None,
|
||||
autoload=False,
|
||||
tablename=None,
|
||||
shortnames=False,
|
||||
auto_primarykey=True,
|
||||
version_id_col=False,
|
||||
allowcoloverride=False,
|
||||
order_by=None,
|
||||
resolve_root=None,
|
||||
mapper_options={},
|
||||
table_options={}
|
||||
)
|
||||
|
||||
valid_options = options_defaults.keys() + [
|
||||
'metadata',
|
||||
'session',
|
||||
'collection'
|
||||
]
|
||||
|
||||
|
||||
def using_options_defaults_handler(entity, **kwargs):
|
||||
for kwarg in kwargs:
|
||||
if kwarg not in valid_options:
|
||||
raise Exception("'%s' is not a valid option for Elixir entities."
|
||||
% kwarg)
|
||||
|
||||
# We use __dict__ instead of hasattr to not check its presence within the
|
||||
# parent, and thus update the parent dict instead of creating a local dict.
|
||||
if not entity.__dict__.get('options_defaults'):
|
||||
entity.options_defaults = {}
|
||||
entity.options_defaults.update(kwargs)
|
||||
|
||||
|
||||
def using_options_handler(entity, *args, **kwargs):
|
||||
for kwarg in kwargs:
|
||||
if kwarg in valid_options:
|
||||
setattr(entity._descriptor, kwarg, kwargs[kwarg])
|
||||
else:
|
||||
raise Exception("'%s' is not a valid option for Elixir entities."
|
||||
% kwarg)
|
||||
|
||||
|
||||
def using_table_options_handler(entity, *args, **kwargs):
|
||||
entity._descriptor.table_args.extend(list(args))
|
||||
entity._descriptor.table_options.update(kwargs)
|
||||
|
||||
|
||||
def using_mapper_options_handler(entity, *args, **kwargs):
|
||||
entity._descriptor.mapper_options.update(kwargs)
|
||||
|
||||
|
||||
using_options_defaults = ClassMutator(using_options_defaults_handler)
|
||||
using_options = ClassMutator(using_options_handler)
|
||||
using_table_options = ClassMutator(using_table_options_handler)
|
||||
using_mapper_options = ClassMutator(using_mapper_options_handler)
|
||||
@@ -0,0 +1,244 @@
|
||||
'''
|
||||
This module provides support for defining properties on your entities. It both
|
||||
provides, the `Property` class which acts as a building block for common
|
||||
properties such as fields and relationships (for those, please consult the
|
||||
corresponding modules), but also provides some more specialized properties,
|
||||
such as `ColumnProperty` and `Synonym`. It also provides the GenericProperty
|
||||
class which allows you to wrap any SQLAlchemy property, and its DSL-syntax
|
||||
equivalent: has_property_.
|
||||
|
||||
`has_property`
|
||||
--------------
|
||||
The ``has_property`` statement allows you to define properties which rely on
|
||||
their entity's table (and columns) being defined before they can be declared
|
||||
themselves. The `has_property` statement takes two arguments: first the name of
|
||||
the property to be defined and second a function (often given as an anonymous
|
||||
lambda) taking one argument and returning the desired SQLAlchemy property. That
|
||||
function will be called whenever the entity table is completely defined, and
|
||||
will be given the .c attribute of the entity as argument (as a way to access
|
||||
the entity columns).
|
||||
|
||||
Here is a quick example of how to use ``has_property``.
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class OrderLine(Entity):
|
||||
has_field('quantity', Float)
|
||||
has_field('unit_price', Float)
|
||||
has_property('price',
|
||||
lambda c: column_property(
|
||||
(c.quantity * c.unit_price).label('price')))
|
||||
'''
|
||||
|
||||
from elixir.statements import PropertyStatement
|
||||
from sqlalchemy.orm import column_property, synonym
|
||||
|
||||
__doc_all__ = ['EntityBuilder', 'Property', 'GenericProperty',
|
||||
'ColumnProperty']
|
||||
|
||||
class EntityBuilder(object):
|
||||
'''
|
||||
Abstract base class for all entity builders. An Entity builder is a class
|
||||
of objects which can be added to an Entity (usually by using special
|
||||
properties or statements) to "build" that entity. Building an entity,
|
||||
meaning to add columns to its "main" table, create other tables, add
|
||||
properties to its mapper, ... To do so an EntityBuilder must override the
|
||||
corresponding method(s). This is to ensure the different operations happen
|
||||
in the correct order (for example, that the table is fully created before
|
||||
the mapper that use it is defined).
|
||||
'''
|
||||
def create_pk_cols(self):
|
||||
pass
|
||||
|
||||
def create_non_pk_cols(self):
|
||||
pass
|
||||
|
||||
def before_table(self):
|
||||
pass
|
||||
|
||||
def create_tables(self):
|
||||
'''
|
||||
Subclasses may override this method to create tables.
|
||||
'''
|
||||
|
||||
def after_table(self):
|
||||
pass
|
||||
|
||||
def create_properties(self):
|
||||
'''
|
||||
Subclasses may override this method to add properties to the involved
|
||||
entity.
|
||||
'''
|
||||
|
||||
def before_mapper(self):
|
||||
pass
|
||||
|
||||
def after_mapper(self):
|
||||
pass
|
||||
|
||||
def finalize(self):
|
||||
pass
|
||||
|
||||
# helper methods
|
||||
def add_table_column(self, column):
|
||||
self.entity._descriptor.add_column(column)
|
||||
|
||||
def add_mapper_property(self, name, prop):
|
||||
self.entity._descriptor.add_property(name, prop)
|
||||
|
||||
def add_mapper_extension(self, ext):
|
||||
self.entity._descriptor.add_mapper_extension(ext)
|
||||
|
||||
|
||||
class CounterMeta(type):
|
||||
'''
|
||||
A simple meta class which adds a ``_counter`` attribute to the instances of
|
||||
the classes it is used on. This counter is simply incremented for each new
|
||||
instance.
|
||||
'''
|
||||
counter = 0
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
instance = type.__call__(self, *args, **kwargs)
|
||||
instance._counter = CounterMeta.counter
|
||||
CounterMeta.counter += 1
|
||||
return instance
|
||||
|
||||
|
||||
class Property(EntityBuilder):
|
||||
'''
|
||||
Abstract base class for all properties of an Entity.
|
||||
'''
|
||||
__metaclass__ = CounterMeta
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.entity = None
|
||||
self.name = None
|
||||
|
||||
def attach(self, entity, name):
|
||||
"""Attach this property to its entity, using 'name' as name.
|
||||
|
||||
Properties will be attached in the order they were declared.
|
||||
"""
|
||||
self.entity = entity
|
||||
self.name = name
|
||||
|
||||
# register this property as a builder
|
||||
entity._descriptor.builders.append(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "Property(%s, %s)" % (self.name, self.entity)
|
||||
|
||||
|
||||
class GenericProperty(Property):
|
||||
'''
|
||||
Generic catch-all class to wrap an SQLAlchemy property.
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class OrderLine(Entity):
|
||||
quantity = Field(Float)
|
||||
unit_price = Field(Numeric)
|
||||
price = GenericProperty(lambda c: column_property(
|
||||
(c.quantity * c.unit_price).label('price')))
|
||||
'''
|
||||
|
||||
def __init__(self, prop, *args, **kwargs):
|
||||
super(GenericProperty, self).__init__(*args, **kwargs)
|
||||
self.prop = prop
|
||||
#XXX: move this to Property?
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def create_properties(self):
|
||||
if hasattr(self.prop, '__call__'):
|
||||
prop_value = self.prop(self.entity.table.c)
|
||||
else:
|
||||
prop_value = self.prop
|
||||
prop_value = self.evaluate_property(prop_value)
|
||||
self.add_mapper_property(self.name, prop_value)
|
||||
|
||||
def evaluate_property(self, prop):
|
||||
if self.args or self.kwargs:
|
||||
raise Exception('superfluous arguments passed to GenericProperty')
|
||||
return prop
|
||||
|
||||
|
||||
class ColumnProperty(GenericProperty):
|
||||
'''
|
||||
A specialized form of the GenericProperty to generate SQLAlchemy
|
||||
``column_property``'s.
|
||||
|
||||
It takes a function (often given as an anonymous lambda) as its first
|
||||
argument. Other arguments and keyword arguments are forwarded to the
|
||||
column_property construct. That first-argument function must accept exactly
|
||||
one argument and must return the desired (scalar-returning) SQLAlchemy
|
||||
ClauseElement.
|
||||
|
||||
The function will be called whenever the entity table is completely
|
||||
defined, and will be given
|
||||
the .c attribute of the table of the entity as argument (as a way to
|
||||
access the entity columns). The ColumnProperty will first wrap your
|
||||
ClauseElement in an
|
||||
"empty" label (ie it will be labelled automatically during queries),
|
||||
then wrap that in a column_property.
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class OrderLine(Entity):
|
||||
quantity = Field(Float)
|
||||
unit_price = Field(Numeric)
|
||||
price = ColumnProperty(lambda c: c.quantity * c.unit_price,
|
||||
deferred=True)
|
||||
|
||||
Please look at the `corresponding SQLAlchemy
|
||||
documentation <http://www.sqlalchemy.org/docs/05/mappers.html
|
||||
#sql-expressions-as-mapped-attributes>`_ for details.
|
||||
'''
|
||||
|
||||
def evaluate_property(self, prop):
|
||||
return column_property(prop.label(None), *self.args, **self.kwargs)
|
||||
|
||||
|
||||
class Synonym(GenericProperty):
|
||||
'''
|
||||
This class represents a synonym property of another property (column, ...)
|
||||
of an entity. As opposed to the `synonym` kwarg to the Field class (which
|
||||
share the same goal), this class can be used to define a synonym of a
|
||||
property defined in a parent class (of the current class). On the other
|
||||
hand, it cannot define a synonym for the purpose of using a standard python
|
||||
property in queries. See the Field class for details on that usage.
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
class Person(Entity):
|
||||
name = Field(String(30))
|
||||
primary_email = Field(String(100))
|
||||
email_address = Synonym('primary_email')
|
||||
|
||||
class User(Person):
|
||||
user_name = Synonym('name')
|
||||
password = Field(String(20))
|
||||
'''
|
||||
|
||||
def evaluate_property(self, prop):
|
||||
return synonym(prop, *self.args, **self.kwargs)
|
||||
|
||||
#class Composite(GenericProperty):
|
||||
# def __init__(self, prop):
|
||||
# super(GenericProperty, self).__init__()
|
||||
# self.prop = prop
|
||||
|
||||
# def evaluate_property(self, prop):
|
||||
# return composite(prop.label(self.name))
|
||||
|
||||
#start = Composite(Point, lambda c: (c.x1, c.y1))
|
||||
|
||||
#mapper(Vertex, vertices, properties={
|
||||
# 'start':composite(Point, vertices.c.x1, vertices.c.y1),
|
||||
# 'end':composite(Point, vertices.c.x2, vertices.c.y2)
|
||||
#})
|
||||
|
||||
|
||||
has_property = PropertyStatement(GenericProperty)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Some helper functions to get by without Python 2.4
|
||||
|
||||
# set
|
||||
try:
|
||||
set = set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
|
||||
orig_cmp = cmp
|
||||
# [].sort
|
||||
def sort_list(l, cmp=None, key=None, reverse=False):
|
||||
try:
|
||||
l.sort(cmp, key, reverse)
|
||||
except TypeError, e:
|
||||
if not str(e).startswith('sort expected at most 1 arguments'):
|
||||
raise
|
||||
if cmp is None:
|
||||
cmp = orig_cmp
|
||||
if key is not None:
|
||||
# the cmp=cmp parameter is required to get the original comparator
|
||||
# into the lambda namespace
|
||||
cmp = lambda self, other, cmp=cmp: cmp(key(self), key(other))
|
||||
if reverse:
|
||||
cmp = lambda self, other, cmp=cmp: -cmp(self,other)
|
||||
l.sort(cmp)
|
||||
|
||||
# sorted
|
||||
try:
|
||||
sorted = sorted
|
||||
except NameError:
|
||||
# global name 'sorted' doesn't exist in Python2.3
|
||||
# this provides a poor-man's emulation of the sorted built-in method
|
||||
def sorted(l, cmp=None, key=None, reverse=False):
|
||||
sorted_list = list(l)
|
||||
sort_list(sorted_list, cmp, key, reverse)
|
||||
return sorted_list
|
||||
|
||||
# rsplit
|
||||
try:
|
||||
''.rsplit
|
||||
def rsplit(s, delim, maxsplit):
|
||||
return s.rsplit(delim, maxsplit)
|
||||
|
||||
except AttributeError:
|
||||
def rsplit(s, delim, maxsplit):
|
||||
"""Return a list of the words of the string s, scanning s
|
||||
from the end. To all intents and purposes, the resulting
|
||||
list of words is the same as returned by split(), except
|
||||
when the optional third argument maxsplit is explicitly
|
||||
specified and nonzero. When maxsplit is nonzero, at most
|
||||
maxsplit number of splits - the rightmost ones - occur,
|
||||
and the remainder of the string is returned as the first
|
||||
element of the list (thus, the list will have at most
|
||||
maxsplit+1 elements). New in version 2.4.
|
||||
>>> rsplit('foo.bar.baz', '.', 0)
|
||||
['foo.bar.baz']
|
||||
>>> rsplit('foo.bar.baz', '.', 1)
|
||||
['foo.bar', 'baz']
|
||||
>>> rsplit('foo.bar.baz', '.', 2)
|
||||
['foo', 'bar', 'baz']
|
||||
>>> rsplit('foo.bar.baz', '.', 99)
|
||||
['foo', 'bar', 'baz']
|
||||
"""
|
||||
assert maxsplit >= 0
|
||||
|
||||
if maxsplit == 0: return [s]
|
||||
|
||||
# the following lines perform the function, but inefficiently.
|
||||
# This may be adequate for compatibility purposes
|
||||
items = s.split(delim)
|
||||
if maxsplit < len(items):
|
||||
items[:-maxsplit] = [delim.join(items[:-maxsplit])]
|
||||
return items
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
import sys
|
||||
|
||||
MUTATORS = '__elixir_mutators__'
|
||||
|
||||
class ClassMutator(object):
|
||||
'''
|
||||
DSL-style syntax
|
||||
|
||||
A ``ClassMutator`` object represents a DSL term.
|
||||
'''
|
||||
|
||||
def __init__(self, handler):
|
||||
'''
|
||||
Create a new ClassMutator, using the `handler` callable to process it
|
||||
when the time will come.
|
||||
'''
|
||||
self.handler = handler
|
||||
|
||||
# called when a mutator (eg. "has_field(...)") is parsed
|
||||
def __call__(self, *args, **kwargs):
|
||||
# self in this case is the "generic" mutator (eg "has_field")
|
||||
|
||||
# jam this mutator into the class's mutator list
|
||||
class_locals = sys._getframe(1).f_locals
|
||||
mutators = class_locals.setdefault(MUTATORS, [])
|
||||
mutators.append((self, args, kwargs))
|
||||
|
||||
def process(self, entity, *args, **kwargs):
|
||||
'''
|
||||
Process one mutator. This version simply calls the handler callable,
|
||||
but another mutator (sub)class could do more processing.
|
||||
'''
|
||||
self.handler(entity, *args, **kwargs)
|
||||
|
||||
|
||||
#TODO: move this to the super class (to be created here) of EntityMeta
|
||||
def process_mutators(entity):
|
||||
'''
|
||||
Apply all mutators of the given entity. That is, loop over all mutators
|
||||
in the class's mutator list and process them.
|
||||
'''
|
||||
# we don't use getattr here to not inherit from the parent mutators
|
||||
# inadvertantly if the current entity hasn't defined any mutator.
|
||||
mutators = entity.__dict__.get(MUTATORS, [])
|
||||
for mutator, args, kwargs in mutators:
|
||||
mutator.process(entity, *args, **kwargs)
|
||||
|
||||
class Statement(ClassMutator):
|
||||
|
||||
def process(self, entity, *args, **kwargs):
|
||||
builder = self.handler(entity, *args, **kwargs)
|
||||
entity._descriptor.builders.append(builder)
|
||||
|
||||
class PropertyStatement(ClassMutator):
|
||||
|
||||
def process(self, entity, name, *args, **kwargs):
|
||||
prop = self.handler(*args, **kwargs)
|
||||
prop.attach(entity, name)
|
||||
|
||||
Reference in New Issue
Block a user