PyModel.py :  » Database » Modeling-Framework » Modeling-0.9 » Modeling » Python Open Source

Home
Python Open Source
1.3.1.2 Python
2.Ajax
3.Aspect Oriented
4.Blog
5.Build
6.Business Application
7.Chart Report
8.Content Management Systems
9.Cryptographic
10.Database
11.Development
12.Editor
13.Email
14.ERP
15.Game 2D 3D
16.GIS
17.GUI
18.IDE
19.Installer
20.IRC
21.Issue Tracker
22.Language Interface
23.Log
24.Math
25.Media Sound Audio
26.Mobile
27.Network
28.Parser
29.PDF
30.Project Management
31.RSS
32.Search
33.Security
34.Template Engines
35.Test
36.UML
37.USB Serial
38.Web Frameworks
39.Web Server
40.Web Services
41.Web Unit
42.Wiki
43.Windows
44.XML
Python Open Source » Database » Modeling Framework 
Modeling Framework » Modeling 0.9 » Modeling » PyModel.py
# -*- coding: iso-8859-1 -*-
#-----------------------------------------------------------------------------
# Modeling Framework: an Object-Relational Bridge for python
#
# Copyright (c) 2001-2004 Sbastien Bigaret <sbigaret@users.sourceforge.net>
# All rights reserved.
#
# This file is part of the Modeling Framework.
#
# This code is distributed under a "3-clause BSD"-style license;
# see the LICENSE file for details.
#-----------------------------------------------------------------------------


"""
Pythonic Description of an OO-RDBM Model

Rules:
- a model is an object of type Model
- a model may specify a list of entities, each of type Entity
- an entity may specify a list of properties, that are either of type Attribute
  or \of type Relation
- all references to attributes, relations, entities, models, is by their "name"
  i.e. not by their columnName or className, etc, as the case may be
- whenever deemed useful, defaults are used
- note that the default "type" for an attribute is int 
  (not string -- seems more logical to me) 
- delete rule values are in lowercase

New Changes to note:
parentEntity -> parent 
Entity only exposes a properties[] (that holds all attributes and relations)
default for Model.adaptorName is None
added doc attribute on all description classes

Previous changes:
defaults class dictionary variable, for all model description classes
deleteRule -> delete
multiplicityLowerBound and miltiplicity upperBound -> multiplicity
parentEntity -> isAlso
default for displayLabel is value of name (not of columnName)
Entity only has attributes[] (that may also be relations)
"""

##
import Modeling
from Entity import externalNameForInternalName
from utils import isListOrTuple

class IncompatibleVersionError(RuntimeError):
  pass

def identity(x):
  "Returns x, unchanged"
  return x

def debug(msg):
  #print msg
  pass

def error(msg):
  raise RuntimeError, msg

class MP:
  def __init__(self, method, *args):
    self.method=method
    self.param=args
  def __repr__(self):
    return '%s%s'%(self.method, self.param)

class BaseDescription:
  ''' Base description class (should not be exported to client code) ''' 
  VERSION='0.1'
  defaults = {}
  def update(self):
    pass
    ## NB pourrait servir a mettre une derniere fois a jour le component
    ## en fct. de self.__dict__ filtr'e par self.__class__.defaults.keys()
    ## ++ ?? peut-etre ajouter un truc du genre 'ignore' dans les defaults
  
  def build(self, model):
    pass
  
  def requiredField(self, field):
    ''' make test more useful '''
    if not field:
      raise ValueError, 'Required field'
    return field

  def defaultedField(self, field, defaultValue):
    if field:
      return field
    else:
      return defaultValue
  
  def clone(self, descriptionObject):
    ''' return an independant copy of object '''
    return descriptionObject

  def toXML(self):
    return '<later/>'

  #def __str__(self):
  #  return '<%s instance at %s>'%(str(self.__class__), hex(id(self)))
##

