Code

.cvsignorey goodness
[roundup.git] / hyperdb.py
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.
234         
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.
237         
238         Any other properties on this class that are missing from the
239         'propvalues' dictionary are set to None.
240         
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.
345         
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.
463         
464         The properties on the node remain available from the get() method,
465         and the node's id is never reused.
466         
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.
570         
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
923 # $Log: not supported by cvs2svn $