Code

bc1013f4967dc286b31566395c56e8ede77ed8f5
[roundup.git] / roundup / hyperdb.py
1 # $Id: hyperdb.py,v 1.8 2001-07-29 08:27:40 richard Exp $
3 # standard python modules
4 import cPickle, re, string
6 # roundup modules
7 import date
10 #
11 # Types
12 #
13 class BaseType:
14     isStringType = 0
15     isDateType = 0
16     isIntervalType = 0
17     isLinkType = 0
18     isMultilinkType = 0
20 class String(BaseType):
21     def __init__(self):
22         """An object designating a String property."""
23         pass
24     def __repr__(self):
25         return '<%s>'%self.__class__
26     isStringType = 1
28 class Date(BaseType, String):
29     isDateType = 1
31 class Interval(BaseType, String):
32     isIntervalType = 1
34 class Link(BaseType):
35     def __init__(self, classname):
36         """An object designating a Link property that links to
37         nodes in a specified class."""
38         self.classname = classname
39     def __repr__(self):
40         return '<%s to "%s">'%(self.__class__, self.classname)
41     isLinkType = 1
43 class Multilink(BaseType, Link):
44     """An object designating a Multilink property that links
45        to nodes in a specified class.
46     """
47     isMultilinkType = 1
49 class DatabaseError(ValueError):
50     pass
53 #
54 # the base Database class
55 #
56 class Database:
57     # flag to set on retired entries
58     RETIRED_FLAG = '__hyperdb_retired'
61 #
62 # The base Class class
63 #
64 class Class:
65     """The handle to a particular class of nodes in a hyperdatabase."""
67     def __init__(self, db, classname, **properties):
68         """Create a new class with a given name and property specification.
70         'classname' must not collide with the name of an existing class,
71         or a ValueError is raised.  The keyword arguments in 'properties'
72         must map names to property objects, or a TypeError is raised.
73         """
74         self.classname = classname
75         self.properties = properties
76         self.db = db
77         self.key = ''
79         # do the db-related init stuff
80         db.addclass(self)
82     # Editing nodes:
84     def create(self, **propvalues):
85         """Create a new node of this class and return its id.
87         The keyword arguments in 'propvalues' map property names to values.
89         The values of arguments must be acceptable for the types of their
90         corresponding properties or a TypeError is raised.
91         
92         If this class has a key property, it must be present and its value
93         must not collide with other key strings or a ValueError is raised.
94         
95         Any other properties on this class that are missing from the
96         'propvalues' dictionary are set to None.
97         
98         If an id in a link or multilink property does not refer to a valid
99         node, an IndexError is raised.
100         """
101         if propvalues.has_key('id'):
102             raise KeyError, '"id" is reserved'
104         if self.db.journaltag is None:
105             raise DatabaseError, 'Database open read-only'
107         # new node's id
108         newid = str(self.count() + 1)
110         # validate propvalues
111         num_re = re.compile('^\d+$')
112         for key, value in propvalues.items():
113             if key == self.key:
114                 try:
115                     self.lookup(value)
116                 except KeyError:
117                     pass
118                 else:
119                     raise ValueError, 'node with key "%s" exists'%value
121             prop = self.properties[key]
123             if prop.isLinkType:
124                 if type(value) != type(''):
125                     raise ValueError, 'link value must be String'
126 #                value = str(value)
127                 link_class = self.properties[key].classname
128                 # if it isn't a number, it's a key
129                 if not num_re.match(value):
130                     try:
131                         value = self.db.classes[link_class].lookup(value)
132                     except:
133                         raise IndexError, 'new property "%s": %s not a %s'%(
134                             key, value, self.properties[key].classname)
135                 propvalues[key] = value
136                 if not self.db.hasnode(link_class, value):
137                     raise IndexError, '%s has no node %s'%(link_class, value)
139                 # register the link with the newly linked node
140                 self.db.addjournal(link_class, value, 'link',
141                     (self.classname, newid, key))
143             elif prop.isMultilinkType:
144                 if type(value) != type([]):
145                     raise TypeError, 'new property "%s" not a list of ids'%key
146                 link_class = self.properties[key].classname
147                 l = []
148                 for entry in value:
149                     if type(entry) != type(''):
150                         raise ValueError, 'link value must be String'
151                     # if it isn't a number, it's a key
152                     if not num_re.match(entry):
153                         try:
154                             entry = self.db.classes[link_class].lookup(entry)
155                         except:
156                             raise IndexError, 'new property "%s": %s not a %s'%(
157                                 key, entry, self.properties[key].classname)
158                     l.append(entry)
159                 value = l
160                 propvalues[key] = value
162                 # handle additions
163                 for id in value:
164                     if not self.db.hasnode(link_class, id):
165                         raise IndexError, '%s has no node %s'%(link_class, id)
166                     # register the link with the newly linked node
167                     self.db.addjournal(link_class, id, 'link',
168                         (self.classname, newid, key))
170             elif prop.isStringType:
171                 if type(value) != type(''):
172                     raise TypeError, 'new property "%s" not a string'%key
174             elif prop.isDateType:
175                 if not hasattr(value, 'isDate'):
176                     raise TypeError, 'new property "%s" not a Date'% key
178             elif prop.isIntervalType:
179                 if not hasattr(value, 'isInterval'):
180                     raise TypeError, 'new property "%s" not an Interval'% key
182         for key, prop in self.properties.items():
183             if propvalues.has_key(key):
184                 continue
185             if prop.isMultilinkType:
186                 propvalues[key] = []
187             else:
188                 propvalues[key] = None
190         # done
191         self.db.addnode(self.classname, newid, propvalues)
192         self.db.addjournal(self.classname, newid, 'create', propvalues)
193         return newid
195     def get(self, nodeid, propname):
196         """Get the value of a property on an existing node of this class.
198         'nodeid' must be the id of an existing node of this class or an
199         IndexError is raised.  'propname' must be the name of a property
200         of this class or a KeyError is raised.
201         """
202         if propname == 'id':
203             return nodeid
204 #        nodeid = str(nodeid)
205         d = self.db.getnode(self.classname, nodeid)
206         return d[propname]
208     # XXX not in spec
209     def getnode(self, nodeid):
210         ''' Return a convenience wrapper for the node
211         '''
212         return Node(self, nodeid)
214     def set(self, nodeid, **propvalues):
215         """Modify a property on an existing node of this class.
216         
217         'nodeid' must be the id of an existing node of this class or an
218         IndexError is raised.
220         Each key in 'propvalues' must be the name of a property of this
221         class or a KeyError is raised.
223         All values in 'propvalues' must be acceptable types for their
224         corresponding properties or a TypeError is raised.
226         If the value of the key property is set, it must not collide with
227         other key strings or a ValueError is raised.
229         If the value of a Link or Multilink property contains an invalid
230         node id, a ValueError is raised.
231         """
232         if not propvalues:
233             return
235         if propvalues.has_key('id'):
236             raise KeyError, '"id" is reserved'
238         if self.db.journaltag is None:
239             raise DatabaseError, 'Database open read-only'
241 #        nodeid = str(nodeid)
242         node = self.db.getnode(self.classname, nodeid)
243         if node.has_key(self.db.RETIRED_FLAG):
244             raise IndexError
245         num_re = re.compile('^\d+$')
246         for key, value in propvalues.items():
247             if not node.has_key(key):
248                 raise KeyError, key
250             if key == self.key:
251                 try:
252                     self.lookup(value)
253                 except KeyError:
254                     pass
255                 else:
256                     raise ValueError, 'node with key "%s" exists'%value
258             prop = self.properties[key]
260             if prop.isLinkType:
261 #                value = str(value)
262                 link_class = self.properties[key].classname
263                 # if it isn't a number, it's a key
264                 if type(value) != type(''):
265                     raise ValueError, 'link value must be String'
266                 if not num_re.match(value):
267                     try:
268                         value = self.db.classes[link_class].lookup(value)
269                     except:
270                         raise IndexError, 'new property "%s": %s not a %s'%(
271                             key, value, self.properties[key].classname)
273                 if not self.db.hasnode(link_class, value):
274                     raise IndexError, '%s has no node %s'%(link_class, value)
276                 # register the unlink with the old linked node
277                 if node[key] is not None:
278                     self.db.addjournal(link_class, node[key], 'unlink',
279                         (self.classname, nodeid, key))
281                 # register the link with the newly linked node
282                 if value is not None:
283                     self.db.addjournal(link_class, value, 'link',
284                         (self.classname, nodeid, key))
286             elif prop.isMultilinkType:
287                 if type(value) != type([]):
288                     raise TypeError, 'new property "%s" not a list of ids'%key
289                 link_class = self.properties[key].classname
290                 l = []
291                 for entry in value:
292                     # if it isn't a number, it's a key
293                     if type(entry) != type(''):
294                         raise ValueError, 'link value must be String'
295                     if not num_re.match(entry):
296                         try:
297                             entry = self.db.classes[link_class].lookup(entry)
298                         except:
299                             raise IndexError, 'new property "%s": %s not a %s'%(
300                                 key, entry, self.properties[key].classname)
301                     l.append(entry)
302                 value = l
303                 propvalues[key] = value
305                 #handle removals
306                 l = node[key]
307                 for id in l[:]:
308                     if id in value:
309                         continue
310                     # register the unlink with the old linked node
311                     self.db.addjournal(link_class, id, 'unlink',
312                         (self.classname, nodeid, key))
313                     l.remove(id)
315                 # handle additions
316                 for id in value:
317                     if not self.db.hasnode(link_class, id):
318                         raise IndexError, '%s has no node %s'%(link_class, id)
319                     if id in l:
320                         continue
321                     # register the link with the newly linked node
322                     self.db.addjournal(link_class, id, 'link',
323                         (self.classname, nodeid, key))
324                     l.append(id)
326             elif prop.isStringType:
327                 if value is not None and type(value) != type(''):
328                     raise TypeError, 'new property "%s" not a string'%key
330             elif prop.isDateType:
331                 if not hasattr(value, 'isDate'):
332                     raise TypeError, 'new property "%s" not a Date'% key
334             elif prop.isIntervalType:
335                 if not hasattr(value, 'isInterval'):
336                     raise TypeError, 'new property "%s" not an Interval'% key
338             node[key] = value
340         self.db.setnode(self.classname, nodeid, node)
341         self.db.addjournal(self.classname, nodeid, 'set', propvalues)
343     def retire(self, nodeid):
344         """Retire a node.
345         
346         The properties on the node remain available from the get() method,
347         and the node's id is never reused.
348         
349         Retired nodes are not returned by the find(), list(), or lookup()
350         methods, and other nodes may reuse the values of their key properties.
351         """
352 #        nodeid = str(nodeid)
353         if self.db.journaltag is None:
354             raise DatabaseError, 'Database open read-only'
355         node = self.db.getnode(self.classname, nodeid)
356         node[self.db.RETIRED_FLAG] = 1
357         self.db.setnode(self.classname, nodeid, node)
358         self.db.addjournal(self.classname, nodeid, 'retired', None)
360     def history(self, nodeid):
361         """Retrieve the journal of edits on a particular node.
363         'nodeid' must be the id of an existing node of this class or an
364         IndexError is raised.
366         The returned list contains tuples of the form
368             (date, tag, action, params)
370         'date' is a Timestamp object specifying the time of the change and
371         'tag' is the journaltag specified when the database was opened.
372         """
373         return self.db.getjournal(self.classname, nodeid)
375     # Locating nodes:
377     def setkey(self, propname):
378         """Select a String property of this class to be the key property.
380         'propname' must be the name of a String property of this class or
381         None, or a TypeError is raised.  The values of the key property on
382         all existing nodes must be unique or a ValueError is raised.
383         """
384         self.key = propname
386     def getkey(self):
387         """Return the name of the key property for this class or None."""
388         return self.key
390     def labelprop(self):
391         ''' Return the property name for a label for the given node.
393         This method attempts to generate a consistent label for the node.
394         It tries the following in order:
395             1. key property
396             2. "name" property
397             3. "title" property
398             4. first property from the sorted property name list
399         '''
400         k = self.getkey()
401         if  k:
402             return k
403         props = self.getprops()
404         if props.has_key('name'):
405             return 'name'
406         elif props.has_key('title'):
407             return 'title'
408         props = props.keys()
409         props.sort()
410         return props[0]
412     # TODO: set up a separate index db file for this? profile?
413     def lookup(self, keyvalue):
414         """Locate a particular node by its key property and return its id.
416         If this class has no key property, a TypeError is raised.  If the
417         'keyvalue' matches one of the values for the key property among
418         the nodes in this class, the matching node's id is returned;
419         otherwise a KeyError is raised.
420         """
421         cldb = self.db.getclassdb(self.classname)
422         for nodeid in self.db.getnodeids(self.classname, cldb):
423             node = self.db.getnode(self.classname, nodeid, cldb)
424             if node.has_key(self.db.RETIRED_FLAG):
425                 continue
426             if node[self.key] == keyvalue:
427                 return nodeid
428         cldb.close()
429         raise KeyError, keyvalue
431     # XXX: change from spec - allows multiple props to match
432     def find(self, **propspec):
433         """Get the ids of nodes in this class which link to a given node.
435         'propspec' consists of keyword args propname=nodeid   
436           'propname' must be the name of a property in this class, or a
437             KeyError is raised.  That property must be a Link or Multilink
438             property, or a TypeError is raised.
440           'nodeid' must be the id of an existing node in the class linked
441             to by the given property, or an IndexError is raised.
442         """
443         propspec = propspec.items()
444         for propname, nodeid in propspec:
445 #            nodeid = str(nodeid)
446             # check the prop is OK
447             prop = self.properties[propname]
448             if not prop.isLinkType and not prop.isMultilinkType:
449                 raise TypeError, "'%s' not a Link/Multilink property"%propname
450             if not self.db.hasnode(prop.classname, nodeid):
451                 raise ValueError, '%s has no node %s'%(link_class, nodeid)
453         # ok, now do the find
454         cldb = self.db.getclassdb(self.classname)
455         l = []
456         for id in self.db.getnodeids(self.classname, cldb):
457             node = self.db.getnode(self.classname, id, cldb)
458             if node.has_key(self.db.RETIRED_FLAG):
459                 continue
460             for propname, nodeid in propspec:
461 #                nodeid = str(nodeid)
462                 property = node[propname]
463                 if prop.isLinkType and nodeid == property:
464                     l.append(id)
465                 elif prop.isMultilinkType and nodeid in property:
466                     l.append(id)
467         cldb.close()
468         return l
470     def stringFind(self, **requirements):
471         """Locate a particular node by matching a set of its String properties.
473         If the property is not a String property, a TypeError is raised.
474         
475         The return is a list of the id of all nodes that match.
476         """
477         for propname in requirements.keys():
478             prop = self.properties[propname]
479             if not prop.isStringType:
480                 raise TypeError, "'%s' not a String property"%propname
481         l = []
482         cldb = self.db.getclassdb(self.classname)
483         for nodeid in self.db.getnodeids(self.classname, cldb):
484             node = self.db.getnode(self.classname, nodeid, cldb)
485             if node.has_key(self.db.RETIRED_FLAG):
486                 continue
487             for key, value in requirements.items():
488                 if node[key] != value:
489                     break
490             else:
491                 l.append(nodeid)
492         cldb.close()
493         return l
495     def list(self):
496         """Return a list of the ids of the active nodes in this class."""
497         l = []
498         cn = self.classname
499         cldb = self.db.getclassdb(cn)
500         for nodeid in self.db.getnodeids(cn, cldb):
501             node = self.db.getnode(cn, nodeid, cldb)
502             if node.has_key(self.db.RETIRED_FLAG):
503                 continue
504             l.append(nodeid)
505         l.sort()
506         cldb.close()
507         return l
509     # XXX not in spec
510     def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
511         ''' Return a list of the ids of the active nodes in this class that
512             match the 'filter' spec, sorted by the group spec and then the
513             sort spec
514         '''
515         cn = self.classname
517         # optimise filterspec
518         l = []
519         props = self.getprops()
520         for k, v in filterspec.items():
521             propclass = props[k]
522             if propclass.isLinkType:
523                 if type(v) is not type([]):
524                     v = [v]
525                 # replace key values with node ids
526                 u = []
527                 link_class =  self.db.classes[propclass.classname]
528                 for entry in v:
529                     if not num_re.match(entry):
530                         try:
531                             entry = link_class.lookup(entry)
532                         except:
533                             raise ValueError, 'new property "%s": %s not a %s'%(
534                                 k, entry, self.properties[k].classname)
535                     u.append(entry)
537                 l.append((0, k, u))
538             elif propclass.isMultilinkType:
539                 if type(v) is not type([]):
540                     v = [v]
541                 # replace key values with node ids
542                 u = []
543                 link_class =  self.db.classes[propclass.classname]
544                 for entry in v:
545                     if not num_re.match(entry):
546                         try:
547                             entry = link_class.lookup(entry)
548                         except:
549                             raise ValueError, 'new property "%s": %s not a %s'%(
550                                 k, entry, self.properties[k].classname)
551                     u.append(entry)
552                 l.append((1, k, u))
553             elif propclass.isStringType:
554                 if '*' in v or '?' in v:
555                     # simple glob searching
556                     v = v.replace('?', '.')
557                     v = v.replace('*', '.*?')
558                     v = re.compile(v)
559                     l.append((2, k, v))
560                 elif v[0] == '^':
561                     # start-anchored
562                     if v[-1] == '$':
563                         # _and_ end-anchored
564                         l.append((6, k, v[1:-1]))
565                     l.append((3, k, v[1:]))
566                 elif v[-1] == '$':
567                     # end-anchored
568                     l.append((4, k, v[:-1]))
569                 else:
570                     # substring
571                     l.append((5, k, v))
572             else:
573                 l.append((6, k, v))
574         filterspec = l
576         # now, find all the nodes that are active and pass filtering
577         l = []
578         cldb = self.db.getclassdb(cn)
579         for nodeid in self.db.getnodeids(cn, cldb):
580             node = self.db.getnode(cn, nodeid, cldb)
581             if node.has_key(self.db.RETIRED_FLAG):
582                 continue
583             # apply filter
584             for t, k, v in filterspec:
585                 if t == 0 and node[k] not in v:
586                     # link - if this node'd property doesn't appear in the
587                     # filterspec's nodeid list, skip it
588                     break
589                 elif t == 1:
590                     # multilink - if any of the nodeids required by the
591                     # filterspec aren't in this node's property, then skip
592                     # it
593                     for value in v:
594                         if value not in node[k]:
595                             break
596                     else:
597                         continue
598                     break
599                 elif t == 2 and not v.search(node[k]):
600                     # RE search
601                     break
602                 elif t == 3 and node[k][:len(v)] != v:
603                     # start anchored
604                     break
605                 elif t == 4 and node[k][-len(v):] != v:
606                     # end anchored
607                     break
608                 elif t == 5 and node[k].find(v) == -1:
609                     # substring search
610                     break
611                 elif t == 6 and node[k] != v:
612                     # straight value comparison for the other types
613                     break
614             else:
615                 l.append((nodeid, node))
616         l.sort()
617         cldb.close()
619         # optimise sort
620         m = []
621         for entry in sort:
622             if entry[0] != '-':
623                 m.append(('+', entry))
624             else:
625                 m.append((entry[0], entry[1:]))
626         sort = m
628         # optimise group
629         m = []
630         for entry in group:
631             if entry[0] != '-':
632                 m.append(('+', entry))
633             else:
634                 m.append((entry[0], entry[1:]))
635         group = m
637         # now, sort the result
638         def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
639                 db = self.db, cl=self):
640             a_id, an = a
641             b_id, bn = b
642             # sort by group and then sort
643             for list in group, sort:
644                 for dir, prop in list:
645                     # handle the properties that might be "faked"
646                     if not an.has_key(prop):
647                         an[prop] = cl.get(a_id, prop)
648                     av = an[prop]
649                     if not bn.has_key(prop):
650                         bn[prop] = cl.get(b_id, prop)
651                     bv = bn[prop]
653                     # sorting is class-specific
654                     propclass = properties[prop]
656                     # String and Date values are sorted in the natural way
657                     if propclass.isStringType:
658                         # clean up the strings
659                         if av and av[0] in string.uppercase:
660                             av = an[prop] = av.lower()
661                         if bv and bv[0] in string.uppercase:
662                             bv = bn[prop] = bv.lower()
663                     if propclass.isStringType or propclass.isDateType:
664                         if dir == '+':
665                             r = cmp(av, bv)
666                             if r != 0: return r
667                         elif dir == '-':
668                             r = cmp(bv, av)
669                             if r != 0: return r
671                     # Link properties are sorted according to the value of
672                     # the "order" property on the linked nodes if it is
673                     # present; or otherwise on the key string of the linked
674                     # nodes; or finally on  the node ids.
675                     elif propclass.isLinkType:
676                         link = db.classes[propclass.classname]
677                         if link.getprops().has_key('order'):
678                             if dir == '+':
679                                 r = cmp(link.get(av, 'order'),
680                                     link.get(bv, 'order'))
681                                 if r != 0: return r
682                             elif dir == '-':
683                                 r = cmp(link.get(bv, 'order'),
684                                     link.get(av, 'order'))
685                                 if r != 0: return r
686                         elif link.getkey():
687                             key = link.getkey()
688                             if dir == '+':
689                                 r = cmp(link.get(av, key), link.get(bv, key))
690                                 if r != 0: return r
691                             elif dir == '-':
692                                 r = cmp(link.get(bv, key), link.get(av, key))
693                                 if r != 0: return r
694                         else:
695                             if dir == '+':
696                                 r = cmp(av, bv)
697                                 if r != 0: return r
698                             elif dir == '-':
699                                 r = cmp(bv, av)
700                                 if r != 0: return r
702                     # Multilink properties are sorted according to how many
703                     # links are present.
704                     elif propclass.isMultilinkType:
705                         if dir == '+':
706                             r = cmp(len(av), len(bv))
707                             if r != 0: return r
708                         elif dir == '-':
709                             r = cmp(len(bv), len(av))
710                             if r != 0: return r
711                 # end for dir, prop in list:
712             # end for list in sort, group:
713             # if all else fails, compare the ids
714             return cmp(a[0], b[0])
716         l.sort(sortfun)
717         return [i[0] for i in l]
719     def count(self):
720         """Get the number of nodes in this class.
722         If the returned integer is 'numnodes', the ids of all the nodes
723         in this class run from 1 to numnodes, and numnodes+1 will be the
724         id of the next node to be created in this class.
725         """
726         return self.db.countnodes(self.classname)
728     # Manipulating properties:
730     def getprops(self):
731         """Return a dictionary mapping property names to property objects."""
732         d = self.properties.copy()
733         d['id'] = String()
734         return d
736     def addprop(self, **properties):
737         """Add properties to this class.
739         The keyword arguments in 'properties' must map names to property
740         objects, or a TypeError is raised.  None of the keys in 'properties'
741         may collide with the names of existing properties, or a ValueError
742         is raised before any properties have been added.
743         """
744         for key in properties.keys():
745             if self.properties.has_key(key):
746                 raise ValueError, key
747         self.properties.update(properties)
750 # XXX not in spec
751 class Node:
752     ''' A convenience wrapper for the given node
753     '''
754     def __init__(self, cl, nodeid):
755         self.__dict__['cl'] = cl
756         self.__dict__['nodeid'] = nodeid
757     def keys(self):
758         return self.cl.getprops().keys()
759     def has_key(self, name):
760         return self.cl.getprops().has_key(name)
761     def __getattr__(self, name):
762         if self.__dict__.has_key(name):
763             return self.__dict__['name']
764         try:
765             return self.cl.get(self.nodeid, name)
766         except KeyError, value:
767             raise AttributeError, str(value)
768     def __getitem__(self, name):
769         return self.cl.get(self.nodeid, name)
770     def __setattr__(self, name, value):
771         try:
772             return self.cl.set(self.nodeid, **{name: value})
773         except KeyError, value:
774             raise AttributeError, str(value)
775     def __setitem__(self, name, value):
776         self.cl.set(self.nodeid, **{name: value})
777     def history(self):
778         return self.cl.history(self.nodeid)
779     def retire(self):
780         return self.cl.retire(self.nodeid)
783 def Choice(name, *options):
784     cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
785     for i in range(len(options)):
786         cl.create(name=option[i], order=i)
787     return hyperdb.Link(name)
790 # $Log: not supported by cvs2svn $
791 # Revision 1.7  2001/07/29 07:01:39  richard
792 # Added vim command to all source so that we don't get no steenkin' tabs :)
794 # Revision 1.6  2001/07/29 05:36:14  richard
795 # Cleanup of the link label generation.
797 # Revision 1.5  2001/07/29 04:05:37  richard
798 # Added the fabricated property "id".
800 # Revision 1.4  2001/07/27 06:25:35  richard
801 # Fixed some of the exceptions so they're the right type.
802 # Removed the str()-ification of node ids so we don't mask oopsy errors any
803 # more.
805 # Revision 1.3  2001/07/27 05:17:14  richard
806 # just some comments
808 # Revision 1.2  2001/07/22 12:09:32  richard
809 # Final commit of Grande Splite
811 # Revision 1.1  2001/07/22 11:58:35  richard
812 # More Grande Splite
815 # vim: set filetype=python ts=4 sw=4 et si