| 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 | def create_pk_cols(self): |
|---|
| 51 | pass |
|---|
| 52 | |
|---|
| 53 | def create_non_pk_cols(self): |
|---|
| 54 | pass |
|---|
| 55 | |
|---|
| 56 | def before_table(self): |
|---|
| 57 | pass |
|---|
| 58 | |
|---|
| 59 | def create_tables(self): |
|---|
| 60 | ''' |
|---|
| 61 | Subclasses may override this method to create tables. |
|---|
| 62 | ''' |
|---|
| 63 | |
|---|
| 64 | def after_table(self): |
|---|
| 65 | pass |
|---|
| 66 | |
|---|
| 67 | def create_properties(self): |
|---|
| 68 | ''' |
|---|
| 69 | Subclasses may override this method to add properties to the involved |
|---|
| 70 | entity. |
|---|
| 71 | ''' |
|---|
| 72 | |
|---|
| 73 | def before_mapper(self): |
|---|
| 74 | pass |
|---|
| 75 | |
|---|
| 76 | def after_mapper(self): |
|---|
| 77 | pass |
|---|
| 78 | |
|---|
| 79 | def finalize(self): |
|---|
| 80 | pass |
|---|
| 81 | |
|---|
| 82 | # helper methods |
|---|
| 83 | def add_table_column(self, column): |
|---|
| 84 | self.entity._descriptor.add_column(column) |
|---|
| 85 | |
|---|
| 86 | def add_mapper_property(self, name, prop): |
|---|
| 87 | self.entity._descriptor.add_property(name, prop) |
|---|
| 88 | |
|---|
| 89 | def add_mapper_extension(self, ext): |
|---|
| 90 | self.entity._descriptor.add_mapper_extension(ext) |
|---|
| 91 | |
|---|
| 92 | |
|---|
| 93 | class CounterMeta(type): |
|---|
| 94 | ''' |
|---|
| 95 | A simple meta class which adds a ``_counter`` attribute to the instances of |
|---|
| 96 | the classes it is used on. This counter is simply incremented for each new |
|---|
| 97 | instance. |
|---|
| 98 | ''' |
|---|
| 99 | counter = 0 |
|---|
| 100 | |
|---|
| 101 | def __call__(self, *args, **kwargs): |
|---|
| 102 | instance = type.__call__(self, *args, **kwargs) |
|---|
| 103 | instance._counter = CounterMeta.counter |
|---|
| 104 | CounterMeta.counter += 1 |
|---|
| 105 | return instance |
|---|
| 106 | |
|---|
| 107 | |
|---|
| 108 | class Property(EntityBuilder): |
|---|
| 109 | ''' |
|---|
| 110 | Abstract base class for all properties of an Entity. |
|---|
| 111 | ''' |
|---|
| 112 | __metaclass__ = CounterMeta |
|---|
| 113 | |
|---|
| 114 | def __init__(self, *args, **kwargs): |
|---|
| 115 | self.entity = None |
|---|
| 116 | self.name = None |
|---|
| 117 | |
|---|
| 118 | def attach(self, entity, name): |
|---|
| 119 | """Attach this property to its entity, using 'name' as name. |
|---|
| 120 | |
|---|
| 121 | Properties will be attached in the order they were declared. |
|---|
| 122 | """ |
|---|
| 123 | self.entity = entity |
|---|
| 124 | self.name = name |
|---|
| 125 | |
|---|
| 126 | # register this property as a builder |
|---|
| 127 | entity._descriptor.builders.append(self) |
|---|
| 128 | |
|---|
| 129 | def __repr__(self): |
|---|
| 130 | return "Property(%s, %s)" % (self.name, self.entity) |
|---|
| 131 | |
|---|
| 132 | |
|---|
| 133 | class GenericProperty(Property): |
|---|
| 134 | ''' |
|---|
| 135 | Generic catch-all class to wrap an SQLAlchemy property. |
|---|
| 136 | |
|---|
| 137 | .. sourcecode:: python |
|---|
| 138 | |
|---|
| 139 | class OrderLine(Entity): |
|---|
| 140 | quantity = Field(Float) |
|---|
| 141 | unit_price = Field(Numeric) |
|---|
| 142 | price = GenericProperty(lambda c: column_property( |
|---|
| 143 | (c.quantity * c.unit_price).label('price'))) |
|---|
| 144 | ''' |
|---|
| 145 | |
|---|
| 146 | def __init__(self, prop, *args, **kwargs): |
|---|
| 147 | super(GenericProperty, self).__init__(*args, **kwargs) |
|---|
| 148 | self.prop = prop |
|---|
| 149 | #XXX: move this to Property? |
|---|
| 150 | self.args = args |
|---|
| 151 | self.kwargs = kwargs |
|---|
| 152 | |
|---|
| 153 | def create_properties(self): |
|---|
| 154 | if hasattr(self.prop, '__call__'): |
|---|
| 155 | prop_value = self.prop(self.entity.table.c) |
|---|
| 156 | else: |
|---|
| 157 | prop_value = self.prop |
|---|
| 158 | prop_value = self.evaluate_property(prop_value) |
|---|
| 159 | self.add_mapper_property(self.name, prop_value) |
|---|
| 160 | |
|---|
| 161 | def evaluate_property(self, prop): |
|---|
| 162 | if self.args or self.kwargs: |
|---|
| 163 | raise Exception('superfluous arguments passed to GenericProperty') |
|---|
| 164 | return prop |
|---|
| 165 | |
|---|
| 166 | |
|---|
| 167 | class ColumnProperty(GenericProperty): |
|---|
| 168 | ''' |
|---|
| 169 | A specialized form of the GenericProperty to generate SQLAlchemy |
|---|
| 170 | ``column_property``'s. |
|---|
| 171 | |
|---|
| 172 | It takes a function (often given as an anonymous lambda) as its first |
|---|
| 173 | argument. Other arguments and keyword arguments are forwarded to the |
|---|
| 174 | column_property construct. That first-argument function must accept exactly |
|---|
| 175 | one argument and must return the desired (scalar-returning) SQLAlchemy |
|---|
| 176 | ClauseElement. |
|---|
| 177 | |
|---|
| 178 | The function will be called whenever the entity table is completely |
|---|
| 179 | defined, and will be given |
|---|
| 180 | the .c attribute of the table of the entity as argument (as a way to |
|---|
| 181 | access the entity columns). The ColumnProperty will first wrap your |
|---|
| 182 | ClauseElement in an |
|---|
| 183 | "empty" label (ie it will be labelled automatically during queries), |
|---|
| 184 | then wrap that in a column_property. |
|---|
| 185 | |
|---|
| 186 | .. sourcecode:: python |
|---|
| 187 | |
|---|
| 188 | class OrderLine(Entity): |
|---|
| 189 | quantity = Field(Float) |
|---|
| 190 | unit_price = Field(Numeric) |
|---|
| 191 | price = ColumnProperty(lambda c: c.quantity * c.unit_price, |
|---|
| 192 | deferred=True) |
|---|
| 193 | |
|---|
| 194 | Please look at the `corresponding SQLAlchemy |
|---|
| 195 | documentation <http://www.sqlalchemy.org/docs/05/mappers.html |
|---|
| 196 | #sql-expressions-as-mapped-attributes>`_ for details. |
|---|
| 197 | ''' |
|---|
| 198 | |
|---|
| 199 | def evaluate_property(self, prop): |
|---|
| 200 | return column_property(prop.label(None), *self.args, **self.kwargs) |
|---|
| 201 | |
|---|
| 202 | |
|---|
| 203 | class Synonym(GenericProperty): |
|---|
| 204 | ''' |
|---|
| 205 | This class represents a synonym property of another property (column, ...) |
|---|
| 206 | of an entity. As opposed to the `synonym` kwarg to the Field class (which |
|---|
| 207 | share the same goal), this class can be used to define a synonym of a |
|---|
| 208 | property defined in a parent class (of the current class). On the other |
|---|
| 209 | hand, it cannot define a synonym for the purpose of using a standard python |
|---|
| 210 | property in queries. See the Field class for details on that usage. |
|---|
| 211 | |
|---|
| 212 | .. sourcecode:: python |
|---|
| 213 | |
|---|
| 214 | class Person(Entity): |
|---|
| 215 | name = Field(String(30)) |
|---|
| 216 | primary_email = Field(String(100)) |
|---|
| 217 | email_address = Synonym('primary_email') |
|---|
| 218 | |
|---|
| 219 | class User(Person): |
|---|
| 220 | user_name = Synonym('name') |
|---|
| 221 | password = Field(String(20)) |
|---|
| 222 | ''' |
|---|
| 223 | |
|---|
| 224 | def evaluate_property(self, prop): |
|---|
| 225 | return synonym(prop, *self.args, **self.kwargs) |
|---|
| 226 | |
|---|
| 227 | #class Composite(GenericProperty): |
|---|
| 228 | # def __init__(self, prop): |
|---|
| 229 | # super(GenericProperty, self).__init__() |
|---|
| 230 | # self.prop = prop |
|---|
| 231 | |
|---|
| 232 | # def evaluate_property(self, prop): |
|---|
| 233 | # return composite(prop.label(self.name)) |
|---|
| 234 | |
|---|
| 235 | #start = Composite(Point, lambda c: (c.x1, c.y1)) |
|---|
| 236 | |
|---|
| 237 | #mapper(Vertex, vertices, properties={ |
|---|
| 238 | # 'start':composite(Point, vertices.c.x1, vertices.c.y1), |
|---|
| 239 | # 'end':composite(Point, vertices.c.x2, vertices.c.y2) |
|---|
| 240 | #}) |
|---|
| 241 | |
|---|
| 242 | |
|---|
| 243 | has_property = PropertyStatement(GenericProperty) |
|---|