from types import StringTypes from datetime import datetime from sqlalchemy import Column, Integer, String, DateTime from sqlalchemy import Table, Column, MetaData, join, ForeignKey from sqlalchemy.orm import relationship, backref from sqlalchemy.orm import column_property from sqlalchemy.orm import object_mapper from sqlalchemy.ext.declarative import declarative_base from sfa.util.sfalogging import logger from sfa.util.xml import XML from sfa.trust.gid import GID ############################## Base=declarative_base() #################### # dicts vs objects #################### # historically the front end to the db dealt with dicts, so the code was only dealing with dicts # sqlalchemy however offers an object interface, meaning that you write obj.id instead of obj['id'] # which is admittedly much nicer # however we still need to deal with dictionaries if only for the xmlrpc layer # # here are a few utilities for this # # (*) first off, when an old pieve of code needs to be used as-is, if only temporarily, the simplest trick # is to use obj.__dict__ # this behaves exactly like required, i.e. obj.__dict__['field']='new value' does change obj.field # however this depends on sqlalchemy's implementation so it should be avoided # # (*) second, when an object needs to be exposed to the xmlrpc layer, we need to convert it into a dict # remember though that writing the resulting dictionary won't change the object # essentially obj.__dict__ would be fine too, except that we want to discard alchemy private keys starting with '_' # 2 ways are provided for that: # . dict(obj) # . obj.todict() # the former dict(obj) relies on __iter__() and next() below, and does not rely on the fields names # although it seems to work fine, I've found cases where it issues a weird python error that I could not get right # so the latter obj.todict() seems more reliable but more hacky as is relies on the form of fields, so this can probably be improved # # (*) finally for converting a dictionary into an sqlalchemy object, we provide # obj.load_from_dict(dict) class AlchemyObj: def __iter__(self): self._i = iter(object_mapper(self).columns) return self def next(self): n = self._i.next().name return n, getattr(self, n) def todict (self): d=self.__dict__ keys=[k for k in d.keys() if not k.startswith('_')] return dict ( [ (k,d[k]) for k in keys ] ) def load_from_dict (self, d): for (k,v) in d.iteritems(): # experimental if isinstance(v, StringTypes): if v.lower() in ['true']: v=True if v.lower() in ['false']: v=False setattr(self,k,v) assert self.type in BUILTIN_TYPES # in addition we provide convenience for converting to and from xml records # for this purpose only, we need the subclasses to define 'fields' as either # a list or a dictionary def xml_fields (self): fields=self.fields if isinstance(fields,dict): fields=fields.keys() return fields def load_from_xml (self, xml): xml_record = XML(xml) xml_dict = xml_record.todict() for k in self.xml_fields(): if k in xml_dict: setattr(self,k,xml_dict[k]) def save_as_xml (self): # xxx unset fields don't get exposed, is that right ? input_dict = dict( [ (key, getattr(self.key), ) for key in self.xml_fields() if getattr(self,key,None) ] ) xml_record=XML("") xml_record.parse_dict (input_dict) return xml_record.toxml() ############################## class Type (Base): __table__ = Table ('types', Base.metadata, Column ('type',String, primary_key=True) ) def __init__ (self, type): self.type=type def __repr__ (self): return ""%self.type #BUILTIN_TYPES = [ 'authority', 'slice', 'node', 'user' ] # xxx for compat but sounds useless BUILTIN_TYPES = [ 'authority', 'slice', 'node', 'user', 'authority+sa', 'authority+am', 'authority+sm' ] def insert_builtin_types(dbsession): for type in BUILTIN_TYPES : count = dbsession.query (Type).filter_by (type=type).count() if count==0: dbsession.add (Type (type)) dbsession.commit() ############################## class RegRecord (Base,AlchemyObj): # xxx tmp would be 'records' __table__ = Table ('records', Base.metadata, Column ('record_id', Integer, primary_key=True), Column ('type', String, ForeignKey ("types.type")), Column ('hrn',String), Column ('gid',String), Column ('authority',String), Column ('peer_authority',String), Column ('pointer',Integer,default=-1), Column ('date_created',DateTime), Column ('last_updated',DateTime), ) fields = [ 'type', 'hrn', 'gid', 'authority', 'peer_authority' ] def __init__ (self, type='unknown', hrn=None, gid=None, authority=None, peer_authority=None, pointer=-1, dict=None): self.type=type if hrn: self.hrn=hrn if gid: if isinstance(gid, StringTypes): self.gid=gid else: self.gid=gid.save_to_string(save_parents=True) if authority: self.authority=authority if peer_authority: self.peer_authority=peer_authority if not hasattr(self,'pointer'): self.pointer=pointer if dict: self.load_from_dict (dict) def __repr__(self): result="[Record(record_id=%s, hrn=%s, type=%s, authority=%s, pointer=%s" % \ (self.record_id, self.hrn, self.type, self.authority, self.pointer) if self.gid: result+=" %s..."%self.gid[:10] else: result+=" no-gid" result += "]" return result # xxx - there might be smarter ways to handle get/set'ing gid using validation hooks def get_gid_object (self): if not self.gid: return None else: return GID(string=self.gid) def just_created (self): now=datetime.now() self.date_created=now self.last_updated=now def just_updated (self): now=datetime.now() self.last_updated=now ############################## class User (Base): __table__ = Table ('users', Base.metadata, Column ('user_id', Integer, primary_key=True), Column ('record_id',Integer, ForeignKey('records.record_id')), Column ('email', String), ) def __init__ (self, email): self.email=email def __repr__ (self): return ""%(self.user_id,self.email,self.record_id,) record_table = RegRecord.__table__ user_table = User.__table__ record_user_join = join (record_table, user_table) class UserRecord (Base): __table__ = record_user_join record_id = column_property (record_table.c.record_id, user_table.c.record_id) user_id = user_table.c.user_id def __init__ (self, gid, email): self.type='user' self.gid=gid self.email=email def __repr__ (self): return ""%(self.email,self.gid) ############################## def init_tables(dbsession): logger.info("Initializing db schema and builtin types") # the doc states we could retrieve the engine this way # engine=dbsession.get_bind() # however I'm getting this # TypeError: get_bind() takes at least 2 arguments (1 given) # so let's import alchemy - but not from toplevel from sfa.storage.alchemy import engine Base.metadata.create_all(engine) insert_builtin_types(dbsession) def drop_tables(dbsession): logger.info("Dropping tables") # same as for init_tables from sfa.storage.alchemy import engine Base.metadata.drop_all(engine)