| 1 | ''' |
|---|
| 2 | This module provides support for defining properties on your entities. It both |
|---|
| 3 | provides, the `Property` class which acts as a building block for common |
|---|
| 4 | properties such as fields and relationships (for those, please consult the |
|---|
| 5 | corresponding modules), but also provides some more specialized properties, |
|---|
| 6 | such as `ColumnProperty`. It also provides the GenericProperty class which allows you to wrap any SQLAlchemy property, and its DSL-syntax equivalent: |
|---|
| 7 | has_property_. |
|---|
| 8 | |
|---|
| 9 | `has_property` |
|---|
| 10 | -------------- |
|---|
| 11 | The ``has_property`` statement allows you to define properties which rely on |
|---|
| 12 | their entity's table (and columns) being defined before they can be declared |
|---|
| 13 | themselves. The `has_property` statement takes two arguments: first the name of |
|---|
| 14 | the property to be defined and second a function (often given as an anonymous |
|---|
| 15 | lambda) taking one argument and returning the desired SQLAlchemy property. That |
|---|
| 16 | function will be called whenever the entity table is completely defined, and |
|---|
| 17 | will be given the .c attribute of the entity as argument (as a way to access |
|---|
| 18 | the entity columns). |
|---|
| 19 | |
|---|
| 20 | Here is a quick example of how to use ``has_property``. |
|---|
| 21 | |
|---|
| 22 | .. sourcecode:: python |
|---|
| 23 | |
|---|
| 24 | class OrderLine(Entity): |
|---|
| 25 | has_field('quantity', Float) |
|---|
| 26 | has_field('unit_price', Float) |
|---|
| 27 | has_property('price', |
|---|
| 28 | lambda c: column_property( |
|---|
| 29 | (c.quantity * c.unit_price).label('price'))) |
|---|
| 30 | ''' |
|---|
| 31 | |
|---|
| 32 | from elixir.statements import PropertyStatement |
|---|
| 33 | from sqlalchemy.orm import column_property |
|---|
| 34 | |
|---|
| 35 | __doc_all__ = ['EntityBuilder', 'Property', 'GenericProperty', |
|---|
| 36 | 'ColumnProperty'] |
|---|
| 37 | |
|---|
| 38 | class EntityBuilder(object): |
|---|
| 39 | ''' |
|---|
| 40 | Abstract base class for all entity builders. An Entity builder is a class |
|---|
| 41 | of objects which can be added to an Entity (usually by using special |
|---|
| 42 | properties or statements) to "build" that entity. Building an entity, |
|---|
| 43 | meaning to add columns to its "main" table, create other tables, add |
|---|
| 44 | properties to its mapper, ... To do so an EntityBuilder must override the |
|---|
| 45 | corresponding method(s). This is to ensure the different operations happen |
|---|
| 46 | in the correct order (for example, that the table is fully created before |
|---|
| 47 | the mapper that use it is defined). |
|---|
| 48 | ''' |
|---|
| 49 | |
|---|
| 50 | #TODO: add stub methods (create_cols, etc...) |
|---|
| 51 | #XXX: add helper methods: add_property, etc... here? |
|---|
| 52 | # either in addition to in EntityDescriptor or instead of there. |
|---|
| 53 | pass |
|---|
| 54 | |
|---|
| 55 | |
|---|
| 56 | class CounterMeta(type): |
|---|
| 57 | ''' |
|---|
| 58 | A simple meta class which adds a ``_counter`` attribute to the instances of |
|---|
| 59 | the classes it is used on. This counter is simply incremented for each new |
|---|
| 60 | instance. |
|---|
| 61 | ''' |
|---|
| 62 | counter = 0 |
|---|
| 63 | |
|---|
| 64 | def __call__(self, *args, **kwargs): |
|---|
| 65 | instance = type.__call__(self, *args, **kwargs) |
|---|
| 66 | instance._counter = CounterMeta.counter |
|---|
| 67 | CounterMeta.counter += 1 |
|---|
| 68 | return instance |
|---|
| 69 | |
|---|
| 70 | |
|---|
| 71 | class Property(EntityBuilder): |
|---|
| 72 | ''' |
|---|
| 73 | Abstract base class for all properties of an Entity. |
|---|
| 74 | ''' |
|---|
| 75 | __metaclass__ = CounterMeta |
|---|
| 76 | |
|---|
| 77 | def __init__(self, *args, **kwargs): |
|---|
| 78 | self.entity = None |
|---|
| 79 | self.name = None |
|---|
| 80 | |
|---|
| 81 | def attach(self, entity, name): |
|---|
| 82 | """Attach this property to its entity, using 'name' as name. |
|---|
| 83 | |
|---|
| 84 | Properties will be attached in the order they were declared. |
|---|
| 85 | """ |
|---|
| 86 | self.entity = entity |
|---|
| 87 | self.name = name |
|---|
| 88 | |
|---|
| 89 | # register this property as a builder |
|---|
| 90 | entity._descriptor.builders.append(self) |
|---|
| 91 | |
|---|
| 92 | def __repr__(self): |
|---|
| 93 | return "Property(%s, %s)" % (self.name, self.entity) |
|---|
| 94 | |
|---|
| 95 | |
|---|
| 96 | class GenericProperty(Property): |
|---|
| 97 | ''' |
|---|
| 98 | Generic catch-all class to wrap an SQLAlchemy property. |
|---|
| 99 | ''' |
|---|
| 100 | |
|---|
| 101 | def __init__(self, prop): |
|---|
| 102 | super(GenericProperty, self).__init__() |
|---|
| 103 | self.prop = prop |
|---|
| 104 | |
|---|
| 105 | def create_properties(self): |
|---|
| 106 | if callable(self.prop): |
|---|
| 107 | prop_value = self.prop(self.entity.table.c) |
|---|
| 108 | else: |
|---|
| 109 | prop_value = self.prop |
|---|
| 110 | prop_value = self.evaluate_property(prop_value) |
|---|
| 111 | self.entity.mapper.add_property(self.name, prop_value) |
|---|
| 112 | |
|---|
| 113 | def evaluate_property(self, prop): |
|---|
| 114 | return prop |
|---|
| 115 | |
|---|
| 116 | class ColumnProperty(GenericProperty): |
|---|
| 117 | ''' |
|---|
| 118 | A specialized form of the GenericProperty to generate SQLAlchemy |
|---|
| 119 | ``column_property``'s. |
|---|
| 120 | |
|---|
| 121 | It takes a single argument, which is a function (often |
|---|
| 122 | given as an anonymous lambda) taking one argument and returning the |
|---|
| 123 | desired (scalar-returning) SQLAlchemy ClauseElement. That function will be |
|---|
| 124 | called whenever the entity table is completely defined, and will be given |
|---|
| 125 | the .c attribute of the entity as argument (as a way to access the entity |
|---|
| 126 | columns). The ColumnProperty will first wrap your ClauseElement in a label |
|---|
| 127 | with the same name as the property, then wrap that in a column_property. |
|---|
| 128 | |
|---|
| 129 | .. sourcecode:: python |
|---|
| 130 | |
|---|
| 131 | class OrderLine(Entity): |
|---|
| 132 | quantity = Field(Float) |
|---|
| 133 | unit_price = Field(Numeric) |
|---|
| 134 | price = ColumnProperty(lambda c: c.quantity * c.unit_price) |
|---|
| 135 | |
|---|
| 136 | Please look at the `corresponding SQLAlchemy |
|---|
| 137 | documentation <http://www.sqlalchemy.org/docs/04/mappers.html |
|---|
| 138 | #advdatamapping_mapper_expressions>`_ for details. |
|---|
| 139 | ''' |
|---|
| 140 | |
|---|
| 141 | def evaluate_property(self, prop): |
|---|
| 142 | return column_property(prop.label(self.name)) |
|---|
| 143 | |
|---|
| 144 | #class Composite(GenericProperty): |
|---|
| 145 | # def __init__(self, prop): |
|---|
| 146 | # super(GenericProperty, self).__init__() |
|---|
| 147 | # self.prop = prop |
|---|
| 148 | |
|---|
| 149 | # def evaluate_property(self, prop): |
|---|
| 150 | # return composite(prop.label(self.name)) |
|---|
| 151 | |
|---|
| 152 | #start = Composite(Point, lambda c: (c.x1, c.y1)) |
|---|
| 153 | |
|---|
| 154 | #mapper(Vertex, vertices, properties={ |
|---|
| 155 | # 'start':composite(Point, vertices.c.x1, vertices.c.y1), |
|---|
| 156 | # 'end':composite(Point, vertices.c.x2, vertices.c.y2) |
|---|
| 157 | #}) |
|---|
| 158 | |
|---|
| 159 | |
|---|
| 160 | has_property = PropertyStatement(GenericProperty) |
|---|