Code

Very close now. The cgi and mailgw now use the new security API. The two
[roundup.git] / roundup / volatiledb.py
1 import weakref, re
3 from roundup import hyperdb
4 from roundup.hyperdb import String, Password, Date, Interval, Link, \
5     Multilink, DatabaseError, Boolean, Number
7 class VolatileClass(hyperdb.Class):
8     ''' This is a class that just sits in memory, no saving to disk.
9         It has no journal.
10     '''
11     def __init__(self, db, classname, **properties):
12         ''' Set up an in-memory store for the nodes of this class
13         '''
14         self.db = weakref.proxy(db)       # use a weak ref to avoid circularity
15         self.classname = classname
16         self.properties = properties
17         self.id_counter = 1
18         self.store = {}
19         self.by_key = {}
20         self.key = ''
21         db.addclass(self)
23     def setkey(self, propname):
24         prop = self.getprops()[propname]
25         if not isinstance(prop, String):
26             raise TypeError, 'key properties must be String'
27         self.key = propname
29     def getprops(self, protected=1):
30         d = self.properties.copy()
31         if protected:
32             d['id'] = String()
33         return d
35     def create(self, **propvalues):
36         ''' Create a new node in the in-memory store
37         '''
38         if propvalues.has_key('id'):
39             raise KeyError, '"id" is reserved'
40         newid = str(self.id_counter)
41         self.id_counter += 1
43         # get the key value, validate it
44         if self.key:
45             keyvalue = propvalues[self.key]
46             try:
47                 self.lookup(keyvalue)
48             except KeyError:
49                 pass
50             else:
51                 raise ValueError, 'node with key "%s" exists'%keyvalue
52             self.by_key[keyvalue] = newid
54         # validate propvalues
55         num_re = re.compile('^\d+$')
57         for key, value in propvalues.items():
59             # try to handle this property
60             try:
61                 prop = self.properties[key]
62             except KeyError:
63                 raise KeyError, '"%s" has no property "%s"'%(self.classname,
64                     key)
66             if isinstance(prop, Link):
67                 if type(value) != type(''):
68                     raise ValueError, 'link value must be String'
69                 link_class = self.properties[key].classname
70                 # if it isn't a number, it's a key
71                 if not num_re.match(value):
72                     try:
73                         value = self.db.classes[link_class].lookup(value)
74                     except (TypeError, KeyError):
75                         raise IndexError, 'new property "%s": %s not a %s'%(
76                             key, value, link_class)
77                 elif not self.db.hasnode(link_class, value):
78                     raise IndexError, '%s has no node %s'%(link_class, value)
80                 # save off the value
81                 propvalues[key] = value
83             elif isinstance(prop, Multilink):
84                 if type(value) != type([]):
85                     raise TypeError, 'new property "%s" not a list of ids'%key
87                 # clean up and validate the list of links
88                 link_class = self.properties[key].classname
89                 l = []
90                 for entry in value:
91                     if type(entry) != type(''):
92                         raise ValueError, '"%s" link value (%s) must be '\
93                             'String'%(key, value)
94                     # if it isn't a number, it's a key
95                     if not num_re.match(entry):
96                         try:
97                             entry = self.db.classes[link_class].lookup(entry)
98                         except (TypeError, KeyError):
99                             raise IndexError, 'new property "%s": %s not a %s'%(
100                                 key, entry, self.properties[key].classname)
101                     l.append(entry)
102                 value = l
103                 propvalues[key] = value
105                 # handle additions
106                 for id in value:
107                     if not self.db.hasnode(link_class, id):
108                         raise IndexError, '%s has no node %s'%(link_class, id)
110             elif isinstance(prop, String):
111                 if type(value) != type(''):
112                     raise TypeError, 'new property "%s" not a string'%key
114             elif isinstance(prop, Password):
115                 if not isinstance(value, password.Password):
116                     raise TypeError, 'new property "%s" not a Password'%key
118             elif isinstance(prop, Date):
119                 if value is not None and not isinstance(value, date.Date):
120                     raise TypeError, 'new property "%s" not a Date'%key
122             elif isinstance(prop, Interval):
123                 if value is not None and not isinstance(value, date.Interval):
124                     raise TypeError, 'new property "%s" not an Interval'%key
126         # make sure there's data where there needs to be
127         for key, prop in self.properties.items():
128             if propvalues.has_key(key):
129                 continue
130             if key == self.key:
131                 raise ValueError, 'key property "%s" is required'%key
132             if isinstance(prop, Multilink):
133                 propvalues[key] = []
134             else:
135                 propvalues[key] = None
137         # done
138         self.store[newid] = propvalues
140         return newid
142     _marker = []
143     def get(self, nodeid, propname, default=_marker, cache=1):
144         ''' Get the node from the in-memory store
145         '''
146         if propname == 'id':
147             return nodeid
148         return self.store[nodeid][propname]
150     def set(self, nodeid, **propvalues):
151         ''' Set properties on the node in the in-memory store
152         '''
153         if not propvalues:
154             return
156         if propvalues.has_key('id'):
157             raise KeyError, '"id" is reserved'
159         node = self.store[nodeid]
160         num_re = re.compile('^\d+$')
162         for propname, value in propvalues.items():
163             # check to make sure we're not duplicating an existing key
164             if propname == self.key and node[propname] != value:
165                 try:
166                     self.lookup(value)
167                 except KeyError:
168                     pass
169                 else:
170                     raise ValueError, 'node with key "%s" exists'%value
172             # this will raise the KeyError if the property isn't valid
173             # ... we don't use getprops() here because we only care about
174             # the writeable properties.
175             prop = self.properties[propname]
177             # if the value's the same as the existing value, no sense in
178             # doing anything
179             if node.has_key(propname) and value == node[propname]:
180                 del propvalues[propname]
181                 continue
183             # do stuff based on the prop type
184             if isinstance(prop, Link):
185                 link_class = self.properties[propname].classname
186                 # if it isn't a number, it's a key
187                 if type(value) != type(''):
188                     raise ValueError, 'link value must be String'
189                 if not num_re.match(value):
190                     try:
191                         value = self.db.classes[link_class].lookup(value)
192                     except (TypeError, KeyError):
193                         raise IndexError, 'new property "%s": %s not a %s'%(
194                             propname, value, self.properties[propname].classname)
196                 if not self.db.hasnode(link_class, value):
197                     raise IndexError, '%s has no node %s'%(link_class, value)
199             elif isinstance(prop, Multilink):
200                 if type(value) != type([]):
201                     raise TypeError, 'new property "%s" not a list of'\
202                         ' ids'%propname
203                 link_class = self.properties[propname].classname
204                 l = []
205                 for entry in value:
206                     # if it isn't a number, it's a key
207                     if type(entry) != type(''):
208                         raise ValueError, 'new property "%s" link value ' \
209                             'must be a string'%propname
210                     if not num_re.match(entry):
211                         try:
212                             entry = self.db.classes[link_class].lookup(entry)
213                         except (TypeError, KeyError):
214                             raise IndexError, 'new property "%s": %s not a %s'%(
215                                 propname, entry,
216                                 self.properties[propname].classname)
217                     l.append(entry)
218                 value = l
219                 propvalues[propname] = value
221             elif isinstance(prop, String):
222                 if value is not None and type(value) != type(''):
223                     raise TypeError, 'new property "%s" not a string'%propname
225             elif isinstance(prop, Password):
226                 if not isinstance(value, password.Password):
227                     raise TypeError, 'new property "%s" not a Password'%propname
228                 propvalues[propname] = value
230             elif value is not None and isinstance(prop, Date):
231                 if not isinstance(value, date.Date):
232                     raise TypeError, 'new property "%s" not a Date'% propname
233                 propvalues[propname] = value
235             elif value is not None and isinstance(prop, Interval):
236                 if not isinstance(value, date.Interval):
237                     raise TypeError, 'new property "%s" not an '\
238                         'Interval'%propname
239                 propvalues[propname] = value
241             elif value is not None and isinstance(prop, Number):
242                 try:
243                     float(value)
244                 except ValueError:
245                     raise TypeError, 'new property "%s" not numeric'%propname
247             elif value is not None and isinstance(prop, Boolean):
248                 try:
249                     int(value)
250                 except ValueError:
251                     raise TypeError, 'new property "%s" not boolean'%propname
253             node[propname] = value
255         # do the set
256         self.store[nodeid] = node
258     def lookup(self, keyvalue):
259         ''' look up the key node in the store
260         '''
261         return self.by_key[keyvalue]
263     def hasnode(self, nodeid):
264         nodeid = str(nodeid)
265         return self.store.has_key(nodeid)
267     def list(self):
268         l = self.store.keys()
269         l.sort()
270         return l
272     def index(self, nodeid):
273         pass
275     def stringFind(self, **requirements):
276         """Locate a particular node by matching a set of its String
277            properties in a caseless search.
279            If the property is not a String property, a TypeError is raised.
280         
281            The return is a list of the id of all nodes that match.
282         """
283         for propname in requirements.keys():
284             prop = self.properties[propname]
285             if isinstance(not prop, String):
286                 raise TypeError, "'%s' not a String property"%propname
287             requirements[propname] = requirements[propname].lower()
288         l = []
289         for nodeid, node in self.store.items():
290             for key, value in requirements.items():
291                 if node[key] and node[key].lower() != value:
292                     break
293             else:
294                 l.append(nodeid)
295         return l
297     def getkey(self):
298         """Return the name of the key property for this class or None."""
299         return self.key
301     def labelprop(self, default_to_id=0):
302         ''' Return the property name for a label for the given node.
304         This method attempts to generate a consistent label for the node.
305         It tries the following in order:
306             1. key property
307             2. "name" property
308             3. "title" property
309             4. first property from the sorted property name list
310         '''
311         k = self.getkey()
312         if  k:
313             return k
314         props = self.getprops()
315         if props.has_key('name'):
316             return 'name'
317         elif props.has_key('title'):
318             return 'title'
319         if default_to_id:
320             return 'id'
321         props = props.keys()
322         props.sort()
323         return props[0]