class Model(BaseDescription):
  ''' Describes a Model '''
  defaults={ 'packageName': MP(identity, 'name'),
             'adaptorName': '',
             'connDict': { 'host': '', 'database': '',
                           'user': '', 'password': '' },
             'entities': [],
             'associations': [],
             'doc': '',
             }
  
  def __init__(self, name, **kw):
    self.is_built=0
    self.name=self.requiredField(name)
    self.entities=list(kw.get('entities', []))
    self.associations=list(kw.get('associations', []))
    from Model import Model
    self.component=MModel(name)
    updateComponent(self, kw, Model.defaults, name,
                    ignore=('entities','associations','version'))
    self.version=kw.get('version', None)

  def entityNamed(self, name):
    for e in self.entities:
      if e.name==name:
        return e
    return None
  
  def validate(self): 
    ''' validate this model instance '''
    pass
  
  def generatePython(self,entityName=None): 
    ''' generate code for model, or only for entityName if specified '''
    pass
      
  def generateDBSchema(self,entityName=None): 
    ''' Generate SQL only '''
    pass
      
  def createDBSchema(self,entityName=None): 
    ''' drops and recreates from scratch '''
    pass
      
  def updateDBSchema(self,mode,entityName=None): 
    ''' 
    operate on existing tables (and defined by this schema), employing mode:
    - check: only report differences between model and existent DB
    - incremental: only add/modify columns in existing tables 
    - clean: add/modify new columns/tables, and drop obsolete ones
    Note: renaming of columns or tables?
    '''
    pass
  
  def addEntity(self, entityDescObj):
    ''' add entity to model '''
    pass
  
  def removeEntity(self, name):
    ''' remove entity from model '''
    pass

  def build_associations(self):
    """  """
    debug('Processing associations')
    for assoc in self.associations:
      assoc.build(self)
      
  def build(self):
    # Check version
    if getattr(self, 'version', None)!=BaseDescription.VERSION:
      raise IncompatibleVersionError, 'Incompatible versions --please check MIGRATION'
    
    #
    if self.is_built:
      raise RuntimeError, 'PyModel has already been built'
    self.is_built=1
    
    # Build ordered list of entities
    self.ordered_entities=oe=[]
    entities=list(self.entities)
    entities_processed=[]
    for entity in [e for e in entities if not e.parent]:
      oe.append(entity)
      entities_processed.append(entity.name)
      entities.remove(entity)
    while entities:
      #print [e.name for e in entities], entities_processed
      es=[e for e in entities if e.parent in entities_processed]
      for entity in es:
        entities.remove(entity)
        entities_processed.append(entity.name)
        oe.append(entity)
        
    # Build defaults, parent, attributes
    for entity in self.ordered_entities:
      entity.prebuild_with_defaults(model=self)
      entity.prebuild_with_parent_properties(model=self)

    # Build Associations
    self.build_associations()
    for entity in self.ordered_entities:
      # propagates changes made by Associations
      entity.prebuild_with_parent_properties(model=self)

    # Build attributes
    for entity in self.ordered_entities:
      entity.build_attributes(model=self)
      self.component.addEntity(entity.component)

      
    # Check inheritance
    for entity in self.ordered_entities:
      if entity.parent:
        superEntity=self.component.entityNamed(entity.parent)
        superEntity.addSubEntity(entity.component)

    # model / build phase 2
    for entity in self.ordered_entities:
      entity.build_preprocess_relationships_and_inverse(self)
    for entity in self.ordered_entities:
      entity.build_toOne_relationships(self)
    for entity in self.ordered_entities:
      entity.build_toMany_relationships(self)


  def toXML(self):
    import cStringIO
    _strIO=cStringIO.StringIO()
    doc=self.component.getXMLDOM(encoding='iso-8859-1')
    from xml.dom.ext import PrettyPrint
    PrettyPrint(doc, stream=_strIO, encoding='iso-8859-1')
    xml=_strIO.getvalue()
    _strIO.close()
    return xml

##

