# -*- 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)
##
|