Remove submodule, just put Dependencies in ./libs

This commit is contained in:
Ruud
2011-02-28 15:25:42 +01:00
parent 2fb578d4de
commit 6888798ef6
541 changed files with 173394 additions and 4 deletions
+119
View File
@@ -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()
+127
View File
@@ -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
+30
View File
@@ -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')
+5
View File
@@ -0,0 +1,5 @@
'''
Ext package
Additional Elixir statements and functionality.
'''
+234
View File
@@ -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)
+124
View File
@@ -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)
+251
View File
@@ -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)
+106
View File
@@ -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)
+288
View File
@@ -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
+191
View File
@@ -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)
+285
View File
@@ -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)
+244
View File
@@ -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)
+73
View File
@@ -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
+59
View File
@@ -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)