class Entity(BaseDescription):
  ''' Describes an Entity '''
  defaults={ 'className': MP(identity, 'name'),
             'externalName': MP(externalNameForInternalName, 'name'),
             'moduleName': MP(identity, 'name'),
             'typeName': '',
             'isAbstract': 0,
             'isReadOnly': 0,
             'properties': [],
             'parent': '',
             'doc': '',
             }
  def __init__(self, name, **kw):
    self.name=self.requiredField(name)
    #self.properties=kw.get('properties', ())
    from Entity import Entity
    self.component=MEntity(name)
    updateComponent(self, kw, Entity.defaults, name,
                    ignore=('parent','properties'))
  
  def addAttribute(self, attributeDescObj, model):
    ''' add attribute to model '''
    debug('Entity.addAttribute e: %s / attr: %s'%(self.name,
                                                  attributeDescObj.name))
    # check it does not already exist
    a=self.attributeNamed(attributeDescObj.name)
    if a:
      debug('  Found it -- ignoring / .entity()=%s'%a.component.entity())
      return
    # continue
    self.properties.append(attributeDescObj)
    attributeDescObj.build(model, self) # bizarre, plutot build(entity, model)
    self.component.addAttribute(attributeDescObj.component)
    self.processAttributes(model)
    self.propagate_attribute(attributeDescObj, model)
    
  def attFKName(self, destEntityName):
    n='fk'+destEntityName
    count=1
    while self.propertyNamed(n):
      n='fk'+destEntityName+str(count)
      count+=1
    return n

  def attributeNamed(self, name):
    for prop in [r for r in self.properties if isinstance(r, Attribute)]:
      if prop.name==name: return prop
    return None
  
  def removeAttribute(self, attributeDescObj):
    ''' remove attribute from model '''
    pass

  def addRelation(self, relationDescObj):
    ''' add relation to model '''
    pass

  def attributes(self):
    "-"
    return [r for r in self.properties if isinstance(r, Attribute)]

  def propertyNamed(self, name):
    a=self.attributeNamed(name)
    if a: return a
    else: return self.relationshipNamed(name)
    
  def relationshipNamed(self, name):
    "-"
    for r in self.relationships():
      if r.name==name:
        return r
    return None

  def relationships(self):
    "-"
    return [r for r in self.properties if isinstance(r, BaseRelation)]

  def removeRelation(self, relationDescObj):
    ''' remove relation from model '''
    pass

  def inheritFrom(self, entityDescObj):
    ''' add to this entity all other atts & rels defined in entityDescObj '''
    pass

  def primaryKey(self):
    """ Returns the entity's PK """
    for prop in self.properties:
      if isinstance(prop, APrimaryKey):
        return prop
    return None

  def propagate_attribute(self, attributeDescObj, model):
    debug('Entity.propagate_attribute e: %s / attr: %s'%(self.name, attributeDescObj.name))
    for e in [ee.name() for ee in self.component.subEntities()]:
      entity=model.entityNamed(e)
      #if not entity.component.attributeNamed(attributeDescObj.name):
      entity.addAttribute(attributeDescObj.clone(), model)
  
  def processAttributes(self, model):
    # Process attributes and look for PKs & attributes usedForLocking
    pks=[]
    usedForLocking=[]
    for prop in self.attributes():
      prop.build(model, self)
      if isinstance(prop, APrimaryKey):
        pks.append(prop.component)
      if prop.usedForLocking:
        usedForLocking.append(prop.component)
    self.component.setPrimaryKeyAttributes(pks)
    self.component.setAttributesUsedForLocking(usedForLocking)

  def prebuild_with_defaults(self, model):
    "Phase 1"
    debug('Entity.build/1: %s'%self.name)
    # check defaults properties
    if Entity.defaults.get('properties'):
      # Update properties iff:
      # - they are not already present
      # - we do not add a PK if one is already defined
      self_prop_names=[prop.name for prop in self.properties]
      for dprop in Entity.defaults['properties']:
        if dprop.name not in self_prop_names:
          if not ( self.primaryKey() and isinstance(dprop, APrimaryKey) ):
            self.properties.append(dprop.clone())
    
  def prebuild_with_parent_properties(self, model):
    """ Builds a Modeling.Entity / Phase 2 """
    debug('Entity.build/2: %s'%self.name)

    # Check inheritance
    if self.parent:
      parent=model.entityNamed(self.parent)
      if not parent:
        raise ValueError, "Unable to find parent '%s' for entity '%s'"%(self.parent, self.name)
      # Update the properties
      self_prop_names=[prop.name for prop in self.properties]
      for pprop in parent.properties:
        if pprop.name in self_prop_names:
          # Overriden property, shouldnt be changed
          continue
        clone=pprop.clone()
        clone.propagated_by=self.parent
        self.properties.append(clone)

  def build_attributes(self, model):
    """ Builds a Modeling.Entity / Phase 3 """
    debug('Entity.build/3: %s'%self.name)

    ## ATTRIBUTES

    # Add all attributes
    for prop in self.attributes():
      prop.build(model, self)
      self.component.addAttribute(prop.component)
      debug('build_attributes %s.%s: %s'%(self.name,prop.name,prop.component.entity()))
    self.processAttributes(model)

  def build_preprocess_relationships_and_inverse(self, model):
    """ Builds a Modeling.Entity / Phase 4 """
    if self.parent: return
    
    debug('Entity.build/4: %s'%self.name)
    for r in self.relationships():
      destEntity=model.entityNamed(r.destination)
      if not destEntity:
        raise ValueError, 'Relation %s.%s: cannot find destination entity %s'%(self.name,r.name,r.destination)
      if getattr(r, 'inverse',None):
        self.forward_rel_info(r, model)

  def forward_rel_info(self, rel, model):
    ''' '''
    debug('forward_rel_info %s.%s'%(self.name,rel.name))

    def check_or_make_equal(x,y):
      if x and y:
        assert x == y
      return x and (x,x) or (y,y)

    for e in self.component.allSubEntities():
      r=model.entityNamed(e.name()).relationshipNamed(rel.name)
      r.inverse=''

      rel.src,r.src=check_or_make_equal(rel.src,r.src)
      rel.dst,r.dst=check_or_make_equal(rel.dst,r.dst)

    if rel.inverse:
      invr=model.entityNamed(rel.destination).relationshipNamed(rel.inverse)
      if invr.inverse: assert invr.inverse==rel.name
      else: invr.inverse=rel.name

      rel.src,invr.dst=check_or_make_equal(rel.src,invr.dst)
      rel.dst,invr.src=check_or_make_equal(rel.dst,invr.src)
      
  def build_toOne_relationships(self, model):
    toOneRels=[r for r in self.properties if isinstance(r, RToOne)]
    for prop in toOneRels:
      # Note: we build() AFTER we add the relationship, or
      # relationship.component.entity==None and then it's impossible to
      # add a join to such a MRelationship
      self.component.addRelationship(prop.component)
      prop.build(model, self)

  def build_toMany_relationships(self, model):
    toManyRels=[r for r in self.properties if isinstance(r, RToMany)]
    for prop in toManyRels:
      # Note: we build() AFTER we add the relationship, or
      # relationship.component.entity==None and then it's impossible to
      # add a join to such a MRelationship
      self.component.addRelationship(prop.component)
      prop.build(model, self)


  def _void_void(self, model):
    # We need to have two phases: building relationships requires that all
    # attributes of all entities are previously initalized and built.
    ## RELATIONS
    toOneRels=[r for r in self.properties if isinstance(r, RToOne)]
    toManyRels={}
    [toManyRels.setdefault(k,v) for k,v in [(r.name,r) for r in self.properties
                                            if isinstance(r, RToMany)]]
    # process inverse
    #for r in toOneRels+toManyRels
    ## TBD: ici il faut encore 2 phases: 1. traiter toutes les toOnes,
    ## puis 2. toutes les toManys, ou alors 1. traiter les inverses, 2. traiter
    ## les relations.
    ## La 2eme solution implique plus de tests dans le build des relations
    ## (si inverse mais pas encore traitee, chercher/creer la fk et sinon
    ## recuperer la bonne avec le bon nom...
    # toOne
    #print toManyRels
    for prop in toOneRels:
      # Note: we build() AFTER we add the relationship, or
      # relationship.component.entity==None and then it's impossible to
      # add a join to such a MRelationship
      self.component.addRelationship(prop.component)
      prop.build(model, self)

    # toMany 
    for prop in toManyRels.values():
      self.component.addRelationship(prop.component)
      prop.build(model, self)

    ## TBD: RETURN les entites modifiees pour update et propagation!
    ## ex. updateAfterChanges()
    ## pourrait tester l'existant avec entity.propertyNamed
      
