| 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` and `Synonym`. It also provides the GenericProperty |
|---|
| 7 | class which allows you to wrap any SQLAlchemy property, and its DSL-syntax |
|---|
| 8 | equivalent: has_property_. |
|---|
| 9 | |
|---|
| 10 | `has_property` |
|---|
| 11 | -------------- |
|---|
| 12 | The ``has_property`` statement allows you to define properties which rely on |
|---|
| 13 | their entity's table (and columns) being defined before they can be declared |
|---|
| 14 | themselves. The `has_property` statement takes two arguments: first the name of |
|---|
| 15 | the property to be defined and second a function (often given as an anonymous |
|---|
| 16 | lambda) taking one argument and returning the desired SQLAlchemy property. That |
|---|
| 17 | function will be called whenever the entity table is completely defined, and |
|---|
| 18 | will be given the .c attribute of the entity as argument (as a way to access |
|---|
| 19 | the entity columns). |
|---|
| 20 | |
|---|
| 21 | Here is a quick example of how to use ``has_property``. |
|---|
| 22 | |
|---|
| 23 | .. sourcecode:: python |
|---|
| 24 | |
|---|
| 25 | class OrderLine(Entity): |
|---|
| 26 | has_field('quantity', Float) |
|---|
| 27 | has_field('unit_price', Float) |
|---|
| 28 | has_property('price', |
|---|
| 29 | lambda c: column_property( |
|---|
| 30 | (c.quantity * c.unit_price).label('price'))) |
|---|
| 31 | ''' |
|---|
| 32 | |
|---|
| 33 | from elixir.statements import PropertyStatement |
|---|
| 34 | from sqlalchemy.orm import column_property, synonym |
|---|
| 35 | |
|---|
| 36 | __doc_all__ = ['EntityBuilder', 'Property', 'GenericProperty', |
|---|
| 37 | 'ColumnProperty'] |
|---|
| 38 | |
|---|
| 39 | class EntityBuilder(object): |
|---|
| 40 | ''' |
|---|
| 41 | Abstract base class for all entity builders. An Entity builder is a class |
|---|
| 42 | of objects which can be added to an Entity (usually by using special |
|---|
| 43 | properties or statements) to "build" that entity. Building an entity, |
|---|
| 44 | meaning to add columns to its "main" table, create other tables, add |
|---|
| 45 | properties to its mapper, ... To do so an EntityBuilder must override the |
|---|
| 46 | corresponding method(s). This is to ensure the different operations happen |
|---|
| 47 | in the correct order (for example, that the table is fully created before |
|---|
| 48 | the mapper that use it is defined). |
|---|
| 49 | ''' |
|---|
| 50 | #XXX: add helper methods: add_property, etc... here? |
|---|
| 51 | # either in addition to in EntityDescriptor or instead of there. |
|---|
| 52 | def create_pk_cols(self): |
|---|
| 53 | pass |
|---|
| 54 | |
|---|
| 55 | def create_non_pk_cols(self): |
|---|
| 56 | pass |
|---|
| 57 | |
|---|
| 58 | def before_table(self): |
|---|
| 59 | pass |
|---|
| 60 | |
|---|
| 61 | def create_tables(self): |
|---|
| 62 | pass |
|---|
| 63 | |
|---|
| 64 | def after_table(self): |
|---|
| 65 | pass |
|---|
| 66 | |
|---|
| 67 | def create_properties(self): |
|---|
| 68 | pass |
|---|
| 69 | |
|---|
| 70 | def before_mapper(self): |
|---|
| 71 | pass |
|---|
| 72 | |
|---|
| 73 | def after_mapper(self): |
|---|
| 74 | pass |
|---|
| 75 | |
|---|
| 76 | def finalize(self): |
|---|
| 77 | pass |
|---|
| 78 | |
|---|
| 79 | |
|---|
| 80 | class CounterMeta(type): |
|---|
| 81 | ''' |
|---|
| 82 | A simple meta class which adds a ``_counter`` attribute to the instances of |
|---|
| 83 | the classes it is used on. This counter is simply incremented for each new |
|---|
| 84 | instance. |
|---|
| 85 | ''' |
|---|
| 86 | counter = 0 |
|---|
| 87 | |
|---|
| 88 | def __call__(self, *args, **kwargs): |
|---|
| 89 | instance = type.__call__(self, *args, **kwargs) |
|---|
| 90 | instance._counter = CounterMeta.counter |
|---|
| 91 | CounterMeta.counter += 1 |
|---|
| 92 | return instance |
|---|
| 93 | |
|---|
| 94 | |
|---|
| 95 | class Property(EntityBuilder): |
|---|
| 96 | ''' |
|---|
| 97 | Abstract base class for all properties of an Entity. |
|---|
| 98 | ''' |
|---|
| 99 | __metaclass__ = CounterMeta |
|---|
| 100 | |
|---|
| 101 | def __init__(self, *args, **kwargs): |
|---|
| 102 | self.entity = None |
|---|
| 103 | self.name = None |
|---|
| 104 | |
|---|
| 105 | def attach(self, entity, name): |
|---|
| 106 | """Attach this property to its entity, using 'name' as name. |
|---|
| 107 | |
|---|
| 108 | Properties will be attached in the order they were declared. |
|---|
| 109 | """ |
|---|
| 110 | self.entity = entity |
|---|
| 111 | self.name = name |
|---|
| 112 | |
|---|
| 113 | # register this property as a builder |
|---|
| 114 | entity._descriptor.builders.append(self) |
|---|
| 115 | |
|---|
| 116 | # delete the original attribute so that it doesn't interfere with |
|---|
| 117 | # SQLAlchemy. |
|---|
| 118 | if name in entity.__dict__: |
|---|
| 119 | delattr(entity, name) |
|---|
| 120 | |
|---|
| 121 | def __repr__(self): |
|---|
| 122 | return "Property(%s, %s)" % (self.name, self.entity) |
|---|
| 123 | |
|---|
| 124 | |
|---|
| 125 | class GenericProperty(Property): |
|---|
| 126 | ''' |
|---|
| 127 | Generic catch-all class to wrap an SQLAlchemy property. |
|---|
| 128 | |
|---|
| 129 | .. sourcecode:: python |
|---|
| 130 | |
|---|
| 131 | class OrderLine(Entity): |
|---|
| 132 | quantity = Field(Float) |
|---|
| 133 | unit_price = Field(Numeric) |
|---|
| 134 | price = GenericProperty(lambda c: column_property( |
|---|
| 135 | (c.quantity * c.unit_price).label('price'))) |
|---|
| 136 | ''' |
|---|
| 137 | |
|---|
| 138 | def __init__(self, prop): |
|---|
| 139 | super(GenericProperty, self).__init__() |
|---|
| 140 | self.prop = prop |
|---|
| 141 | |
|---|
| 142 | def create_properties(self): |
|---|
| 143 | if callable(self.prop): |
|---|
| 144 | prop_value = self.prop(self.entity.table.c) |
|---|
| 145 | else: |
|---|
| 146 | prop_value = self.prop |
|---|
| 147 | prop_value = self.evaluate_property(prop_value) |
|---|
| 148 | self.entity._descriptor.add_property(self.name, prop_value) |
|---|
| 149 | |
|---|
| 150 | def evaluate_property(self, prop): |
|---|
| 151 | return prop |
|---|
| 152 | |
|---|
| 153 | |
|---|
| 154 | class ColumnProperty(GenericProperty): |
|---|
| 155 | ''' |
|---|
| 156 | A specialized form of the GenericProperty to generate SQLAlchemy |
|---|
| 157 | ``column_property``'s. |
|---|
| 158 | |
|---|
| 159 | It takes a single argument, which is a function (often |
|---|
| 160 | given as an anonymous lambda) taking one argument and returning the |
|---|
| 161 | desired (scalar-returning) SQLAlchemy ClauseElement. That function will be |
|---|
| 162 | called whenever the entity table is completely defined, and will be given |
|---|
| 163 | the .c attribute of the entity as argument (as a way to access the entity |
|---|
| 164 | columns). The ColumnProperty will first wrap your ClauseElement in a label |
|---|
| 165 | with the same name as the property, then wrap that in a column_property. |
|---|
| 166 | |
|---|
| 167 | .. sourcecode:: python |
|---|
| 168 | |
|---|
| 169 | class OrderLine(Entity): |
|---|
| 170 | quantity = Field(Float) |
|---|
| 171 | unit_price = Field(Numeric) |
|---|
| 172 | price = ColumnProperty(lambda c: c.quantity * c.unit_price) |
|---|
| 173 | |
|---|
| 174 | Please look at the `corresponding SQLAlchemy |
|---|
| 175 | documentation <http://www.sqlalchemy.org/docs/04/mappers.html |
|---|
| 176 | #advdatamapping_mapper_expressions>`_ for details. |
|---|
| 177 | ''' |
|---|
| 178 | |
|---|
| 179 | def evaluate_property(self, prop): |
|---|
| 180 | return column_property(prop.label(None)) |
|---|
| 181 | |
|---|
| 182 | |
|---|
| 183 | class Synonym(GenericProperty): |
|---|
| 184 | ''' |
|---|
| 185 | This class represents a synonym property of another property (column, ...) |
|---|
| 186 | of an entity. As opposed to the `synonym` kwarg to the Field class (which |
|---|
| 187 | share the same goal), this class can be used to define a synonym of a |
|---|
| 188 | property defined in a parent class (of the current class). On the other |
|---|
| 189 | hand, it cannot define a synonym for the purpose of using a standard python |
|---|
| 190 | property in queries. See the Field class for details on that usage. |
|---|
| 191 | |
|---|
| 192 | .. sourcecode:: python |
|---|
| 193 | |
|---|
| 194 | class Person(Entity): |
|---|
| 195 | name = Field(String(30)) |
|---|
| 196 | primary_email = Field(String(100)) |
|---|
| 197 | email_address = Synonym('primary_email') |
|---|
| 198 | |
|---|
| 199 | class User(Person): |
|---|
| 200 | user_name = Synonym('name') |
|---|
| 201 | password = Field(String(20)) |
|---|
| 202 | ''' |
|---|
| 203 | |
|---|
| 204 | def evaluate_property(self, prop): |
|---|
| 205 | return synonym(prop) |
|---|
| 206 | |
|---|
| 207 | #class Composite(GenericProperty): |
|---|
| 208 | # def __init__(self, prop): |
|---|
| 209 | # super(GenericProperty, self).__init__() |
|---|
| 210 | # self.prop = prop |
|---|
| 211 | |
|---|
| 212 | # def evaluate_property(self, prop): |
|---|
| 213 | # return composite(prop.label(self.name)) |
|---|
| 214 | |
|---|
| 215 | #start = Composite(Point, lambda c: (c.x1, c.y1)) |
|---|
| 216 | |
|---|
| 217 | #mapper(Vertex, vertices, properties={ |
|---|
| 218 | # 'start':composite(Point, vertices.c.x1, vertices.c.y1), |
|---|
| 219 | # 'end':composite(Point, vertices.c.x2, vertices.c.y2) |
|---|
| 220 | #}) |
|---|
| 221 | |
|---|
| 222 | |
|---|
| 223 | has_property = PropertyStatement(GenericProperty) |
|---|