Roundup Wiki

Introduction

This wrapper can be used to have easy access to roundup's hyper database without having to know too much knowledge about the class structures and (multi) links.

This document contains a few examples. In those cases you will see objects like 'db', 'dbw', 'cl', 'clw' and 'nodew'. These object are::

Features

Addressing a specific node in a class is easier with this wrapper. It almost has the looks and feels of the wrapper used in the TAL templates. Also with this wrapper you don't really need to use 'get' methods to get the data. Just address the data as if they are member of a tree with the database object as root. The class objects as limbs and the class properties as leaves. What's left is how to address the right node. Well that can be done with indexing the class (sorry but I don't know were to put that in a tree). As index you can either use the node id as integer or string, but you may also use a key value (if the class has a key column). Here are some examples::

In general the benefit of this wrapper is that you don't really have to know any of the node ids. All 'set' or indexing methods will resolve key values into id's. Even so will all 'get' methods return the key values and not the id's when it comes to linked items.

_Searching_<br> The wrapper is capable of handling regular expression searches. It will not only return you the found nodes, but it also will return the found matches per node.

Usage

we placed it in a sub-folder of roundup's core code which remains in the 'site-packages' folder of the Python library tree. In this /lib/site-packages/roundup we created a folder named 'extensions'. This is about conform the standard that Richard uses for additional stuff in tracker templates (we look at the wrapper as additional stuff for the roundup core code). In other words the complete path to the wrapper will be:<br>     '/lib/site-packages/roundup/extensions/dbwrapper.py'

It will be easiest if the wrapper can be imported as sub package. This will require a 'init.py' file in that directory to. The content of that file will only be this line:<br>     <code>all = [ 'dbwrapper' ]</code>

What's left is importing the wrapper in your Python sources (e.g. detectors and action handlers). You do that by adding the line:<br>     'from roundup.extensions import dbwrapper'<br> to all sources in which you want to use the wrapper.

There's only one comment left. As you can see, the wrapper has a lot of documentation strings inside. Therefor we suggest to run all scripts with the '-OO' option passed to Python's interpreter. This will prevent that your scripts will load these documentation strings too. If you're not sure, then please read section **6.1.2 Compiled Python files** of the tutorial in the Python documentation.

_Detectors_<br> The wrapper can easily be used in a detector without having to change anything to roundup's core code. This is what you do in an auditor::

And this in a reactor::

As you can see, they are both the same. In both cases 'dbw' will be the root object of the tree. 'clw' will be a wrapper around hyperdb object 'cl'. In most cases you don't really need that one, but it is needed to create the node wrapper 'nodew'.

In case of an auditor, 'nodeid' can be 'None' if the auditor was fired by a 'create' operation. In that case 'nodew' will be a phantom node which doesn't have any properties (except 'id' which will be 'None'). A test like 'nodew is None' will return True on phantom nodes. It will return False if the node isn't a phantom node but a real living one (auditor fired by 'set' operation).

_Action handlers_<br> The wrapper is almost used the same way in an action handler as it is used in a detector. This is what you do::

Below you find some more examples and of course the source code for the wrapper.

Best regards,<br> Marlon van den Berg

PS: The wrapper has been tested with 0.7.12 and 1.1.0.

<hr>

Examples

1. Reading a linked property::

2. Changing a linked property::

3. Adding a link to a multi linked property::

4. Removing a link from a multi linked property::

5. Duplicating an issue with dbwrapper::

6. Retiring a node::

7. Filtering::

8. Regular expression search::

9. Combination RE search::

10. More search possibilities::

11. Iteration::

<hr>

The Source

And finally the source code

   1     """\
   2     Introduction
   3     ============
   4     Database wrapper implementation.
   5     This wrapper can be used to have easy access to RoundUp's
   6     hyper database without having to know too much knowledge
   7     about the class structures and (multi) links.
   8 
   9     This document contains a few examples. In those cases
  10     you will see objects like 'db', 'dbw', 'cl', 'clw'
  11     and 'nodew'. These object are:
  12     db    : hyperdb.Database
  13     cl    : hyperdb.Class (or FileClass or IssueClass)
  14     dbw   : dbwrapper.DBWrapper
  15     clw   : dbwrapper.ClassWrapper
  16     nodew : dbwrapper.NodeClass
  17     lw    : dbwrapper.ListWrapper
  18 
  19 
  20     Features
  21     ========
  22 
  23     Addressing
  24     ----------
  25     Addressing a specific node in a class is easier with this
  26     wrapper. It almost has the looks and feels of the wrapper
  27     used in the TAL templates. Also with this wrapper you don't
  28     really need to use 'get' methods to get the data. Just address
  29     the data as if they are member of a tree with the database
  30     object as root. The class objects as limbs and the class
  31     properties as leaves. What's left is how to address the right
  32     node. Well that can be done with indexing the class (sorry
  33     but I don't know were to put that in a tree). As index you
  34     can either use the node id as integer or string, but you
  35     may also use a key value (if the class has a key column).
  36     Here are some examples:
  37     - dbw.issue[1].title
  38       dbw.issue['1'].title
  39         Both these examples will return the title of issue 1.
  40     - dbw.priority['urgent'].name
  41         This example will return 'urgent'. Not really
  42         meaningful, but it's only meant as an example.
  43     - dbw.status['unread'].id
  44         Will return the nodeid of status node 'unread'.
  45 
  46     In general the benefit of this wrapper is that you don't
  47     really have to know any of the node ids. All 'set' or indexing
  48     methods will resolve key values into id's. Even so will all
  49     'get' methods return the key values and not the id's when it
  50     comes to linked items.
  51 
  52     Searching
  53     ---------
  54     The wrapper is capable of handling regular expression searches.
  55     It will not only return you the found nodes, but it also will
  56     return the found matches per node.
  57 
  58 
  59     Usage
  60     =====
  61     To use the wrapper you need to place it somewhere. In our case
  62     we placed it in a sub-folder of RoundUp's core code which remains
  63     in the 'site-packages' folder of the Python library tree.
  64     In this /lib/site-packages/roundup we created a folder named
  65     'extensions'. This is about conform the standard that Richard uses
  66     for additional stuff in tracker templates (we look at the wrapper
  67     as additional stuff for the RoundUp core code). In other words
  68     the complete path to the wrapper will be:
  69         /lib/site-packages/roundup/extensions/dbwrapper.py
  70     It will be easiest if the wrapper can be imported as sub package.
  71     This will require a '__init__.py' file in that directory to. The
  72     content of that file will only be this line:
  73         __all__ = [ 'dbwrapper' ]
  74 
  75     What's left is importing the wrapper in your Python sources (e.g.
  76     detectors and action handlers). You do that by adding the line:
  77         from roundup.extensions import dbwrapper
  78     to all sources in which you want to use the wrapper.
  79 
  80     There's only one comment left. As you can see, the wrapper has a
  81     lot of documentation strings inside. Therefor we suggest to run
  82     all scripts with the '-OO' option passed to Python's interpreter.
  83     This will prevent that your scripts will load these documentation
  84     strings too. If you're not sure, then please read section
  85     '6.1.2 "Compiled" Python files' of the tutorial in the Python
  86     documentation.
  87 
  88     Detectors
  89     ---------
  90     The wrapper can easily be used in a detector without having to
  91     change anything to RoundUp's core code.
  92     This is what you do in an auditor:
  93         def auditor(db, cl, nodeid, newvalues):
  94             dbw   = dbwrapper.DBWrapper(db)
  95             clw   = dbwrapper.ClassWrapper(dbw, cl)
  96             nodew = dbwrapper.NodeWrapper(clw, nodeid)
  97 
  98     And this in a reactor:
  99         def reactor(db, cl, nodeid, oldvalues):
 100             dbw   = dbwrapper.DBWrapper(db)
 101             clw   = dbwrapper.ClassWrapper(dbw, cl)
 102             nodew = dbwrapper.NodeWrapper(clw, nodeid)
 103 
 104     As you can see, they are both the same.
 105     In both cases 'dbw' will be the root object of the tree.
 106     'clw' will be a wrapper around hyperdb object 'cl'. In most cases
 107     you don't really need that one, but it is needed to create the
 108     node wrapper 'nodew'.
 109 
 110     In case of an auditor, 'nodeid' can be 'None' if the auditor was
 111     fired by a 'create' operation. In that case 'nodew' will be a
 112     phantom node which doesn't have any properties (except 'id' which
 113     will be 'None'). A test like:
 114         nodew is None
 115     will return True on phantom nodes. It will return False if the node
 116     isn't a phantom node but a real living one (auditor fired by 'set'
 117     operation).
 118 
 119     Action handlers
 120     ---------------
 121     The wrapper is almost used the same way in an action handler as it
 122     is used in a detector.
 123     This is what you do:
 124         def handler(self):
 125             dbw   = dbwrapper.DBWrapper(self.db)
 126             clw   = dbwrapper.ClassWrapper(dbw, self.cl)
 127             nodew = dbwrapper.NodeWrapper(clw, self.nodeid)
 128 
 129 
 130     Examples
 131     ========
 132     Here are a few more examples what dbwrapper can do for you:
 133     1.  Reading a linked property:
 134         - With RoundUp's hyperdb:
 135             unread_id = db.status.lookup('unread')
 136             db.status.get(unread_id, 'order')
 137 
 138         - With dbwrapper:
 139             dbw.status['unread'].order
 140 
 141     2.  Changing a linked property:
 142         - With RoundUp's hyperdb:
 143             unread_id = db.status.lookup('unread')
 144             db.issue.set('1', status=unread_id)
 145 
 146         - With dbwrapper:
 147             dbw.issue[1].status = 'unread'
 148 
 149     3.  Adding a link to a multi linked property:
 150         - With RoundUp's hyperdb:
 151             topics = db.issue.get('1', 'topic')
 152             keyword_id = db.keyword.lookup('DBWrapper')
 153             topics.append(keyword_id)
 154             db.issue.set('1', topic=topics)
 155 
 156         - With dbwrapper:
 157             dbw.issue[1].topic += 'DBWrapper'
 158           or:
 159             dbw.issue[1].topic += ['DBWrapper']
 160 
 161     4.  Removing a link from a multi linked property:
 162         - With RoundUp's hyperdb:
 163             topics = db.issue.get('1', 'topic')
 164             keyword_id = db.keyword.lookup('DBWrapper')
 165             topics.remove(keyword_id)
 166             db.issue.set('1', topic=topics)
 167 
 168         - With dbwrapper:
 169             dbw.issue[1].topic -= 'DBWrapper'
 170           or:
 171             dbw.issue[1].topic -= ['DBWrapper']
 172 
 173     5.  Duplicating an issue with dbwrapper:
 174         - With dbwrapper:
 175             issue = db.issue[1].properties
 176             issue['title'] = 'Copy of %s'%issue['title']
 177             db.issue += issue
 178 
 179     6.  Retiring a node:
 180         - With RoundUp's hyperdb:
 181             nodeid = db.status.lookup('chatting')
 182             db.status.retire(nodeid)
 183 
 184         - With dbwrapper:
 185             del dbw.status['chatting']
 186 
 187     7.  Filtering:
 188         - With RoundUp's hyperdb:
 189             nodeids = [ db.status.lookup(name) for name in ['chatting', 'unread'] ]
 190             db.filter(None, {'status':nodeids})
 191 
 192         - With dbwrapper:
 193             dbw.filter(None, {'status':['chatting', 'unread']})
 194 
 195     8.  Regular expression search:
 196         - With dbwrapper:
 197             To lookup all issues of which the title starts witch 'RoundUp':
 198                 dbw.issue.re.search('^RoundUp', dbwrapper.IGNORECASE)
 199 
 200     9.  Combination RE search:
 201         - With dbwrapper:
 202             To lookup all issues of which the title starts witch 'RoundUp'
 203             and have status 'unread':
 204                 lw = dbw.issue.filter(None, {'status':'unread'})
 205                 lw.re.search('^RoundUp', dbwrapper.IGNORECASE)
 206             The next will have the same result but takes longer:
 207                 lw = dbw.issue.re.search('^RoundUp', dbwrapper.IGNORECASE)
 208                 lw.issue.filter(None, {'status':'unread'})
 209 
 210     10. More search possibilities:
 211         - With dbwrapper:
 212             Lookup all issues that have at least one message attached which
 213             starts with the word 'RoundUp':
 214                 flags = dbwrapper.IGNORECASE + dbwrapper.MULTILINE
 215                 lw = dbw.msg.re.search('\ARoundUp', flags, column='content')
 216                 dbw.issue.find(messages=lw)
 217 
 218     11. Iteration:
 219         - With dbwrapper:
 220             The next code will step trough all nodes in all classes and print
 221             the content on the screen. Please don't try this on your 10,000
 222             issue tracker. If you do, well I guess you will be spending your
 223             day next to the coffee machine.
 224                 for clw in dbw:
 225                     print clw.classname
 226                     for nodew in clw:
 227                         print "    %s"%nodew.plain()
 228                         for property in nodew:
 229                             print "    |   %s"%property
 230     """
 231 
 232     __docformat__ = 'restructuredtext'
 233 
 234     import types, os
 235 
 236     from re import IGNORECASE, DOTALL, LOCALE, MULTILINE, UNICODE, VERBOSE
 237     from re import I,          S,      L,      M,         U,       X
 238 
 239     from roundup import instance, hyperdb, date
 240 
 241 
 242     isHyperDB      = lambda object: isinstance(object, hyperdb.Database)
 243     isHyperDBClass = lambda object: isinstance(object, hyperdb.Class)
 244     isDB           = lambda object: isinstance(object, DBWrapper)
 245     isClass        = lambda object: isinstance(object, ClassWrapper)
 246     isNode         = lambda object: isinstance(object, NodeWrapper)
 247 
 248 
 249 
 250     def to_hyperdb(*nodes):
 251         """\
 252         Converts a list (or single node) of dbwrapper nodes to a list of
 253         hyperdb compliant nodes.
 254         """
 255 
 256         result = []
 257 
 258         if nodes:
 259             if len(nodes) == 1 \
            and (isinstance(nodes[0], types.ListType) \
            or isinstance(nodes[0], types.TupleType)):
 260                 nodes = nodes[0]
 261 
 262             for node in nodes:
 263                 if isinstance(node, types.TupleType) \
                and len(node) == 2:
 264                     # regular expression list
 265                     node = node[0]
 266 
 267                 if not isNode(node):
 268                     raise ValueError, \
                    "Node '%s' isn't an instance of 'NodeWrapper'."%node
 269 
 270                 result.append(str(node._id))
 271 
 272         return result
 273 
 274 
 275 
 276     class DBWrapper:
 277         """\
 278         A wrapper around RoundUp's hyperdb.Database.
 279 
 280         Initialization
 281         ==============
 282         There are three methods to initialize the DBWrapper:
 283         1.  dbw = dbwrapper.DBWrapper(<tracker home>)
 284             In this case the wrapper will open the database
 285             belonging to <tracker_home>. On destruction it
 286             will always close the database.
 287             This form is useful in separate scripts which
 288             need to access the RoundUp database.
 289 
 290         2.  dbw = dbwrapper.DBWrapper()
 291             The wrapper will open the database for you if
 292             the environment has a variable TRACKER_HOME
 293             which points to a valid tracker home folder.
 294             On destruction the database will be closed.
 295             This form is useful in separate scripts which
 296             need to access the RoundUp database.
 297 
 298         3.  dbw = dbwrapper.DBWrapper(<hyperdb.Dtabase object>)
 299             The wrapper will not open the database, but will
 300             use an already opened database. The database
 301             object is passed as parameter to the wrapper and
 302             should be an opened hyperdb.Database instance.
 303             The wrapper will never close the database.
 304             This form is useful in detectors and action
 305             handlers.
 306 
 307         Usage
 308         =====
 309         You have full access to the database as soon as the
 310         DBWrapper is initialized. To access a class, just access
 311         it as being a member of the wrapper (same as with hyperdb).
 312         Syntax:
 313             <DBWrapper>.<class name>
 314         The returned object will be an initialized ClassWrapper
 315         object.
 316 
 317         Examples:
 318             dbw.issue
 319             dbw.status
 320 
 321         Transparency
 322         ============
 323         Members defined in hyperdb.Database, but not
 324         defined in this 'DBWrapper' class can still be
 325         used as if they are member of this class.
 326         """
 327 
 328         def __init__(self, arg=None, *user):
 329             """\
 330             Initiate a wrapper around RoundUp's hyperdb.Database.
 331 
 332             "arg" is used to specify the database.
 333             It can have three different types:
 334                 1 - an instance of an open RoundUp hyperdb.Database.
 335                     This is the most common usage if the wrapper is
 336                     used within any detector or action handler.
 337                 2 - a valid tracker home path.
 338                     This allows us to use the wrapper in any external
 339                     script without having to open a tracker instance
 340                     and the database before using the wrapper.
 341                     If used like this, the destructor of this class
 342                     will close the database to prevent pending open
 343                     database links.
 344                 3 - not specified.
 345                     This is almost similar with type 2 except that
 346                     environment variable TRACKER_HOME will be used
 347                     to open the database.
 348 
 349             "user" has only meaning if 'arg' is of type 2 or 3.
 350             In those cases the database will be opened as being 'user'.
 351             Default it opens the database as 'admin'.
 352             """
 353 
 354             if isHyperDB(arg):
 355                 # arg is a roundup database instance
 356                 # no need to open the database
 357                 self._instance = None
 358                 self._db       = arg
 359 
 360             else:
 361                 if isinstance(arg, types.StringType):
 362                     # arg is a string instance
 363                     # we assume it is a valid tracker_home location
 364                     # and we will use it to open the database
 365                     tracker_home = arg
 366 
 367                 elif arg is None:
 368                     # arg is None -> no argument was passed
 369                     # user TRACKER_HOME environment variable
 370                     # to open the database
 371                     tracker_home = os.getenv('TRACKER_HOME')
 372 
 373                 else:
 374                     # and what now?
 375                     raise TypeError, \
                    "No 'hyperdb' to access"
 376 
 377                 if not user:
 378                     user = ('admin',)
 379 
 380                 self._instance = instance.open(tracker_home)
 381                 self._db       = self._instance.open(user[0])
 382 
 383             # create storage for classes
 384             self._classes = {}
 385             for classname in self._db.getclasses():
 386                 self._classes[classname] = {}
 387 
 388         def __del__(self):
 389             """\
 390             Closes the RoundUp database if it was opened by the wrapper
 391             it selves (initiated with an 'arg' of type 2 or 3)
 392             """
 393 
 394             # restore possible turned off detectors
 395             for classname in self._db.getclasses():
 396                 # restore auditors
 397                 if self._classes[classname].has_key('auditors'):
 398                     self._db.getclass(classname).auditors = \
                                        self._classes[classname]['auditors']
 399 
 400                 # restore reactors
 401                 if self._classes[classname].has_key('reactors'):
 402                     self._db.getclass(classname).reactors = \
                                        self._classes[classname]['reactors']
 403 
 404             # make sure we close the database if we did open it ourselves
 405             if not self._instance is None:
 406                 self._db.close()
 407 
 408         def __repr__(self):
 409             """\
 410             Slightly more useful representation
 411             """
 412 
 413             return '''<dbwrapper.DBWrapper '%s'>'''%self._db
 414 
 415         def __str__(self):
 416             """\
 417             Slightly more useful representation
 418             """
 419 
 420             return self.__repr__()
 421 
 422         def __getattr__(self, attr):
 423             """\
 424             Grant access to ClassWrapper objects of all
 425             classes like they are member of this class.
 426 
 427             It also creates transparency to hyperdb.Database
 428             members if they aren't defined in the DBWrapper
 429             class.
 430             """
 431 
 432             # If 'attr' is a member, then access that member
 433             if self.__dict__.has_key(attr):
 434                 value = self.__dict__[attr]
 435 
 436             # If 'attr' is a hyperdatabse class, wrap it in
 437             # a 'ClassWrapper'
 438             elif attr in self._db.getclasses():
 439                 value = ClassWrapper(self, attr)
 440 
 441             # If 'attr' is a member of class 'Database', return
 442             # the address of that member to create the transparency
 443             else:
 444                 value = getattr(self._db, attr)
 445 
 446             return value
 447 
 448         def __setattr__(self, attr, value):
 449             """\
 450             Prevent that attributes which refer to ClassWrapper
 451             objects are overwritten by a wrong assign statement.
 452             """
 453 
 454             # If 'attr' is a member or member to be,
 455             # then access that member
 456             if attr in ['_instance', '_db', '_classes'] \
            or self.__dict__.has_key(attr):
 457                 self.__dict__[attr] = value
 458 
 459             # If 'attr' is a hyperdatabse class, wrap it in
 460             # a 'ClassWrapper'
 461             elif attr in self._db.getclasses():
 462                 # Check if it isn't the result of 'ClassWrapper.__iadd__'
 463                 if not isClass(value) or value._cl.classname != attr:
 464                     raise ValueError, \
                    "'%s' can't be assigned any value"%attr
 465 
 466             # If 'attr' is a member of class 'Database', return
 467             # the address of that member to create the transparency
 468             else:
 469                 setattr(self._db, attr, value)
 470 
 471         def __iter__(self):
 472             """\
 473             Create iteration of all classes as ClassWrapper's.
 474             """
 475 
 476             return iter( [ClassWrapper(self, classname) \
                          for classname in self._db.getclasses()] )
 477 
 478         def getclass(self, classname):
 479             """\
 480             Get the ClassWrapper object representing a particular class.
 481             """
 482 
 483             return self.__getattr__(classname)
 484 
 485 
 486 
 487     class ClassWrapper:
 488         """\
 489         A wrapper around a RoundUp hyperdb.Class.
 490 
 491         Introduction
 492         ============
 493         In most cases you don't need to initialize a
 494         ClassWrapper because the DBWrapper will do that
 495         for you where needed. But sometimes it can be
 496         more convenient to initialize one yourself.
 497 
 498         Initialization
 499         ==============
 500         There are three methods to initialize a ClassWrapper:
 501         1.  clw = dbwrapper.ClassWrapper(dbw, <class name>)
 502         A wrapper will be created for class <class name>.
 503 
 504         2.  clw = dbwrapper.ClassWrapper(dbw, <hyperdb.Class>)
 505         A wrapper will be created for the hyperdb.Class object.
 506 
 507         3.  clw = dbwrapper.ClassWrapper(dbw, <ClassWrapper>)
 508         A wrapper will be created for the ClassWrapper object.
 509         This won't make much sense because the wrapper was already there.
 510 
 511         Usage
 512         =====
 513         The wrapper behaves like a dictionary, meaning you can access the
 514         nodes by indexing them as dictionary items.
 515         Syntax:
 516         <DBWrapper>.<class name>[<key>|<nodeid>|<NodeWrapper>]
 517         As index you can either use the node id as integer or string,
 518         a key value (if the class has a key column) or an instance of
 519         NodeWrapper.
 520         The returned value is a NodeWrapper object.
 521 
 522         Examples:
 523         dbw.status['chatting']
 524         dbw.issue[1]
 525         dbw.priority['3']
 526         dbw.msg[nodew]
 527 
 528         Some of the methods in this class do have a "key"
 529         parameter. In all these cases this parameter can
 530         have one of the next types:
 531         1 - an integer
 532         The 'key' will be treated as the id of the node
 533         and used for the indexing.
 534         2 - a string
 535         The 'key' will be treated as key argument of the
 536         node and used to lookup the node id.
 537         If the lookup action raises a KeyError and 'key'
 538         only contains digits, the integer value of 'key'
 539         will be used as the node id.
 540         In both cases the node id is used for the indexing.
 541         3 - an instance of NodeWrapper
 542         The node id will be obtained from the NodeWrapper
 543         class and used for the indexing.
 544 
 545         Transparency
 546         ============
 547         Members defined in the hyperdb.Class, but not
 548         defined in this 'ClassWrapper' class can still
 549         be used as if they are member of this class.
 550         """
 551 
 552         class RegExpClass:
 553             """\
 554             A class with regular expression methods that will filter nodes
 555             from hyperdb.Class. The methods in this class are one to one
 556             with the regular expression methods in python module 're'.
 557             """
 558 
 559             def __init__(self, owner):
 560                 """\
 561                 Initialize class
 562 
 563                 "owner" must be of type 'ClassWrapper'
 564                 """
 565 
 566                 import re
 567 
 568                 if not isClass(owner):
 569                     raise TypeError, \
                    "Owner object isn't an instance of 'ClassWrapper'"
 570 
 571                 self._clw = owner
 572                 self.__re = re
 573 
 574             def __repr__(self):
 575                 """\
 576                 Slightly more useful representation
 577                 """
 578 
 579                 return '''<dbwrapper.ClassWrapper.RegExpClass '%s'>'''% \
                       self._clw._cl
 580 
 581             def __str__(self):
 582                 """\
 583                 Slightly more useful representation
 584                 """
 585 
 586                 return self.__repr__()
 587 
 588             def __inner_re(self, re_func, column, nodelist):
 589                 """\
 590                 Handles the main reg. expression search.
 591 
 592                 "re_func" must contain one of the search methods
 593                 from standard python module 're'.
 594 
 595                 "column" can be used to perform a search on an
 596                 other column than the column returned by
 597                 ClassWrapper.getlabel().
 598 
 599                 The return value is a list object containing tuples
 600                 pairs.
 601 
 602                 The first index of each tuple contains a NodeWrapper
 603                 object to a node matching the search.
 604 
 605                 The second index of the tuples holds the result object
 606                 of the corresponding regular expression method passed
 607                 in "re_func".
 608 
 609                 "nodelist" can be used to limit the search within the list
 610                 of nodes. 'nodelist' may be a list of ids or any list created
 611                 by any of the search methods in this module.
 612                 """
 613 
 614                 if not column:
 615                     column = self._clw.getlabel()
 616 
 617                 if nodelist is None:
 618                     list = self._clw._cl.list()
 619                 else:
 620                     list = ListWrapper(self._clw, nodelist).getnodeids()
 621 
 622                 result = []
 623                 for nodeid in list:
 624                     value = self._clw._cl.get(nodeid, column)
 625 
 626                     if value is None:
 627                         value = ''
 628 
 629                     res = re_func(value)
 630 
 631                     if res:
 632                         result.append( (NodeWrapper(self._clw, nodeid, False), res) )
 633 
 634                 return ListWrapper(self._clw, result)
 635 
 636             def match(self, pattern, flags=0, column=None, nodelist=None):
 637                 """\
 638                 Find all nodes in the class that have a result when
 639                 applying the "pattern" at the start of the string in
 640                 "column", returning a list object with tuples pairs.
 641 
 642                 The first index of each tuple contains a
 643                 dbwraper.NodeWrapper object of a node that matches
 644                 with "pattern".
 645 
 646                 The second index in the tuples holds the match object.
 647 
 648                 "flags" may have the same flags as used/defined in
 649                 python module 're'. These flags are also available
 650                 as members of module 'dbwrapper'.
 651                 (e.g. dbwrapper.IGNORECASE)
 652 
 653                 "column" can be used to perform a search on an
 654                 other column than the column returned by
 655                 ClassWrapper.getlabel().
 656 
 657                 "nodelist" can be used to limit the search within the list
 658                 of nodes. 'nodelist' may be a list of ids or any list created
 659                 by any of the search methods in this module.
 660                 """
 661 
 662                 reg = self.__re.compile(pattern, flags)
 663 
 664                 return self.__inner_re(reg.match, column, nodelist)
 665 
 666             def search(self, pattern, flags=0, column=None, nodelist=None):
 667                 """\
 668                 Find all nodes in the class that have a result when
 669                 scanning through the string in "column" looking for a
 670                 match to the "pattern", returning a list object with
 671                 tuples pairs.
 672 
 673                 The first index of each tuple contains a
 674                 dbwraper.NodeWrapper object of a node that matches
 675                 with "pattern".
 676 
 677                 The second index in the tuples holds the search object.
 678 
 679                 "flags" may have the same flags as used/defined in
 680                 python module 're'. These flags are also available
 681                 as members of module 'dbwrapper'.
 682                 (e.g. dbwrapper.IGNORECASE)
 683 
 684                 "column" can be used to perform a search on an
 685                 other column than the column returned by
 686                 ClassWrapper.getlabel().
 687 
 688                 "nodelist" can be used to limit the search within the list
 689                 of nodes. 'nodelist' may be a list of ids or any list created
 690                 by any of the search methods in this module.
 691                 """
 692 
 693                 reg = self.__re.compile(pattern, flags)
 694 
 695                 return self.__inner_re(reg.search, column, nodelist)
 696 
 697             def findall(self, pattern, flags=0, column=None, nodelist=None):
 698                 """\
 699                 Return all nodes in class that have "pattern" matches
 700                 in the strings in "column". Returned is a list of tuple
 701                 pairs.
 702 
 703                 The first tuple index contains a NodeWrapper object of
 704                 a matching node.
 705 
 706                 The second tuple index contains a list of all
 707                 non-overlapping matches in the node's "column" string.
 708 
 709                 If one or more groups are present in the "pattern", the
 710                 second tuple index will contain a list of groups; this
 711                 will be a list of tuples if the "pattern" has more than
 712                 one group.
 713 
 714                 "flags" may have the same flags as used/defined in
 715                 python module 're'. These flags are also available
 716                 as members of module 'dbwrapper'.
 717                 (e.g. dbwrapper.IGNORECASE)
 718 
 719                 "column" can be used to perform a search on an
 720                 other column than the column returned by
 721                 ClassWrapper.getlabel().
 722 
 723                 "nodelist" can be used to limit the search within the list
 724                 of nodes. 'nodelist' may be a list of ids or any list created
 725                 by any of the search methods in this module.
 726                 """
 727 
 728                 reg = self.__re.compile(pattern, flags)
 729 
 730                 return self.__inner_re(reg.findall, column, nodelist)
 731 
 732 
 733         def __init__(self, parent, cl):
 734             """\
 735             Initiate a wrapper around a hyperdb.Class.
 736 
 737             "parent" is the DBWrapper that owns this ClassWrapper.
 738 
 739             "cl" is used to specify the class.
 740             It can have three different types:
 741                 1 - an instance of a hyperdb.Class.
 742                 2 - an instance of a ClassWrapper.
 743                 3 - the class name.
 744             """
 745 
 746             if not isDB(parent):
 747                 raise TypeError, \
                "Parent object isn't an instance of 'DBWrapper'"
 748 
 749             self._dbw = parent
 750             self._db  = parent._db
 751 
 752             if isHyperDBClass(cl):
 753                 self._cl = cl
 754             elif isClass(cl):
 755                 self._cl = cl._cl
 756             else:
 757                 self._cl = self._db.getclass(cl)
 758 
 759             self.re = self.RegExpClass(self)
 760 
 761             self.__lastid = None
 762 
 763         def __repr__(self):
 764             """\
 765             Slightly more useful representation
 766             """
 767 
 768             return '''<dbwrapper.ClassWrapper '%s'>'''%self._cl
 769 
 770         def __str__(self):
 771             """\
 772             Slightly more useful representation
 773             """
 774 
 775             return self.__repr__()
 776 
 777         def __call__(self, *nodeids):
 778             """\
 779             Converts a list (or single id) of hyperdb node ids to a list of
 780             dbwrapper compliant nodes.
 781             """
 782 
 783             result = ListWrapper(self)
 784 
 785             if nodeids:
 786                 if len(nodeids) == 1 \
                and (isinstance(nodeids[0], types.ListType) \
                or isinstance(nodeids[0], types.TupleType)):
 787                     nodeids = nodeids[0]
 788 
 789                 for nodeid in nodeids:
 790                     result.append(NodeWrapper(self, int(nodeid)))
 791 
 792             return result
 793 
 794         def __getattr__(self, attr):
 795             """\
 796             Create transparency to hyperdb.Class members
 797             if they aren't defined in the ClassWrapper
 798             class.
 799             """
 800 
 801             if self.__dict__.has_key(attr):
 802                 value = self.__dict__[attr]
 803             else:
 804                 value = getattr(self._cl, attr)
 805 
 806             return value
 807 
 808         def __len__(self):
 809             """\
 810             Number of nodes in this class (excluding retired nodes).
 811             """
 812 
 813             return len(self._cl.list())
 814 
 815         def __contains__(self, key):
 816             """\
 817             Determine if the class has a given node.
 818             """
 819 
 820             return self.has_key(key)
 821 
 822         def __getitem__(self, key):
 823             """\
 824             Grant get access to all class nodes by indexing them as
 825             dictionary items and wrapping them in a NodeWrapper.
 826             """
 827 
 828             return NodeWrapper(self, key, False)
 829 
 830         def __setitem__(self, key, columns):
 831             """\
 832             Grant set access to all class nodes by indexing them as
 833             dictionary items and wrapping them in a NodeWrapper.
 834 
 835             "columns" must be a dictionary containing class properties
 836             as the keys. The values in the dictionary are used as
 837             values for the properties.
 838             """
 839 
 840             if isinstance(columns, types.DictType):
 841                 node = NodeWrapper(self, key, False)
 842 
 843                 for key in columns.keys():
 844                     setattr(node, key, columns[key])
 845 
 846             else:
 847                 raise TypeError, \
                "Item can only be assigned a dictionary"
 848 
 849         def __delitem__(self, key):
 850             """\
 851             Retire a class node by using the python 'del' command and
 852             indexing the node as dictionary item.
 853             """
 854 
 855             self._cl.retire( str(self.getid(key)) )
 856 
 857         def __iadd__(self, columns):
 858             """\
 859             Add a new node to the class.
 860 
 861             "columns" must be a dictionary containing class properties
 862             as the keys. The values in the dictionary are used as
 863             values for the properties.
 864             """
 865 
 866             if isinstance(columns, types.DictType):
 867                 self.create(**columns)
 868             else:
 869                 raise TypeError, \
                "Item can only be assigned a dictionary"
 870 
 871             return self
 872 
 873         def __iter__(self):
 874             """\
 875             Create iteration of all nodes as NodeWrapper's.
 876             """
 877 
 878             return iter(self.list())
 879 
 880         def getid(self, key):
 881             """\
 882             Get the node id of the node that matches the key.
 883 
 884             "key" can have three different types:
 885                 1 - an integer
 886                     The 'key' will be treated as the id of the node
 887                     and used for the indexing.
 888                 2 - a string
 889                     The 'key' will be treated as key argument of the
 890                     node and used to lookup the node id.
 891                     If the lookup action raises a KeyError and 'key'
 892                     only contains digits, the integer value of 'key'
 893                     will be used as the node id.
 894                     In both cases the node id is used for the indexing.
 895                 3 - an instance of NodeWrapper
 896                     The node id will be obtained from the NodeWrapper
 897                     class and used for the indexing.
 898             """
 899 
 900             if isinstance(key, types.IntType):
 901                 nodeid = key
 902 
 903             elif isNode(key):
 904                 nodeid = key._id
 905 
 906             else:
 907                 try:
 908                     nodeid = self._cl.lookup(key)
 909 
 910                 except (KeyError, TypeError):
 911                     if isinstance(key, types.StringType) \
                    and key.isdigit() \
                    and self._cl.hasnode(key):
 912                         nodeid = int(key)
 913 
 914                     else:
 915                         raise IndexError, \
                        "Class '%s' doesn't have a node that can be \
 916                         indexed with '%s'"%(self._cl.classname, key)
 917 
 918                 else:
 919                     nodeid = int(nodeid)
 920 
 921             return nodeid
 922 
 923         def auditors(self, enable):
 924             """\
 925             Enable/Disable auditors for this class.
 926             """
 927 
 928             p = self._dbw._classes[self._cl.classname]
 929 
 930             if enable:
 931                 if p.has_key('auditors'):
 932                     self._cl.auditors = p['auditors']
 933                     del p['auditors']
 934             else:
 935                 if not p.has_key('auditors'):
 936                     p['auditors'] = dict(self._cl.auditors)
 937                     self._cl.auditors = {
 938                                             'create': [],
 939                                             'set': [],
 940                                             'retire': [],
 941                                             'restore': []
 942                                         }
 943 
 944         def reactors(self, enable):
 945             """\
 946             Enable/Disable reactors for this class.
 947             """
 948 
 949             p = self._dbw._classes[self._cl.classname]
 950 
 951             if enable:
 952                 if p.has_key('reactors'):
 953                     self._cl.reactors = p['reactors']
 954                     del p['reactors']
 955             else:
 956                 if not p.has_key('reactors'):
 957                     p['reactors'] = dict(self._cl.reactors)
 958                     self._cl.reactors = {
 959                                             'create': [],
 960                                             'set': [],
 961                                             'retire': [],
 962                                             'restore': []
 963                                         }
 964 
 965         def setlabel(self, label=None):
 966             """\
 967             Set an alternative label to be used as label property.
 968 
 969             If label is None, the result of hyperdb 'labelprop()' will be
 970             used as label.
 971             """
 972 
 973             if not label is None:
 974                 props = self._cl.getprops()
 975 
 976                 if label in props.keys():
 977                     if isinstance(props[label], hyperdb.String):
 978                         self._dbw._classes[self._cl.classname]['label'] = label
 979                     else:
 980                         raise TypeError, \
                        "'label' should be a string type column"
 981 
 982                 else:
 983                     raise AttributeError, \
                    "Class '%s' doesn't have a property '%s'"% \
                    (self._cl.classname, label)
 984 
 985             else:
 986                 del self._dbw._classes[self._cl.classname]['label']
 987 
 988         def getlabel(self):
 989             """\
 990             Get the label to be used as label property.
 991 
 992             If an alternative label is set, it will be returned.
 993             """
 994 
 995             p = self._dbw._classes[self._cl.classname]
 996 
 997             return p.get('label', self._cl.labelprop())
 998 
 999         def create(self, **propvalues):
1000             """\
1001             Create a new node of this class and return its id.
1002 
1003             The keyword arguments in 'propvalues' map property names to values.
1004 
1005             The values of arguments must be acceptable for the types of their
1006             corresponding properties or a TypeError is raised.
1007 
1008             If this class has a key property, it must be present and its value
1009             must not collide with other key strings or a ValueError is raised.
1010 
1011             Any other properties on this class that are missing from the
1012             'propvalues' dictionary are set to None.
1013 
1014             If an id in a link or multilink property does not refer to a valid
1015             node, an IndexError is raised. Valid are node ids (as integer or
1016             as string), key values (for links to classes that have a key column)
1017             or a NodeWrapper object.
1018             """
1019 
1020             props = self._cl.getprops()
1021 
1022             creator = {}
1023             for column, value in propvalues.items():
1024                 if props.has_key(column):
1025                     if isinstance(props[column], hyperdb.Link):
1026                         cl = self._db.getclass(props[column].classname)
1027                         wrapper = ClassWrapper(self._dbw, cl)
1028                         creator[column] = str(wrapper.getid(value))
1029 
1030                     elif isinstance(props[column], hyperdb.Multilink):
1031                         cl = self._db.getclass(props[column].classname)
1032                         wrapper = ClassWrapper(self._dbw, cl)
1033 
1034                         if not isinstance(value, types.ListType) \
                        and not isinstance(value, types.TupleType):
1035                             value = [value]
1036 
1037                         creator[column] = [ str(wrapper.getid(key)) for key in value ]
1038 
1039                     elif isinstance(props[column], hyperdb.Interval) \
                    and not isinstance(value, date.Interval):
1040                         creator[column] = date.Interval(value)
1041 
1042                     elif isinstance(props[column], hyperdb.Date) \
                    and not isinstance(value, date.Date):
1043                         creator[column] = date.Date(value)
1044 
1045                     else:
1046                         creator[column] = value
1047 
1048                 else:
1049                     creator[column] = value
1050 
1051             self.__lastid = self._cl.create(**creator)
1052 
1053             return self.__lastid
1054 
1055         def get(self, key, propname):
1056             """\
1057             Get the value of a property on an existing node of this class.
1058 
1059             'propname' must be the name of a property of this class or a
1060             KeyError is raised.
1061             """
1062 
1063             node = self.__getitem__(key)
1064 
1065             return getattr(node, propname)
1066 
1067         def set(self, key, **propvalues):
1068             """\
1069             Modify a property on an existing node of this class.
1070 
1071             Each key in 'propvalues' must be the name of a property of this
1072             class or a KeyError is raised.
1073 
1074             All values in 'propvalues' must be acceptable types for their
1075             corresponding properties or a TypeError is raised.
1076 
1077             If the value of the key property is set, it must not collide with
1078             other key strings or a ValueError is raised.
1079 
1080             If the value of a Link or Multilink property contains an invalid
1081             node id, a ValueError is raised.
1082             """
1083 
1084             self.__setitem__(key, propvalues)
1085 
1086         def list(self):
1087             """\
1088             Return a list of NodeWrappers of the active nodes in this class.
1089             """
1090 
1091             id_list = [ int(nodeid) for nodeid in self._cl.list() ]
1092             id_list.sort()
1093 
1094             return ListWrapper(self,
1095             [ NodeWrapper(self, int(nodeid), False) for nodeid in id_list ] )
1096 
1097         def filter(self, search_matches, filterspec, sort=(None,None),
1098                    group=(None,None), nodelist=None):
1099             """\
1100             Return a list of the NodeWrappers of the active nodes in this class
1101             that match the 'filter' spec, sorted by the group spec and then the
1102             sort spec.
1103 
1104             "filterspec" is {propname: value(s)}
1105             'value(s)' may be any node id as integer or string or a key value
1106             (for classes that have a key column).
1107 
1108             "sort" and "group" are (dir, prop) where dir is '+', '-' or None
1109             and prop is a prop name or None
1110 
1111             "search_matches" is {nodeid: marker}
1112 
1113             "nodelist" can be used to limit the search within the list
1114             of nodes. 'nodelist' may be a list of ids or any list created
1115             by any of the search methods in this module.
1116 
1117             The filter must match all properties specified - but if the
1118             property value to match is a list, any one of the values in the
1119             list may match for that property to match.
1120             """
1121 
1122             props = self._cl.getprops()
1123 
1124             for column in filterspec.keys():
1125                 if isinstance(props[column], hyperdb.Link):
1126                     cl     = self._db.getclass(props[column].classname)
1127                     parent = ClassWrapper(self._dbw, cl)
1128                     node   = NodeWrapper(parent, filterspec[column])
1129                     filterspec[column] = str(node._id)
1130 
1131                 elif isinstance(props[column], hyperdb.Multilink):
1132                     cl     = self._db.getclass(props[column].classname)
1133                     parent = ClassWrapper(self._dbw, cl)
1134                     list   = ListWrapper(parent, filterspec[column])
1135                     filterspec[column] = list.getnodeids()
1136 
1137             if not nodelist is None:
1138                 filterspec['id'] = ListWrapper(self, nodelist).getnodeids()
1139 
1140             return ListWrapper(self,
1141             [ NodeWrapper(self, int(nodeid), False) for nodeid in self._cl.filter(search_matches, filterspec, sort, group) ])
1142 
1143         def find(self, **propspec):
1144             """\
1145             Get the NodeWrappers of items in this class which link to the given
1146             items.
1147 
1148             'propspec' consists of keyword args propname=itemid or
1149                        propname=key
1150             'propname' must be the name of a property in this class, or a
1151                        KeyError is raised.  That property must be a Link or
1152                        Multilink property, or a TypeError is raised.
1153 
1154             "propspec" can also be passed a list with results from anu of the
1155             search methods in this wrapper (also regular expression search
1156             results can be passed 1-to-1).
1157 
1158             Any item in this class whose 'propname' property links to any of the
1159             itemids will be returned. Used by the full text indexing, which knows
1160             that "foo" occurs in msg1, msg3 and file7, so we have hits on these
1161             issues:
1162                 db.issue.find(messages=[1,3], files=[7])
1163             """
1164 
1165             for column, values in propspec.items():
1166                 if isinstance(values, ListWrapper):
1167                     values.simple()
1168 
1169                 elif not isinstance(values, types.ListType) \
                and not isinstance(values, types.TupleType):
1170                     values = [values]
1171 
1172                 links = {}
1173                 for key in values:
1174                     links[str(self.getid(key))] = 1
1175 
1176                 propspec[column] = dict(links)
1177 
1178             id_list = [ int(nodeid) for nodeid in self._cl.find(**propspec) ]
1179             id_list.sort()
1180 
1181             return ListWrapper(self,
1182             [ NodeWrapper(self, nodeid, False) for nodeid in id_list ])
1183 
1184         def newid(self):
1185             """\
1186             Generate a new id for this class.
1187             """
1188 
1189             return self._db.newid(self._cl.classname)
1190 
1191         def newnodeid(self):
1192             """\
1193             Return the id of the last created node.
1194             """
1195 
1196             return self.__lastid
1197 
1198         def getnodeids(self, retired=None):
1199             """\
1200             Retrieve all NodeWrappers of the nodes for a particular Class.
1201 
1202             if 'retired' is True, all retired nodes will be returned.
1203             """
1204 
1205             id_list = [ int(nodeid) for nodeid in self._cl.getnodeids(retired) ]
1206             id_list.sort()
1207 
1208             return id_list
1209 
1210         def hasnode(self, key):
1211             """\
1212             Determine if the class has a given node (also true on retired nodes).
1213             """
1214 
1215             try:
1216                 nodeid = self.getid(key)
1217 
1218             except:
1219                 label = self._cl.getkey()
1220 
1221                 if label \
                and isinstance(key, types.StringType):
1222                     value = None
1223 
1224                     for nodeid in self._cl.getnodeids(True):
1225                         value = self._cl.get(nodeid, label)
1226 
1227                         if value == key:
1228                             break
1229 
1230                     if not value is None \
                    and value == key:
1231                         result = True
1232                     else:
1233                         result = False
1234 
1235                 else:
1236                     result = False
1237 
1238             else:
1239                 result = self._cl.hasnode( str(nodeid) )
1240 
1241             return result
1242 
1243         def lookup(self, keyvalue):
1244             """\
1245             Locate a particular node by its key property and return it in a
1246             NodeClass object.
1247 
1248             If this class has no key property, a TypeError is raised.  If the
1249             'keyvalue' matches one of the values for the key property among
1250             the nodes in this class, the matching node's id is returned;
1251             otherwise a KeyError is raised.
1252             """
1253 
1254             return NodeWrapper(self, int(self._cl.lookup(keyvalue)), False)
1255 
1256         def search(self, value, column=None, nodelist=None):
1257             """\
1258             Lookup all nodes that have a 100% match with 'value' and
1259             return them in a list.
1260 
1261             "value" is the pattern to look for.
1262 
1263             "column" can be set to any string property. A TypeError is
1264             raised if 'column' isn't a string property. The label column
1265             will be used if 'column' is omitted.
1266 
1267             "nodelist" can be used to limit the search within the list
1268             of nodes. 'nodelist' may be a list of ids or any list created
1269             by any of the search methods in this module.
1270             """
1271 
1272             if not isinstance(value, types.StringType):
1273                 raise TypeError, \
                "'value' should be a string"
1274 
1275             if not column:
1276                 column = self.getlabel()
1277 
1278             props = self._cl.getprops()
1279             if not isinstance(props[column], hyperdb.String):
1280                 raise TypeError, \
                "'column' should be a string type column"
1281 
1282             if nodelist is None:
1283                 list = self._cl.filter(None, {column:value})
1284             else:
1285                 list = ListWrapper(self, nodelist).getnodeids()
1286 
1287             list.sort()
1288 
1289             value = value.lower()
1290             result = []
1291             for nodeid in list:
1292                 if self._cl.get(nodeid, column).lower() == value:
1293                     result.append( NodeWrapper(self, int(nodeid), False) )
1294 
1295             return ListWrapper(self, result)
1296 
1297         def is_retired(self, key):
1298             """\
1299             Return true if the node is retired.
1300             """
1301 
1302             return self._cl.is_retired( str(self.getid(key)) )
1303 
1304         def safeget(self, key, propname, default=None):
1305             """\
1306             Safely get the value of a property on an existing node of this class.
1307 
1308             Return 'default' if the node doesn't exist.
1309             """
1310 
1311             value = getattr(NodeWrapper(self, key), propname, default)
1312 
1313             if value is None \
            or (isinstance(value, types.ListType) and not value):
1314                 value = default
1315 
1316             return value
1317 
1318         def has_key(self, key):
1319             """\
1320             Determine if the class has a given node, but then in
1321             a dictionary known method (false on retired nodes).
1322             """
1323 
1324             try:
1325                 nodeid = self.getid(key)
1326 
1327             except (KeyError, TypeError, IndexError):
1328                 result = False
1329 
1330             else:
1331                 result = str(nodeid) in self._cl.list()
1332 
1333             return result
1334 
1335         def keys(self):
1336             """\
1337             Get a list with all key values of this class. If no key is defined
1338             for this class, a list of ids will be returned.
1339             """
1340 
1341             key = self._cl.getkey()
1342 
1343             if key:
1344                 keys = [ self._cl.get(nodeid, key) for nodeid in self._cl.list() ]
1345             else:
1346                 keys = [ int(nodeid) for nodeid in self._cl.list() ]
1347 
1348             return keys
1349 
1350         def items(self):
1351             """\
1352             Get a list with tuples containing all keys with there NodeWrapper kept
1353             as pairs.
1354             """
1355 
1356             return [ (key, NodeWrapper(self, key, False)) for key in self.keys() ]
1357 
1358         def pop(self, key):
1359             """\
1360             Get node 'key' and remove (retire) it from this class.
1361             """
1362 
1363             value = self.__getitem__(key)
1364             self.__delitem__(key)
1365 
1366             return value
1367 
1368         def resurrect(self, key):
1369             """\
1370             Bring any deleted (retired) node back alive.
1371             """
1372 
1373             retireds = self._cl.getnodeids(retired=1)
1374 
1375             if isinstance(key, types.IntType):
1376                 nodeid = str(key)
1377             else:
1378                 prop = self._cl.getkey()
1379                 nodeid = None
1380                 for retired_id in retireds:
1381                     value = self._cl.get(retired_id, prop)
1382 
1383                     if value == key:
1384                         nodeid = retired_id
1385                         break;
1386 
1387             if nodeid and nodeid in retireds:
1388                 self._cl.restore(nodeid)
1389             else:
1390                 raise KeyError, \
                "No retired node '%s' in class '%s'"%(key, self._cl.classname)
1391 
1392 
1393 
1394     class NodeWrapper:
1395         """\
1396         A wrapper around one specific node in a
1397         RoundUp hyperdb.Class.
1398 
1399         Introduction
1400         ============
1401         In most cases you don't need to initialize a
1402         NodeWrapper because the ClassWrapper will do that
1403         for you where needed. But sometimes it can be
1404         more convenient to initialize one yourself.
1405 
1406         Initialization
1407         ==============
1408         There are three methods to initialize a NodeWrapper:
1409         1.  nodew = dbwrapper.NodeWrapper(clw, <nodeid>)
1410             A wrapper will be created for node <nodeid>.
1411             <nodeid> may be either an integer or a string.
1412 
1413         2.  nodew = dbwrapper.NodeWrapper(clw, <key>)
1414             For classes that have a key column, a key value can be
1415             used to create a wrapper for the node indexed by <key>.
1416 
1417         3.  nodew = dbwrapper.NodeWrapper(clw, <NodeWrapper>)
1418             A wrapper will be created for the NodeWrapper object.
1419             This won't make much sense because the wrapper was already there.
1420 
1421         4.  nodew = dbwrapper.NodeWrapper(clw, None)
1422             This is a phantom object and can be used to test against None.
1423             This might be convenient in auditors where there isn't a node
1424             if they are fired by the create event.
1425 
1426         Usage
1427         =====
1428         All node properties can be accessed as if they are members of this
1429         wrapper object.
1430         The syntax is:
1431             <NodeWrapper>.<property>
1432         The returned type depends on the requested property. In most cases
1433         a simple type is returned (like string and integer), but in case of
1434         a linked property, a NodeWrapper object will be returned for the
1435         node to which the property is linked. In case of multi linked
1436         properties, a ListWrapper object will be returned containing
1437         NodeWrapper objects for all nodes to which the property is linked.
1438 
1439         Examples:
1440             nodew.title
1441             nodew.name
1442             nodew.id (read only)
1443             nodew.nodeid (read only) (same as str(nodew.id))
1444 
1445         It is also possible to set a property by accessing them as wrapper
1446         members.
1447         The syntax is:
1448             <NodeWrapper>.<property> = <value>
1449         The assigned value depends on the property type. In case of the
1450         simple types (String, Number, Boolean) value will be of the
1451         corresponding Python type (string, integer, boolean).
1452         In case of a linked type, the value can have more than one type:
1453         1. node id as integer
1454         2. node id as string
1455         3. key value (for classes that have a key column)
1456         4. NodeWrapper instance
1457         In case of multi linked type, value must be a list (either Python's
1458         list object or dbwrapper's ListWrapper object) containing node
1459         specifications like mentioned for link type properties (see above).
1460 
1461         Examples:
1462             nodew.title  = 'My new title'
1463             nodew.status = 'chatting'
1464             nodew.topic  = ['Keyword 1', 'Keyword 2', 3, '4']
1465 
1466         In case of multi linked properties, new links to other nodes can be
1467         created by simply adding them to the property. Even so can links be
1468         removed by subtracting them from the property.
1469 
1470         Examples:
1471             Add:
1472                 nodew.topic += 'Keyword 3'
1473                 nodew.topic += ['Keyword 4', 'Keyword 5']
1474             Remove:
1475                 nodew.topic -= 'Keyword 3'
1476                 nodew.topic -= ['Keyword 4', 'Keyword 5']
1477         """
1478 
1479         def __init__(self, parent, key, readonly=True):
1480             """\
1481             Initiate a wrapper around a hyperdb.Class for one
1482             specific node.
1483 
1484             "parent" is the ClassWrapper that owns this NodeWrapper.
1485 
1486             "key" identifies the node.
1487             It can have one of the next types:
1488                 1 - an integer
1489                     The 'key' will be treated as the id of the node
1490                     and used for the indexing.
1491                 2 - a string
1492                     The 'key' will be treated as key argument of the
1493                     node and used to lookup the node id.
1494                     If the lookup action raises a KeyError and 'key'
1495                     only contains digits, the integer value of 'key'
1496                     will be used as the node id.
1497                     In both cases the node id is used for the indexing.
1498                 3 - an instance of NodeWrapper
1499                     The node id will be obtained from the NodeWrapper
1500                     class and used for the indexing.
1501                 4 - None type
1502                     This is a special option implemented to make it
1503                     easy usable in detectors.
1504 
1505             "readonly" can be set to False to allow set operations
1506             on any of the properties of this node.
1507             """
1508 
1509             if not isClass(parent):
1510                 raise TypeError, \
                "Parent object isn't an instance of 'ClassWrapper'"
1511 
1512             self._clw      = parent
1513             self._db       = parent._db
1514             self._cl       = parent._cl
1515             self._readonly = readonly
1516 
1517             if key is None:
1518                 self._id   = None
1519             else:
1520                 self._id   = parent.getid(key)
1521 
1522                 if not self._cl.hasnode(str(self._id)):
1523                     raise IndexError, \
                    "Class '%s' doesn't have a node with id '%s'"% \
                    (self._cl.classname, self._id)
1524 
1525         def __repr__(self):
1526             """\
1527             Return a resolved property.
1528             """
1529 
1530             import re
1531 
1532             if self._id is None:
1533                 value = "<dbwrapper.NodeWrapper 'Phantom'>"
1534 
1535             else:
1536                 value = self.plain()
1537 
1538                 if isinstance(value, types.StringType):
1539                     value = "'%s'"%re.sub("'", "\\'", value)
1540 
1541             return str(value)
1542 
1543         def __str__(self):
1544             """\
1545             Slightly more useful representation
1546             """
1547 
1548             if self._id is None:
1549                 value = "<dbwrapper.NodeWrapper 'Phantom'>"
1550 
1551             else:
1552                 value = str(self.plain())
1553 
1554             return value
1555 
1556         def __getattr__(self, attr):
1557             """\
1558             Grant easy get access to all properties of this node.
1559             """
1560 
1561             # is it a NoneClass member?
1562             if self.__dict__.has_key(attr):
1563                 value = self.__dict__[attr]
1564 
1565             elif attr == 'id':
1566                 value = self._id
1567 
1568             elif attr == 'nodeid':
1569                 if self._id is None:
1570                     value = None
1571                 else:
1572                     value = str(self._id)
1573 
1574             elif attr in ['properties', 'values']:
1575                 if self._id is None:
1576                     value = None
1577 
1578                 else:
1579                     internals = ('id', 'actor', 'activity', 'creator', 'creation')
1580 
1581                     props = self._cl.getprops()
1582 
1583                     value = {}
1584                     for prop in props.keys():
1585                         if not prop in internals:
1586                             data = self._cl.get(str(self._id), prop)
1587 
1588                             if not data is None \
                            and (not isinstance(data, types.ListType) or data):
1589                                 if attr == 'properties':
1590                                     if isinstance(props[prop], hyperdb.Link):
1591                                         if data:
1592                                             cl = self._db.getclass(props[prop].classname)
1593                                             key = cl.getkey()
1594 
1595                                             if key:
1596                                                 value[prop] = cl.get(data, key)
1597                                             else:
1598                                                 value[prop] = int(data)
1599 
1600                                     elif isinstance(props[prop], hyperdb.Multilink):
1601                                         cl = self._db.getclass(props[prop].classname)
1602                                         key = cl.getkey()
1603 
1604                                         if key:
1605                                             value[prop] = [ cl.get(nodeid, key) for nodeid in data ]
1606                                         else:
1607                                             value[prop] = [ int(nodeid) for nodeid in data ]
1608 
1609                                     elif not isinstance(props[prop], hyperdb.String):
1610                                         value[prop] = str(data)
1611 
1612                                     else:
1613                                         value[prop] = data
1614 
1615                                 else:
1616                                     if isinstance(props[prop], hyperdb.Link):
1617                                         if data:
1618                                             value[prop] = int(data)
1619 
1620                                     elif isinstance(props[prop], hyperdb.Multilink):
1621                                         value[prop] = [ int(nodeid) for nodeid in data ]
1622 
1623                                     else:
1624                                         value[prop] = data
1625 
1626             else:
1627                 if self._id is None:
1628                     raise ValueError, \
                    "Phantom nodes do not have an attribute '%s'"%attr
1629 
1630                 else:
1631                     props = self._cl.getprops()
1632 
1633                     # is it a class property?
1634                     if props.has_key(attr):
1635                         if isinstance(props[attr], hyperdb.Link):
1636                             elementid = self._cl.get(str(self._id), attr)
1637 
1638                             if elementid:
1639                                 cl = self._db.getclass(props[attr].classname)
1640                                 parent = ClassWrapper(self._clw._dbw, cl)
1641                                 value = NodeWrapper(parent, elementid)
1642                             else:
1643                                 value = None
1644 
1645                         elif isinstance(props[attr], hyperdb.Multilink):
1646                             elements = self._cl.get(str(self._id), attr)
1647                             cl = self._db.getclass(props[attr].classname)
1648                             parent = ClassWrapper(self._clw._dbw, cl)
1649 
1650                             value = ListWrapper(parent, True, elements)
1651 
1652                         else:
1653                             if attr == 'id':
1654                                 value = int(self._cl.get(str(self._id), attr))
1655                             else:
1656                                 value = self._cl.get(str(self._id), attr)
1657 
1658                     # I don't know you
1659                     else:
1660                         raise AttributeError, \
                        "'%s' is not a member of object 'NodeWrapper'"%attr
1661 
1662             return value
1663 
1664         def __setattr__(self, attr, value):
1665             """\
1666             Grant easy set access to all properties of this node.
1667 
1668             If "read only" than all set operations will raise exception
1669             "ValueError".
1670             """
1671 
1672             # test if we are a member of NodeClass or if we will become
1673             # a member of NodeClass
1674             if attr in ['_clw', '_db', '_cl', '_id', '_readonly'] \
            or self.__dict__.has_key(attr):
1675                 self.__dict__[attr] = value
1676 
1677             elif attr in ['properties', 'values']:
1678                 raise ValueError, \
                "'properties' can't be assigned any value"
1679 
1680             else:
1681                 if self._id is None:
1682                     raise ValueError, \
                    "Phantom nodes do not have an attribute '%s'"%attr
1683 
1684                 else:
1685                     props = self._cl.getprops()
1686 
1687                     # is it a class property?
1688                     if props.has_key(attr):
1689                         if not self._readonly:
1690                             if isinstance(props[attr], hyperdb.Link):
1691                                 cl = self._db.getclass(props[attr].classname)
1692                                 parent = ClassWrapper(self._clw._dbw, cl)
1693 
1694                                 value = str(parent.getid(value))
1695 
1696                             elif isinstance(props[attr], hyperdb.Multilink):
1697                                 cl = self._db.getclass(props[attr].classname)
1698                                 parent = ClassWrapper(self._clw._dbw, cl)
1699 
1700                                 if isinstance(value, types.StringType):
1701                                     value = [value]
1702 
1703                                 value = ListWrapper(parent, value)
1704 
1705                                 value = [ str(nodeid) for nodeid in value.getnodeids() ]
1706 
1707                             elif isinstance(props[attr], hyperdb.Interval) \
                            and not isinstance(value, date.Interval):
1708                                 value = date.Interval(value)
1709 
1710                             elif isinstance(props[attr], hyperdb.Date) \
                            and not isinstance(value, date.Date):
1711                                 value = date.Date(value)
1712 
1713                             self._cl.set(str(self._id), **{attr:value})
1714 
1715                         else:
1716                             raise ValueError, \
                            "This node is marked as readonly and can't \
1717                             be assigned and value"
1718 
1719                     # not a member or class property
1720                     else:
1721                         raise AttributeError, \
                        "'%s' is not a member of object 'NodeWrapper'"%attr
1722 
1723         def __iter__(self):
1724             """\
1725             Create iteration of all properties in this node. The iteration contains
1726             tuples with the property name on the first index and the value on the
1727             second index. For linked properties an instance to a NodeWrapper is
1728             used and not the value.
1729             """
1730 
1731             return iter( [(prop, self.__getattr__(prop)) for prop in self._cl.getprops()] )
1732 
1733         def __hash__(self):
1734             """\
1735             Node identifier.
1736             """
1737 
1738             return self._id
1739 
1740         def __eq__(self, other):
1741             """\
1742             Are these nodes equal?
1743             """
1744 
1745             same_class = isinstance(other, NodeWrapper) \
                         and self._cl.classname == other._cl.classname
1746 
1747             if not other is None:
1748                 other = self._clw.getid(other)
1749 
1750             return same_class and (other == self._id)
1751 
1752         def __ne__(self, other):
1753             """\
1754             Are these nodes different?
1755             """
1756 
1757             return not self.__eq__(other)
1758 
1759         def __nonzero__(self):
1760             """\
1761             Truth value testing.
1762             """
1763 
1764             return not self._id is None
1765 
1766         def plain(self, property=None):
1767             """\
1768             Return the property in a nice way. Links are resolved.
1769             """
1770 
1771             if not property is None:
1772                 value = self.__getattr__(property)
1773 
1774                 if isinstance(value, types.ListType):
1775                     list = []
1776                     for one in value:
1777                         label = one._clw.getlabel()
1778 
1779                         if isNode(one):
1780                             list.append( getattr(one, label) )
1781                         else:
1782                             list.append(one)
1783                     value = ', '.join( [str(item) for item in list] )
1784 
1785                 elif isinstance(value, types.NoneType):
1786                     value = ''
1787 
1788                 elif isNode(value):
1789                     label = value._clw.getlabel()
1790                     value = getattr(value, label)
1791             else:
1792                 label = self._clw.getlabel()
1793                 value = self.__getattr__(label)
1794 
1795             if value is None:
1796                 value = ''
1797 
1798             return value
1799 
1800         def get(self, propname):
1801             """\
1802             Get the value of a property on an existing node of this class.
1803 
1804             'propname' must be the name of a property of this class or a
1805             KeyError is raised.
1806             """
1807 
1808             if self._id is None:
1809                 raise NotImplementedError, \
                "This method is not implemented for phantom nodes"
1810 
1811             else:
1812                 return self._clw.get(self._id, propname)
1813 
1814         def set(self, **propvalues):
1815             """\
1816             Modify a property on an existing node of this class.
1817 
1818             Each key in 'propvalues' must be the name of a property of this
1819             class or a KeyError is raised.
1820 
1821             All values in 'propvalues' must be acceptable types for their
1822             corresponding properties or a TypeError is raised.
1823 
1824             If the value of the key property is set, it must not collide with
1825             other key strings or a ValueError is raised.
1826 
1827             If the value of a Link or Multilink property contains an invalid
1828             node id, a ValueError is raised.
1829             """
1830 
1831             if self._id is None:
1832                 raise NotImplementedError, \
                "This method is not implemented for phantom nodes"
1833 
1834             else:
1835                 self._clw.set(self._id, propvalues)
1836 
1837         def is_retired(self):
1838             """\
1839             Return true if the node is retired.
1840             """
1841 
1842             if self._id is None:
1843                 raise NotImplementedError, \
                "This method is not implemented for phantom nodes"
1844 
1845             else:
1846                 return self._clw.is_retired(self._id)
1847 
1848         def safeget(self, propname, default=None):
1849             """\
1850             Safely get the value of a property on an existing node of this class.
1851 
1852             Return 'default' if the node doesn't exist.
1853             """
1854 
1855             if self._id is None:
1856                 raise NotImplementedError, \
                "This method is not implemented for phantom nodes"
1857 
1858             else:
1859                 return self._clw.safeget(self._id, propname, default)
1860 
1861         def pop(self):
1862             """\
1863             Get node 'key' and remove (retire) it from this class.
1864             """
1865 
1866             if self._id is None:
1867                 raise NotImplementedError, \
                "This method is not implemented for phantom nodes"
1868 
1869             else:
1870                 return self._clw.pop(self._id)
1871 
1872         def resurrect(self):
1873             """\
1874             Bring any deleted (retired) node back alive.
1875             """
1876 
1877             if self._id is None:
1878                 raise NotImplementedError, \
                "This method is not implemented for phantom nodes"
1879 
1880             else:
1881                 self._clw.resurrect(self._id)
1882 
1883 
1884 
1885     class ListWrapper(types.ListType):
1886         """\
1887         List object to give easier access to NodeWrapper's.
1888 
1889         Introduction
1890         ============
1891         This wrapper almost fully supports all available operations for
1892         Python's list object. The difference is that it is meant to be used
1893         in ClassWrapper's and NodeWrapper's.
1894 
1895         Initialization
1896         ==============
1897         The wrapper can be initialized as follows:
1898         1.  lw = dbwrapper.ClassWrapper(<ClassWrapper>)
1899             This is the simplest form. The wrapper will be created to hold
1900             only NodeWrapper objects from class <ClassWrapper>.
1901             The list will be empty.
1902 
1903         2.  lw = dbwrapper.ClassWrapper(<ClassWrapper>, [<key>|<nodeid>|<NodeWrapper>])
1904             With this form you will fill the list with initial data.
1905             The data must be nodes in <ClassWrapper> and may be addressed
1906             by either key (in case the class has a key column), or by node id
1907             (as integer or string), or by a NodeWrapper object.
1908 
1909         For both forms an additional parameter can be set to tell the dbwrapper
1910         that all NodeWrapper classes in the ListClass are either read-only or not.
1911         Default they are set to not read-only, meaning all nodes in the list
1912         may be changed.
1913 
1914         Several methods in the ClassWrapper and the NodeWrapper do return
1915         ListWrapper objects. In general these methods are all the search
1916         methods in the ClassWrapper and the 'get' method in the NodeWrapper.
1917 
1918         Default the list won't accept duplicate nodes. If you try to add a node
1919         which is already present, a ValueError will be raised. This behavior
1920         can be omitted by setting ListWrapper member 'unique' to False.
1921         If False, then duplicate nodes are accepted. Switching member
1922         'unique' back to True while there are duplicate nodes will raise
1923         a ValueError too.
1924         """
1925 
1926         class RegExpClass:
1927             """\
1928             A class with regular expression methods that will filter nodes
1929             from hyperdb.Class. The methods in this class are one to one
1930             with the regular expression methods in python module 're'.
1931             """
1932 
1933             def __init__(self, owner):
1934                 """\
1935                 Initialize class
1936 
1937                 "owner" must be of type ListWrapper
1938                 """
1939 
1940                 if not isinstance(owner, ListWrapper):
1941                     raise TypeError, \
                    "Owner object isn't an instance of 'ListWrapper'"
1942 
1943                 self._lw = owner
1944 
1945             def __repr__(self):
1946                 """\
1947                 Slightly more useful representation
1948                 """
1949 
1950                 return '''<dbwrapper.ListWrapper.RegExpClass '%s'>'''% \
                                                        self._lw._clw._cl
1951 
1952             def __str__(self):
1953                 """\
1954                 Slightly more useful representation
1955                 """
1956 
1957                 return self.__repr__()
1958 
1959             def match(self, pattern, flags=0, column=None):
1960                 """\
1961                 Find all nodes in the class that have a result when
1962                 applying the "pattern" at the start of the string in
1963                 "column", returning a list object with tuples pairs.
1964 
1965                 The first index of each tuple contains a
1966                 dbwraper.NodeWrapper object of a node that matches
1967                 with "pattern".
1968 
1969                 The second index in the tuples holds the match object.
1970 
1971                 "flags" may have the same flags as used/defined in
1972                 python module 're'. These flags are also available
1973                 as members of module 'dbwrapper'.
1974                 (e.g. dbwrapper.IGNORECASE)
1975 
1976                 "column" can be used to perform a search on an
1977                 other column than the column returned by
1978                 ClassWrapper.getlabel().
1979                 """
1980 
1981                 return self._lw._clw.re.match(pattern, flags, column, nodelist=self._lw)
1982 
1983             def search(self, pattern, flags=0, column=None):
1984                 """\
1985                 Find all nodes in the class that have a result when
1986                 scanning through the string in "column" looking for a
1987                 match to the "pattern", returning a list object with
1988                 tuples pairs.
1989 
1990                 The first index of each tuple contains a
1991                 dbwraper.NodeWrapper object of a node that matches
1992                 with "pattern".
1993 
1994                 The second index in the tuples holds the search object.
1995 
1996                 "flags" may have the same flags as used/defined in
1997                 python module 're'. These flags are also available
1998                 as members of module 'dbwrapper'.
1999                 (e.g. dbwrapper.IGNORECASE)
2000 
2001                 "column" can be used to perform a search on an
2002                 other column than the column returned by
2003                 ClassWrapper.getlabel().
2004                 """
2005 
2006                 return self._lw._clw.re.search(pattern, flags, column, nodelist=self._lw)
2007 
2008             def findall(self, pattern, flags=0, column=None):
2009                 """\
2010                 Return all nodes in class that have "pattern" matches
2011                 in the strings in "column". Returned is a list of tuple
2012                 pairs.
2013 
2014                 The first tuple index contains a NodeWrapper object of
2015                 a matching node.
2016 
2017                 The second tuple index contains a list of all
2018                 non-overlapping matches in the node's "column" string.
2019 
2020                 If one or more groups are present in the "pattern", the
2021                 second tuple index will contain a list of groups; this
2022                 will be a list of tuples if the "pattern" has more than
2023                 one group.
2024 
2025                 "flags" may have the same flags as used/defined in
2026                 python module 're'. These flags are also available
2027                 as members of module 'dbwrapper'.
2028                 (e.g. dbwrapper.IGNORECASE)
2029 
2030                 "column" can be used to perform a search on an
2031                 other column than the column returned by
2032                 ClassWrapper.getlabel().
2033                 """
2034 
2035                 return self._lw._clw.re.findall(pattern, flags, column, nodelist=self._lw)
2036 
2037 
2038         def __init__(self, owner, readonly=False, *list):
2039             """\
2040             Create a list object for dbwrapper.
2041 
2042             "owner" must be a 'ClassWrapper' object. Only nodes present
2043             in 'ClassWrapper' will be allowed in the list.
2044 
2045             "readonly" can be set to prevent that properties of the nodes
2046             in the list can be changed. The list content can always be
2047             changed.
2048 
2049             "list" can be a list of any node specification like key,
2050             nodeid or NodeClass object.
2051             """
2052 
2053             if not isClass(owner):
2054                 raise TypeError, \
                "owner object isn't an instance of 'ClassWrapper'"
2055 
2056             self.__list    = []
2057             self._clw      = owner
2058             self.__regexp  = False
2059             self.re        = self.RegExpClass(self)
2060             self.unique    = True
2061 
2062             if isinstance(readonly, types.ListType) \
            or isinstance(readonly, types.TupleType):
2063                 self._readonly = False
2064                 list = readonly
2065             else:
2066                 self._readonly = readonly
2067 
2068             if list:
2069                 if len(list) == 1 \
                and isinstance(list[0], types.ListType):
2070                     list = list[0]
2071 
2072