##

class Attribute(BaseDescription):
  ''' Describes an Attribute '''
  defaults={ 'columnName': MP(externalNameForInternalName, 'name'),
             #'key': 0,
             'usedForLocking': 0, 
             'type': 'int',
             'externalType': 'INTEGER', 
             'isClassProperty': 1,
             'isRequired': 0, 
             'precision': 0,
             'width': 0,
             'scale': 0,
             'defaultValue': None,
             'displayLabel': '',
             'doc': '',
             }
  def __init__(self, name, **kw):
    self.name=self.requiredField(name)
    from Attribute import Attribute
    self.component=MAttribute(name)
    updateComponent(self, kw, Attribute.defaults, name,
                    ignore='usedForLocking')

  def build(self, model, entity):
    d={}
    selfdict=self.__dict__
    [d.setdefault(k, selfdict[k]) for k in selfdict.keys()
                                  if k in Attribute.defaults.keys()]
    updateComponent(self, d, Attribute.defaults, self.name,
                   ignore=('usedForLocking'))

  def clone(self):
    d=self.__dict__.copy()
    try: del d['component']
    except: pass
    clone=apply(self.__class__, (), d)
    return clone
  
class AInteger(Attribute):
  defaults={ 'type': 'int',
             'externalType': 'INTEGER',
             'defaultValue': 0,
             'usedForLocking': 1,
             }
  def __init__(self, name, **kw):
    apply(Attribute.__init__.im_func, (self, name), kw)
    updateComponent(self, kw, AInteger.defaults, name,
                    ignore='usedForLocking')

