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