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):
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 props = props.keys()
417 props.sort()
418 return props[0]
420 # TODO: set up a separate index db file for this? profile?
421 def lookup(self, keyvalue):
422 """Locate a particular node by its key property and return its id.
424 If this class has no key property, a TypeError is raised. If the
425 'keyvalue' matches one of the values for the key property among
426 the nodes in this class, the matching node's id is returned;
427 otherwise a KeyError is raised.
428 """
429 cldb = self.db.getclassdb(self.classname)
430 for nodeid in self.db.getnodeids(self.classname, cldb):
431 node = self.db.getnode(self.classname, nodeid, cldb)
432 if node.has_key(self.db.RETIRED_FLAG):
433 continue
434 if node[self.key] == keyvalue:
435 return nodeid
436 cldb.close()
437 raise KeyError, keyvalue
439 # XXX: change from spec - allows multiple props to match
440 def find(self, **propspec):
441 """Get the ids of nodes in this class which link to a given node.
443 'propspec' consists of keyword args propname=nodeid
444 'propname' must be the name of a property in this class, or a
445 KeyError is raised. That property must be a Link or Multilink
446 property, or a TypeError is raised.
448 'nodeid' must be the id of an existing node in the class linked
449 to by the given property, or an IndexError is raised.
450 """
451 propspec = propspec.items()
452 for propname, nodeid in propspec:
453 # nodeid = str(nodeid)
454 # check the prop is OK
455 prop = self.properties[propname]
456 if not prop.isLinkType and not prop.isMultilinkType:
457 raise TypeError, "'%s' not a Link/Multilink property"%propname
458 if not self.db.hasnode(prop.classname, nodeid):
459 raise ValueError, '%s has no node %s'%(link_class, nodeid)
461 # ok, now do the find
462 cldb = self.db.getclassdb(self.classname)
463 l = []
464 for id in self.db.getnodeids(self.classname, cldb):
465 node = self.db.getnode(self.classname, id, cldb)
466 if node.has_key(self.db.RETIRED_FLAG):
467 continue
468 for propname, nodeid in propspec:
469 # nodeid = str(nodeid)
470 property = node[propname]
471 if prop.isLinkType and nodeid == property:
472 l.append(id)
473 elif prop.isMultilinkType and nodeid in property:
474 l.append(id)
475 cldb.close()
476 return l
478 def stringFind(self, **requirements):
479 """Locate a particular node by matching a set of its String properties.
481 If the property is not a String property, a TypeError is raised.
483 The return is a list of the id of all nodes that match.
484 """
485 for propname in requirements.keys():
486 prop = self.properties[propname]
487 if not prop.isStringType:
488 raise TypeError, "'%s' not a String property"%propname
489 l = []
490 cldb = self.db.getclassdb(self.classname)
491 for nodeid in self.db.getnodeids(self.classname, cldb):
492 node = self.db.getnode(self.classname, nodeid, cldb)
493 if node.has_key(self.db.RETIRED_FLAG):
494 continue
495 for key, value in requirements.items():
496 if node[key] != value:
497 break
498 else:
499 l.append(nodeid)
500 cldb.close()
501 return l
503 def list(self):
504 """Return a list of the ids of the active nodes in this class."""
505 l = []
506 cn = self.classname
507 cldb = self.db.getclassdb(cn)
508 for nodeid in self.db.getnodeids(cn, cldb):
509 node = self.db.getnode(cn, nodeid, cldb)
510 if node.has_key(self.db.RETIRED_FLAG):
511 continue
512 l.append(nodeid)
513 l.sort()
514 cldb.close()
515 return l
517 # XXX not in spec
518 def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
519 ''' Return a list of the ids of the active nodes in this class that
520 match the 'filter' spec, sorted by the group spec and then the
521 sort spec
522 '''
523 cn = self.classname
525 # optimise filterspec
526 l = []
527 props = self.getprops()
528 for k, v in filterspec.items():
529 propclass = props[k]
530 if propclass.isLinkType:
531 if type(v) is not type([]):
532 v = [v]
533 # replace key values with node ids
534 u = []
535 link_class = self.db.classes[propclass.classname]
536 for entry in v:
537 if not num_re.match(entry):
538 try:
539 entry = link_class.lookup(entry)
540 except:
541 raise ValueError, 'new property "%s": %s not a %s'%(
542 k, entry, self.properties[k].classname)
543 u.append(entry)
545 l.append((0, k, u))
546 elif propclass.isMultilinkType:
547 if type(v) is not type([]):
548 v = [v]
549 # replace key values with node ids
550 u = []
551 link_class = self.db.classes[propclass.classname]
552 for entry in v:
553 if not num_re.match(entry):
554 try:
555 entry = link_class.lookup(entry)
556 except:
557 raise ValueError, 'new property "%s": %s not a %s'%(
558 k, entry, self.properties[k].classname)
559 u.append(entry)
560 l.append((1, k, u))
561 elif propclass.isStringType:
562 if '*' in v or '?' in v:
563 # simple glob searching
564 v = v.replace('?', '.')
565 v = v.replace('*', '.*?')
566 v = re.compile(v)
567 l.append((2, k, v))
568 elif v[0] == '^':
569 # start-anchored
570 if v[-1] == '$':
571 # _and_ end-anchored
572 l.append((6, k, v[1:-1]))
573 l.append((3, k, v[1:]))
574 elif v[-1] == '$':
575 # end-anchored
576 l.append((4, k, v[:-1]))
577 else:
578 # substring
579 l.append((5, k, v))
580 else:
581 l.append((6, k, v))
582 filterspec = l
584 # now, find all the nodes that are active and pass filtering
585 l = []
586 cldb = self.db.getclassdb(cn)
587 for nodeid in self.db.getnodeids(cn, cldb):
588 node = self.db.getnode(cn, nodeid, cldb)
589 if node.has_key(self.db.RETIRED_FLAG):
590 continue
591 # apply filter
592 for t, k, v in filterspec:
593 if t == 0 and node[k] not in v:
594 # link - if this node'd property doesn't appear in the
595 # filterspec's nodeid list, skip it
596 break
597 elif t == 1:
598 # multilink - if any of the nodeids required by the
599 # filterspec aren't in this node's property, then skip
600 # it
601 for value in v:
602 if value not in node[k]:
603 break
604 else:
605 continue
606 break
607 elif t == 2 and not v.search(node[k]):
608 # RE search
609 break
610 elif t == 3 and node[k][:len(v)] != v:
611 # start anchored
612 break
613 elif t == 4 and node[k][-len(v):] != v:
614 # end anchored
615 break
616 elif t == 5 and node[k].find(v) == -1:
617 # substring search
618 break
619 elif t == 6 and node[k] != v:
620 # straight value comparison for the other types
621 break
622 else:
623 l.append((nodeid, node))
624 l.sort()
625 cldb.close()
627 # optimise sort
628 m = []
629 for entry in sort:
630 if entry[0] != '-':
631 m.append(('+', entry))
632 else:
633 m.append((entry[0], entry[1:]))
634 sort = m
636 # optimise group
637 m = []
638 for entry in group:
639 if entry[0] != '-':
640 m.append(('+', entry))
641 else:
642 m.append((entry[0], entry[1:]))
643 group = m
644 # now, sort the result
645 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
646 db = self.db, cl=self):
647 a_id, an = a
648 b_id, bn = b
649 # sort by group and then sort
650 for list in group, sort:
651 for dir, prop in list:
652 # handle the properties that might be "faked"
653 if not an.has_key(prop):
654 an[prop] = cl.get(a_id, prop)
655 av = an[prop]
656 if not bn.has_key(prop):
657 bn[prop] = cl.get(b_id, prop)
658 bv = bn[prop]
660 # sorting is class-specific
661 propclass = properties[prop]
663 # String and Date values are sorted in the natural way
664 if propclass.isStringType:
665 # clean up the strings
666 if av and av[0] in string.uppercase:
667 av = an[prop] = av.lower()
668 if bv and bv[0] in string.uppercase:
669 bv = bn[prop] = bv.lower()
670 if propclass.isStringType or propclass.isDateType:
671 if dir == '+':
672 r = cmp(av, bv)
673 if r != 0: return r
674 elif dir == '-':
675 r = cmp(bv, av)
676 if r != 0: return r
678 # Link properties are sorted according to the value of
679 # the "order" property on the linked nodes if it is
680 # present; or otherwise on the key string of the linked
681 # nodes; or finally on the node ids.
682 elif propclass.isLinkType:
683 link = db.classes[propclass.classname]
684 if av is None and bv is not None: return -1
685 if av is not None and bv is None: return 1
686 if av is None and bv is None: return 0
687 if link.getprops().has_key('order'):
688 if dir == '+':
689 r = cmp(link.get(av, 'order'),
690 link.get(bv, 'order'))
691 if r != 0: return r
692 elif dir == '-':
693 r = cmp(link.get(bv, 'order'),
694 link.get(av, 'order'))
695 if r != 0: return r
696 elif link.getkey():
697 key = link.getkey()
698 if dir == '+':
699 r = cmp(link.get(av, key), link.get(bv, key))
700 if r != 0: return r
701 elif dir == '-':
702 r = cmp(link.get(bv, key), link.get(av, key))
703 if r != 0: return r
704 else:
705 if dir == '+':
706 r = cmp(av, bv)
707 if r != 0: return r
708 elif dir == '-':
709 r = cmp(bv, av)
710 if r != 0: return r
712 # Multilink properties are sorted according to how many
713 # links are present.
714 elif propclass.isMultilinkType:
715 if dir == '+':
716 r = cmp(len(av), len(bv))
717 if r != 0: return r
718 elif dir == '-':
719 r = cmp(len(bv), len(av))
720 if r != 0: return r
721 # end for dir, prop in list:
722 # end for list in sort, group:
723 # if all else fails, compare the ids
724 return cmp(a[0], b[0])
726 l.sort(sortfun)
727 return [i[0] for i in l]
729 def count(self):
730 """Get the number of nodes in this class.
732 If the returned integer is 'numnodes', the ids of all the nodes
733 in this class run from 1 to numnodes, and numnodes+1 will be the
734 id of the next node to be created in this class.
735 """
736 return self.db.countnodes(self.classname)
738 # Manipulating properties:
740 def getprops(self):
741 """Return a dictionary mapping property names to property objects."""
742 d = self.properties.copy()
743 d['id'] = String()
744 return d
746 def addprop(self, **properties):
747 """Add properties to this class.
749 The keyword arguments in 'properties' must map names to property
750 objects, or a TypeError is raised. None of the keys in 'properties'
751 may collide with the names of existing properties, or a ValueError
752 is raised before any properties have been added.
753 """
754 for key in properties.keys():
755 if self.properties.has_key(key):
756 raise ValueError, key
757 self.properties.update(properties)
760 # XXX not in spec
761 class Node:
762 ''' A convenience wrapper for the given node
763 '''
764 def __init__(self, cl, nodeid):
765 self.__dict__['cl'] = cl
766 self.__dict__['nodeid'] = nodeid
767 def keys(self):
768 return self.cl.getprops().keys()
769 def has_key(self, name):
770 return self.cl.getprops().has_key(name)
771 def __getattr__(self, name):
772 if self.__dict__.has_key(name):
773 return self.__dict__['name']
774 try:
775 return self.cl.get(self.nodeid, name)
776 except KeyError, value:
777 raise AttributeError, str(value)
778 def __getitem__(self, name):
779 return self.cl.get(self.nodeid, name)
780 def __setattr__(self, name, value):
781 try:
782 return self.cl.set(self.nodeid, **{name: value})
783 except KeyError, value:
784 raise AttributeError, str(value)
785 def __setitem__(self, name, value):
786 self.cl.set(self.nodeid, **{name: value})
787 def history(self):
788 return self.cl.history(self.nodeid)
789 def retire(self):
790 return self.cl.retire(self.nodeid)
793 def Choice(name, *options):
794 cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
795 for i in range(len(options)):
796 cl.create(name=option[i], order=i)
797 return hyperdb.Link(name)
799 #
800 # $Log: not supported by cvs2svn $
801 # Revision 1.10 2001/07/30 02:38:31 richard
802 # get() now has a default arg - for migration only.
803 #
804 # Revision 1.9 2001/07/29 09:28:23 richard
805 # Fixed sorting by clicking on column headings.
806 #
807 # Revision 1.8 2001/07/29 08:27:40 richard
808 # Fixed handling of passed-in values in form elements (ie. during a
809 # drill-down)
810 #
811 # Revision 1.7 2001/07/29 07:01:39 richard
812 # Added vim command to all source so that we don't get no steenkin' tabs :)
813 #
814 # Revision 1.6 2001/07/29 05:36:14 richard
815 # Cleanup of the link label generation.
816 #
817 # Revision 1.5 2001/07/29 04:05:37 richard
818 # Added the fabricated property "id".
819 #
820 # Revision 1.4 2001/07/27 06:25:35 richard
821 # Fixed some of the exceptions so they're the right type.
822 # Removed the str()-ification of node ids so we don't mask oopsy errors any
823 # more.
824 #
825 # Revision 1.3 2001/07/27 05:17:14 richard
826 # just some comments
827 #
828 # Revision 1.2 2001/07/22 12:09:32 richard
829 # Final commit of Grande Splite
830 #
831 # Revision 1.1 2001/07/22 11:58:35 richard
832 # More Grande Splite
833 #
834 #
835 # vim: set filetype=python ts=4 sw=4 et si