1 import bsddb, os, cPickle, re, string
3 import date
4 #
5 # Types
6 #
7 class BaseType:
8 isStringType = 0
9 isDateType = 0
10 isIntervalType = 0
11 isLinkType = 0
12 isMultilinkType = 0
14 class String(BaseType):
15 def __init__(self):
16 """An object designating a String property."""
17 pass
18 def __repr__(self):
19 return '<%s>'%self.__class__
20 isStringType = 1
22 class Date(BaseType, String):
23 isDateType = 1
25 class Interval(BaseType, String):
26 isIntervalType = 1
28 class Link(BaseType):
29 def __init__(self, classname):
30 """An object designating a Link property that links to
31 nodes in a specified class."""
32 self.classname = classname
33 def __repr__(self):
34 return '<%s to "%s">'%(self.__class__, self.classname)
35 isLinkType = 1
37 class Multilink(BaseType, Link):
38 """An object designating a Multilink property that links
39 to nodes in a specified class.
40 """
41 isMultilinkType = 1
43 class DatabaseError(ValueError):
44 pass
46 #
47 # Now the database
48 #
49 RETIRED_FLAG = '__hyperdb_retired'
50 class Database:
51 """A database for storing records containing flexible data types."""
53 def __init__(self, storagelocator, journaltag=None):
54 """Open a hyperdatabase given a specifier to some storage.
56 The meaning of 'storagelocator' depends on the particular
57 implementation of the hyperdatabase. It could be a file name,
58 a directory path, a socket descriptor for a connection to a
59 database over the network, etc.
61 The 'journaltag' is a token that will be attached to the journal
62 entries for any edits done on the database. If 'journaltag' is
63 None, the database is opened in read-only mode: the Class.create(),
64 Class.set(), and Class.retire() methods are disabled.
65 """
66 self.dir, self.journaltag = storagelocator, journaltag
67 self.classes = {}
69 #
70 # Classes
71 #
72 def __getattr__(self, classname):
73 """A convenient way of calling self.getclass(classname)."""
74 return self.classes[classname]
76 def addclass(self, cl):
77 cn = cl.classname
78 if self.classes.has_key(cn):
79 raise ValueError, cn
80 self.classes[cn] = cl
82 def getclasses(self):
83 """Return a list of the names of all existing classes."""
84 l = self.classes.keys()
85 l.sort()
86 return l
88 def getclass(self, classname):
89 """Get the Class object representing a particular class.
91 If 'classname' is not a valid class name, a KeyError is raised.
92 """
93 return self.classes[classname]
95 #
96 # Class DBs
97 #
98 def clear(self):
99 for cn in self.classes.keys():
100 db = os.path.join(self.dir, 'nodes.%s'%cn)
101 bsddb.btopen(db, 'n')
102 db = os.path.join(self.dir, 'journals.%s'%cn)
103 bsddb.btopen(db, 'n')
105 def getclassdb(self, classname, mode='r'):
106 ''' grab a connection to the class db that will be used for
107 multiple actions
108 '''
109 path = os.path.join(os.getcwd(), self.dir, 'nodes.%s'%classname)
110 return bsddb.btopen(path, mode)
112 def addnode(self, classname, nodeid, node):
113 ''' add the specified node to its class's db
114 '''
115 db = self.getclassdb(classname, 'c')
116 db[nodeid] = cPickle.dumps(node, 1)
117 db.close()
118 setnode = addnode
120 def getnode(self, classname, nodeid, cldb=None):
121 ''' add the specified node to its class's db
122 '''
123 db = cldb or self.getclassdb(classname)
124 if not db.has_key(nodeid):
125 raise IndexError, nodeid
126 res = cPickle.loads(db[nodeid])
127 if not cldb: db.close()
128 return res
130 def hasnode(self, classname, nodeid, cldb=None):
131 ''' add the specified node to its class's db
132 '''
133 db = cldb or self.getclassdb(classname)
134 res = db.has_key(nodeid)
135 if not cldb: db.close()
136 return res
138 def countnodes(self, classname, cldb=None):
139 db = cldb or self.getclassdb(classname)
140 return len(db.keys())
141 if not cldb: db.close()
142 return res
144 def getnodeids(self, classname, cldb=None):
145 db = cldb or self.getclassdb(classname)
146 res = db.keys()
147 if not cldb: db.close()
148 return res
150 #
151 # Journal
152 #
153 def addjournal(self, classname, nodeid, action, params):
154 ''' Journal the Action
155 'action' may be:
157 'create' or 'set' -- 'params' is a dictionary of property values
158 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
159 'retire' -- 'params' is None
160 '''
161 entry = (nodeid, date.Date(), self.journaltag, action, params)
162 db = bsddb.btopen(os.path.join(self.dir, 'journals.%s'%classname), 'c')
163 if db.has_key(nodeid):
164 s = db[nodeid]
165 l = cPickle.loads(db[nodeid])
166 l.append(entry)
167 else:
168 l = [entry]
169 db[nodeid] = cPickle.dumps(l)
170 db.close()
172 def getjournal(self, classname, nodeid):
173 ''' get the journal for id
174 '''
175 db = bsddb.btopen(os.path.join(self.dir, 'journals.%s'%classname), 'r')
176 res = cPickle.loads(db[nodeid])
177 db.close()
178 return res
180 def close(self):
181 ''' Close the Database - we must release the circular refs so that
182 we can be del'ed and the underlying bsddb connections closed
183 cleanly.
184 '''
185 self.classes = None
188 #
189 # Basic transaction support
190 #
191 # TODO: well, write these methods (and then use them in other code)
192 def register_action(self):
193 ''' Register an action to the transaction undo log
194 '''
196 def commit(self):
197 ''' Commit the current transaction, start a new one
198 '''
200 def rollback(self):
201 ''' Reverse all actions from the current transaction
202 '''
205 class Class:
206 """The handle to a particular class of nodes in a hyperdatabase."""
208 def __init__(self, db, classname, **properties):
209 """Create a new class with a given name and property specification.
211 'classname' must not collide with the name of an existing class,
212 or a ValueError is raised. The keyword arguments in 'properties'
213 must map names to property objects, or a TypeError is raised.
214 """
215 self.classname = classname
216 self.properties = properties
217 self.db = db
218 self.key = ''
220 # do the db-related init stuff
221 db.addclass(self)
223 # Editing nodes:
225 def create(self, **propvalues):
226 """Create a new node of this class and return its id.
228 The keyword arguments in 'propvalues' map property names to values.
230 The values of arguments must be acceptable for the types of their
231 corresponding properties or a TypeError is raised.
233 If this class has a key property, it must be present and its value
234 must not collide with other key strings or a ValueError is raised.
236 Any other properties on this class that are missing from the
237 'propvalues' dictionary are set to None.
239 If an id in a link or multilink property does not refer to a valid
240 node, an IndexError is raised.
241 """
242 if self.db.journaltag is None:
243 raise DatabaseError, 'Database open read-only'
244 newid = str(self.count() + 1)
246 # validate propvalues
247 num_re = re.compile('^\d+$')
248 for key, value in propvalues.items():
249 if key == self.key:
250 try:
251 self.lookup(value)
252 except KeyError:
253 pass
254 else:
255 raise ValueError, 'node with key "%s" exists'%value
257 prop = self.properties[key]
259 if prop.isLinkType:
260 value = str(value)
261 link_class = self.properties[key].classname
262 if not num_re.match(value):
263 try:
264 value = self.db.classes[link_class].lookup(value)
265 except:
266 raise ValueError, 'new property "%s": %s not a %s'%(
267 key, value, self.properties[key].classname)
268 propvalues[key] = value
269 if not self.db.hasnode(link_class, value):
270 raise ValueError, '%s has no node %s'%(link_class, value)
272 # register the link with the newly linked node
273 self.db.addjournal(link_class, value, 'link',
274 (self.classname, newid, key))
276 elif prop.isMultilinkType:
277 if type(value) != type([]):
278 raise TypeError, 'new property "%s" not a list of ids'%key
279 link_class = self.properties[key].classname
280 l = []
281 for entry in map(str, value):
282 if not num_re.match(entry):
283 try:
284 entry = self.db.classes[link_class].lookup(entry)
285 except:
286 raise ValueError, 'new property "%s": %s not a %s'%(
287 key, entry, self.properties[key].classname)
288 l.append(entry)
289 value = l
290 propvalues[key] = value
292 # handle additions
293 for id in value:
294 if not self.db.hasnode(link_class, id):
295 raise ValueError, '%s has no node %s'%(link_class, id)
296 # register the link with the newly linked node
297 self.db.addjournal(link_class, id, 'link',
298 (self.classname, newid, key))
300 elif prop.isStringType:
301 if type(value) != type(''):
302 raise TypeError, 'new property "%s" not a string'%key
304 elif prop.isDateType:
305 if not hasattr(value, 'isDate'):
306 raise TypeError, 'new property "%s" not a Date'% key
308 elif prop.isIntervalType:
309 if not hasattr(value, 'isInterval'):
310 raise TypeError, 'new property "%s" not an Interval'% key
312 for key,prop in self.properties.items():
313 if propvalues.has_key(str(key)):
314 continue
315 if prop.isMultilinkType:
316 propvalues[key] = []
317 else:
318 propvalues[key] = None
320 # done
321 self.db.addnode(self.classname, newid, propvalues)
322 self.db.addjournal(self.classname, newid, 'create', propvalues)
323 return newid
325 def get(self, nodeid, propname):
326 """Get the value of a property on an existing node of this class.
328 'nodeid' must be the id of an existing node of this class or an
329 IndexError is raised. 'propname' must be the name of a property
330 of this class or a KeyError is raised.
331 """
332 d = self.db.getnode(self.classname, str(nodeid))
333 return d[propname]
335 # XXX not in spec
336 def getnode(self, nodeid):
337 ''' Return a convenience wrapper for the node
338 '''
339 return Node(self, nodeid)
341 def set(self, nodeid, **propvalues):
342 """Modify a property on an existing node of this class.
344 'nodeid' must be the id of an existing node of this class or an
345 IndexError is raised.
347 Each key in 'propvalues' must be the name of a property of this
348 class or a KeyError is raised.
350 All values in 'propvalues' must be acceptable types for their
351 corresponding properties or a TypeError is raised.
353 If the value of the key property is set, it must not collide with
354 other key strings or a ValueError is raised.
356 If the value of a Link or Multilink property contains an invalid
357 node id, a ValueError is raised.
358 """
359 if not propvalues:
360 return
361 if self.db.journaltag is None:
362 raise DatabaseError, 'Database open read-only'
363 nodeid = str(nodeid)
364 node = self.db.getnode(self.classname, nodeid)
365 if node.has_key(RETIRED_FLAG):
366 raise IndexError
367 num_re = re.compile('^\d+$')
368 for key, value in propvalues.items():
369 if not node.has_key(key):
370 raise KeyError, key
372 if key == self.key:
373 try:
374 self.lookup(value)
375 except KeyError:
376 pass
377 else:
378 raise ValueError, 'node with key "%s" exists'%value
380 prop = self.properties[key]
382 if prop.isLinkType:
383 value = str(value)
384 link_class = self.properties[key].classname
385 if not num_re.match(value):
386 try:
387 value = self.db.classes[link_class].lookup(value)
388 except:
389 raise ValueError, 'new property "%s": %s not a %s'%(
390 key, value, self.properties[key].classname)
392 if not self.db.hasnode(link_class, value):
393 raise ValueError, '%s has no node %s'%(link_class, value)
395 # register the unlink with the old linked node
396 if node[key] is not None:
397 self.db.addjournal(link_class, node[key], 'unlink',
398 (self.classname, nodeid, key))
400 # register the link with the newly linked node
401 if value is not None:
402 self.db.addjournal(link_class, value, 'link',
403 (self.classname, nodeid, key))
405 elif prop.isMultilinkType:
406 if type(value) != type([]):
407 raise TypeError, 'new property "%s" not a list of ids'%key
408 link_class = self.properties[key].classname
409 l = []
410 for entry in map(str, value):
411 if not num_re.match(entry):
412 try:
413 entry = self.db.classes[link_class].lookup(entry)
414 except:
415 raise ValueError, 'new property "%s": %s not a %s'%(
416 key, entry, self.properties[key].classname)
417 l.append(entry)
418 value = l
419 propvalues[key] = value
421 #handle removals
422 l = node[key]
423 for id in l[:]:
424 if id in value:
425 continue
426 # register the unlink with the old linked node
427 self.db.addjournal(link_class, id, 'unlink',
428 (self.classname, nodeid, key))
429 l.remove(id)
431 # handle additions
432 for id in value:
433 if not self.db.hasnode(link_class, id):
434 raise ValueError, '%s has no node %s'%(link_class, id)
435 if id in l:
436 continue
437 # register the link with the newly linked node
438 self.db.addjournal(link_class, id, 'link',
439 (self.classname, nodeid, key))
440 l.append(id)
442 elif prop.isStringType:
443 if value is not None and type(value) != type(''):
444 raise TypeError, 'new property "%s" not a string'%key
446 elif prop.isDateType:
447 if not hasattr(value, 'isDate'):
448 raise TypeError, 'new property "%s" not a Date'% key
450 elif prop.isIntervalType:
451 if not hasattr(value, 'isInterval'):
452 raise TypeError, 'new property "%s" not an Interval'% key
454 node[key] = value
456 self.db.setnode(self.classname, nodeid, node)
457 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
459 def retire(self, nodeid):
460 """Retire a node.
462 The properties on the node remain available from the get() method,
463 and the node's id is never reused.
465 Retired nodes are not returned by the find(), list(), or lookup()
466 methods, and other nodes may reuse the values of their key properties.
467 """
468 nodeid = str(nodeid)
469 if self.db.journaltag is None:
470 raise DatabaseError, 'Database open read-only'
471 node = self.db.getnode(self.classname, nodeid)
472 node[RETIRED_FLAG] = 1
473 self.db.setnode(self.classname, nodeid, node)
474 self.db.addjournal(self.classname, nodeid, 'retired', None)
476 def history(self, nodeid):
477 """Retrieve the journal of edits on a particular node.
479 'nodeid' must be the id of an existing node of this class or an
480 IndexError is raised.
482 The returned list contains tuples of the form
484 (date, tag, action, params)
486 'date' is a Timestamp object specifying the time of the change and
487 'tag' is the journaltag specified when the database was opened.
488 """
489 return self.db.getjournal(self.classname, nodeid)
491 # Locating nodes:
493 def setkey(self, propname):
494 """Select a String property of this class to be the key property.
496 'propname' must be the name of a String property of this class or
497 None, or a TypeError is raised. The values of the key property on
498 all existing nodes must be unique or a ValueError is raised.
499 """
500 self.key = propname
502 def getkey(self):
503 """Return the name of the key property for this class or None."""
504 return self.key
506 # TODO: set up a separate index db file for this? profile?
507 def lookup(self, keyvalue):
508 """Locate a particular node by its key property and return its id.
510 If this class has no key property, a TypeError is raised. If the
511 'keyvalue' matches one of the values for the key property among
512 the nodes in this class, the matching node's id is returned;
513 otherwise a KeyError is raised.
514 """
515 cldb = self.db.getclassdb(self.classname)
516 for nodeid in self.db.getnodeids(self.classname, cldb):
517 node = self.db.getnode(self.classname, nodeid, cldb)
518 if node.has_key(RETIRED_FLAG):
519 continue
520 if node[self.key] == keyvalue:
521 return nodeid
522 cldb.close()
523 raise KeyError, keyvalue
525 # XXX: change from spec - allows multiple props to match
526 def find(self, **propspec):
527 """Get the ids of nodes in this class which link to a given node.
529 'propspec' consists of keyword args propname=nodeid
530 'propname' must be the name of a property in this class, or a
531 KeyError is raised. That property must be a Link or Multilink
532 property, or a TypeError is raised.
534 'nodeid' must be the id of an existing node in the class linked
535 to by the given property, or an IndexError is raised.
536 """
537 propspec = propspec.items()
538 for propname, nodeid in propspec:
539 nodeid = str(nodeid)
540 # check the prop is OK
541 prop = self.properties[propname]
542 if not prop.isLinkType and not prop.isMultilinkType:
543 raise TypeError, "'%s' not a Link/Multilink property"%propname
544 if not self.db.hasnode(prop.classname, nodeid):
545 raise ValueError, '%s has no node %s'%(link_class, nodeid)
547 # ok, now do the find
548 cldb = self.db.getclassdb(self.classname)
549 l = []
550 for id in self.db.getnodeids(self.classname, cldb):
551 node = self.db.getnode(self.classname, id, cldb)
552 if node.has_key(RETIRED_FLAG):
553 continue
554 for propname, nodeid in propspec:
555 nodeid = str(nodeid)
556 property = node[propname]
557 if prop.isLinkType and nodeid == property:
558 l.append(id)
559 elif prop.isMultilinkType and nodeid in property:
560 l.append(id)
561 cldb.close()
562 return l
564 def stringFind(self, **requirements):
565 """Locate a particular node by matching a set of its String properties.
567 If the property is not a String property, a TypeError is raised.
569 The return is a list of the id of all nodes that match.
570 """
571 for propname in requirements.keys():
572 prop = self.properties[propname]
573 if not prop.isStringType:
574 raise TypeError, "'%s' not a String property"%propname
575 l = []
576 cldb = self.db.getclassdb(self.classname)
577 for nodeid in self.db.getnodeids(self.classname, cldb):
578 node = self.db.getnode(self.classname, nodeid, cldb)
579 if node.has_key(RETIRED_FLAG):
580 continue
581 for key, value in requirements.items():
582 if node[key] != value:
583 break
584 else:
585 l.append(nodeid)
586 cldb.close()
587 return l
589 def list(self):
590 """Return a list of the ids of the active nodes in this class."""
591 l = []
592 cn = self.classname
593 cldb = self.db.getclassdb(cn)
594 for nodeid in self.db.getnodeids(cn, cldb):
595 node = self.db.getnode(cn, nodeid, cldb)
596 if node.has_key(RETIRED_FLAG):
597 continue
598 l.append(nodeid)
599 l.sort()
600 cldb.close()
601 return l
603 # XXX not in spec
604 def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
605 ''' Return a list of the ids of the active nodes in this class that
606 match the 'filter' spec, sorted by the group spec and then the
607 sort spec
608 '''
609 cn = self.classname
611 # optimise filterspec
612 l = []
613 props = self.getprops()
614 for k, v in filterspec.items():
615 propclass = props[k]
616 if propclass.isLinkType:
617 if type(v) is not type([]):
618 v = [v]
619 # replace key values with node ids
620 u = []
621 link_class = self.db.classes[propclass.classname]
622 for entry in v:
623 if not num_re.match(entry):
624 try:
625 entry = link_class.lookup(entry)
626 except:
627 raise ValueError, 'new property "%s": %s not a %s'%(
628 key, entry, self.properties[key].classname)
629 u.append(entry)
631 l.append((0, k, u))
632 elif propclass.isMultilinkType:
633 if type(v) is not type([]):
634 v = [v]
635 # replace key values with node ids
636 u = []
637 link_class = self.db.classes[propclass.classname]
638 for entry in v:
639 if not num_re.match(entry):
640 try:
641 entry = link_class.lookup(entry)
642 except:
643 raise ValueError, 'new property "%s": %s not a %s'%(
644 key, entry, self.properties[key].classname)
645 u.append(entry)
646 l.append((1, k, u))
647 elif propclass.isStringType:
648 v = v[0]
649 if '*' in v or '?' in v:
650 # simple glob searching
651 v = v.replace('?', '.')
652 v = v.replace('*', '.*?')
653 v = re.compile(v)
654 l.append((2, k, v))
655 elif v[0] == '^':
656 # start-anchored
657 if v[-1] == '$':
658 # _and_ end-anchored
659 l.append((6, k, v[1:-1]))
660 l.append((3, k, v[1:]))
661 elif v[-1] == '$':
662 # end-anchored
663 l.append((4, k, v[:-1]))
664 else:
665 # substring
666 l.append((5, k, v))
667 else:
668 l.append((6, k, v))
669 filterspec = l
671 # now, find all the nodes that are active and pass filtering
672 l = []
673 cldb = self.db.getclassdb(cn)
674 for nodeid in self.db.getnodeids(cn, cldb):
675 node = self.db.getnode(cn, nodeid, cldb)
676 if node.has_key(RETIRED_FLAG):
677 continue
678 # apply filter
679 for t, k, v in filterspec:
680 if t == 0 and node[k] not in v:
681 # link - if this node'd property doesn't appear in the
682 # filterspec's nodeid list, skip it
683 break
684 elif t == 1:
685 # multilink - if any of the nodeids required by the
686 # filterspec aren't in this node's property, then skip
687 # it
688 for value in v:
689 if value not in node[k]:
690 break
691 else:
692 continue
693 break
694 elif t == 2 and not v.search(node[k]):
695 # RE search
696 break
697 elif t == 3 and node[k][:len(v)] != v:
698 # start anchored
699 break
700 elif t == 4 and node[k][-len(v):] != v:
701 # end anchored
702 break
703 elif t == 5 and node[k].find(v) == -1:
704 # substring search
705 break
706 elif t == 6 and node[k] != v:
707 # straight value comparison for the other types
708 break
709 else:
710 l.append((nodeid, node))
711 l.sort()
712 cldb.close()
714 # optimise sort
715 m = []
716 for entry in sort:
717 if entry[0] != '-':
718 m.append(('+', entry))
719 else:
720 m.append((entry[0], entry[1:]))
721 sort = m
723 # optimise group
724 m = []
725 for entry in group:
726 if entry[0] != '-':
727 m.append(('+', entry))
728 else:
729 m.append((entry[0], entry[1:]))
730 group = m
732 # now, sort the result
733 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
734 db = self.db, cl=self):
735 a_id, an = a
736 b_id, bn = b
737 for list in group, sort:
738 for dir, prop in list:
739 # handle the properties that might be "faked"
740 if not an.has_key(prop):
741 an[prop] = cl.get(a_id, prop)
742 av = an[prop]
743 if not bn.has_key(prop):
744 bn[prop] = cl.get(b_id, prop)
745 bv = bn[prop]
747 # sorting is class-specific
748 propclass = properties[prop]
750 # String and Date values are sorted in the natural way
751 if propclass.isStringType:
752 # clean up the strings
753 if av and av[0] in string.uppercase:
754 av = an[prop] = av.lower()
755 if bv and bv[0] in string.uppercase:
756 bv = bn[prop] = bv.lower()
757 if propclass.isStringType or propclass.isDateType:
758 if dir == '+':
759 r = cmp(av, bv)
760 if r != 0: return r
761 elif dir == '-':
762 r = cmp(bv, av)
763 if r != 0: return r
765 # Link properties are sorted according to the value of
766 # the "order" property on the linked nodes if it is
767 # present; or otherwise on the key string of the linked
768 # nodes; or finally on the node ids.
769 elif propclass.isLinkType:
770 link = db.classes[propclass.classname]
771 if link.getprops().has_key('order'):
772 if dir == '+':
773 r = cmp(link.get(av, 'order'),
774 link.get(bv, 'order'))
775 if r != 0: return r
776 elif dir == '-':
777 r = cmp(link.get(bv, 'order'),
778 link.get(av, 'order'))
779 if r != 0: return r
780 elif link.getkey():
781 key = link.getkey()
782 if dir == '+':
783 r = cmp(link.get(av, key), link.get(bv, key))
784 if r != 0: return r
785 elif dir == '-':
786 r = cmp(link.get(bv, key), link.get(av, key))
787 if r != 0: return r
788 else:
789 if dir == '+':
790 r = cmp(av, bv)
791 if r != 0: return r
792 elif dir == '-':
793 r = cmp(bv, av)
794 if r != 0: return r
796 # Multilink properties are sorted according to how many
797 # links are present.
798 elif propclass.isMultilinkType:
799 if dir == '+':
800 r = cmp(len(av), len(bv))
801 if r != 0: return r
802 elif dir == '-':
803 r = cmp(len(bv), len(av))
804 if r != 0: return r
805 return cmp(a[0], b[0])
806 l.sort(sortfun)
807 return [i[0] for i in l]
809 def count(self):
810 """Get the number of nodes in this class.
812 If the returned integer is 'numnodes', the ids of all the nodes
813 in this class run from 1 to numnodes, and numnodes+1 will be the
814 id of the next node to be created in this class.
815 """
816 return self.db.countnodes(self.classname)
818 # Manipulating properties:
820 def getprops(self):
821 """Return a dictionary mapping property names to property objects."""
822 return self.properties
824 def addprop(self, **properties):
825 """Add properties to this class.
827 The keyword arguments in 'properties' must map names to property
828 objects, or a TypeError is raised. None of the keys in 'properties'
829 may collide with the names of existing properties, or a ValueError
830 is raised before any properties have been added.
831 """
832 for key in properties.keys():
833 if self.properties.has_key(key):
834 raise ValueError, key
835 self.properties.update(properties)
838 # XXX not in spec
839 class Node:
840 ''' A convenience wrapper for the given node
841 '''
842 def __init__(self, cl, nodeid):
843 self.__dict__['cl'] = cl
844 self.__dict__['nodeid'] = nodeid
845 def keys(self):
846 return self.cl.getprops().keys()
847 def has_key(self, name):
848 return self.cl.getprops().has_key(name)
849 def __getattr__(self, name):
850 if self.__dict__.has_key(name):
851 return self.__dict__['name']
852 try:
853 return self.cl.get(self.nodeid, name)
854 except KeyError, value:
855 raise AttributeError, str(value)
856 def __getitem__(self, name):
857 return self.cl.get(self.nodeid, name)
858 def __setattr__(self, name, value):
859 try:
860 return self.cl.set(self.nodeid, **{name: value})
861 except KeyError, value:
862 raise AttributeError, str(value)
863 def __setitem__(self, name, value):
864 self.cl.set(self.nodeid, **{name: value})
865 def history(self):
866 return self.cl.history(self.nodeid)
867 def retire(self):
868 return self.cl.retire(self.nodeid)
871 def Choice(name, *options):
872 cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
873 for i in range(len(options)):
874 cl.create(name=option[i], order=i)
875 return hyperdb.Link(name)
878 if __name__ == '__main__':
879 import pprint
880 db = Database("test_db", "richard")
881 status = Class(db, "status", name=String())
882 status.setkey("name")
883 print db.status.create(name="unread")
884 print db.status.create(name="in-progress")
885 print db.status.create(name="testing")
886 print db.status.create(name="resolved")
887 print db.status.count()
888 print db.status.list()
889 print db.status.lookup("in-progress")
890 db.status.retire(3)
891 print db.status.list()
892 issue = Class(db, "issue", title=String(), status=Link("status"))
893 db.issue.create(title="spam", status=1)
894 db.issue.create(title="eggs", status=2)
895 db.issue.create(title="ham", status=4)
896 db.issue.create(title="arguments", status=2)
897 db.issue.create(title="abuse", status=1)
898 user = Class(db, "user", username=String(), password=String())
899 user.setkey("username")
900 db.issue.addprop(fixer=Link("user"))
901 print db.issue.getprops()
902 #{"title": <hyperdb.String>, "status": <hyperdb.Link to "status">,
903 #"user": <hyperdb.Link to "user">}
904 db.issue.set(5, status=2)
905 print db.issue.get(5, "status")
906 print db.status.get(2, "name")
907 print db.issue.get(5, "title")
908 print db.issue.find(status = db.status.lookup("in-progress"))
909 print db.issue.history(5)
910 # [(<Date 2000-06-28.19:09:43>, "ping", "create", {"title": "abuse", "status": 1}),
911 # (<Date 2000-06-28.19:11:04>, "ping", "set", {"status": 2})]
912 print db.status.history(1)
913 # [(<Date 2000-06-28.19:09:43>, "ping", "link", ("issue", 5, "status")),
914 # (<Date 2000-06-28.19:11:04>, "ping", "unlink", ("issue", 5, "status"))]
915 print db.status.history(2)
916 # [(<Date 2000-06-28.19:11:04>, "ping", "link", ("issue", 5, "status"))]
918 # TODO: set up some filter tests