c225e6ac7f7ff7d34bfac7fb1d4862d4484c6ccf
1 # $iD: HYperdb.py,v 1.10 2001/07/30 02:38:31 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 _marker = []
62 #
63 # The base Class class
64 #
65 class Class:
66 """The handle to a particular class of nodes in a hyperdatabase."""
68 def __init__(self, db, classname, **properties):
69 """Create a new class with a given name and property specification.
71 'classname' must not collide with the name of an existing class,
72 or a ValueError is raised. The keyword arguments in 'properties'
73 must map names to property objects, or a TypeError is raised.
74 """
75 self.classname = classname
76 self.properties = properties
77 self.db = db
78 self.key = ''
80 # do the db-related init stuff
81 db.addclass(self)
83 # Editing nodes:
85 def create(self, **propvalues):
86 """Create a new node of this class and return its id.
88 The keyword arguments in 'propvalues' map property names to values.
90 The values of arguments must be acceptable for the types of their
91 corresponding properties or a TypeError is raised.
93 If this class has a key property, it must be present and its value
94 must not collide with other key strings or a ValueError is raised.
96 Any other properties on this class that are missing from the
97 'propvalues' dictionary are set to None.
99 If an id in a link or multilink property does not refer to a valid
100 node, an IndexError is raised.
101 """
102 if propvalues.has_key('id'):
103 raise KeyError, '"id" is reserved'
105 if self.db.journaltag is None:
106 raise DatabaseError, 'Database open read-only'
108 # new node's id
109 newid = str(self.count() + 1)
111 # validate propvalues
112 num_re = re.compile('^\d+$')
113 for key, value in propvalues.items():
114 if key == self.key:
115 try:
116 self.lookup(value)
117 except KeyError:
118 pass
119 else:
120 raise ValueError, 'node with key "%s" exists'%value
122 # try to handle this property
123 try:
124 prop = self.properties[key]
125 except KeyError:
126 raise KeyError, '"%s" has no property "%s"'%(self.classname,
127 key)
129 if prop.isLinkType:
130 if type(value) != type(''):
131 raise ValueError, 'link value must be String'
132 # value = str(value)
133 link_class = self.properties[key].classname
134 # if it isn't a number, it's a key
135 if not num_re.match(value):
136 try:
137 value = self.db.classes[link_class].lookup(value)
138 except:
139 raise IndexError, 'new property "%s": %s not a %s'%(
140 key, value, self.properties[key].classname)
141 propvalues[key] = value
142 if not self.db.hasnode(link_class, value):
143 raise IndexError, '%s has no node %s'%(link_class, value)
145 # register the link with the newly linked node
146 self.db.addjournal(link_class, value, 'link',
147 (self.classname, newid, key))
149 elif prop.isMultilinkType:
150 if type(value) != type([]):
151 raise TypeError, 'new property "%s" not a list of ids'%key
152 link_class = self.properties[key].classname
153 l = []
154 for entry in value:
155 if type(entry) != type(''):
156 raise ValueError, 'link value must be String'
157 # if it isn't a number, it's a key
158 if not num_re.match(entry):
159 try:
160 entry = self.db.classes[link_class].lookup(entry)
161 except:
162 raise IndexError, 'new property "%s": %s not a %s'%(
163 key, entry, self.properties[key].classname)
164 l.append(entry)
165 value = l
166 propvalues[key] = value
168 # handle additions
169 for id in value:
170 if not self.db.hasnode(link_class, id):
171 raise IndexError, '%s has no node %s'%(link_class, id)
172 # register the link with the newly linked node
173 self.db.addjournal(link_class, id, 'link',
174 (self.classname, newid, key))
176 elif prop.isStringType:
177 if type(value) != type(''):
178 raise TypeError, 'new property "%s" not a string'%key
180 elif prop.isDateType:
181 if not hasattr(value, 'isDate'):
182 raise TypeError, 'new property "%s" not a Date'% key
184 elif prop.isIntervalType:
185 if not hasattr(value, 'isInterval'):
186 raise TypeError, 'new property "%s" not an Interval'% key
188 for key, prop in self.properties.items():
189 if propvalues.has_key(key):
190 continue
191 if prop.isMultilinkType:
192 propvalues[key] = []
193 else:
194 propvalues[key] = None
196 # done
197 self.db.addnode(self.classname, newid, propvalues)
198 self.db.addjournal(self.classname, newid, 'create', propvalues)
199 return newid
201 def get(self, nodeid, propname, default=_marker):
202 """Get the value of a property on an existing node of this class.
204 'nodeid' must be the id of an existing node of this class or an
205 IndexError is raised. 'propname' must be the name of a property
206 of this class or a KeyError is raised.
207 """
208 if propname == 'id':
209 return nodeid
210 # nodeid = str(nodeid)
211 d = self.db.getnode(self.classname, nodeid)
212 if not d.has_key(propname) and default is not _marker:
213 return default
214 return d[propname]
216 # XXX not in spec
217 def getnode(self, nodeid):
218 ''' Return a convenience wrapper for the node
219 '''
220 return Node(self, nodeid)
222 def set(self, nodeid, **propvalues):
223 """Modify a property on an existing node of this class.
225 'nodeid' must be the id of an existing node of this class or an
226 IndexError is raised.
228 Each key in 'propvalues' must be the name of a property of this
229 class or a KeyError is raised.
231 All values in 'propvalues' must be acceptable types for their
232 corresponding properties or a TypeError is raised.
234 If the value of the key property is set, it must not collide with
235 other key strings or a ValueError is raised.
237 If the value of a Link or Multilink property contains an invalid
238 node id, a ValueError is raised.
239 """
240 if not propvalues:
241 return
243 if propvalues.has_key('id'):
244 raise KeyError, '"id" is reserved'
246 if self.db.journaltag is None:
247 raise DatabaseError, 'Database open read-only'
249 # nodeid = str(nodeid)
250 node = self.db.getnode(self.classname, nodeid)
251 if node.has_key(self.db.RETIRED_FLAG):
252 raise IndexError
253 num_re = re.compile('^\d+$')
254 for key, value in propvalues.items():
255 if not node.has_key(key):
256 raise KeyError, key
258 if key == self.key:
259 try:
260 self.lookup(value)
261 except KeyError:
262 pass
263 else:
264 raise ValueError, 'node with key "%s" exists'%value
266 prop = self.properties[key]
268 if prop.isLinkType:
269 # value = str(value)
270 link_class = self.properties[key].classname
271 # if it isn't a number, it's a key
272 if type(value) != type(''):
273 raise ValueError, 'link value must be String'
274 if not num_re.match(value):
275 try:
276 value = self.db.classes[link_class].lookup(value)
277 except:
278 raise IndexError, 'new property "%s": %s not a %s'%(
279 key, value, self.properties[key].classname)
281 if not self.db.hasnode(link_class, value):
282 raise IndexError, '%s has no node %s'%(link_class, value)
284 # register the unlink with the old linked node
285 if node[key] is not None:
286 self.db.addjournal(link_class, node[key], 'unlink',
287 (self.classname, nodeid, key))
289 # register the link with the newly linked node
290 if value is not None:
291 self.db.addjournal(link_class, value, 'link',
292 (self.classname, nodeid, key))
294 elif prop.isMultilinkType:
295 if type(value) != type([]):
296 raise TypeError, 'new property "%s" not a list of ids'%key
297 link_class = self.properties[key].classname
298 l = []
299 for entry in value:
300 # if it isn't a number, it's a key
301 if type(entry) != type(''):
302 raise ValueError, 'link value must be String'
303 if not num_re.match(entry):
304 try:
305 entry = self.db.classes[link_class].lookup(entry)
306 except:
307 raise IndexError, 'new property "%s": %s not a %s'%(
308 key, entry, self.properties[key].classname)
309 l.append(entry)
310 value = l
311 propvalues[key] = value
313 #handle removals
314 l = node[key]
315 for id in l[:]:
316 if id in value:
317 continue
318 # register the unlink with the old linked node
319 self.db.addjournal(link_class, id, 'unlink',
320 (self.classname, nodeid, key))
321 l.remove(id)
323 # handle additions
324 for id in value:
325 if not self.db.hasnode(link_class, id):
326 raise IndexError, '%s has no node %s'%(link_class, id)
327 if id in l:
328 continue
329 # register the link with the newly linked node
330 self.db.addjournal(link_class, id, 'link',
331 (self.classname, nodeid, key))
332 l.append(id)
334 elif prop.isStringType:
335 if value is not None and type(value) != type(''):
336 raise TypeError, 'new property "%s" not a string'%key
338 elif prop.isDateType:
339 if not hasattr(value, 'isDate'):
340 raise TypeError, 'new property "%s" not a Date'% key
342 elif prop.isIntervalType:
343 if not hasattr(value, 'isInterval'):
344 raise TypeError, 'new property "%s" not an Interval'% key
346 node[key] = value
348 self.db.setnode(self.classname, nodeid, node)
349 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
351 def retire(self, nodeid):
352 """Retire a node.
354 The properties on the node remain available from the get() method,
355 and the node's id is never reused.
357 Retired nodes are not returned by the find(), list(), or lookup()
358 methods, and other nodes may reuse the values of their key properties.
359 """
360 # nodeid = str(nodeid)
361 if self.db.journaltag is None:
362 raise DatabaseError, 'Database open read-only'
363 node = self.db.getnode(self.classname, nodeid)
364 node[self.db.RETIRED_FLAG] = 1
365 self.db.setnode(self.classname, nodeid, node)
366 self.db.addjournal(self.classname, nodeid, 'retired', None)
368 def history(self, nodeid):
369 """Retrieve the journal of edits on a particular node.
371 'nodeid' must be the id of an existing node of this class or an
372 IndexError is raised.
374 The returned list contains tuples of the form
376 (date, tag, action, params)
378 'date' is a Timestamp object specifying the time of the change and
379 'tag' is the journaltag specified when the database was opened.
380 """
381 return self.db.getjournal(self.classname, nodeid)
383 # Locating nodes:
385 def setkey(self, propname):
386 """Select a String property of this class to be the key property.
388 'propname' must be the name of a String property of this class or
389 None, or a TypeError is raised. The values of the key property on
390 all existing nodes must be unique or a ValueError is raised.
391 """
392 self.key = propname
394 def getkey(self):
395 """Return the name of the key property for this class or None."""
396 return self.key
398 def labelprop(self, default_to_id=0):
399 ''' Return the property name for a label for the given node.
401 This method attempts to generate a consistent label for the node.
402 It tries the following in order:
403 1. key property
404 2. "name" property
405 3. "title" property
406 4. first property from the sorted property name list
407 '''
408 k = self.getkey()
409 if k:
410 return k
411 props = self.getprops()
412 if props.has_key('name'):
413 return 'name'
414 elif props.has_key('title'):
415 return 'title'
416 if default_to_id:
417 return 'id'
418 props = props.keys()
419 props.sort()
420 return props[0]
422 # TODO: set up a separate index db file for this? profile?
423 def lookup(self, keyvalue):
424 """Locate a particular node by its key property and return its id.
426 If this class has no key property, a TypeError is raised. If the
427 'keyvalue' matches one of the values for the key property among
428 the nodes in this class, the matching node's id is returned;
429 otherwise a KeyError is raised.
430 """
431 cldb = self.db.getclassdb(self.classname)
432 for nodeid in self.db.getnodeids(self.classname, cldb):
433 node = self.db.getnode(self.classname, nodeid, cldb)
434 if node.has_key(self.db.RETIRED_FLAG):
435 continue
436 if node[self.key] == keyvalue:
437 return nodeid
438 cldb.close()
439 raise KeyError, keyvalue
441 # XXX: change from spec - allows multiple props to match
442 def find(self, **propspec):
443 """Get the ids of nodes in this class which link to a given node.
445 'propspec' consists of keyword args propname=nodeid
446 'propname' must be the name of a property in this class, or a
447 KeyError is raised. That property must be a Link or Multilink
448 property, or a TypeError is raised.
450 'nodeid' must be the id of an existing node in the class linked
451 to by the given property, or an IndexError is raised.
452 """
453 propspec = propspec.items()
454 for propname, nodeid in propspec:
455 # nodeid = str(nodeid)
456 # check the prop is OK
457 prop = self.properties[propname]
458 if not prop.isLinkType and not prop.isMultilinkType:
459 raise TypeError, "'%s' not a Link/Multilink property"%propname
460 if not self.db.hasnode(prop.classname, nodeid):
461 raise ValueError, '%s has no node %s'%(link_class, nodeid)
463 # ok, now do the find
464 cldb = self.db.getclassdb(self.classname)
465 l = []
466 for id in self.db.getnodeids(self.classname, cldb):
467 node = self.db.getnode(self.classname, id, cldb)
468 if node.has_key(self.db.RETIRED_FLAG):
469 continue
470 for propname, nodeid in propspec:
471 # nodeid = str(nodeid)
472 property = node[propname]
473 if prop.isLinkType and nodeid == property:
474 l.append(id)
475 elif prop.isMultilinkType and nodeid in property:
476 l.append(id)
477 cldb.close()
478 return l
480 def stringFind(self, **requirements):
481 """Locate a particular node by matching a set of its String properties.
483 If the property is not a String property, a TypeError is raised.
485 The return is a list of the id of all nodes that match.
486 """
487 for propname in requirements.keys():
488 prop = self.properties[propname]
489 if not prop.isStringType:
490 raise TypeError, "'%s' not a String property"%propname
491 l = []
492 cldb = self.db.getclassdb(self.classname)
493 for nodeid in self.db.getnodeids(self.classname, cldb):
494 node = self.db.getnode(self.classname, nodeid, cldb)
495 if node.has_key(self.db.RETIRED_FLAG):
496 continue
497 for key, value in requirements.items():
498 if node[key] != value:
499 break
500 else:
501 l.append(nodeid)
502 cldb.close()
503 return l
505 def list(self):
506 """Return a list of the ids of the active nodes in this class."""
507 l = []
508 cn = self.classname
509 cldb = self.db.getclassdb(cn)
510 for nodeid in self.db.getnodeids(cn, cldb):
511 node = self.db.getnode(cn, nodeid, cldb)
512 if node.has_key(self.db.RETIRED_FLAG):
513 continue
514 l.append(nodeid)
515 l.sort()
516 cldb.close()
517 return l
519 # XXX not in spec
520 def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
521 ''' Return a list of the ids of the active nodes in this class that
522 match the 'filter' spec, sorted by the group spec and then the
523 sort spec
524 '''
525 cn = self.classname
527 # optimise filterspec
528 l = []
529 props = self.getprops()
530 for k, v in filterspec.items():
531 propclass = props[k]
532 if propclass.isLinkType:
533 if type(v) is not type([]):
534 v = [v]
535 # replace key values with node ids
536 u = []
537 link_class = self.db.classes[propclass.classname]
538 for entry in v:
539 if not num_re.match(entry):
540 try:
541 entry = link_class.lookup(entry)
542 except:
543 raise ValueError, 'new property "%s": %s not a %s'%(
544 k, entry, self.properties[k].classname)
545 u.append(entry)
547 l.append((0, k, u))
548 elif propclass.isMultilinkType:
549 if type(v) is not type([]):
550 v = [v]
551 # replace key values with node ids
552 u = []
553 link_class = self.db.classes[propclass.classname]
554 for entry in v:
555 if not num_re.match(entry):
556 try:
557 entry = link_class.lookup(entry)
558 except:
559 raise ValueError, 'new property "%s": %s not a %s'%(
560 k, entry, self.properties[k].classname)
561 u.append(entry)
562 l.append((1, k, u))
563 elif propclass.isStringType:
564 if '*' in v or '?' in v:
565 # simple glob searching
566 v = v.replace('?', '.')
567 v = v.replace('*', '.*?')
568 v = re.compile(v)
569 l.append((2, k, v))
570 elif v[0] == '^':
571 # start-anchored
572 if v[-1] == '$':
573 # _and_ end-anchored
574 l.append((6, k, v[1:-1]))
575 l.append((3, k, v[1:]))
576 elif v[-1] == '$':
577 # end-anchored
578 l.append((4, k, v[:-1]))
579 else:
580 # substring
581 l.append((5, k, v))
582 else:
583 l.append((6, k, v))
584 filterspec = l
586 # now, find all the nodes that are active and pass filtering
587 l = []
588 cldb = self.db.getclassdb(cn)
589 for nodeid in self.db.getnodeids(cn, cldb):
590 node = self.db.getnode(cn, nodeid, cldb)
591 if node.has_key(self.db.RETIRED_FLAG):
592 continue
593 # apply filter
594 for t, k, v in filterspec:
595 if t == 0 and node[k] not in v:
596 # link - if this node'd property doesn't appear in the
597 # filterspec's nodeid list, skip it
598 break
599 elif t == 1:
600 # multilink - if any of the nodeids required by the
601 # filterspec aren't in this node's property, then skip
602 # it
603 for value in v:
604 if value not in node[k]:
605 break
606 else:
607 continue
608 break
609 elif t == 2 and not v.search(node[k]):
610 # RE search
611 break
612 elif t == 3 and node[k][:len(v)] != v:
613 # start anchored
614 break
615 elif t == 4 and node[k][-len(v):] != v:
616 # end anchored
617 break
618 elif t == 5 and node[k].find(v) == -1:
619 # substring search
620 break
621 elif t == 6 and node[k] != v:
622 # straight value comparison for the other types
623 break
624 else:
625 l.append((nodeid, node))
626 l.sort()
627 cldb.close()
629 # optimise sort
630 m = []
631 for entry in sort:
632 if entry[0] != '-':
633 m.append(('+', entry))
634 else:
635 m.append((entry[0], entry[1:]))
636 sort = m
638 # optimise group
639 m = []
640 for entry in group:
641 if entry[0] != '-':
642 m.append(('+', entry))
643 else:
644 m.append((entry[0], entry[1:]))
645 group = m
646 # now, sort the result
647 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
648 db = self.db, cl=self):
649 a_id, an = a
650 b_id, bn = b
651 # sort by group and then sort
652 for list in group, sort:
653 for dir, prop in list:
654 # handle the properties that might be "faked"
655 if not an.has_key(prop):
656 an[prop] = cl.get(a_id, prop)
657 av = an[prop]
658 if not bn.has_key(prop):
659 bn[prop] = cl.get(b_id, prop)
660 bv = bn[prop]
662 # sorting is class-specific
663 propclass = properties[prop]
665 # String and Date values are sorted in the natural way
666 if propclass.isStringType:
667 # clean up the strings
668 if av and av[0] in string.uppercase:
669 av = an[prop] = av.lower()
670 if bv and bv[0] in string.uppercase:
671 bv = bn[prop] = bv.lower()
672 if propclass.isStringType or propclass.isDateType:
673 if dir == '+':
674 r = cmp(av, bv)
675 if r != 0: return r
676 elif dir == '-':
677 r = cmp(bv, av)
678 if r != 0: return r
680 # Link properties are sorted according to the value of
681 # the "order" property on the linked nodes if it is
682 # present; or otherwise on the key string of the linked
683 # nodes; or finally on the node ids.
684 elif propclass.isLinkType:
685 link = db.classes[propclass.classname]
686 if av is None and bv is not None: return -1
687 if av is not None and bv is None: return 1
688 if av is None and bv is None: return 0
689 if link.getprops().has_key('order'):
690 if dir == '+':
691 r = cmp(link.get(av, 'order'),
692 link.get(bv, 'order'))
693 if r != 0: return r
694 elif dir == '-':
695 r = cmp(link.get(bv, 'order'),
696 link.get(av, 'order'))
697 if r != 0: return r
698 elif link.getkey():
699 key = link.getkey()
700 if dir == '+':
701 r = cmp(link.get(av, key), link.get(bv, key))
702 if r != 0: return r
703 elif dir == '-':
704 r = cmp(link.get(bv, key), link.get(av, key))
705 if r != 0: return r
706 else:
707 if dir == '+':
708 r = cmp(av, bv)
709 if r != 0: return r
710 elif dir == '-':
711 r = cmp(bv, av)
712 if r != 0: return r
714 # Multilink properties are sorted according to how many
715 # links are present.
716 elif propclass.isMultilinkType:
717 if dir == '+':
718 r = cmp(len(av), len(bv))
719 if r != 0: return r
720 elif dir == '-':
721 r = cmp(len(bv), len(av))
722 if r != 0: return r
723 # end for dir, prop in list:
724 # end for list in sort, group:
725 # if all else fails, compare the ids
726 return cmp(a[0], b[0])
728 l.sort(sortfun)
729 return [i[0] for i in l]
731 def count(self):
732 """Get the number of nodes in this class.
734 If the returned integer is 'numnodes', the ids of all the nodes
735 in this class run from 1 to numnodes, and numnodes+1 will be the
736 id of the next node to be created in this class.
737 """
738 return self.db.countnodes(self.classname)
740 # Manipulating properties:
742 def getprops(self):
743 """Return a dictionary mapping property names to property objects."""
744 d = self.properties.copy()
745 d['id'] = String()
746 return d
748 def addprop(self, **properties):
749 """Add properties to this class.
751 The keyword arguments in 'properties' must map names to property
752 objects, or a TypeError is raised. None of the keys in 'properties'
753 may collide with the names of existing properties, or a ValueError
754 is raised before any properties have been added.
755 """
756 for key in properties.keys():
757 if self.properties.has_key(key):
758 raise ValueError, key
759 self.properties.update(properties)
762 # XXX not in spec
763 class Node:
764 ''' A convenience wrapper for the given node
765 '''
766 def __init__(self, cl, nodeid):
767 self.__dict__['cl'] = cl
768 self.__dict__['nodeid'] = nodeid
769 def keys(self):
770 return self.cl.getprops().keys()
771 def has_key(self, name):
772 return self.cl.getprops().has_key(name)
773 def __getattr__(self, name):
774 if self.__dict__.has_key(name):
775 return self.__dict__['name']
776 try:
777 return self.cl.get(self.nodeid, name)
778 except KeyError, value:
779 raise AttributeError, str(value)
780 def __getitem__(self, name):
781 return self.cl.get(self.nodeid, name)
782 def __setattr__(self, name, value):
783 try:
784 return self.cl.set(self.nodeid, **{name: value})
785 except KeyError, value:
786 raise AttributeError, str(value)
787 def __setitem__(self, name, value):
788 self.cl.set(self.nodeid, **{name: value})
789 def history(self):
790 return self.cl.history(self.nodeid)
791 def retire(self):
792 return self.cl.retire(self.nodeid)
795 def Choice(name, *options):
796 cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
797 for i in range(len(options)):
798 cl.create(name=option[i], order=i)
799 return hyperdb.Link(name)
801 #
802 # $Log: not supported by cvs2svn $
803 # Revision 1.11 2001/08/01 04:24:21 richard
804 # mailgw was assuming certain properties existed on the issues being created.
805 #
806 # Revision 1.10 2001/07/30 02:38:31 richard
807 # get() now has a default arg - for migration only.
808 #
809 # Revision 1.9 2001/07/29 09:28:23 richard
810 # Fixed sorting by clicking on column headings.
811 #
812 # Revision 1.8 2001/07/29 08:27:40 richard
813 # Fixed handling of passed-in values in form elements (ie. during a
814 # drill-down)
815 #
816 # Revision 1.7 2001/07/29 07:01:39 richard
817 # Added vim command to all source so that we don't get no steenkin' tabs :)
818 #
819 # Revision 1.6 2001/07/29 05:36:14 richard
820 # Cleanup of the link label generation.
821 #
822 # Revision 1.5 2001/07/29 04:05:37 richard
823 # Added the fabricated property "id".
824 #
825 # Revision 1.4 2001/07/27 06:25:35 richard
826 # Fixed some of the exceptions so they're the right type.
827 # Removed the str()-ification of node ids so we don't mask oopsy errors any
828 # more.
829 #
830 # Revision 1.3 2001/07/27 05:17:14 richard
831 # just some comments
832 #
833 # Revision 1.2 2001/07/22 12:09:32 richard
834 # Final commit of Grande Splite
835 #
836 # Revision 1.1 2001/07/22 11:58:35 richard
837 # More Grande Splite
838 #
839 #
840 # vim: set filetype=python ts=4 sw=4 et si