various tweaks around using records; the client-side is still broken
[sfa.git] / sfa / storage / persistentobjs.py
1 from types import StringTypes
2 from datetime import datetime
3
4 from sqlalchemy import Column, Integer, String, DateTime
5 from sqlalchemy import Table, Column, MetaData, join, ForeignKey
6 from sqlalchemy.orm import relationship, backref
7 from sqlalchemy.orm import column_property
8 from sqlalchemy.orm import object_mapper
9 from sqlalchemy.ext.declarative import declarative_base
10
11 from sfa.util.sfalogging import logger
12 from sfa.util.xml import XML 
13
14 from sfa.trust.gid import GID
15
16 ##############################
17 Base=declarative_base()
18
19 ####################
20 # dicts vs objects
21 ####################
22 # historically the front end to the db dealt with dicts, so the code was only dealing with dicts
23 # sqlalchemy however offers an object interface, meaning that you write obj.id instead of obj['id']
24 # which is admittedly much nicer
25 # however we still need to deal with dictionaries if only for the xmlrpc layer
26
27 # here are a few utilities for this 
28
29 # (*) first off, when an old pieve of code needs to be used as-is, if only temporarily, the simplest trick
30 # is to use obj.__dict__
31 # this behaves exactly like required, i.e. obj.__dict__['field']='new value' does change obj.field
32 # however this depends on sqlalchemy's implementation so it should be avoided 
33 #
34 # (*) second, when an object needs to be exposed to the xmlrpc layer, we need to convert it into a dict
35 # remember though that writing the resulting dictionary won't change the object
36 # essentially obj.__dict__ would be fine too, except that we want to discard alchemy private keys starting with '_'
37 # 2 ways are provided for that:
38 # . dict(obj)
39 # . obj.todict()
40 # the former dict(obj) relies on __iter__() and next() below, and does not rely on the fields names
41 # although it seems to work fine, I've found cases where it issues a weird python error that I could not get right
42 # 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
43 #
44 # (*) finally for converting a dictionary into an sqlalchemy object, we provide
45 # obj.load_from_dict(dict)
46
47 class AlchemyObj:
48     def __iter__(self): 
49         self._i = iter(object_mapper(self).columns)
50         return self 
51     def next(self): 
52         n = self._i.next().name
53         return n, getattr(self, n)
54     def todict (self):
55         d=self.__dict__
56         keys=[k for k in d.keys() if not k.startswith('_')]
57         return dict ( [ (k,d[k]) for k in keys ] )
58     def load_from_dict (self, d):
59         for (k,v) in d.iteritems():
60             # experimental
61             if isinstance(v, StringTypes):
62                 if v.lower() in ['true']: v=True
63                 if v.lower() in ['false']: v=False
64             setattr(self,k,v)
65         assert self.type in BUILTIN_TYPES
66     
67     # in addition we provide convenience for converting to and from xml records
68     # for this purpose only, we need the subclasses to define 'fields' as either 
69     # a list or a dictionary
70     def xml_fields (self):
71         fields=self.fields
72         if isinstance(fields,dict): fields=fields.keys()
73         return fields
74     def load_from_xml (self, xml):
75         xml_record = XML(xml)
76         xml_dict = xml_record.todict()
77         for k in self.xml_fields():
78             if k in xml_dict:
79                 setattr(self,k,xml_dict[k])
80
81     def save_as_xml (self):
82         # xxx unset fields don't get exposed, is that right ?
83         input_dict = dict( [ (key, getattr(self.key), ) for key in self.xml_fields() if getattr(self,key,None) ] )
84         xml_record=XML("<record />")
85         xml_record.parse_dict (input_dict)
86         return xml_record.toxml()
87
88 ##############################
89 class Type (Base):
90     __table__ = Table ('types', Base.metadata,
91                        Column ('type',String, primary_key=True)
92                        )
93     def __init__ (self, type): self.type=type
94     def __repr__ (self): return "<Type %s>"%self.type
95     
96 #BUILTIN_TYPES = [ 'authority', 'slice', 'node', 'user' ]
97 # xxx for compat but sounds useless
98 BUILTIN_TYPES = [ 'authority', 'slice', 'node', 'user',
99                   'authority+sa', 'authority+am', 'authority+sm' ]
100
101 def insert_builtin_types(dbsession):
102     for type in BUILTIN_TYPES :
103         count = dbsession.query (Type).filter_by (type=type).count()
104         if count==0:
105             dbsession.add (Type (type))
106     dbsession.commit()
107
108 ##############################
109 class RegRecord (Base,AlchemyObj):
110     # xxx tmp would be 'records'
111     __table__ = Table ('records', Base.metadata,
112                        Column ('record_id', Integer, primary_key=True),
113                        Column ('type', String, ForeignKey ("types.type")),
114                        Column ('hrn',String),
115                        Column ('gid',String),
116                        Column ('authority',String),
117                        Column ('peer_authority',String),
118                        Column ('pointer',Integer,default=-1),
119                        Column ('date_created',DateTime),
120                        Column ('last_updated',DateTime),
121                        )
122     fields = [ 'type', 'hrn', 'gid', 'authority', 'peer_authority' ]
123     def __init__ (self, type='unknown', hrn=None, gid=None, authority=None, peer_authority=None, 
124                   pointer=-1, dict=None):
125         self.type=type
126         if hrn: self.hrn=hrn
127         if gid: 
128             if isinstance(gid, StringTypes): self.gid=gid
129             else: self.gid=gid.save_to_string(save_parents=True)
130         if authority: self.authority=authority
131         if peer_authority: self.peer_authority=peer_authority
132         if not hasattr(self,'pointer'): self.pointer=pointer
133         if dict:
134             self.load_from_dict (dict)
135
136     def __repr__(self):
137         result="[Record(record_id=%s, hrn=%s, type=%s, authority=%s, pointer=%s" % \
138                 (self.record_id, self.hrn, self.type, self.authority, self.pointer)
139         if self.gid: result+=" %s..."%self.gid[:10]
140         else: result+=" no-gid"
141         result += "]"
142         return result
143
144     # xxx - there might be smarter ways to handle get/set'ing gid using validation hooks 
145     def get_gid_object (self):
146         if not self.gid: return None
147         else: return GID(string=self.gid)
148
149     def just_created (self):
150         now=datetime.now()
151         self.date_created=now
152         self.last_updated=now
153
154     def just_updated (self):
155         now=datetime.now()
156         self.last_updated=now
157
158 ##############################
159 class User (Base):
160     __table__ = Table ('users', Base.metadata,
161                        Column ('user_id', Integer, primary_key=True),
162                        Column ('record_id',Integer, ForeignKey('records.record_id')),
163                        Column ('email', String),
164                        )
165     def __init__ (self, email):
166         self.email=email
167     def __repr__ (self): return "<User(%d) %s, record_id=%d>"%(self.user_id,self.email,self.record_id,)
168                            
169 record_table = RegRecord.__table__
170 user_table = User.__table__
171 record_user_join = join (record_table, user_table)
172
173 class UserRecord (Base):
174     __table__ = record_user_join
175     record_id = column_property (record_table.c.record_id, user_table.c.record_id)
176     user_id = user_table.c.user_id
177     def __init__ (self, gid, email):
178         self.type='user'
179         self.gid=gid
180         self.email=email
181     def __repr__ (self): return "<UserRecord %s %s>"%(self.email,self.gid)
182
183 ##############################
184 def init_tables(dbsession):
185     logger.info("Initializing db schema and builtin types")
186     # the doc states we could retrieve the engine this way
187     # engine=dbsession.get_bind()
188     # however I'm getting this
189     # TypeError: get_bind() takes at least 2 arguments (1 given)
190     # so let's import alchemy - but not from toplevel 
191     from sfa.storage.alchemy import engine
192     Base.metadata.create_all(engine)
193     insert_builtin_types(dbsession)
194
195 def drop_tables(dbsession):
196     logger.info("Dropping tables")
197     # same as for init_tables
198     from sfa.storage.alchemy import engine
199     Base.metadata.drop_all(engine)