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.
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]