Package trunk :: Package src :: Module model
[hide private]
[frames] | no frames]

Source Code for Module trunk.src.model

  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   
 18  path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../lib/sqlalchemy.zip')) 
 19  if path not in sys.path: # pragma: no cover 
 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: # pragma: no cover 
 39      import simplejson as json 
 40   
 41  #: Metadata object for this model 
 42  metadata = MetaData() 
 43  #: Name-indexed dict containing the definition of every table 
 44  tables = {} 
 45  #: Name-indexed dict containing the mapper of every table 
 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 # Bind engine 59 metadata.bind = session_maker.bind 60 # Setup model 61 setup_tables() 62 setup_mappers(**kwargs) 63 setup_events(session_maker, **kwargs)
64
65 66 -def setup_tables():
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
113 -def setup_mappers(**kwargs):
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 # FIXME: Add project_id to parent and children properties when #1401 is fixed 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
193 194 -def setup_events(session, **kwargs):
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
210 -def _session_before_flush(session, flush_context, instances):
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
221 -class ORM_Base(object):
222 """ 223 Base class for mapping the tables. 224 """ 225
226 - def __init__(self, *args, **kwargs):
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
239 240 -class Element(ORM_Base):
241 """ 242 Mapping class for the table «element». 243 """ 244 245 @hybrid_property
246 - def id(self):
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
256 - def children(self):
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
266 - def parent_id(self):
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
276 - def parent(self):
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
286 - def parent(self, parent):
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 # Avoid infinite recursion 296 if self.parent == parent: 297 return 298 assert parent == None or isinstance(parent, Element) 299 # Check project coherence (parent - self) 300 if self.parent != None and parent != None: 301 assert parent.project == self.project 302 # Ensure children initialization and check for existence. DO NOT REMOVE 303 if parent != None: 304 assert parent.children != None 305 # Assign new parent 306 self._parent = parent 307 if self.parent != None: 308 self.project = parent.project 309 # Cascade status changes only if it has a parent 310 if self.status and not self.parent.forced_status: 311 self._cascade_status()
312 313 @hybrid_property
314 - def project_id(self):
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
324 - def project(self):
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
334 - def project(self, project):
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 # Avoid infinite recursion 343 if self.project == project: 344 return 345 assert isinstance(project, Project) 346 # Avoid second assignation 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
353 - def status(self):
354 """ 355 The status of this instance 356 357 @return: status 358 @rtype: str 359 """ 360 return self._status
361 362 @status.setter
363 - def status(self, status):
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 # Avoid infinite recursion 372 if self.status == status: 373 return 374 else: 375 self._status = status 376 # Cascade status changes 377 if self.parent and not self.parent.forced_status: 378 self._cascade_status()
379
380 - def __init__(self, *args, **kwargs):
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 # Assign first force_status field 392 if 'forced_status' in kwargs: 393 setattr(self, 'forced_status', kwargs['forced_status']) 394 del kwargs['forced_status'] 395 # Assign the rest of the fields 396 super(Element, self).__init__(*args, **kwargs)
397 398 @classmethod
399 - def _children_added(cls, parent, child, initiator):
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
418 - def _children_removed(cls, parent, child, initiator):
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
438 - def _cascade_status(self):
439 """ 440 Propagate its status to its parent, in a recursive manner. 441 """ 442 if self.parent.status > self.status: 443 self.parent.status = self.status 444 elif self.parent.status < self.status: 445 new_status = min([c.status for c in self.parent.children]) 446 if self.parent.status != new_status: 447 self.parent.status = new_status
448 449 @classmethod
450 - def query_tags(cls, session, tags={}):
451 """ 452 Overriden query method to apply tag-based custom filtering, on top of 453 common equality filter. 454 455 @param session: The database session in which to execute the query 456 @type session: Session 457 @param tags: Tag names and values to apply as a filter 458 @type tags: dict 459 @return: A query selecting Element instances, filtered by tags 460 @rtype: Query<L{Element}> 461 """ 462 463 q = session.query(cls) 464 if tags: 465 crit = literal(False) 466 for tag, value in tags.iteritems(): 467 if value == True: 468 crit |= (tables['tag_element'].c.tag == tag) 469 elif value == None or value != False : 470 crit |= (tables['tag_element'].c.tag == tag) & (tables['tag_element'].c.value == value) 471 q = q.join(tables['tag_element']) \ 472 .group_by(*[c for c in tables['element'].columns]) \ 473 .having(func.count(tables['element'].c.id) == len(tags)) \ 474 .filter(crit) 475 return q.from_self()
476
477 - def __repr__(self):
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
486 - def __str__(self):
487 """ 488 Coerces this instance to a string. 489 490 @return: The name field 491 @rtype: str 492 """ 493 return str(self.name)
494
495 496 -class Project(ORM_Base):
497 """ 498 Mapping class for the table «project». 499 """ 500 501 @hybrid_property
502 - def id(self):
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
512 - def query_tags(cls, session, tags={}):
513 """ 514 Overriden query method to apply tag-based custom filtering, on top of 515 common equality filter. 516 517 @param session: The database session in which to execute the query 518 @type session: Session 519 @param tags: Tag names and values to apply as a filter 520 @type tags: dict 521 @return: A query selecting Project instances, filtered by tags 522 @rtype: Query<L{Project}> 523 """ 524 q = session.query(cls) 525 if tags: 526 crit = literal(False) 527 for tag, value in tags.iteritems(): 528 if value == True: 529 crit |= (tables['tag_project'].c.tag == tag) 530 elif value == None or value != False : 531 crit |= (tables['tag_project'].c.tag == tag) & (tables['tag_project'].c.value == value) 532 q = q.join(tables['tag_project']) \ 533 .group_by(*[c for c in tables['project'].columns]) \ 534 .having(func.count(tables['project'].c.id) == len(tags)) \ 535 .filter(crit) 536 return q.from_self()
537
538 - def __repr__(self):
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
547 - def __str__(self):
548 """ 549 Coerces this instance to a string. 550 551 @return: The name field 552 @rtype: str 553 """ 554 return str(self.name)
555
556 557 -class TagElement(ORM_Base):
558 """ 559 Mapping class for the table «tagelement». 560 """ 561
562 - def __init__(self, *args, **kwargs):
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
576 577 -class TagProject(ORM_Base):
578 """ 579 Mapping class for the table «tagproject». 580 """ 581
582 - def __init__(self, *args, **kwargs):
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 # TODOS: 598 # - Reevaluate status cascading method. CTE? Triggers? 599 # - Avoid root nodes with the same name (unique key does not enforce with NULL parent). Each project needs an official single root node. 600 601 if __name__ == '__main__': # pragma: no cover 602 603 config = json.load(open(os.path.dirname(__file__) + '/config.json', 'r')) 604 # Create and configure engine and session 605 engine = engine_from_config(config) 606 session_maker = scoped_session(sessionmaker(bind=engine, twophase=config['session.twophase']))
607 608 - class EElement(Element):
609 pass
610
611 - class PProject(Project):
612 pass
613
614 - class TTagProject(TagProject):
615 pass
616
617 - class TTagElement(TagElement):
618 pass
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