class AString(Attribute):
  import types
  defaults={ 'type': 'string', #types.StringType.__name__,
             'externalType': 'VARCHAR',
             'width': 255,
             'defaultValue': '',
             'usedForLocking': 1,
             }
  def __init__(self, name, **kw):
    apply(Attribute.__init__.im_func, (self, name), kw)
    updateComponent(self, kw, AString.defaults, name,
                    ignore='usedForLocking')

# http://www.onlamp.com/pub/a/onlamp/2001/09/13/aboutSQL.html
# 
# SQL Wisdom #7) The data type is invariably different -- even if it has the
#   same name -- in another database. Always check the documentation.
class AFloat(Attribute):
  defaults={ 'type': 'float',
             'externalType': 'NUMERIC',
             'precision': 15,
             'scale': 5,
             'defaultValue': 0.0,
             'usedForLocking': 1,
             }
  def __init__(self, name, **kw):
    apply(Attribute.__init__.im_func, (self, name), kw)
    updateComponent(self, kw, AFloat.defaults, name,
                    ignore='usedForLocking')

class ADateTime(Attribute):
  defaults={ 'type': 'DateTime',
             'externalType': 'TIMESTAMP',
             'defaultValue': None,
             'usedForLocking': 1,
             }
  def __init__(self, name, **kw):
    apply(Attribute.__init__.im_func, (self, name), kw)
    updateComponent(self, kw, ADateTime.defaults, name,
                    ignore='usedForLocking')

class APrimaryKey(AInteger):
  defaults={ 'isClassProperty': 0,
             'isRequired': 1,
             'defaultValue': MP(lambda i: (i and [0] or [None])[0],
                                'isClassProperty'),
             'doc': 'Primary Key',
             'usedForLocking': 0,
             }
  def __init__(self, name='id', **kw):
    apply(AInteger.__init__.im_func, (self, name), kw)
    updateComponent(self, kw, APrimaryKey.defaults, name,
                    ignore='usedForLocking')
    
    
class AForeignKey(AInteger):
  defaults={ 'isClassProperty': 0,
             'isRequired': 0,
             'defaultValue': None,
             'doc': 'Foreign Key',
             'usedForLocking': 0,
             }
  def __init__(self, name='id', **kw):
    apply(AInteger.__init__.im_func, (self, name), kw)
    updateComponent(self, kw, AForeignKey.defaults, name,
                    ignore='usedForLocking')

##
def xor(a,b):
   return not not ((a and not b) or (not a and b))

class BaseRelation(BaseDescription):
  ''' Describes a Relation '''
  defaults={ 'delete': 'nullify',
             #'key': 0,
             #'destinationEntity': '',
             ##'usedForLocking': 0,
             'isClassProperty': 1, 
             'multiplicity': [0,1],
             'joinSemantic': 0,
             'src': '',
             'dst': '',
             'displayLabel': '',
             'doc': '',
             'inverse': '',
             }
  # Additinal attr. for self.: src_dst_specified
  def __init__(self, name, destination, **kw):
    self.name=self.requiredField(name)
    self.destination=self.requiredField(destination)
    from Relationship import SimpleRelationship
    self.component=MRelationship(name)
    updateComponent(self,kw,BaseRelation.defaults,name,
                    ignore=('src', 'dst', 'multiplicity', 'src_dst_specified', 'inverse'))

    if xor(self.src, self.dst):
      raise ValueError, "Parameters src and dst should be both specified"

  def clone(self):
    d=self.__dict__.copy()
    try: del d['component'] # we do not want to copy this
    except: pass
    try: del d['src_dst_specified']
    except: pass
    clone=apply(self.__class__, (), d)
    return clone



