comparison roundup/hyperdb.py @ 858:2dd862af72ee

all storage-specific code (ie. backend) is now implemented by the backends
author Richard Jones <richard@users.sourceforge.net>
date Sun, 14 Jul 2002 02:05:54 +0000
parents 234996e85699
children de3da99a7c02
comparison
equal deleted inserted replaced
857:6dd691e37aa8 858:2dd862af72ee
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" 14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 # 17 #
18 # $Id: hyperdb.py,v 1.74 2002-07-10 00:24:10 richard Exp $ 18 # $Id: hyperdb.py,v 1.75 2002-07-14 02:05:53 richard Exp $
19 19
20 __doc__ = """ 20 __doc__ = """
21 Hyperdatabase implementation, especially field types. 21 Hyperdatabase implementation, especially field types.
22 """ 22 """
23 23
24 # standard python modules 24 # standard python modules
25 import sys, re, string, weakref, os, time 25 import sys, os, time, re
26 26
27 # roundup modules 27 # roundup modules
28 import date, password 28 import date, password
29 29
30 # configure up the DEBUG and TRACE captures 30 # configure up the DEBUG and TRACE captures
108 self.do_journal = do_journal == 'yes' 108 self.do_journal = do_journal == 'yes'
109 def __repr__(self): 109 def __repr__(self):
110 ' more useful for dumps ' 110 ' more useful for dumps '
111 return '<%s to "%s">'%(self.__class__, self.classname) 111 return '<%s to "%s">'%(self.__class__, self.classname)
112 112
113 #
114 # Support for splitting designators
115 #
116 class DesignatorError(ValueError):
117 pass
118 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
119 ''' Take a foo123 and return ('foo', 123)
120 '''
121 m = dre.match(designator)
122 if m is None:
123 raise DesignatorError, '"%s" not a node designator'%designator
124 return m.group(1), m.group(2)
125
126 #
127 # the base Database class
128 #
113 class DatabaseError(ValueError): 129 class DatabaseError(ValueError):
114 '''Error to be raised when there is some problem in the database code 130 '''Error to be raised when there is some problem in the database code
115 ''' 131 '''
116 pass 132 pass
117
118
119 #
120 # the base Database class
121 #
122 class Database: 133 class Database:
123 '''A database for storing records containing flexible data types. 134 '''A database for storing records containing flexible data types.
124 135
125 This class defines a hyperdatabase storage layer, which the Classes use to 136 This class defines a hyperdatabase storage layer, which the Classes use to
126 store their data. 137 store their data.
135 An implementation must provide an override for the get() method so that the 146 An implementation must provide an override for the get() method so that the
136 in-database value is returned in preference to the in-transaction value. 147 in-database value is returned in preference to the in-transaction value.
137 This is necessary to determine if any values have changed during a 148 This is necessary to determine if any values have changed during a
138 transaction. 149 transaction.
139 150
151
152 Implementation
153 --------------
154
155 All methods except __repr__ and getnode must be implemented by a
156 concrete backend Class.
157
140 ''' 158 '''
141 159
142 # flag to set on retired entries 160 # flag to set on retired entries
143 RETIRED_FLAG = '__hyperdb_retired' 161 RETIRED_FLAG = '__hyperdb_retired'
144 162
201 219
202 def serialise(self, classname, node): 220 def serialise(self, classname, node):
203 '''Copy the node contents, converting non-marshallable data into 221 '''Copy the node contents, converting non-marshallable data into
204 marshallable data. 222 marshallable data.
205 ''' 223 '''
206 if __debug__: 224 return node
207 print >>DEBUG, 'serialise', classname, node
208 properties = self.getclass(classname).getprops()
209 d = {}
210 for k, v in node.items():
211 # if the property doesn't exist, or is the "retired" flag then
212 # it won't be in the properties dict
213 if not properties.has_key(k):
214 d[k] = v
215 continue
216
217 # get the property spec
218 prop = properties[k]
219
220 if isinstance(prop, Password):
221 d[k] = str(v)
222 elif isinstance(prop, Date) and v is not None:
223 d[k] = v.get_tuple()
224 elif isinstance(prop, Interval) and v is not None:
225 d[k] = v.get_tuple()
226 else:
227 d[k] = v
228 return d
229 225
230 def setnode(self, classname, nodeid, node): 226 def setnode(self, classname, nodeid, node):
231 '''Change the specified node. 227 '''Change the specified node.
232 ''' 228 '''
233 raise NotImplementedError 229 raise NotImplementedError
234 230
235 def unserialise(self, classname, node): 231 def unserialise(self, classname, node):
236 '''Decode the marshalled node data 232 '''Decode the marshalled node data
237 ''' 233 '''
238 if __debug__: 234 return node
239 print >>DEBUG, 'unserialise', classname, node
240 properties = self.getclass(classname).getprops()
241 d = {}
242 for k, v in node.items():
243 # if the property doesn't exist, or is the "retired" flag then
244 # it won't be in the properties dict
245 if not properties.has_key(k):
246 d[k] = v
247 continue
248
249 # get the property spec
250 prop = properties[k]
251
252 if isinstance(prop, Date) and v is not None:
253 d[k] = date.Date(v)
254 elif isinstance(prop, Interval) and v is not None:
255 d[k] = date.Interval(v)
256 elif isinstance(prop, Password):
257 p = password.Password()
258 p.unpack(v)
259 d[k] = p
260 else:
261 d[k] = v
262 return d
263 235
264 def getnode(self, classname, nodeid, db=None, cache=1): 236 def getnode(self, classname, nodeid, db=None, cache=1):
265 '''Get a node from the database. 237 '''Get a node from the database.
266 ''' 238 '''
267 raise NotImplementedError 239 raise NotImplementedError
328 Undo all the changes made since the database was opened or the last 300 Undo all the changes made since the database was opened or the last
329 commit() or rollback() was performed. 301 commit() or rollback() was performed.
330 ''' 302 '''
331 raise NotImplementedError 303 raise NotImplementedError
332 304
333 _marker = []
334 # 305 #
335 # The base Class class 306 # The base Class class
336 # 307 #
337 class Class: 308 class Class:
338 """The handle to a particular class of nodes in a hyperdatabase.""" 309 """ The handle to a particular class of nodes in a hyperdatabase.
310
311 All methods except __repr__ and getnode must be implemented by a
312 concrete backend Class.
313 """
339 314
340 def __init__(self, db, classname, **properties): 315 def __init__(self, db, classname, **properties):
341 """Create a new class with a given name and property specification. 316 """Create a new class with a given name and property specification.
342 317
343 'classname' must not collide with the name of an existing class, 318 'classname' must not collide with the name of an existing class,
344 or a ValueError is raised. The keyword arguments in 'properties' 319 or a ValueError is raised. The keyword arguments in 'properties'
345 must map names to property objects, or a TypeError is raised. 320 must map names to property objects, or a TypeError is raised.
346 """ 321 """
347 self.classname = classname 322 raise NotImplementedError
348 self.properties = properties
349 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
350 self.key = ''
351
352 # do the db-related init stuff
353 db.addclass(self)
354 323
355 def __repr__(self): 324 def __repr__(self):
356 '''Slightly more useful representation 325 '''Slightly more useful representation
357 ''' 326 '''
358 return '<hypderdb.Class "%s">'%self.classname 327 return '<hypderdb.Class "%s">'%self.classname
374 'propvalues' dictionary are set to None. 343 'propvalues' dictionary are set to None.
375 344
376 If an id in a link or multilink property does not refer to a valid 345 If an id in a link or multilink property does not refer to a valid
377 node, an IndexError is raised. 346 node, an IndexError is raised.
378 """ 347 """
379 if propvalues.has_key('id'): 348 raise NotImplementedError
380 raise KeyError, '"id" is reserved' 349
381 350 _marker = []
382 if self.db.journaltag is None:
383 raise DatabaseError, 'Database open read-only'
384
385 # new node's id
386 newid = self.db.newid(self.classname)
387
388 # validate propvalues
389 num_re = re.compile('^\d+$')
390 for key, value in propvalues.items():
391 if key == self.key:
392 try:
393 self.lookup(value)
394 except KeyError:
395 pass
396 else:
397 raise ValueError, 'node with key "%s" exists'%value
398
399 # try to handle this property
400 try:
401 prop = self.properties[key]
402 except KeyError:
403 raise KeyError, '"%s" has no property "%s"'%(self.classname,
404 key)
405
406 if isinstance(prop, Link):
407 if type(value) != type(''):
408 raise ValueError, 'link value must be String'
409 link_class = self.properties[key].classname
410 # if it isn't a number, it's a key
411 if not num_re.match(value):
412 try:
413 value = self.db.classes[link_class].lookup(value)
414 except (TypeError, KeyError):
415 raise IndexError, 'new property "%s": %s not a %s'%(
416 key, value, link_class)
417 elif not self.db.hasnode(link_class, value):
418 raise IndexError, '%s has no node %s'%(link_class, value)
419
420 # save off the value
421 propvalues[key] = value
422
423 # register the link with the newly linked node
424 if self.properties[key].do_journal:
425 self.db.addjournal(link_class, value, 'link',
426 (self.classname, newid, key))
427
428 elif isinstance(prop, Multilink):
429 if type(value) != type([]):
430 raise TypeError, 'new property "%s" not a list of ids'%key
431
432 # clean up and validate the list of links
433 link_class = self.properties[key].classname
434 l = []
435 for entry in value:
436 if type(entry) != type(''):
437 raise ValueError, '"%s" link value (%s) must be String' % (key, value)
438 # if it isn't a number, it's a key
439 if not num_re.match(entry):
440 try:
441 entry = self.db.classes[link_class].lookup(entry)
442 except (TypeError, KeyError):
443 raise IndexError, 'new property "%s": %s not a %s'%(
444 key, entry, self.properties[key].classname)
445 l.append(entry)
446 value = l
447 propvalues[key] = value
448
449 # handle additions
450 for id in value:
451 if not self.db.hasnode(link_class, id):
452 raise IndexError, '%s has no node %s'%(link_class, id)
453 # register the link with the newly linked node
454 if self.properties[key].do_journal:
455 self.db.addjournal(link_class, id, 'link',
456 (self.classname, newid, key))
457
458 elif isinstance(prop, String):
459 if type(value) != type(''):
460 raise TypeError, 'new property "%s" not a string'%key
461
462 elif isinstance(prop, Password):
463 if not isinstance(value, password.Password):
464 raise TypeError, 'new property "%s" not a Password'%key
465
466 elif isinstance(prop, Date):
467 if value is not None and not isinstance(value, date.Date):
468 raise TypeError, 'new property "%s" not a Date'%key
469
470 elif isinstance(prop, Interval):
471 if value is not None and not isinstance(value, date.Interval):
472 raise TypeError, 'new property "%s" not an Interval'%key
473
474 # make sure there's data where there needs to be
475 for key, prop in self.properties.items():
476 if propvalues.has_key(key):
477 continue
478 if key == self.key:
479 raise ValueError, 'key property "%s" is required'%key
480 if isinstance(prop, Multilink):
481 propvalues[key] = []
482 else:
483 # TODO: None isn't right here, I think...
484 propvalues[key] = None
485
486 # done
487 self.db.addnode(self.classname, newid, propvalues)
488 self.db.addjournal(self.classname, newid, 'create', propvalues)
489 return newid
490
491 def get(self, nodeid, propname, default=_marker, cache=1): 351 def get(self, nodeid, propname, default=_marker, cache=1):
492 """Get the value of a property on an existing node of this class. 352 """Get the value of a property on an existing node of this class.
493 353
494 'nodeid' must be the id of an existing node of this class or an 354 'nodeid' must be the id of an existing node of this class or an
495 IndexError is raised. 'propname' must be the name of a property 355 IndexError is raised. 'propname' must be the name of a property
498 'cache' indicates whether the transaction cache should be queried 358 'cache' indicates whether the transaction cache should be queried
499 for the node. If the node has been modified and you need to 359 for the node. If the node has been modified and you need to
500 determine what its values prior to modification are, you need to 360 determine what its values prior to modification are, you need to
501 set cache=0. 361 set cache=0.
502 """ 362 """
503 if propname == 'id': 363 raise NotImplementedError
504 return nodeid
505
506 # get the property (raises KeyErorr if invalid)
507 prop = self.properties[propname]
508
509 # get the node's dict
510 d = self.db.getnode(self.classname, nodeid, cache=cache)
511
512 if not d.has_key(propname):
513 if default is _marker:
514 if isinstance(prop, Multilink):
515 return []
516 else:
517 # TODO: None isn't right here, I think...
518 return None
519 else:
520 return default
521
522 return d[propname]
523 364
524 # XXX not in spec 365 # XXX not in spec
525 def getnode(self, nodeid, cache=1): 366 def getnode(self, nodeid, cache=1):
526 ''' Return a convenience wrapper for the node. 367 ''' Return a convenience wrapper for the node.
527 368
551 other key strings or a ValueError is raised. 392 other key strings or a ValueError is raised.
552 393
553 If the value of a Link or Multilink property contains an invalid 394 If the value of a Link or Multilink property contains an invalid
554 node id, a ValueError is raised. 395 node id, a ValueError is raised.
555 """ 396 """
556 if not propvalues: 397 raise NotImplementedError
557 return
558
559 if propvalues.has_key('id'):
560 raise KeyError, '"id" is reserved'
561
562 if self.db.journaltag is None:
563 raise DatabaseError, 'Database open read-only'
564
565 node = self.db.getnode(self.classname, nodeid)
566 if node.has_key(self.db.RETIRED_FLAG):
567 raise IndexError
568 num_re = re.compile('^\d+$')
569 for key, value in propvalues.items():
570 # check to make sure we're not duplicating an existing key
571 if key == self.key and node[key] != value:
572 try:
573 self.lookup(value)
574 except KeyError:
575 pass
576 else:
577 raise ValueError, 'node with key "%s" exists'%value
578
579 # this will raise the KeyError if the property isn't valid
580 # ... we don't use getprops() here because we only care about
581 # the writeable properties.
582 prop = self.properties[key]
583
584 # if the value's the same as the existing value, no sense in
585 # doing anything
586 if node.has_key(key) and value == node[key]:
587 del propvalues[key]
588 continue
589
590 # do stuff based on the prop type
591 if isinstance(prop, Link):
592 link_class = self.properties[key].classname
593 # if it isn't a number, it's a key
594 if type(value) != type(''):
595 raise ValueError, 'link value must be String'
596 if not num_re.match(value):
597 try:
598 value = self.db.classes[link_class].lookup(value)
599 except (TypeError, KeyError):
600 raise IndexError, 'new property "%s": %s not a %s'%(
601 key, value, self.properties[key].classname)
602
603 if not self.db.hasnode(link_class, value):
604 raise IndexError, '%s has no node %s'%(link_class, value)
605
606 if self.properties[key].do_journal:
607 # register the unlink with the old linked node
608 if node[key] is not None:
609 self.db.addjournal(link_class, node[key], 'unlink',
610 (self.classname, nodeid, key))
611
612 # register the link with the newly linked node
613 if value is not None:
614 self.db.addjournal(link_class, value, 'link',
615 (self.classname, nodeid, key))
616
617 elif isinstance(prop, Multilink):
618 if type(value) != type([]):
619 raise TypeError, 'new property "%s" not a list of ids'%key
620 link_class = self.properties[key].classname
621 l = []
622 for entry in value:
623 # if it isn't a number, it's a key
624 if type(entry) != type(''):
625 raise ValueError, 'new property "%s" link value ' \
626 'must be a string'%key
627 if not num_re.match(entry):
628 try:
629 entry = self.db.classes[link_class].lookup(entry)
630 except (TypeError, KeyError):
631 raise IndexError, 'new property "%s": %s not a %s'%(
632 key, entry, self.properties[key].classname)
633 l.append(entry)
634 value = l
635 propvalues[key] = value
636
637 # handle removals
638 if node.has_key(key):
639 l = node[key]
640 else:
641 l = []
642 for id in l[:]:
643 if id in value:
644 continue
645 # register the unlink with the old linked node
646 if self.properties[key].do_journal:
647 self.db.addjournal(link_class, id, 'unlink',
648 (self.classname, nodeid, key))
649 l.remove(id)
650
651 # handle additions
652 for id in value:
653 if not self.db.hasnode(link_class, id):
654 raise IndexError, '%s has no node %s'%(
655 link_class, id)
656 if id in l:
657 continue
658 # register the link with the newly linked node
659 if self.properties[key].do_journal:
660 self.db.addjournal(link_class, id, 'link',
661 (self.classname, nodeid, key))
662 l.append(id)
663
664 elif isinstance(prop, String):
665 if value is not None and type(value) != type(''):
666 raise TypeError, 'new property "%s" not a string'%key
667
668 elif isinstance(prop, Password):
669 if not isinstance(value, password.Password):
670 raise TypeError, 'new property "%s" not a Password'% key
671 propvalues[key] = value
672
673 elif value is not None and isinstance(prop, Date):
674 if not isinstance(value, date.Date):
675 raise TypeError, 'new property "%s" not a Date'% key
676 propvalues[key] = value
677
678 elif value is not None and isinstance(prop, Interval):
679 if not isinstance(value, date.Interval):
680 raise TypeError, 'new property "%s" not an Interval'% key
681 propvalues[key] = value
682
683 node[key] = value
684
685 # nothing to do?
686 if not propvalues:
687 return
688
689 # do the set, and journal it
690 self.db.setnode(self.classname, nodeid, node)
691 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
692 398
693 def retire(self, nodeid): 399 def retire(self, nodeid):
694 """Retire a node. 400 """Retire a node.
695 401
696 The properties on the node remain available from the get() method, 402 The properties on the node remain available from the get() method,
697 and the node's id is never reused. 403 and the node's id is never reused.
698 404
699 Retired nodes are not returned by the find(), list(), or lookup() 405 Retired nodes are not returned by the find(), list(), or lookup()
700 methods, and other nodes may reuse the values of their key properties. 406 methods, and other nodes may reuse the values of their key properties.
701 """ 407 """
702 if self.db.journaltag is None: 408 raise NotImplementedError
703 raise DatabaseError, 'Database open read-only'
704 node = self.db.getnode(self.classname, nodeid)
705 node[self.db.RETIRED_FLAG] = 1
706 self.db.setnode(self.classname, nodeid, node)
707 self.db.addjournal(self.classname, nodeid, 'retired', None)
708 409
709 def history(self, nodeid): 410 def history(self, nodeid):
710 """Retrieve the journal of edits on a particular node. 411 """Retrieve the journal of edits on a particular node.
711 412
712 'nodeid' must be the id of an existing node of this class or an 413 'nodeid' must be the id of an existing node of this class or an
717 (date, tag, action, params) 418 (date, tag, action, params)
718 419
719 'date' is a Timestamp object specifying the time of the change and 420 'date' is a Timestamp object specifying the time of the change and
720 'tag' is the journaltag specified when the database was opened. 421 'tag' is the journaltag specified when the database was opened.
721 """ 422 """
722 return self.db.getjournal(self.classname, nodeid) 423 raise NotImplementedError
723 424
724 # Locating nodes: 425 # Locating nodes:
725 def hasnode(self, nodeid): 426 def hasnode(self, nodeid):
726 '''Determine if the given nodeid actually exists 427 '''Determine if the given nodeid actually exists
727 ''' 428 '''
728 return self.db.hasnode(self.classname, nodeid) 429 raise NotImplementedError
729 430
730 def setkey(self, propname): 431 def setkey(self, propname):
731 """Select a String property of this class to be the key property. 432 """Select a String property of this class to be the key property.
732 433
733 'propname' must be the name of a String property of this class or 434 'propname' must be the name of a String property of this class or
734 None, or a TypeError is raised. The values of the key property on 435 None, or a TypeError is raised. The values of the key property on
735 all existing nodes must be unique or a ValueError is raised. 436 all existing nodes must be unique or a ValueError is raised.
736 """ 437 """
737 # TODO: validate that the property is a String! 438 raise NotImplementedError
738 self.key = propname
739 439
740 def getkey(self): 440 def getkey(self):
741 """Return the name of the key property for this class or None.""" 441 """Return the name of the key property for this class or None."""
742 return self.key 442 raise NotImplementedError
743 443
744 def labelprop(self, default_to_id=0): 444 def labelprop(self, default_to_id=0):
745 ''' Return the property name for a label for the given node. 445 ''' Return the property name for a label for the given node.
746 446
747 This method attempts to generate a consistent label for the node. 447 This method attempts to generate a consistent label for the node.
749 1. key property 449 1. key property
750 2. "name" property 450 2. "name" property
751 3. "title" property 451 3. "title" property
752 4. first property from the sorted property name list 452 4. first property from the sorted property name list
753 ''' 453 '''
754 k = self.getkey() 454 raise NotImplementedError
755 if k: 455
756 return k
757 props = self.getprops()
758 if props.has_key('name'):
759 return 'name'
760 elif props.has_key('title'):
761 return 'title'
762 if default_to_id:
763 return 'id'
764 props = props.keys()
765 props.sort()
766 return props[0]
767
768 # TODO: set up a separate index db file for this? profile?
769 def lookup(self, keyvalue): 456 def lookup(self, keyvalue):
770 """Locate a particular node by its key property and return its id. 457 """Locate a particular node by its key property and return its id.
771 458
772 If this class has no key property, a TypeError is raised. If the 459 If this class has no key property, a TypeError is raised. If the
773 'keyvalue' matches one of the values for the key property among 460 'keyvalue' matches one of the values for the key property among
774 the nodes in this class, the matching node's id is returned; 461 the nodes in this class, the matching node's id is returned;
775 otherwise a KeyError is raised. 462 otherwise a KeyError is raised.
776 """ 463 """
777 cldb = self.db.getclassdb(self.classname) 464 raise NotImplementedError
778 try:
779 for nodeid in self.db.getnodeids(self.classname, cldb):
780 node = self.db.getnode(self.classname, nodeid, cldb)
781 if node.has_key(self.db.RETIRED_FLAG):
782 continue
783 if node[self.key] == keyvalue:
784 cldb.close()
785 return nodeid
786 finally:
787 cldb.close()
788 raise KeyError, keyvalue
789 465
790 # XXX: change from spec - allows multiple props to match 466 # XXX: change from spec - allows multiple props to match
791 def find(self, **propspec): 467 def find(self, **propspec):
792 """Get the ids of nodes in this class which link to the given nodes. 468 """Get the ids of nodes in this class which link to the given nodes.
793 469
796 KeyError is raised. That property must be a Link or Multilink 472 KeyError is raised. That property must be a Link or Multilink
797 property, or a TypeError is raised. 473 property, or a TypeError is raised.
798 474
799 Any node in this class whose 'propname' property links to any of the 475 Any node in this class whose 'propname' property links to any of the
800 nodeids will be returned. Used by the full text indexing, which knows 476 nodeids will be returned. Used by the full text indexing, which knows
801 that "foo" occurs in msg1, msg3 and file7, so we have hits on these issues: 477 that "foo" occurs in msg1, msg3 and file7, so we have hits on these
478 issues:
479
802 db.issue.find(messages={'1':1,'3':1}, files={'7':1}) 480 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
803 """ 481 """
804 propspec = propspec.items() 482 raise NotImplementedError
805 for propname, nodeids in propspec:
806 # check the prop is OK
807 prop = self.properties[propname]
808 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
809 raise TypeError, "'%s' not a Link/Multilink property"%propname
810 #XXX edit is expensive and of questionable use
811 #for nodeid in nodeids:
812 # if not self.db.hasnode(prop.classname, nodeid):
813 # raise ValueError, '%s has no node %s'%(prop.classname, nodeid)
814
815 # ok, now do the find
816 cldb = self.db.getclassdb(self.classname)
817 l = []
818 try:
819 for id in self.db.getnodeids(self.classname, db=cldb):
820 node = self.db.getnode(self.classname, id, db=cldb)
821 if node.has_key(self.db.RETIRED_FLAG):
822 continue
823 for propname, nodeids in propspec:
824 # can't test if the node doesn't have this property
825 if not node.has_key(propname):
826 continue
827 if type(nodeids) is type(''):
828 nodeids = {nodeids:1}
829 prop = self.properties[propname]
830 value = node[propname]
831 if isinstance(prop, Link) and nodeids.has_key(value):
832 l.append(id)
833 break
834 elif isinstance(prop, Multilink):
835 hit = 0
836 for v in value:
837 if nodeids.has_key(v):
838 l.append(id)
839 hit = 1
840 break
841 if hit:
842 break
843 finally:
844 cldb.close()
845 return l
846
847 def stringFind(self, **requirements):
848 """Locate a particular node by matching a set of its String
849 properties in a caseless search.
850
851 If the property is not a String property, a TypeError is raised.
852
853 The return is a list of the id of all nodes that match.
854 """
855 for propname in requirements.keys():
856 prop = self.properties[propname]
857 if isinstance(not prop, String):
858 raise TypeError, "'%s' not a String property"%propname
859 requirements[propname] = requirements[propname].lower()
860 l = []
861 cldb = self.db.getclassdb(self.classname)
862 try:
863 for nodeid in self.db.getnodeids(self.classname, cldb):
864 node = self.db.getnode(self.classname, nodeid, cldb)
865 if node.has_key(self.db.RETIRED_FLAG):
866 continue
867 for key, value in requirements.items():
868 if node[key] and node[key].lower() != value:
869 break
870 else:
871 l.append(nodeid)
872 finally:
873 cldb.close()
874 return l
875
876 def list(self):
877 """Return a list of the ids of the active nodes in this class."""
878 l = []
879 cn = self.classname
880 cldb = self.db.getclassdb(cn)
881 try:
882 for nodeid in self.db.getnodeids(cn, cldb):
883 node = self.db.getnode(cn, nodeid, cldb)
884 if node.has_key(self.db.RETIRED_FLAG):
885 continue
886 l.append(nodeid)
887 finally:
888 cldb.close()
889 l.sort()
890 return l
891 483
892 # XXX not in spec 484 # XXX not in spec
893 def filter(self, search_matches, filterspec, sort, group, 485 def filter(self, search_matches, filterspec, sort, group,
894 num_re = re.compile('^\d+$')): 486 num_re = re.compile('^\d+$')):
895 ''' Return a list of the ids of the active nodes in this class that 487 ''' Return a list of the ids of the active nodes in this class that
896 match the 'filter' spec, sorted by the group spec and then the 488 match the 'filter' spec, sorted by the group spec and then the
897 sort spec 489 sort spec
898 ''' 490 '''
899 cn = self.classname 491 raise NotImplementedError
900
901 # optimise filterspec
902 l = []
903 props = self.getprops()
904 for k, v in filterspec.items():
905 propclass = props[k]
906 if isinstance(propclass, Link):
907 if type(v) is not type([]):
908 v = [v]
909 # replace key values with node ids
910 u = []
911 link_class = self.db.classes[propclass.classname]
912 for entry in v:
913 if entry == '-1': entry = None
914 elif not num_re.match(entry):
915 try:
916 entry = link_class.lookup(entry)
917 except (TypeError,KeyError):
918 raise ValueError, 'property "%s": %s not a %s'%(
919 k, entry, self.properties[k].classname)
920 u.append(entry)
921
922 l.append((0, k, u))
923 elif isinstance(propclass, Multilink):
924 if type(v) is not type([]):
925 v = [v]
926 # replace key values with node ids
927 u = []
928 link_class = self.db.classes[propclass.classname]
929 for entry in v:
930 if not num_re.match(entry):
931 try:
932 entry = link_class.lookup(entry)
933 except (TypeError,KeyError):
934 raise ValueError, 'new property "%s": %s not a %s'%(
935 k, entry, self.properties[k].classname)
936 u.append(entry)
937 l.append((1, k, u))
938 elif isinstance(propclass, String):
939 # simple glob searching
940 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
941 v = v.replace('?', '.')
942 v = v.replace('*', '.*?')
943 l.append((2, k, re.compile(v, re.I)))
944 else:
945 l.append((6, k, v))
946 filterspec = l
947
948 # now, find all the nodes that are active and pass filtering
949 l = []
950 cldb = self.db.getclassdb(cn)
951 try:
952 for nodeid in self.db.getnodeids(cn, cldb):
953 node = self.db.getnode(cn, nodeid, cldb)
954 if node.has_key(self.db.RETIRED_FLAG):
955 continue
956 # apply filter
957 for t, k, v in filterspec:
958 # this node doesn't have this property, so reject it
959 if not node.has_key(k): break
960
961 if t == 0 and node[k] not in v:
962 # link - if this node'd property doesn't appear in the
963 # filterspec's nodeid list, skip it
964 break
965 elif t == 1:
966 # multilink - if any of the nodeids required by the
967 # filterspec aren't in this node's property, then skip
968 # it
969 for value in v:
970 if value not in node[k]:
971 break
972 else:
973 continue
974 break
975 elif t == 2 and (node[k] is None or not v.search(node[k])):
976 # RE search
977 break
978 elif t == 6 and node[k] != v:
979 # straight value comparison for the other types
980 break
981 else:
982 l.append((nodeid, node))
983 finally:
984 cldb.close()
985 l.sort()
986
987 # filter based on full text search
988 if search_matches is not None:
989 k = []
990 l_debug = []
991 for v in l:
992 l_debug.append(v[0])
993 if search_matches.has_key(v[0]):
994 k.append(v)
995 l = k
996
997 # optimise sort
998 m = []
999 for entry in sort:
1000 if entry[0] != '-':
1001 m.append(('+', entry))
1002 else:
1003 m.append((entry[0], entry[1:]))
1004 sort = m
1005
1006 # optimise group
1007 m = []
1008 for entry in group:
1009 if entry[0] != '-':
1010 m.append(('+', entry))
1011 else:
1012 m.append((entry[0], entry[1:]))
1013 group = m
1014 # now, sort the result
1015 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
1016 db = self.db, cl=self):
1017 a_id, an = a
1018 b_id, bn = b
1019 # sort by group and then sort
1020 for list in group, sort:
1021 for dir, prop in list:
1022 # sorting is class-specific
1023 propclass = properties[prop]
1024
1025 # handle the properties that might be "faked"
1026 # also, handle possible missing properties
1027 try:
1028 if not an.has_key(prop):
1029 an[prop] = cl.get(a_id, prop)
1030 av = an[prop]
1031 except KeyError:
1032 # the node doesn't have a value for this property
1033 if isinstance(propclass, Multilink): av = []
1034 else: av = ''
1035 try:
1036 if not bn.has_key(prop):
1037 bn[prop] = cl.get(b_id, prop)
1038 bv = bn[prop]
1039 except KeyError:
1040 # the node doesn't have a value for this property
1041 if isinstance(propclass, Multilink): bv = []
1042 else: bv = ''
1043
1044 # String and Date values are sorted in the natural way
1045 if isinstance(propclass, String):
1046 # clean up the strings
1047 if av and av[0] in string.uppercase:
1048 av = an[prop] = av.lower()
1049 if bv and bv[0] in string.uppercase:
1050 bv = bn[prop] = bv.lower()
1051 if (isinstance(propclass, String) or
1052 isinstance(propclass, Date)):
1053 # it might be a string that's really an integer
1054 try:
1055 av = int(av)
1056 bv = int(bv)
1057 except:
1058 pass
1059 if dir == '+':
1060 r = cmp(av, bv)
1061 if r != 0: return r
1062 elif dir == '-':
1063 r = cmp(bv, av)
1064 if r != 0: return r
1065
1066 # Link properties are sorted according to the value of
1067 # the "order" property on the linked nodes if it is
1068 # present; or otherwise on the key string of the linked
1069 # nodes; or finally on the node ids.
1070 elif isinstance(propclass, Link):
1071 link = db.classes[propclass.classname]
1072 if av is None and bv is not None: return -1
1073 if av is not None and bv is None: return 1
1074 if av is None and bv is None: continue
1075 if link.getprops().has_key('order'):
1076 if dir == '+':
1077 r = cmp(link.get(av, 'order'),
1078 link.get(bv, 'order'))
1079 if r != 0: return r
1080 elif dir == '-':
1081 r = cmp(link.get(bv, 'order'),
1082 link.get(av, 'order'))
1083 if r != 0: return r
1084 elif link.getkey():
1085 key = link.getkey()
1086 if dir == '+':
1087 r = cmp(link.get(av, key), link.get(bv, key))
1088 if r != 0: return r
1089 elif dir == '-':
1090 r = cmp(link.get(bv, key), link.get(av, key))
1091 if r != 0: return r
1092 else:
1093 if dir == '+':
1094 r = cmp(av, bv)
1095 if r != 0: return r
1096 elif dir == '-':
1097 r = cmp(bv, av)
1098 if r != 0: return r
1099
1100 # Multilink properties are sorted according to how many
1101 # links are present.
1102 elif isinstance(propclass, Multilink):
1103 if dir == '+':
1104 r = cmp(len(av), len(bv))
1105 if r != 0: return r
1106 elif dir == '-':
1107 r = cmp(len(bv), len(av))
1108 if r != 0: return r
1109 # end for dir, prop in list:
1110 # end for list in sort, group:
1111 # if all else fails, compare the ids
1112 return cmp(a[0], b[0])
1113
1114 l.sort(sortfun)
1115 return [i[0] for i in l]
1116 492
1117 def count(self): 493 def count(self):
1118 """Get the number of nodes in this class. 494 """Get the number of nodes in this class.
1119 495
1120 If the returned integer is 'numnodes', the ids of all the nodes 496 If the returned integer is 'numnodes', the ids of all the nodes
1121 in this class run from 1 to numnodes, and numnodes+1 will be the 497 in this class run from 1 to numnodes, and numnodes+1 will be the
1122 id of the next node to be created in this class. 498 id of the next node to be created in this class.
1123 """ 499 """
1124 return self.db.countnodes(self.classname) 500 raise NotImplementedError
1125 501
1126 # Manipulating properties: 502 # Manipulating properties:
1127
1128 def getprops(self, protected=1): 503 def getprops(self, protected=1):
1129 """Return a dictionary mapping property names to property objects. 504 """Return a dictionary mapping property names to property objects.
1130 If the "protected" flag is true, we include protected properties - 505 If the "protected" flag is true, we include protected properties -
1131 those which may not be modified.""" 506 those which may not be modified.
1132 d = self.properties.copy() 507 """
1133 if protected: 508 raise NotImplementedError
1134 d['id'] = String()
1135 return d
1136 509
1137 def addprop(self, **properties): 510 def addprop(self, **properties):
1138 """Add properties to this class. 511 """Add properties to this class.
1139 512
1140 The keyword arguments in 'properties' must map names to property 513 The keyword arguments in 'properties' must map names to property
1141 objects, or a TypeError is raised. None of the keys in 'properties' 514 objects, or a TypeError is raised. None of the keys in 'properties'
1142 may collide with the names of existing properties, or a ValueError 515 may collide with the names of existing properties, or a ValueError
1143 is raised before any properties have been added. 516 is raised before any properties have been added.
1144 """ 517 """
1145 for key in properties.keys(): 518 raise NotImplementedError
1146 if self.properties.has_key(key):
1147 raise ValueError, key
1148 self.properties.update(properties)
1149 519
1150 def index(self, nodeid): 520 def index(self, nodeid):
1151 '''Add (or refresh) the node to search indexes 521 '''Add (or refresh) the node to search indexes
1152 ''' 522 '''
1153 # find all the String properties that have indexme 523 raise NotImplementedError
1154 for prop, propclass in self.getprops().items():
1155 if isinstance(propclass, String) and propclass.indexme:
1156 # and index them under (classname, nodeid, property)
1157 self.db.indexer.add_text((self.classname, nodeid, prop),
1158 str(self.get(nodeid, prop)))
1159 524
1160 # XXX not in spec 525 # XXX not in spec
1161 class Node: 526 class Node:
1162 ''' A convenience wrapper for the given node 527 ''' A convenience wrapper for the given node
1163 ''' 528 '''
1213 cl.create(name=options[i], order=i) 578 cl.create(name=options[i], order=i)
1214 return hyperdb.Link(name) 579 return hyperdb.Link(name)
1215 580
1216 # 581 #
1217 # $Log: not supported by cvs2svn $ 582 # $Log: not supported by cvs2svn $
583 # Revision 1.74 2002/07/10 00:24:10 richard
584 # braino
585 #
1218 # Revision 1.73 2002/07/10 00:19:48 richard 586 # Revision 1.73 2002/07/10 00:19:48 richard
1219 # Added explicit closing of backend database handles. 587 # Added explicit closing of backend database handles.
1220 # 588 #
1221 # Revision 1.72 2002/07/09 21:53:38 gmcm 589 # Revision 1.72 2002/07/09 21:53:38 gmcm
1222 # Optimize Class.find so that the propspec can contain a set of ids to match. 590 # Optimize Class.find so that the propspec can contain a set of ids to match.

Roundup Issue Tracker: http://roundup-tracker.org/