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