class RToOne(BaseRelation):
  defaults=BaseRelation.defaults.copy()
  defaults.update({ 'multiplicity': [0,1],
                    'joinSemantic': 0,
                    })
  propagated_by=None

  def __init__(self, name, destination, **kw):
    apply(BaseRelation.__init__.im_func, (self, name, destination), kw)
    updateComponent(self, kw, RToOne.defaults, name,
                    ignore=('src', 'dst', 'multiplicity', 'src_dst_specified', 'inverse'))

  def build(self, model, entity):
    debug('(build toOne) Relation %s.%s'%(entity.name,self.name))
    self.src_dst_specified=not not self.src

    destEntity=model.entityNamed(self.destination)
    destMEntity=destEntity.component
    #print model.component.entitiesNames()
    # Check multiplicity
    lowB=int(self.multiplicity[0])
    uppB=int(self.multiplicity[1])
    err=''
    if lowB not in (0,1): err+='Lower bound must be 0 or 1. '
    if uppB not in (0,1): err+='Upper bound must be 0 or 1. '
    if err:
      raise ValueError, 'Relation %s.%s: %s'%(entity.component.name(),
                                              self.component.name(), err)
    self.component.setMultiplicity(lowB, uppB)
    
    # Process src & dst attributes
    if not self.src_dst_specified:
      destinationMAttribute=destMEntity.primaryKeyAttributes()[0]
      if not self.propagated_by:
        sourceAttribute=AForeignKey(entity.attFKName(destEntity.name))
      else:
        # The attribute has already been propagated, find & re-use it
        mparent=model.entityNamed(self.propagated_by).component
        mparent_destAttr=mparent.relationshipNamed(self.name).sourceAttributes()
        fkName=mparent_destAttr[0].name()
        sourceAttribute=entity.attributeNamed(fkName)
        if not sourceAttribute:
          error('Could not find src.attr. %s.%s for relationship %s.%s with %s inheriting from parent=%s (propagated_by=%s).\nThis should not happen, please report to <modeling-users@lists.sourceforge.net> along with your model'%(entity.name,fkName,entity.name,self.name,entity.name,entity.parent, self.propagated_by))

      entity.addAttribute(sourceAttribute, model)
      sourceMAttribute=sourceAttribute.component

      self.src=sourceAttribute.name
      self.dst=destinationMAttribute.name()
      entity.forward_rel_info(self, model)
    else: # src & dst specified
      sourceMAttribute=entity.component.attributeNamed(self.src)
      if not sourceMAttribute:
        raise ValueError, 'Relation %s.%s: unable to find source attribute %s.%s'%(entity.component.name(), self.component.name(), entity.name, self.src)
      destinationMAttribute=destMEntity.attributeNamed(self.dst)

    # check destinationAttribute
    if not destinationMAttribute:
      if self.dst:
        err_msg='Relation %s.%s: unable to find destination attribute %s.%s'%(entity.name, self.name, self.destination, self.dst)
      else:
        err_msg='Relation %s.%s: unable to find destination attribute: no primary key declared for entity %s'%(entity.name, self.name, self.destination)
      raise ValueError, err_msg

    # src and dest are set, ok
    from Join import Join
    j=MJoin(sourceMAttribute, destinationMAttribute)
    self.component.addJoin(j)
    #if not self.component.destinationEntity(): import pdb ; pdb.set_trace()
      

