Package multivac
[hide private]
[frames] | no frames]

Source Code for Package multivac

  1  #! /usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  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: # pragma: no cover 
 35      import simplejson as json 
 36   
 37  #: Metadata object for this model 
 38  metadata = MetaData() 
 39  #: Name-indexed dict containing the definition of every table 
 40  tables = {} 
 41  #: Name-indexed dict containing the mapper of every table 
 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 # Bind engine 55 metadata.bind = session_maker.bind 56 # Setup model 57 setup_tables() 58 setup_mappers(**kwargs) 59 setup_events(session_maker, **kwargs)
60
61 62 -def setup_tables():
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
109 -def setup_mappers(**kwargs):
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 # FIXME: Add project_id to parent and children properties when #1401 is fixed 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
189 190 -def setup_events(session_maker, **kwargs):
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
206 -def _session_before_flush(session, flush_context, instances):
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
216 -class ORM_Base(object):
217 """ 218 Base class for mapping the tables. 219 """ 220
221 - def __init__(self, *args, **kwargs):
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
234 235 -class Element(ORM_Base):
236 """ 237 Mapping class for the table «element». 238 """ 239 240 @hybrid_property
241 - def id(self):
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
251 - def children(self):
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
261 - def parent_id(self):
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
271 - def parent(self):
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
281 - def parent(self, parent):
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 # Avoid infinite recursion 291 if self.parent == parent: 292 return 293 assert parent == None or isinstance(parent, Element) 294 # Check project coherence (parent - self) 295 if self.parent != None and parent != None: 296 assert parent.project == self.project 297 # Ensure children initialization and check for existence. DO NOT REMOVE 298 if parent != None: 299 assert parent.children != None 300 # Assign new parent 301 self._parent = parent 302 if self.parent != None: 303 self.project = parent.project 304 # Cascade status changes only if it has a parent 305 if self.status and not self.parent.forced_status: 306 self._cascade_status()
307 308 @hybrid_property
309 - def project_id(self):
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
319 - def project(self):
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
329 - def project(self, project):
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 # Avoid infinite recursion 338 if self.project == project: 339 return 340 assert isinstance(project, Project) 341 # Avoid second assignation 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
348 - def status(self):
349 """ 350 The status of this instance 351 352 @return: status 353 @rtype: str 354 """ 355 return self._status
356 357 @status.setter
358 - def status(self, status):
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 # Avoid infinite recursion 367 if self.status == status: 368 return 369 else: 370 self._status = status 371 # Cascade status changes 372 if self.parent and not self.parent.forced_status: 373 self._cascade_status()
374
375 - def __init__(self, *args, **kwargs):
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 # Assign first force_status field 387 if 'forced_status' in kwargs: 388 setattr(self, 'forced_status', kwargs['forced_status']) 389 del kwargs['forced_status'] 390 # Assign the rest of the fields 391 super(Element, self).__init__(*args, **kwargs)
392 393 @classmethod
394 - def _children_added(cls, parent, child, initiator):
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
413 - def _children_removed(cls, parent, child, initiator):
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
433 - def _cascade_status(self):
434 """ 435 Propagate its status to its parent, in a recursive manner. 436 """ 437 if self.parent.status > self.status: 438 self.parent.status = self.status 439 elif self.parent.status < self.status: 440 new_status = min([c.status for c in self.parent.children]) 441 if self.parent.status != new_status: 442 self.parent.status = new_status
443 444 @classmethod
445 - def query_tags(cls, session, tags={}):
446 """ 447 Overriden query method to apply tag-based custom filtering, on top of 448 common equality filter. 449 450 @param session: The database session in which to execute the query 451 @type session: Session 452 @param tags: Tag names and values to apply as a filter 453 @type tags: dict 454 @return: A query selecting Element instances, filtered by tags 455 @rtype: Query<L{Element}> 456 """ 457 458 q = session.query(cls) 459 if tags: 460 crit = literal(False) 461 for tag, value in tags.iteritems(): 462 if value == True: 463 crit |= (tables['tag_element'].c.tag == tag) 464 elif value == None or value != False : 465 crit |= (tables['tag_element'].c.tag == tag) & (tables['tag_element'].c.value == value) 466 q = q.join(tables['tag_element']) \ 467 .group_by(*[c for c in tables['element'].columns]) \ 468 .having(func.count(tables['element'].c.id) == len(tags)) \ 469 .filter(crit) 470 return q.from_self()
471
472 - def __repr__(self):
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
481 - def __str__(self):
482 """ 483 Coerces this instance to a string. 484 485 @return: The name field 486 @rtype: str 487 """ 488 return str(self.name)
489
490 491 -class Project(ORM_Base):
492 """ 493 Mapping class for the table «project». 494 """ 495 496 @hybrid_property
497 - def id(self):
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
507 - def query_tags(cls, session, tags={}):
508 """ 509 Overriden query method to apply tag-based custom filtering, on top of 510 common equality filter. 511 512 @param session: The database session in which to execute the query 513 @type session: Session 514 @param tags: Tag names and values to apply as a filter 515 @type tags: dict 516 @return: A query selecting Project instances, filtered by tags 517 @rtype: Query<L{Project}> 518 """ 519 q = session.query(cls) 520 if tags: 521 crit = literal(False) 522 for tag, value in tags.iteritems(): 523 if value == True: 524 crit |= (tables['tag_project'].c.tag == tag) 525 elif value == None or value != False : 526 crit |= (tables['tag_project'].c.tag == tag) & (tables['tag_project'].c.value == value) 527 q = q.join(tables['tag_project']) \ 528 .group_by(*[c for c in tables['project'].columns]) \ 529 .having(func.count(tables['project'].c.id) == len(tags)) \ 530 .filter(crit) 531 return q.from_self()
532
533 - def __repr__(self):
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
542 - def __str__(self):
543 """ 544 Coerces this instance to a string. 545 546 @return: The name field 547 @rtype: str 548 """ 549 return str(self.name)
550
551 552 -class TagElement(ORM_Base):
553 """ 554 Mapping class for the table «tagelement». 555 """ 556
557 - def __init__(self, *args, **kwargs):
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
571 572 -class TagProject(ORM_Base):
573 """ 574 Mapping class for the table «tagproject». 575 """ 576
577 - def __init__(self, *args, **kwargs):
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 # TODOS: 593 # - Reevaluate status cascading method. CTE? Triggers? 594 595 if __name__ == '__main__': # pragma: no cover 596 597 config = json.load(open(os.path.dirname(__file__) + '/config.json', 'r')) 598 # Create and configure engine and session 599 engine = engine_from_config(config) 600 session_maker = scoped_session(sessionmaker(bind=engine, twophase=config['session.twophase']))
601 602 - class EElement(Element):
603 pass
604
605 - class PProject(Project):
606 pass
607
608 - class TTagProject(TagProject):
609 pass
610
611 - class TTagElement(TagElement):
612 pass
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