Package trunk ::
Package src ::
Module model
|
|
1
2
3 """
4 MultiVAC generic framework for building tree-based status applications.
5
6 This modules provides an implementation of a generic framework, called
7 MultiVAC, for use in building applications working with trees of elements, in
8 which the status of a node derives from the status of the leaves.
9
10 Two classes are defined, one for each table in the relational model
11 L{Element}, L{Project}. Also, two additional classes are defined for the
12 association relationships between the former: L{TagElement} and L{TagProject}.
13 """
14
15 import os
16 import sys
17
18 path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../lib/sqlalchemy.zip'))
19 if path not in sys.path:
20 sys.path.append(path)
21
22 import sqlalchemy
23
24 from sqlalchemy import (Column, MetaData, Table, Index,
25 ForeignKeyConstraint, PrimaryKeyConstraint, UniqueConstraint,
26 Boolean, Integer, String, Text,
27 engine_from_config, func, event)
28 from sqlalchemy.ext.associationproxy import association_proxy
29 from sqlalchemy.ext.hybrid import hybrid_property
30 from sqlalchemy.orm import mapper, relationship, scoped_session, sessionmaker
31 from sqlalchemy.orm.collections import attribute_mapped_collection
32 from sqlalchemy.orm.exc import NoResultFound
33 from sqlalchemy.orm.session import Session
34 from sqlalchemy.sql.expression import literal
35
36 try:
37 import json
38 except:
39 import simplejson as json
40
41
42 metadata = MetaData()
43
44 tables = {}
45
46 mappers = {}
47
48 -def init(session_maker, **kwargs):
49 """
50 Initialize the db connection, set up the tables and map the classes.
51
52 @param session_maker: Session generator to bind to the model
53 @type session_maker: sqlalchemy.orm.session.Session factory
54 @param kwargs: Additional settings for the mapper
55 @type kwargs: dict
56 """
57
58
59 metadata.bind = session_maker.bind
60
61 setup_tables()
62 setup_mappers(**kwargs)
63 setup_events(session_maker, **kwargs)
64
67 """
68 Define the tables, columns, keys and constraints of the DB.
69 """
70
71 global tables
72
73 tables['element'] = Table('element', metadata,
74 Column('id', Integer, nullable=False),
75 Column('forced_status', Boolean, nullable=False, default=False),
76 Column('name', String(120), nullable=False),
77 Column('parent_id', Integer, nullable=True),
78 Column('project_id', Integer, nullable=False),
79 Column('status', String(20), nullable=False),
80 PrimaryKeyConstraint('id'),
81 ForeignKeyConstraint(['project_id'], ['project.id'], ondelete='CASCADE'),
82 ForeignKeyConstraint(['project_id', 'parent_id'], ['element.project_id', 'element.id'], ondelete='CASCADE'),
83 UniqueConstraint('project_id', 'parent_id', 'name'),
84 UniqueConstraint('project_id', 'id')
85 )
86 Index('element_uk_root', tables['element'].c.project_id, tables['element'].c.name,
87 postgresql_where=tables['element'].c.parent_id == None, unique=True
88 )
89
90 tables['project'] = Table('project', metadata,
91 Column('id', Integer, nullable=False),
92 Column('name', String(20), nullable=False),
93 PrimaryKeyConstraint('id'),
94 UniqueConstraint('name'),
95 )
96
97 tables['tag_element'] = Table('tag_element', metadata,
98 Column('tag', String(20), nullable=False),
99 Column('element_id', Integer, nullable=False),
100 Column('value', Text(), nullable=True),
101 PrimaryKeyConstraint('element_id', 'tag'),
102 ForeignKeyConstraint(['element_id'], ['element.id'], ondelete='CASCADE'),
103 )
104
105 tables['tag_project'] = Table('tag_project', metadata,
106 Column('tag', String(20), nullable=False),
107 Column('project_id', Integer, nullable=False),
108 Column('value', Text(), nullable=True),
109 PrimaryKeyConstraint('project_id', 'tag'),
110 ForeignKeyConstraint(['project_id'], ['project.id'], ondelete='CASCADE'),
111 )
112
114 """
115 Define the mapping between tables and classes, and the relationships that link them.
116
117 @kwarg extra_mapping: Mapping between database tables and classes
118 @type extra_mapping: dict
119 @kwarg extra_properties: Dictionary of additional properties for a table
120 @type extra_properties: dict
121 @kwarg extra_extensions: Dictionary of additional extensions for a table
122 @type extra_extensions: dict
123 @kwarg extra_kwargs: Dictionary of additional arguments for a mapper
124 @type extra_kwargs: dict
125 """
126
127 global mappers
128
129 mapping = {
130 'element' : Element,
131 'project' : Project,
132 'tag_element' : TagElement,
133 'tag_project' : TagProject,
134 }
135 mapping.update(kwargs.get('extra_mapping', dict()))
136
137 assert issubclass(mapping['element'], Element)
138 assert issubclass(mapping['project'], Project)
139 assert issubclass(mapping['tag_element'], TagElement)
140 assert issubclass(mapping['tag_project'], TagProject)
141
142 properties = {}
143 properties['element'] = {
144 '_id' : tables['element'].c.id,
145 '_parent_id' : tables['element'].c.parent_id,
146 '_project_id' : tables['element'].c.project_id,
147 '_status' : tables['element'].c.status,
148
149 '_children' : relationship(mapping['element'], back_populates='_parent', collection_class=set,
150 primaryjoin = tables['element'].c.parent_id == tables['element'].c.id,
151 cascade='all', passive_deletes=True),
152 '_parent' : relationship(mapping['element'], back_populates='_children', collection_class=set,
153 primaryjoin = tables['element'].c.parent_id == tables['element'].c.id,
154 remote_side = [ tables['element'].c.id ]),
155 '_project' : relationship(mapping['project'], back_populates='elements', collection_class=set),
156 '_tag' : relationship(mapping['tag_element'], collection_class=attribute_mapped_collection('tag'),
157 cascade='all, delete-orphan', passive_deletes=True),
158 }
159 properties['project'] = {
160 '_id' : tables['project'].c.id,
161 'elements' : relationship(mapping['element'], back_populates='_project', collection_class=set,
162 primaryjoin = tables['element'].c.project_id == tables['project'].c.id,
163 cascade='all', passive_deletes=True),
164 '_tag' : relationship(mapping['tag_project'], collection_class=attribute_mapped_collection('tag'),
165 cascade='all, delete-orphan', passive_deletes=True),
166 }
167 properties['tag_element'] = {}
168 properties['tag_project'] = {}
169
170 extra_properties = kwargs.get('extra_properties', dict())
171 for entity in mapping.iterkeys():
172 properties[entity].update(extra_properties.get(entity, dict()))
173
174 extensions = {}
175 extensions.update(kwargs.get('extra_extensions', dict()))
176
177 options = {}
178 options.update(kwargs.get('extra_kwargs', dict()))
179
180 for name, cls in mapping.iteritems():
181 mappers[name] = mapper(cls, tables[name],
182 properties=properties.get(name, None),
183 extension=extensions.get(name, None),
184 **options.get(name, {}))
185
186 """
187 Association proxy to access its tags and retrieve their corresponding value.
188 Example: instance.tag['name'] = 'value'
189 """
190 Element.tag = association_proxy('_tag', 'value', creator=lambda tag, value: mapping['tag_element'](tag=tag, value=value))
191 Project.tag = association_proxy('_tag', 'value', creator=lambda tag, value: mapping['tag_project'](tag=tag, value=value))
192
195 """
196 Define the events of the model.
197 """
198 mapping = {
199 'element' : Element,
200 'project' : Project,
201 'tag_element' : TagElement,
202 'tag_project' : TagProject,
203 }
204 mapping.update(kwargs.get('extra_mapping', dict()))
205
206 event.listen(mapping['element']._children, 'append', mapping['element']._children_added)
207 event.listen(mapping['element']._children, 'remove', mapping['element']._children_removed)
208 event.listen(session, 'before_flush', _session_before_flush)
209
211 """
212 Ensure that when an Element instance is deleted, the children collection of
213 its parent is notified to update and cascade the status change.
214 """
215 print "* * * * * * * * * * * * * * * SESSION BEFORE FLUSH * * * * * * * * * * * * * * *"
216 for instance in session.deleted:
217 if isinstance(instance, Element):
218 if instance.parent:
219 instance.parent.children.remove(instance)
220
222 """
223 Base class for mapping the tables.
224 """
225
227 """
228 Base contructor for all mapped entities.
229 Set the value of attributes based on keyword arguments.
230
231 @param args: Optional arguments to the constructor
232 @type args: tuple
233 @param kwargs: Optional keyword arguments to the constructor
234 @type kwargs: dict
235 """
236 for name, value in kwargs.iteritems():
237 setattr(self, name, value)
238
241 """
242 Mapping class for the table «element».
243 """
244
245 @hybrid_property
247 """
248 Read only accessor to prevent setting this field.
249
250 @return: Surrogate primary key
251 @rtype: int
252 """
253 return self._id
254
255 @hybrid_property
257 """
258 The collection of child Elements of this instance.
259
260 @return: This instance collection of children Elements
261 @rtype: set<L{Element}>
262 """
263 return self._children
264
265 @hybrid_property
267 """
268 Read only accessor to prevent setting this field.
269
270 @return: Foreign key for the parent L{Element} relationship
271 @rtype: int
272 """
273 return self._parent_id
274
275 @hybrid_property
277 """
278 The parent Element of this instance.
279
280 @return: The parent Element
281 @rtype: L{Element}
282 """
283 return self._parent
284
285 @parent.setter
287 """
288 Setter for the related parent Element of this instance.
289 Ensures project coherence between itself and the parent, and proper
290 children collection initialization. Also cascades status changes.
291
292 @param parent: The parent Element to be assigned
293 @type parent: L{Element}
294 """
295
296 if self.parent == parent:
297 return
298 assert parent == None or isinstance(parent, Element)
299
300 if self.parent != None and parent != None:
301 assert parent.project == self.project
302
303 if parent != None:
304 assert parent.children != None
305
306 self._parent = parent
307 if self.parent != None:
308 self.project = parent.project
309
310 if self.status and not self.parent.forced_status:
311 self._cascade_status()
312
313 @hybrid_property
315 """
316 Read only accessor to prevent setting this field.
317
318 @return: Foreign key for the parent L{Project} relationship
319 @rtype: int
320 """
321 return self._project_id
322
323 @hybrid_property
325 """
326 The related Project of this instance.
327
328 @return: The related Project
329 @rtype: L{Project}
330 """
331 return self._project
332
333 @project.setter
335 """
336 Setter for the related Project of this instance.
337 Prevents a second assignation.
338
339 @param project: The Project to be assigned
340 @type project: L{Project}
341 """
342
343 if self.project == project:
344 return
345 assert isinstance(project, Project)
346
347 if self.project == None:
348 self._project = project
349 else:
350 raise AttributeError('This attribute cannot be modified once it has been assigned.')
351
352 @hybrid_property
354 """
355 The status of this instance
356
357 @return: status
358 @rtype: str
359 """
360 return self._status
361
362 @status.setter
364 """
365 Setter for the status of this instance.
366 Ensures the cascade of a status change.
367
368 @param status: The status to be assigned
369 @type status: str
370 """
371
372 if self.status == status:
373 return
374 else:
375 self._status = status
376
377 if self.parent and not self.parent.forced_status:
378 self._cascade_status()
379
381 """
382 Constructor for Element instances.
383 Ensures that the «forced_status» field is assigned first to cascade
384 status properly.
385
386 @param args: Optional arguments to the constructor
387 @type args: tuple
388 @param kwargs: Optional keyword arguments to the constructor
389 @type kwargs: dict
390 """
391
392 if 'forced_status' in kwargs:
393 setattr(self, 'forced_status', kwargs['forced_status'])
394 del kwargs['forced_status']
395
396 super(Element, self).__init__(*args, **kwargs)
397
398 @classmethod
400 """
401 Listener to be executed when an element has to be added to a
402 children collection.
403 Check the added child status and update the parent's one.
404
405 @param parent: The Element that has a new child added
406 @type parent: L{Element}
407 @param child: The Element being added as a child
408 @type child: L{Element}
409 """
410 if not child.status or parent.forced_status:
411 return
412 if parent.status > child.status:
413 parent.status = child.status
414 elif (parent.status < child.status) and (len(parent.children) == 1):
415 parent.status = child.status
416
417 @classmethod
419 """
420 Listener to be executed when an element has to be removed from a
421 children collection.
422 Check the removed child status and update the parent's one.
423
424 @param parent: The Element that has a child removed
425 @type parent: L{Element}
426 @param child: The Element being removed as a child
427 @type child: L{Element}
428 """
429
430 if not child.status or parent.forced_status:
431 return
432 new_children = parent.children.difference([child])
433 if parent.status == child.status and new_children:
434 new_status = min([c.status for c in new_children])
435 if parent.status != new_status:
436 parent.status = new_status
437
448
449 @classmethod
476
478 """
479 Returns a printable representation of this instance.
480
481 @return: A descriptive string containing most of this instance fields
482 @rtype: str
483 """
484 return u"<Element %s: name '%s', parent_id '%s', project_id '%s', status '%s', forced_status '%s'>" % (self.id, self.name, self.parent_id, self.project_id, self.status, self.forced_status)
485
487 """
488 Coerces this instance to a string.
489
490 @return: The name field
491 @rtype: str
492 """
493 return str(self.name)
494
497 """
498 Mapping class for the table «project».
499 """
500
501 @hybrid_property
503 """
504 Read only accessor to prevent setting this field.
505
506 @return: Surrogate primary key
507 @rtype: int
508 """
509 return self._id
510
511 @classmethod
537
539 """
540 Returns a printable representation of this instance.
541
542 @return: A descriptive string containing most of this instance fields
543 @rtype: str
544 """
545 return u"<Project %s: name '%s'>" % (self.id, self.name)
546
548 """
549 Coerces this instance to a string.
550
551 @return: The name field
552 @rtype: str
553 """
554 return str(self.name)
555
558 """
559 Mapping class for the table «tagelement».
560 """
561
563 """
564 Constructor for TagElement instances.
565 Ensures that the «tag» field is specified.
566
567 @param args: Optional arguments to the constructor
568 @type args: tuple
569 @param kwargs: Optional keyword arguments to the constructor
570 @type kwargs: dict
571 """
572 assert 'tag' in kwargs
573 assert kwargs['tag'] is not None
574 super(TagElement, self).__init__(*args, **kwargs)
575
578 """
579 Mapping class for the table «tagproject».
580 """
581
583 """
584 Constructor for TagProject instances.
585 Ensures that the «tag» field is specified.
586
587 @param args: Optional arguments to the constructor
588 @type args: tuple
589 @param kwargs: Optional keyword arguments to the constructor
590 @type kwargs: dict
591 """
592 assert 'tag' in kwargs
593 assert kwargs['tag'] is not None
594 super(TagProject, self).__init__(*args, **kwargs)
595
596
597
598
599
600
601 if __name__ == '__main__':
602
603 config = json.load(open(os.path.dirname(__file__) + '/config.json', 'r'))
604
605 engine = engine_from_config(config)
606 session_maker = scoped_session(sessionmaker(bind=engine, twophase=config['session.twophase']))
610
613
616
619
620 init(session_maker, extra_mapping={
621 'element' : EElement,
622 'project' : PProject,
623 'tag_project' : TTagProject,
624 'tag_element' : TTagElement,
625 })
626
627 session = session_maker()
628
629 metadata.drop_all()
630 metadata.create_all()
631
632 p1 = PProject()
633 p1.name = 'p1'
634
635 p2 = PProject(name='p2')
636
637 p2.tag['t1'] = 't1'
638 p2.tag['t1'] = 'p2t1'
639
640 p3 = PProject(name='p3')
641
642 e1 = EElement(name='e1', status='50_DONE', forced_status=False, project=p1)
643
644 e2 = EElement()
645 e2.name = 'e2'
646 e2.status = '30_PENDING'
647 e2.forced_status = True
648 e2.parent = e1
649 e2.project = p1
650
651 e2.tag['t2'] = 't2'
652 e2.tag['t2'] = 'e2t2'
653
654 e3 = EElement(name='e3', status='05_ERROR', forced_status=False, project=p1)
655
656 session.add(p1)
657 session.commit()
658
659 e1.children.remove(e2)
660 e2.children.add(e1)
661
662 p1.elements.remove(e3)
663 p2.elements.add(e3)
664
665 session.commit()
666
667 print session.query(PProject).filter_by(name='p1').one()
668 print session.query(EElement).filter_by(name='e1').one()
669