class RToMany(BaseRelation):
  defaults=BaseRelation.defaults.copy()
  defaults.update({ 'multiplicity': [0,None],
                    'joinSemantic': 0,
                    })
  propagated_by=None
  
  def __init__(self, name, destination, **kw):
    apply(BaseRelation.__init__.im_func, (self, name, destination), kw)
    updateComponent(self, kw, RToMany.defaults, name, ignore=('src', 'dst', 'multiplicity','inverse'))
    
  def build(self, model, entity):
    self.src_dst_specified=not not self.src

    debug('(build toMany) Relation %s.%s(%s->%s)'%(entity.name,self.name,
                                                   self.src, self.dst))
    #print model.component.entitiesNames()
    destEntity=model.entityNamed(self.destination)
    destMEntity=destEntity.component

    # Check multiplicity
    lowB=int(self.multiplicity[0])
    uppB=self.multiplicity[1]
    err=''
    if uppB in (0,1): err+='Invalid value for upper bound of a toMany rel.'
    if err:
      raise ValueError, 'Relation %s.%s: %s'%(entity.component.name(),
                                              self.component.name(), err)
    self.component.setMultiplicity(lowB, uppB)
    
    # Process src & dst attributes
    if not self.src_dst_specified:
      sourceMAttribute=entity.component.primaryKeyAttributes()[0]
      if not self.propagated_by:
        destinationAttribute=AForeignKey(destEntity.attFKName(entity.name))
      else:
        # The attribute has already been propagated, find it
        mparent=model.entityNamed(self.propagated_by).component
        mparent_destAttr=mparent.relationshipNamed(self.name).destinationAttributes()
        
        fkName=mparent_destAttr[0].name()
        destinationAttribute=destEntity.attributeNamed(fkName)
        if not destinationAttribute:
          error('Could not find dest.attr. %s.%s for relationship %s.%s with %s inheriting from parent=%s (propagated_by=%s).\nThis should not happen, please report to <modeling-users@lists.sourceforge.net> along with your model'%(destEntity.name,fkName,entity.name,self.name,entity.name,entity.parent, self.propagated_by))
          
      destEntity.addAttribute(destinationAttribute, model)
      destinationMAttribute=destinationAttribute.component

      self.src=sourceMAttribute.name()
      self.dst=destinationMAttribute.name()
      entity.forward_rel_info(self, model)

    else: # src and dst specified
      sourceMAttribute=entity.component.attributeNamed(self.src)
      destinationMAttribute=destMEntity.attributeNamed(self.dst)

      if not destinationMAttribute:
        raise ValueError, 'Relation %s.%s: unable to find destination attribute %s.%s'%(entity.component.name(), self.component.name(), self.destination, self.dst)

    # Check source attribute
    if not sourceMAttribute:
      if self.src:
        err_msg='Relation %s.%s: unable to find source attribute %s.%s'%(entity.name, self.name, entity.name, self.src)
      else:
        err_msg='Relation %s.%s: unable to find source attribute: no primary key declared for entity %s'%(entity.name, self.name, entity.name)
      raise ValueError, err_msg

    # src and dest are set
    from Join import Join
    j=MJoin(sourceMAttribute, destinationMAttribute)
    self.component.addJoin(j)
    #if not self.component.destinationEntity(): import pdb ; pdb.set_trace()


def relationsForSrcAndDst(src,dst):
  return ('to'+dst, 'to'+src+'s')

class Association(BaseDescription):
  '''
  Describes an Association between 2 entities

  An Association binds two entities, 'src' and 'dst' (cf. __init__()),
  with two relationships:

    - a toOne relationship src -> dst

    - a toMany relationship dst -> src
  
  '''
  defaults = { 'multiplicity': [ [0,1], [0,None] ],
               'relations': MP(relationsForSrcAndDst, 'src', 'dst'),
               'keys': [ None, None ],
               'delete': [ RToOne.defaults['delete'],
                           RToMany.defaults['delete'] ],
               'isClassProperty': [ RToOne.defaults['isClassProperty'],
                                    RToMany.defaults['isClassProperty'] ],
               'joinSemantic': [ RToOne.defaults['joinSemantic'],
                                 RToMany.defaults['joinSemantic'] ],
               'displayLabel': [ RToOne.defaults['displayLabel'],
                                 RToMany.defaults['displayLabel'] ],
               'doc': [ RToOne.defaults['doc'],
                        RToMany.defaults['doc'] ],
               }

  def __init__(self, src, dst, **kw):
    self.src=kw['src']=src
    self.dst=kw['dst']=dst
    updateComponent(self,kw,Association.defaults,
                    name='Association(%s,%s)'%(src, dst))
    self.check_multiplicity()

  def check_multiplicity(self):
    """
    Check the self.multiplicity is correct

    Returns: (src->dst multiplicity, dst->src multiplicity)
    """
    ## TBD 1: inverser src/dst si multiplicities sont inversees
    ## TBD 2: possibilite d'ajout des srcKey/dstKey
    sd_mult=self.multiplicity[0]
    ds_mult=self.multiplicity[1]
    if sd_mult[1]>1:
      raise ValueError, 'invalid multiplicity src->dst: should be toOne'
    if not (ds_mult[1] in (None, '*', -1) or ds_mult[1]>1):
      raise ValueError, 'invalid multiplicity dst->src: should be toMany'
    return sd_mult, ds_mult
  
  def build(self, model):
    srcE=model.entityNamed(self.src)
    myName="Association %s.%s"%(srcE.name,self.name)
    if not srcE:
      raise ValueError, '%s: Unable to find source entity %s'%(my_name,self.src)
    dstE=model.entityNamed(self.dst)
    if not dstE:
      raise ValueError, '%s: Unable to find destination entity %s'%(my_name,self.dst)

    if not (self.relations[0] or self.relations[1]):
      raise ValueError, '%s: no relation defined'%my_name

    ## PK
    dstE_pk=dstE.primaryKey()
    if not self.keys[1]: # unspecified: retrieve it
      if not dstE_pk:
        raise ValueError, '%s: Unable to find a PK in entity %s'%(self.name, self.dst)
      dstAtt=dstE_pk
      self.keys[1]=dstAtt.name

    else: # destination PK was specified
      if dstE_pk and dstE_pk.name != self.keys[1]:
        raise ValueError, '%s: specifies a destination attribute %s for entity %s but this entity already has a PK named %s'%(self.name,self.keys[1],self.dst,dstE_pk.name)
      
      dstAtt=dstE.attributeNamed(self.keys[1])
      if not dstAtt:
        debug('Creating default PK in %s'%self.dst)
        dstAtt=APrimaryKey()
        dstE.properties.append(dstAtt)
    
    ## FK
    srcAtt=srcE.attributeNamed(self.keys[0])
    if not srcAtt:
      if not self.keys[0]:
        self.keys[0]=srcE.attFKName(dstE.name)
      debug('%s: Creating FK %s.%s'%(self.name, self.src, self.keys[0]))
      srcAtt=AForeignKey(self.keys[0])
      srcE.properties.append(srcAtt)
      # automatic creation of the FK: set isRequired if applicable
      srcAtt.isRequired=self.multiplicity[0][0]
    ## Now srcAtt and dstAtt are set

    # Relationships
    toOne_kw={} ; toMany_kw={}
    for k in ('delete','isClassProperty','joinSemantic','displayLabel','doc',
              'multiplicity'):
      toOne_kw[k]=getattr(self, k)[0]
      toMany_kw[k]=getattr(self, k)[1]
    toOne_kw['src']=srcAtt.name ; toOne_kw['dst']=dstAtt.name
    toMany_kw['src']=dstAtt.name ; toMany_kw['dst']=srcAtt.name

    #srcRel=RToOne(self.relations[0], dstE.name, ## TBD: +properties
    #              src=srcAtt.name, dst=dstAtt.name) 
    if self.relations[0]:
      srcRel=apply(RToOne, (self.relations[0], dstE.name), toOne_kw)
      srcE.properties.append(srcRel)
    #dstRel=RToMany(self.relations[1], srcE.name,   ## TBD: +properties
    #              src=dstAtt.name, dst=srcAtt.name)
    if self.relations[1]:
      dstRel=apply(RToMany, (self.relations[1], srcE.name), toMany_kw)
      dstE.properties.append(dstRel)

