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