##
def updateComponent(object, kw, defaults, name, ignore=()):
    """
    Parameters:
    
      component --
  
      kw -- NB: kw is not modified
  
      defaults --
  
      name --
  
    """
    def copy(v):
      if isListOrTuple(v):
        return list(v)
        #return list(v)
      if type(v) is type({}): return v.copy()
      #if hasattr(v, 'clone') and callable(v.clone):
      #  print '###Cloning'
      #  return v.clone() # props
      else:
        return v
      
    if type(ignore) is type(''): ignore=[ignore]
    ignore=list(ignore) ; ignore.extend(['name','propagated_by'])
    _kw=defaults.copy()

    # We need to clone the defaults['properties'], such as in Entity.defaults:
    # when this is not done, Entities.default is reused as is in
    # different entities; e.g. each default attr. is assigned
    # to different entities, which in turn manipulate and change that attr.
    # and its component --> a test like in
    # test_PyModel.checkEntitiesProperties() then fails.
    
    clone_or_self=lambda p: (hasattr(p, 'clone') and callable(p.clone)) and p.clone() or p
    if _kw.get('properties'):
      _kw['properties']=map(clone_or_self, _kw['properties'])

    _kw.update(kw) ; kw=_kw
    kw['name']=name
    for k,v in kw.items():
        # Add everything to the object, for use at build time
        setattr(object,k,copy(v))
        if k in ignore: continue
        if isinstance(v,MP):
            l=[kw[n] for n in v.param]
            v=apply(v.method, l)
            setattr(object, k, v)
        else:
            v=copy(v)
        if hasattr(object, 'component'):#Associations dont have any 'component'
          # use takeValueForKey(), not takeStoredValueForKey,
          # or we won't take advantage of custom logic defined in some
          # setters (like in 'Attribute.setType()')
          if k=='type':
            object.component.setType(v, check=0)
          else:
            object.component.takeStoredValueForKey(v,k)
          
##

www.java2java.com | Contact Us
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.