Introduction

Database wrapper implementation. 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:

    db    : hyperdb.Database
    cl    : hyperdb.Class (or FileClass or IssueClass)
    dbw   : dbwrapper.DBWrapper
    clw   : dbwrapper.ClassWrapper
    nodew : dbwrapper.NodeClass
    lw    : dbwrapper.ListWrapper

Features

Addressing
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:

    - dbw.issue[1].title
      dbw.issue['1'].title
        Both these examples will return the title of issue 1.
    - dbw.priority['urgent'].name
        This example will return 'urgent'. Not really
        meaningful, but it's only meant as an example.
    - dbw.status['unread'].id
        Will return the nodeid of status node 'unread'.

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
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

To use the wrapper you need to place it somewhere. In our case 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:
    /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:
    __all__ = [ dbwrapper ]

What's left is importing the wrapper in your Python sources (e.g. detectors and action handlers). You do that by adding the line:
    from roundup.extensions import dbwrapper
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
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:

    def auditor(db, cl, nodeid, newvalues):
        dbw   = dbwrapper.DBWrapper(db)
        clw   = dbwrapper.ClassWrapper(dbw, cl)
        nodew = dbwrapper.NodeWrapper(clw, nodeid)

And this in a reactor:

    def reactor(db, cl, nodeid, oldvalues):
        dbw   = dbwrapper.DBWrapper(db)
        clw   = dbwrapper.ClassWrapper(dbw, cl)
        nodew = dbwrapper.NodeWrapper(clw, nodeid)

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
The wrapper is almost used the same way in an action handler as it is used in a detector. This is what you do:

    def handler(self):
        dbw   = dbwrapper.DBWrapper(self.db)
        clw   = dbwrapper.ClassWrapper(dbw, self.cl)
        nodew = dbwrapper.NodeWrapper(clw, self.nodeid)

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

Best regards,
Marlon van den Berg

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


Examples

Here are a few more examples what dbwrapper can do for you:

  1. Reading a linked property:
        - With roundup's hyperdb:
            unread_id = db.status.lookup('unread')
            db.status.get(unread_id, 'order')
    
        - With dbwrapper:
            dbw.status['unread'].order
    
  2. Changing a linked property:
        - With roundup's hyperdb:
            unread_id = db.status.lookup('unread')
            db.issue.set('1', status=unread_id)
    
        - With dbwrapper:
            dbw.issue[1].status = 'unread'
    
  3. Adding a link to a multi linked property:
        - With roundup's hyperdb:
            topics = db.issue.get('1', 'topic')
            keyword_id = db.keyword.lookup('DBWrapper')
            topics.append(keyword_id)
            db.issue.set('1', topic=topics)
    
        - With dbwrapper:
            dbw.issue[1].topic += 'DBWrapper'
          or:
            dbw.issue[1].topic += ['DBWrapper']
    
  4. Removing a link from a multi linked property:
        - With roundup's hyperdb:
            topics = db.issue.get('1', 'topic')
            keyword_id = db.keyword.lookup('DBWrapper')
            topics.remove(keyword_id)
            db.issue.set('1', topic=topics)
    
        - With dbwrapper:
            dbw.issue[1].topic -= 'DBWrapper'
          or:
            dbw.issue[1].topic -= ['DBWrapper']
    
  5. Duplicating an issue with dbwrapper:
        - With dbwrapper:
            issue = db.issue[1].properties
            issue['title'] = 'Copy of %s'%issue['title']
            db.issue += issue
    
  6. Retiring a node:
        - With roundup's hyperdb:
            nodeid = db.status.lookup('chatting')
            db.status.retire(nodeid)
    
        - With dbwrapper:
            del dbw.status['chatting']
    
  7. Filtering:
        - With roundup's hyperdb:
            nodeids = [ db.status.lookup(name) for name in ['chatting', 'unread'] ]
            db.filter(None, {'status':nodeids})
    
        - With dbwrapper:
            dbw.filter(None, {'status':['chatting', 'unread']})
    
  8. Regular expression search:
        - With dbwrapper:
            To lookup all issues of which the title starts witch 'roundup':
                dbw.issue.re.search('^roundup', dbwrapper.IGNORECASE)
    
  9. Combination RE search:
        - With dbwrapper:
            To lookup all issues of which the title starts witch 'roundup'
            and have status 'unread':
                lw = dbw.issue.filter(None, {'status':'unread'})
                lw.re.search('^roundup', dbwrapper.IGNORECASE)
            The next will have the same result but takes longer:
                lw = dbw.issue.re.search('^roundup', dbwrapper.IGNORECASE)
                lw.issue.filter(None, {'status':'unread'})
    
  10. More search possibilities:
        - With dbwrapper:
            Lookup all issues that have at least one message attached which
            starts with the word 'roundup':
                flags = dbwrapper.IGNORECASE + dbwrapper.MULTILINE
                lw = dbw.msg.re.search('\Aroundup', flags, column='content')
                dbw.issue.find(messages=lw)
    
  11. Iteration:
        - With dbwrapper:
            The next code will step trough all nodes in all classes and print
            the content on the screen. Please don't try this on your 10,000
            issue tracker. If you do, well I guess you will be spending your
            day next to the coffee machine.
                for clw in dbw:
                    print clw.classname
                    for nodew in clw:
                        print "    %s"%nodew.plain()
                        for property in nodew:
                            print "    |   %s"%property
    


The Source

And finally the source code:

    """\
    Introduction
    ============
    Database wrapper implementation.
    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:
    db    : hyperdb.Database
    cl    : hyperdb.Class (or FileClass or IssueClass)
    dbw   : dbwrapper.DBWrapper
    clw   : dbwrapper.ClassWrapper
    nodew : dbwrapper.NodeClass
    lw    : dbwrapper.ListWrapper

    Features
    ========

    Addressing
    ----------
    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:
    - dbw.issue[1].title
      dbw.issue['1'].title
        Both these examples will return the title of issue 1.
    - dbw.priority['urgent'].name
        This example will return 'urgent'. Not really
        meaningful, but it's only meant as an example.
    - dbw.status['unread'].id
        Will return the nodeid of status node 'unread'.

    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
    ---------
    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
    =====
    To use the wrapper you need to place it somewhere. In our case
    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:
        /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:
        __all__ = [ 'dbwrapper' ]

    What's left is importing the wrapper in your Python sources (e.g.
    detectors and action handlers). You do that by adding the line:
        from roundup.extensions import dbwrapper
    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
    ---------
    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:
        def auditor(db, cl, nodeid, newvalues):
            dbw   = dbwrapper.DBWrapper(db)
            clw   = dbwrapper.ClassWrapper(dbw, cl)
            nodew = dbwrapper.NodeWrapper(clw, nodeid)

    And this in a reactor:
        def reactor(db, cl, nodeid, oldvalues):
            dbw   = dbwrapper.DBWrapper(db)
            clw   = dbwrapper.ClassWrapper(dbw, cl)
            nodew = dbwrapper.NodeWrapper(clw, nodeid)

    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
    ---------------
    The wrapper is almost used the same way in an action handler as it
    is used in a detector.
    This is what you do:
        def handler(self):
            dbw   = dbwrapper.DBWrapper(self.db)
            clw   = dbwrapper.ClassWrapper(dbw, self.cl)
            nodew = dbwrapper.NodeWrapper(clw, self.nodeid)

    Examples
    ========
    Here are a few more examples what dbwrapper can do for you:
    1.  Reading a linked property:
        - With RoundUp's hyperdb:
            unread_id = db.status.lookup('unread')
            db.status.get(unread_id, 'order')

        - With dbwrapper:
            dbw.status['unread'].order

    2.  Changing a linked property:
        - With RoundUp's hyperdb:
            unread_id = db.status.lookup('unread')
            db.issue.set('1', status=unread_id)

        - With dbwrapper:
            dbw.issue[1].status = 'unread'

    3.  Adding a link to a multi linked property:
        - With RoundUp's hyperdb:
            topics = db.issue.get('1', 'topic')
            keyword_id = db.keyword.lookup('DBWrapper')
            topics.append(keyword_id)
            db.issue.set('1', topic=topics)

        - With dbwrapper:
            dbw.issue[1].topic += 'DBWrapper'
          or:
            dbw.issue[1].topic += ['DBWrapper']

    4.  Removing a link from a multi linked property:
        - With RoundUp's hyperdb:
            topics = db.issue.get('1', 'topic')
            keyword_id = db.keyword.lookup('DBWrapper')
            topics.remove(keyword_id)
            db.issue.set('1', topic=topics)

        - With dbwrapper:
            dbw.issue[1].topic -= 'DBWrapper'
          or:
            dbw.issue[1].topic -= ['DBWrapper']

    5.  Duplicating an issue with dbwrapper:
        - With dbwrapper:
            issue = db.issue[1].properties
            issue['title'] = 'Copy of %s'%issue['title']
            db.issue += issue

    6.  Retiring a node:
        - With RoundUp's hyperdb:
            nodeid = db.status.lookup('chatting')
            db.status.retire(nodeid)

        - With dbwrapper:
            del dbw.status['chatting']

    7.  Filtering:
        - With RoundUp's hyperdb:
            nodeids = [ db.status.lookup(name) for name in ['chatting', 'unread'] ]
            db.filter(None, {'status':nodeids})

        - With dbwrapper:
            dbw.filter(None, {'status':['chatting', 'unread']})

    8.  Regular expression search:
        - With dbwrapper:
            To lookup all issues of which the title starts witch 'RoundUp':
                dbw.issue.re.search('^RoundUp', dbwrapper.IGNORECASE)

    9.  Combination RE search:
        - With dbwrapper:
            To lookup all issues of which the title starts witch 'RoundUp'
            and have status 'unread':
                lw = dbw.issue.filter(None, {'status':'unread'})
                lw.re.search('^RoundUp', dbwrapper.IGNORECASE)
            The next will have the same result but takes longer:
                lw = dbw.issue.re.search('^RoundUp', dbwrapper.IGNORECASE)
                lw.issue.filter(None, {'status':'unread'})

    10. More search possibilities:
        - With dbwrapper:
            Lookup all issues that have at least one message attached which
            starts with the word 'RoundUp':
                flags = dbwrapper.IGNORECASE + dbwrapper.MULTILINE
                lw = dbw.msg.re.search('\ARoundUp', flags, column='content')
                dbw.issue.find(messages=lw)

    11. Iteration:
        - With dbwrapper:
            The next code will step trough all nodes in all classes and print
            the content on the screen. Please don't try this on your 10,000
            issue tracker. If you do, well I guess you will be spending your
            day next to the coffee machine.
                for clw in dbw:
                    print clw.classname
                    for nodew in clw:
                        print "    %s"%nodew.plain()
                        for property in nodew:
                            print "    |   %s"%property
    """

    __docformat__ = 'restructuredtext'

    import types, os

    from re import IGNORECASE, DOTALL, LOCALE, MULTILINE, UNICODE, VERBOSE
    from re import I,          S,      L,      M,         U,       X

    from roundup import instance, hyperdb, date

    isHyperDB      = lambda object: isinstance(object, hyperdb.Database)
    isHyperDBClass = lambda object: isinstance(object, hyperdb.Class)
    isDB           = lambda object: isinstance(object, DBWrapper)
    isClass        = lambda object: isinstance(object, ClassWrapper)
    isNode         = lambda object: isinstance(object, NodeWrapper)

    def to_hyperdb(*nodes):
        """\
        Converts a list (or single node) of dbwrapper nodes to a list of
        hyperdb compliant nodes.
        """

        result = []

        if nodes:
            if len(nodes) == 1 \
            and (isinstance(nodes[0], types.ListType) \
            or isinstance(nodes[0], types.TupleType)):
                nodes = nodes[0]

            for node in nodes:
                if isinstance(node, types.TupleType) \
                and len(node) == 2:
                    # regular expression list
                    node = node[0]

                if not isNode(node):
                    raise ValueError, \
                    "Node '%s' isn't an instance of 'NodeWrapper'."%node

                result.append(str(node._id))

        return result

    class DBWrapper:
        """\
        A wrapper around RoundUp's hyperdb.Database.

        Initialization
        ==============
        There are three methods to initialize the DBWrapper:
        1.  dbw = dbwrapper.DBWrapper(<tracker home>)
            In this case the wrapper will open the database
            belonging to <tracker_home>. On destruction it
            will always close the database.
            This form is useful in separate scripts which
            need to access the RoundUp database.

        2.  dbw = dbwrapper.DBWrapper()
            The wrapper will open the database for you if
            the environment has a variable TRACKER_HOME
            which points to a valid tracker home folder.
            On destruction the database will be closed.
            This form is useful in separate scripts which
            need to access the RoundUp database.

        3.  dbw = dbwrapper.DBWrapper(<hyperdb.Dtabase object>)
            The wrapper will not open the database, but will
            use an already opened database. The database
            object is passed as parameter to the wrapper and
            should be an opened hyperdb.Database instance.
            The wrapper will never close the database.
            This form is useful in detectors and action
            handlers.

        Usage
        =====
        You have full access to the database as soon as the
        DBWrapper is initialized. To access a class, just access
        it as being a member of the wrapper (same as with hyperdb).
        Syntax:
            <DBWrapper>.<class name>
        The returned object will be an initialized ClassWrapper
        object.

        Examples:
            dbw.issue
            dbw.status

        Transparency
        ============
        Members defined in hyperdb.Database, but not
        defined in this 'DBWrapper' class can still be
        used as if they are member of this class.
        """

        def __init__(self, arg=None, *user):
            """\
            Initiate a wrapper around RoundUp's hyperdb.Database.

            "arg" is used to specify the database.
            It can have three different types:
                1 - an instance of an open RoundUp hyperdb.Database.
                    This is the most common usage if the wrapper is
                    used within any detector or action handler.
                2 - a valid tracker home path.
                    This allows us to use the wrapper in any external
                    script without having to open a tracker instance
                    and the database before using the wrapper.
                    If used like this, the destructor of this class
                    will close the database to prevent pending open
                    database links.
                3 - not specified.
                    This is almost similar with type 2 except that
                    environment variable TRACKER_HOME will be used
                    to open the database.

            "user" has only meaning if 'arg' is of type 2 or 3.
            In those cases the database will be opened as being 'user'.
            Default it opens the database as 'admin'.
            """

            if isHyperDB(arg):
                # arg is a roundup database instance
                # no need to open the database
                self._instance = None
                self._db       = arg

            else:
                if isinstance(arg, types.StringType):
                    # arg is a string instance
                    # we assume it is a valid tracker_home location
                    # and we will use it to open the database
                    tracker_home = arg

                elif arg is None:
                    # arg is None -> no argument was passed
                    # user TRACKER_HOME environment variable
                    # to open the database
                    tracker_home = os.getenv('TRACKER_HOME')

                else:
                    # and what now?
                    raise TypeError, \
                    "No 'hyperdb' to access"

                if not user:
                    user = ('admin',)

                self._instance = instance.open(tracker_home)
                self._db       = self._instance.open(user[0])

            # create storage for classes
            self._classes = {}
            for classname in self._db.getclasses():
                self._classes[classname] = {}

        def __del__(self):
            """\
            Closes the RoundUp database if it was opened by the wrapper
            it selves (initiated with an 'arg' of type 2 or 3)
            """

            # restore possible turned off detectors
            for classname in self._db.getclasses():
                # restore auditors
                if self._classes[classname].has_key('auditors'):
                    self._db.getclass(classname).auditors = \
                                        self._classes[classname]['auditors']

                # restore reactors
                if self._classes[classname].has_key('reactors'):
                    self._db.getclass(classname).reactors = \
                                        self._classes[classname]['reactors']

            # make sure we close the database if we did open it ourselves
            if not self._instance is None:
                self._db.close()

        def __repr__(self):
            """\
            Slightly more useful representation
            """

            return '''<dbwrapper.DBWrapper '%s'>'''%self._db

        def __str__(self):
            """\
            Slightly more useful representation
            """

            return self.__repr__()

        def __getattr__(self, attr):
            """\
            Grant access to ClassWrapper objects of all
            classes like they are member of this class.

            It also creates transparency to hyperdb.Database
            members if they aren't defined in the DBWrapper
            class.
            """

            # If 'attr' is a member, then access that member
            if self.__dict__.has_key(attr):
                value = self.__dict__[attr]

            # If 'attr' is a hyperdatabse class, wrap it in
            # a 'ClassWrapper'
            elif attr in self._db.getclasses():
                value = ClassWrapper(self, attr)

            # If 'attr' is a member of class 'Database', return
            # the address of that member to create the transparency
            else:
                value = getattr(self._db, attr)

            return value

        def __setattr__(self, attr, value):
            """\
            Prevent that attributes which refer to ClassWrapper
            objects are overwritten by a wrong assign statement.
            """

            # If 'attr' is a member or member to be,
            # then access that member
            if attr in ['_instance', '_db', '_classes'] \
            or self.__dict__.has_key(attr):
                self.__dict__[attr] = value

            # If 'attr' is a hyperdatabse class, wrap it in
            # a 'ClassWrapper'
            elif attr in self._db.getclasses():
                # Check if it isn't the result of 'ClassWrapper.__iadd__'
                if not isClass(value) or value._cl.classname != attr:
                    raise ValueError, \
                    "'%s' can't be assigned any value"%attr

            # If 'attr' is a member of class 'Database', return
            # the address of that member to create the transparency
            else:
                setattr(self._db, attr, value)

        def __iter__(self):
            """\
            Create iteration of all classes as ClassWrapper's.
            """

            return iter( [ClassWrapper(self, classname) \
                          for classname in self._db.getclasses()] )

        def getclass(self, classname):
            """\
            Get the ClassWrapper object representing a particular class.
            """

            return self.__getattr__(classname)

    class ClassWrapper:
        """\
        A wrapper around a RoundUp hyperdb.Class.

        Introduction
        ============
        In most cases you don't need to initialize a
        ClassWrapper because the DBWrapper will do that
        for you where needed. But sometimes it can be
        more convenient to initialize one yourself.

        Initialization
        ==============
        There are three methods to initialize a ClassWrapper:
        1.  clw = dbwrapper.ClassWrapper(dbw, <class name>)
        A wrapper will be created for class <class name>.

        2.  clw = dbwrapper.ClassWrapper(dbw, <hyperdb.Class>)
        A wrapper will be created for the hyperdb.Class object.

        3.  clw = dbwrapper.ClassWrapper(dbw, <ClassWrapper>)
        A wrapper will be created for the ClassWrapper object.
        This won't make much sense because the wrapper was already there.

        Usage
        =====
        The wrapper behaves like a dictionary, meaning you can access the
        nodes by indexing them as dictionary items.
        Syntax:
        <DBWrapper>.<class name>[<key>|<nodeid>|<NodeWrapper>]
        As index you can either use the node id as integer or string,
        a key value (if the class has a key column) or an instance of
        NodeWrapper.
        The returned value is a NodeWrapper object.

        Examples:
        dbw.status['chatting']
        dbw.issue[1]
        dbw.priority['3']
        dbw.msg[nodew]

        Some of the methods in this class do have a "key"
        parameter. In all these cases this parameter can
        have one of the next types:
        1 - an integer
        The 'key' will be treated as the id of the node
        and used for the indexing.
        2 - a string
        The 'key' will be treated as key argument of the
        node and used to lookup the node id.
        If the lookup action raises a KeyError and 'key'
        only contains digits, the integer value of 'key'
        will be used as the node id.
        In both cases the node id is used for the indexing.
        3 - an instance of NodeWrapper
        The node id will be obtained from the NodeWrapper
        class and used for the indexing.

        Transparency
        ============
        Members defined in the hyperdb.Class, but not
        defined in this 'ClassWrapper' class can still
        be used as if they are member of this class.
        """

        class RegExpClass:
            """\
            A class with regular expression methods that will filter nodes
            from hyperdb.Class. The methods in this class are one to one
            with the regular expression methods in python module 're'.
            """

            def __init__(self, owner):
                """\
                Initialize class

                "owner" must be of type 'ClassWrapper'
                """

                import re

                if not isClass(owner):
                    raise TypeError, \
                    "Owner object isn't an instance of 'ClassWrapper'"

                self._clw = owner
                self.__re = re

            def __repr__(self):
                """\
                Slightly more useful representation
                """

                return '''<dbwrapper.ClassWrapper.RegExpClass '%s'>'''% \
                       self._clw._cl

            def __str__(self):
                """\
                Slightly more useful representation
                """

                return self.__repr__()

            def __inner_re(self, re_func, column, nodelist):
                """\
                Handles the main reg. expression search.

                "re_func" must contain one of the search methods
                from standard python module 're'.

                "column" can be used to perform a search on an
                other column than the column returned by
                ClassWrapper.getlabel().

                The return value is a list object containing tuples
                pairs.

                The first index of each tuple contains a NodeWrapper
                object to a node matching the search.

                The second index of the tuples holds the result object
                of the corresponding regular expression method passed
                in "re_func".

                "nodelist" can be used to limit the search within the list
                of nodes. 'nodelist' may be a list of ids or any list created
                by any of the search methods in this module.
                """

                if not column:
                    column = self._clw.getlabel()

                if nodelist is None:
                    list = self._clw._cl.list()
                else:
                    list = ListWrapper(self._clw, nodelist).getnodeids()

                result = []
                for nodeid in list:
                    value = self._clw._cl.get(nodeid, column)

                    if value is None:
                        value = ''

                    res = re_func(value)

                    if res:
                        result.append( (NodeWrapper(self._clw, nodeid, False), res) )

                return ListWrapper(self._clw, result)

            def match(self, pattern, flags=0, column=None, nodelist=None):
                """\
                Find all nodes in the class that have a result when
                applying the "pattern" at the start of the string in
                "column", returning a list object with tuples pairs.

                The first index of each tuple contains a
                dbwraper.NodeWrapper object of a node that matches
                with "pattern".

                The second index in the tuples holds the match object.

                "flags" may have the same flags as used/defined in
                python module 're'. These flags are also available
                as members of module 'dbwrapper'.
                (e.g. dbwrapper.IGNORECASE)

                "column" can be used to perform a search on an
                other column than the column returned by
                ClassWrapper.getlabel().

                "nodelist" can be used to limit the search within the list
                of nodes. 'nodelist' may be a list of ids or any list created
                by any of the search methods in this module.
                """

                reg = self.__re.compile(pattern, flags)

                return self.__inner_re(reg.match, column, nodelist)

            def search(self, pattern, flags=0, column=None, nodelist=None):
                """\
                Find all nodes in the class that have a result when
                scanning through the string in "column" looking for a
                match to the "pattern", returning a list object with
                tuples pairs.

                The first index of each tuple contains a
                dbwraper.NodeWrapper object of a node that matches
                with "pattern".

                The second index in the tuples holds the search object.

                "flags" may have the same flags as used/defined in
                python module 're'. These flags are also available
                as members of module 'dbwrapper'.
                (e.g. dbwrapper.IGNORECASE)

                "column" can be used to perform a search on an
                other column than the column returned by
                ClassWrapper.getlabel().

                "nodelist" can be used to limit the search within the list
                of nodes. 'nodelist' may be a list of ids or any list created
                by any of the search methods in this module.
                """

                reg = self.__re.compile(pattern, flags)

                return self.__inner_re(reg.search, column, nodelist)

            def findall(self, pattern, flags=0, column=None, nodelist=None):
                """\
                Return all nodes in class that have "pattern" matches
                in the strings in "column". Returned is a list of tuple
                pairs.

                The first tuple index contains a NodeWrapper object of
                a matching node.

                The second tuple index contains a list of all
                non-overlapping matches in the node's "column" string.

                If one or more groups are present in the "pattern", the
                second tuple index will contain a list of groups; this
                will be a list of tuples if the "pattern" has more than
                one group.

                "flags" may have the same flags as used/defined in
                python module 're'. These flags are also available
                as members of module 'dbwrapper'.
                (e.g. dbwrapper.IGNORECASE)

                "column" can be used to perform a search on an
                other column than the column returned by
                ClassWrapper.getlabel().

                "nodelist" can be used to limit the search within the list
                of nodes. 'nodelist' may be a list of ids or any list created
                by any of the search methods in this module.
                """

                reg = self.__re.compile(pattern, flags)

                return self.__inner_re(reg.findall, column, nodelist)

        def __init__(self, parent, cl):
            """\
            Initiate a wrapper around a hyperdb.Class.

            "parent" is the DBWrapper that owns this ClassWrapper.

            "cl" is used to specify the class.
            It can have three different types:
                1 - an instance of a hyperdb.Class.
                2 - an instance of a ClassWrapper.
                3 - the class name.
            """

            if not isDB(parent):
                raise TypeError, \
                "Parent object isn't an instance of 'DBWrapper'"

            self._dbw = parent
            self._db  = parent._db

            if isHyperDBClass(cl):
                self._cl = cl
            elif isClass(cl):
                self._cl = cl._cl
            else:
                self._cl = self._db.getclass(cl)

            self.re = self.RegExpClass(self)

            self.__lastid = None

        def __repr__(self):
            """\
            Slightly more useful representation
            """

            return '''<dbwrapper.ClassWrapper '%s'>'''%self._cl

        def __str__(self):
            """\
            Slightly more useful representation
            """

            return self.__repr__()

        def __call__(self, *nodeids):
            """\
            Converts a list (or single id) of hyperdb node ids to a list of
            dbwrapper compliant nodes.
            """

            result = ListWrapper(self)

            if nodeids:
                if len(nodeids) == 1 \
                and (isinstance(nodeids[0], types.ListType) \
                or isinstance(nodeids[0], types.TupleType)):
                    nodeids = nodeids[0]

                for nodeid in nodeids:
                    result.append(NodeWrapper(self, int(nodeid)))

            return result

        def __getattr__(self, attr):
            """\
            Create transparency to hyperdb.Class members
            if they aren't defined in the ClassWrapper
            class.
            """

            if self.__dict__.has_key(attr):
                value = self.__dict__[attr]
            else:
                value = getattr(self._cl, attr)

            return value

        def __len__(self):
            """\
            Number of nodes in this class (excluding retired nodes).
            """

            return len(self._cl.list())

        def __contains__(self, key):
            """\
            Determine if the class has a given node.
            """

            return self.has_key(key)

        def __getitem__(self, key):
            """\
            Grant get access to all class nodes by indexing them as
            dictionary items and wrapping them in a NodeWrapper.
            """

            return NodeWrapper(self, key, False)

        def __setitem__(self, key, columns):
            """\
            Grant set access to all class nodes by indexing them as
            dictionary items and wrapping them in a NodeWrapper.

            "columns" must be a dictionary containing class properties
            as the keys. The values in the dictionary are used as
            values for the properties.
            """

            if isinstance(columns, types.DictType):
                node = NodeWrapper(self, key, False)

                for key in columns.keys():
                    setattr(node, key, columns[key])

            else:
                raise TypeError, \
                "Item can only be assigned a dictionary"

        def __delitem__(self, key):
            """\
            Retire a class node by using the python 'del' command and
            indexing the node as dictionary item.
            """

            self._cl.retire( str(self.getid(key)) )

        def __iadd__(self, columns):
            """\
            Add a new node to the class.

            "columns" must be a dictionary containing class properties
            as the keys. The values in the dictionary are used as
            values for the properties.
            """

            if isinstance(columns, types.DictType):
                self.create(**columns)
            else:
                raise TypeError, \
                "Item can only be assigned a dictionary"

            return self

        def __iter__(self):
            """\
            Create iteration of all nodes as NodeWrapper's.
            """

            return iter(self.list())

        def getid(self, key):
            """\
            Get the node id of the node that matches the key.

            "key" can have three different types:
                1 - an integer
                    The 'key' will be treated as the id of the node
                    and used for the indexing.
                2 - a string
                    The 'key' will be treated as key argument of the
                    node and used to lookup the node id.
                    If the lookup action raises a KeyError and 'key'
                    only contains digits, the integer value of 'key'
                    will be used as the node id.
                    In both cases the node id is used for the indexing.
                3 - an instance of NodeWrapper
                    The node id will be obtained from the NodeWrapper
                    class and used for the indexing.
            """

            if isinstance(key, types.IntType):
                nodeid = key

            elif isNode(key):
                nodeid = key._id

            else:
                try:
                    nodeid = self._cl.lookup(key)

                except (KeyError, TypeError):
                    if isinstance(key, types.StringType) \
                    and key.isdigit() \
                    and self._cl.hasnode(key):
                        nodeid = int(key)

                    else:
                        raise IndexError, \
                        "Class '%s' doesn't have a node that can be \
                        indexed with '%s'"%(self._cl.classname, key)

                else:
                    nodeid = int(nodeid)

            return nodeid

        def auditors(self, enable):
            """\
            Enable/Disable auditors for this class.
            """

            p = self._dbw._classes[self._cl.classname]

            if enable:
                if p.has_key('auditors'):
                    self._cl.auditors = p['auditors']
                    del p['auditors']
            else:
                if not p.has_key('auditors'):
                    p['auditors'] = dict(self._cl.auditors)
                    self._cl.auditors = {
                                            'create': [],
                                            'set': [],
                                            'retire': [],
                                            'restore': []
                                        }

        def reactors(self, enable):
            """\
            Enable/Disable reactors for this class.
            """

            p = self._dbw._classes[self._cl.classname]

            if enable:
                if p.has_key('reactors'):
                    self._cl.reactors = p['reactors']
                    del p['reactors']
            else:
                if not p.has_key('reactors'):
                    p['reactors'] = dict(self._cl.reactors)
                    self._cl.reactors = {
                                            'create': [],
                                            'set': [],
                                            'retire': [],
                                            'restore': []
                                        }

        def setlabel(self, label=None):
            """\
            Set an alternative label to be used as label property.

            If label is None, the result of hyperdb 'labelprop()' will be
            used as label.
            """

            if not label is None:
                props = self._cl.getprops()

                if label in props.keys():
                    if isinstance(props[label], hyperdb.String):
                        self._dbw._classes[self._cl.classname]['label'] = label
                    else:
                        raise TypeError, \
                        "'label' should be a string type column"

                else:
                    raise AttributeError, \
                    "Class '%s' doesn't have a property '%s'"% \
                    (self._cl.classname, label)

            else:
                del self._dbw._classes[self._cl.classname]['label']

        def getlabel(self):
            """\
            Get the label to be used as label property.

            If an alternative label is set, it will be returned.
            """

            p = self._dbw._classes[self._cl.classname]

            return p.get('label', self._cl.labelprop())

        def create(self, **propvalues):
            """\
            Create a new node of this class and return its id.

            The keyword arguments in 'propvalues' map property names to values.

            The values of arguments must be acceptable for the types of their
            corresponding properties or a TypeError is raised.

            If this class has a key property, it must be present and its value
            must not collide with other key strings or a ValueError is raised.

            Any other properties on this class that are missing from the
            'propvalues' dictionary are set to None.

            If an id in a link or multilink property does not refer to a valid
            node, an IndexError is raised. Valid are node ids (as integer or
            as string), key values (for links to classes that have a key column)
            or a NodeWrapper object.
            """

            props = self._cl.getprops()

            creator = {}
            for column, value in propvalues.items():
                if props.has_key(column):
                    if isinstance(props[column], hyperdb.Link):
                        cl = self._db.getclass(props[column].classname)
                        wrapper = ClassWrapper(self._dbw, cl)
                        creator[column] = str(wrapper.getid(value))

                    elif isinstance(props[column], hyperdb.Multilink):
                        cl = self._db.getclass(props[column].classname)
                        wrapper = ClassWrapper(self._dbw, cl)

                        if not isinstance(value, types.ListType) \
                        and not isinstance(value, types.TupleType):
                            value = [value]

                        creator[column] = [ str(wrapper.getid(key)) for key in value ]

                    elif isinstance(props[column], hyperdb.Interval) \
                    and not isinstance(value, date.Interval):
                        creator[column] = date.Interval(value)

                    elif isinstance(props[column], hyperdb.Date) \
                    and not isinstance(value, date.Date):
                        creator[column] = date.Date(value)

                    else:
                        creator[column] = value

                else:
                    creator[column] = value

            self.__lastid = self._cl.create(**creator)

            return self.__lastid

        def get(self, key, propname):
            """\
            Get the value of a property on an existing node of this class.

            'propname' must be the name of a property of this class or a
            KeyError is raised.
            """

            node = self.__getitem__(key)

            return getattr(node, propname)

        def set(self, key, **propvalues):
            """\
            Modify a property on an existing node of this class.

            Each key in 'propvalues' must be the name of a property of this
            class or a KeyError is raised.

            All values in 'propvalues' must be acceptable types for their
            corresponding properties or a TypeError is raised.

            If the value of the key property is set, it must not collide with
            other key strings or a ValueError is raised.

            If the value of a Link or Multilink property contains an invalid
            node id, a ValueError is raised.
            """

            self.__setitem__(key, propvalues)

        def list(self):
            """\
            Return a list of NodeWrappers of the active nodes in this class.
            """

            id_list = [ int(nodeid) for nodeid in self._cl.list() ]
            id_list.sort()

            return ListWrapper(self,
            [ NodeWrapper(self, int(nodeid), False) for nodeid in id_list ] )

        def filter(self, search_matches, filterspec, sort=(None,None),
                   group=(None,None), nodelist=None):
            """\
            Return a list of the NodeWrappers of the active nodes in this class
            that match the 'filter' spec, sorted by the group spec and then the
            sort spec.

            "filterspec" is {propname: value(s)}
            'value(s)' may be any node id as integer or string or a key value
            (for classes that have a key column).

            "sort" and "group" are (dir, prop) where dir is '+', '-' or None
            and prop is a prop name or None

            "search_matches" is {nodeid: marker}

            "nodelist" can be used to limit the search within the list
            of nodes. 'nodelist' may be a list of ids or any list created
            by any of the search methods in this module.

            The filter must match all properties specified - but if the
            property value to match is a list, any one of the values in the
            list may match for that property to match.
            """

            props = self._cl.getprops()

            for column in filterspec.keys():
                if isinstance(props[column], hyperdb.Link):
                    cl     = self._db.getclass(props[column].classname)
                    parent = ClassWrapper(self._dbw, cl)
                    node   = NodeWrapper(parent, filterspec[column])
                    filterspec[column] = str(node._id)

                elif isinstance(props[column], hyperdb.Multilink):
                    cl     = self._db.getclass(props[column].classname)
                    parent = ClassWrapper(self._dbw, cl)
                    list   = ListWrapper(parent, filterspec[column])
                    filterspec[column] = list.getnodeids()

            if not nodelist is None:
                filterspec['id'] = ListWrapper(self, nodelist).getnodeids()

            return ListWrapper(self,
            [ NodeWrapper(self, int(nodeid), False) for nodeid in self._cl.filter(search_matches, filterspec, sort, group) ])

        def find(self, **propspec):
            """\
            Get the NodeWrappers of items in this class which link to the given
            items.

            'propspec' consists of keyword args propname=itemid or
                       propname=key
            'propname' must be the name of a property in this class, or a
                       KeyError is raised.  That property must be a Link or
                       Multilink property, or a TypeError is raised.

            "propspec" can also be passed a list with results from anu of the
            search methods in this wrapper (also regular expression search
            results can be passed 1-to-1).

            Any item in this class whose 'propname' property links to any of the
            itemids will be returned. Used by the full text indexing, which knows
            that "foo" occurs in msg1, msg3 and file7, so we have hits on these
            issues:
                db.issue.find(messages=[1,3], files=[7])
            """

            for column, values in propspec.items():
                if isinstance(values, ListWrapper):
                    values.simple()

                elif not isinstance(values, types.ListType) \
                and not isinstance(values, types.TupleType):
                    values = [values]

                links = {}
                for key in values:
                    links[str(self.getid(key))] = 1

                propspec[column] = dict(links)

            id_list = [ int(nodeid) for nodeid in self._cl.find(**propspec) ]
            id_list.sort()

            return ListWrapper(self,
            [ NodeWrapper(self, nodeid, False) for nodeid in id_list ])

        def newid(self):
            """\
            Generate a new id for this class.
            """

            return self._db.newid(self._cl.classname)

        def newnodeid(self):
            """\
            Return the id of the last created node.
            """

            return self.__lastid

        def getnodeids(self, retired=None):
            """\
            Retrieve all NodeWrappers of the nodes for a particular Class.

            if 'retired' is True, all retired nodes will be returned.
            """

            id_list = [ int(nodeid) for nodeid in self._cl.getnodeids(retired) ]
            id_list.sort()

            return id_list

        def hasnode(self, key):
            """\
            Determine if the class has a given node (also true on retired nodes).
            """

            try:
                nodeid = self.getid(key)

            except:
                label = self._cl.getkey()

                if label \
                and isinstance(key, types.StringType):
                    value = None

                    for nodeid in self._cl.getnodeids(True):
                        value = self._cl.get(nodeid, label)

                        if value == key:
                            break

                    if not value is None \
                    and value == key:
                        result = True
                    else:
                        result = False

                else:
                    result = False

            else:
                result = self._cl.hasnode( str(nodeid) )

            return result

        def lookup(self, keyvalue):
            """\
            Locate a particular node by its key property and return it in a
            NodeClass object.

            If this class has no key property, a TypeError is raised.  If the
            'keyvalue' matches one of the values for the key property among
            the nodes in this class, the matching node's id is returned;
            otherwise a KeyError is raised.
            """

            return NodeWrapper(self, int(self._cl.lookup(keyvalue)), False)

        def search(self, value, column=None, nodelist=None):
            """\
            Lookup all nodes that have a 100% match with 'value' and
            return them in a list.

            "value" is the pattern to look for.

            "column" can be set to any string property. A TypeError is
            raised if 'column' isn't a string property. The label column
            will be used if 'column' is omitted.

            "nodelist" can be used to limit the search within the list
            of nodes. 'nodelist' may be a list of ids or any list created
            by any of the search methods in this module.
            """

            if not isinstance(value, types.StringType):
                raise TypeError, \
                "'value' should be a string"

            if not column:
                column = self.getlabel()

            props = self._cl.getprops()
            if not isinstance(props[column], hyperdb.String):
                raise TypeError, \
                "'column' should be a string type column"

            if nodelist is None:
                list = self._cl.filter(None, {column:value})
            else:
                list = ListWrapper(self, nodelist).getnodeids()

            list.sort()

            value = value.lower()
            result = []
            for nodeid in list:
                if self._cl.get(nodeid, column).lower() == value:
                    result.append( NodeWrapper(self, int(nodeid), False) )

            return ListWrapper(self, result)

        def is_retired(self, key):
            """\
            Return true if the node is retired.
            """

            return self._cl.is_retired( str(self.getid(key)) )

        def safeget(self, key, propname, default=None):
            """\
            Safely get the value of a property on an existing node of this class.

            Return 'default' if the node doesn't exist.
            """

            value = getattr(NodeWrapper(self, key), propname, default)

            if value is None \
            or (isinstance(value, types.ListType) and not value):
                value = default

            return value

        def has_key(self, key):
            """\
            Determine if the class has a given node, but then in
            a dictionary known method (false on retired nodes).
            """

            try:
                nodeid = self.getid(key)

            except (KeyError, TypeError, IndexError):
                result = False

            else:
                result = str(nodeid) in self._cl.list()

            return result

        def keys(self):
            """\
            Get a list with all key values of this class. If no key is defined
            for this class, a list of ids will be returned.
            """

            key = self._cl.getkey()

            if key:
                keys = [ self._cl.get(nodeid, key) for nodeid in self._cl.list() ]
            else:
                keys = [ int(nodeid) for nodeid in self._cl.list() ]

            return keys

        def items(self):
            """\
            Get a list with tuples containing all keys with there NodeWrapper kept
            as pairs.
            """

            return [ (key, NodeWrapper(self, key, False)) for key in self.keys() ]

        def pop(self, key):
            """\
            Get node 'key' and remove (retire) it from this class.
            """

            value = self.__getitem__(key)
            self.__delitem__(key)

            return value

        def resurrect(self, key):
            """\
            Bring any deleted (retired) node back alive.
            """

            retireds = self._cl.getnodeids(retired=1)

            if isinstance(key, types.IntType):
                nodeid = str(key)
            else:
                prop = self._cl.getkey()
                nodeid = None
                for retired_id in retireds:
                    value = self._cl.get(retired_id, prop)

                    if value == key:
                        nodeid = retired_id
                        break;

            if nodeid and nodeid in retireds:
                self._cl.restore(nodeid)
            else:
                raise KeyError, \
                "No retired node '%s' in class '%s'"%(key, self._cl.classname)

    class NodeWrapper:
        """\
        A wrapper around one specific node in a
        RoundUp hyperdb.Class.

        Introduction
        ============
        In most cases you don't need to initialize a
        NodeWrapper because the ClassWrapper will do that
        for you where needed. But sometimes it can be
        more convenient to initialize one yourself.

        Initialization
        ==============
        There are three methods to initialize a NodeWrapper:
        1.  nodew = dbwrapper.NodeWrapper(clw, <nodeid>)
            A wrapper will be created for node <nodeid>.
            <nodeid> may be either an integer or a string.

        2.  nodew = dbwrapper.NodeWrapper(clw, <key>)
            For classes that have a key column, a key value can be
            used to create a wrapper for the node indexed by <key>.

        3.  nodew = dbwrapper.NodeWrapper(clw, <NodeWrapper>)
            A wrapper will be created for the NodeWrapper object.
            This won't make much sense because the wrapper was already there.

        4.  nodew = dbwrapper.NodeWrapper(clw, None)
            This is a phantom object and can be used to test against None.
            This might be convenient in auditors where there isn't a node
            if they are fired by the create event.

        Usage
        =====
        All node properties can be accessed as if they are members of this
        wrapper object.
        The syntax is:
            <NodeWrapper>.<property>
        The returned type depends on the requested property. In most cases
        a simple type is returned (like string and integer), but in case of
        a linked property, a NodeWrapper object will be returned for the
        node to which the property is linked. In case of multi linked
        properties, a ListWrapper object will be returned containing
        NodeWrapper objects for all nodes to which the property is linked.

        Examples:
            nodew.title
            nodew.name
            nodew.id (read only)
            nodew.nodeid (read only) (same as str(nodew.id))

        It is also possible to set a property by accessing them as wrapper
        members.
        The syntax is:
            <NodeWrapper>.<property> = <value>
        The assigned value depends on the property type. In case of the
        simple types (String, Number, Boolean) value will be of the
        corresponding Python type (string, integer, boolean).
        In case of a linked type, the value can have more than one type:
        1. node id as integer
        2. node id as string
        3. key value (for classes that have a key column)
        4. NodeWrapper instance
        In case of multi linked type, value must be a list (either Python's
        list object or dbwrapper's ListWrapper object) containing node
        specifications like mentioned for link type properties (see above).

        Examples:
            nodew.title  = 'My new title'
            nodew.status = 'chatting'
            nodew.topic  = ['Keyword 1', 'Keyword 2', 3, '4']

        In case of multi linked properties, new links to other nodes can be
        created by simply adding them to the property. Even so can links be
        removed by subtracting them from the property.

        Examples:
            Add:
                nodew.topic += 'Keyword 3'
                nodew.topic += ['Keyword 4', 'Keyword 5']
            Remove:
                nodew.topic -= 'Keyword 3'
                nodew.topic -= ['Keyword 4', 'Keyword 5']
        """

        def __init__(self, parent, key, readonly=True):
            """\
            Initiate a wrapper around a hyperdb.Class for one
            specific node.

            "parent" is the ClassWrapper that owns this NodeWrapper.

            "key" identifies the node.
            It can have one of the next types:
                1 - an integer
                    The 'key' will be treated as the id of the node
                    and used for the indexing.
                2 - a string
                    The 'key' will be treated as key argument of the
                    node and used to lookup the node id.
                    If the lookup action raises a KeyError and 'key'
                    only contains digits, the integer value of 'key'
                    will be used as the node id.
                    In both cases the node id is used for the indexing.
                3 - an instance of NodeWrapper
                    The node id will be obtained from the NodeWrapper
                    class and used for the indexing.
                4 - None type
                    This is a special option implemented to make it
                    easy usable in detectors.

            "readonly" can be set to False to allow set operations
            on any of the properties of this node.
            """

            if not isClass(parent):
                raise TypeError, \
                "Parent object isn't an instance of 'ClassWrapper'"

            self._clw      = parent
            self._db       = parent._db
            self._cl       = parent._cl
            self._readonly = readonly

            if key is None:
                self._id   = None
            else:
                self._id   = parent.getid(key)

                if not self._cl.hasnode(str(self._id)):
                    raise IndexError, \
                    "Class '%s' doesn't have a node with id '%s'"% \
                    (self._cl.classname, self._id)

        def __repr__(self):
            """\
            Return a resolved property.
            """

            import re

            if self._id is None:
                value = "<dbwrapper.NodeWrapper 'Phantom'>"

            else:
                value = self.plain()

                if isinstance(value, types.StringType):
                    value = "'%s'"%re.sub("'", "\\'", value)

            return str(value)

        def __str__(self):
            """\
            Slightly more useful representation
            """

            if self._id is None:
                value = "<dbwrapper.NodeWrapper 'Phantom'>"

            else:
                value = str(self.plain())

            return value

        def __getattr__(self, attr):
            """\
            Grant easy get access to all properties of this node.
            """

            # is it a NoneClass member?
            if self.__dict__.has_key(attr):
                value = self.__dict__[attr]

            elif attr == 'id':
                value = self._id

            elif attr == 'nodeid':
                if self._id is None:
                    value = None
                else:
                    value = str(self._id)

            elif attr in ['properties', 'values']:
                if self._id is None:
                    value = None

                else:
                    internals = ('id', 'actor', 'activity', 'creator', 'creation')

                    props = self._cl.getprops()

                    value = {}
                    for prop in props.keys():
                        if not prop in internals:
                            data = self._cl.get(str(self._id), prop)

                            if not data is None \
                            and (not isinstance(data, types.ListType) or data):
                                if attr == 'properties':
                                    if isinstance(props[prop], hyperdb.Link):
                                        if data:
                                            cl = self._db.getclass(props[prop].classname)
                                            key = cl.getkey()

                                            if key:
                                                value[prop] = cl.get(data, key)
                                            else:
                                                value[prop] = int(data)

                                    elif isinstance(props[prop], hyperdb.Multilink):
                                        cl = self._db.getclass(props[prop].classname)
                                        key = cl.getkey()

                                        if key:
                                            value[prop] = [ cl.get(nodeid, key) for nodeid in data ]
                                        else:
                                            value[prop] = [ int(nodeid) for nodeid in data ]

                                    elif not isinstance(props[prop], hyperdb.String):
                                        value[prop] = str(data)

                                    else:
                                        value[prop] = data

                                else:
                                    if isinstance(props[prop], hyperdb.Link):
                                        if data:
                                            value[prop] = int(data)

                                    elif isinstance(props[prop], hyperdb.Multilink):
                                        value[prop] = [ int(nodeid) for nodeid in data ]

                                    else:
                                        value[prop] = data

            else:
                if self._id is None:
                    raise ValueError, \
                    "Phantom nodes do not have an attribute '%s'"%attr

                else:
                    props = self._cl.getprops()

                    # is it a class property?
                    if props.has_key(attr):
                        if isinstance(props[attr], hyperdb.Link):
                            elementid = self._cl.get(str(self._id), attr)

                            if elementid:
                                cl = self._db.getclass(props[attr].classname)
                                parent = ClassWrapper(self._clw._dbw, cl)
                                value = NodeWrapper(parent, elementid)
                            else:
                                value = None

                        elif isinstance(props[attr], hyperdb.Multilink):
                            elements = self._cl.get(str(self._id), attr)
                            cl = self._db.getclass(props[attr].classname)
                            parent = ClassWrapper(self._clw._dbw, cl)

                            value = ListWrapper(parent, True, elements)

                        else:
                            if attr == 'id':
                                value = int(self._cl.get(str(self._id), attr))
                            else:
                                value = self._cl.get(str(self._id), attr)

                    # I don't know you
                    else:
                        raise AttributeError, \
                        "'%s' is not a member of object 'NodeWrapper'"%attr

            return value

        def __setattr__(self, attr, value):
            """\
            Grant easy set access to all properties of this node.

            If "read only" than all set operations will raise exception
            "ValueError".
            """

            # test if we are a member of NodeClass or if we will become
            # a member of NodeClass
            if attr in ['_clw', '_db', '_cl', '_id', '_readonly'] \
            or self.__dict__.has_key(attr):
                self.__dict__[attr] = value

            elif attr in ['properties', 'values']:
                raise ValueError, \
                "'properties' can't be assigned any value"

            else:
                if self._id is None:
                    raise ValueError, \
                    "Phantom nodes do not have an attribute '%s'"%attr

                else:
                    props = self._cl.getprops()

                    # is it a class property?
                    if props.has_key(attr):
                        if not self._readonly:
                            if isinstance(props[attr], hyperdb.Link):
                                cl = self._db.getclass(props[attr].classname)
                                parent = ClassWrapper(self._clw._dbw, cl)

                                value = str(parent.getid(value))

                            elif isinstance(props[attr], hyperdb.Multilink):
                                cl = self._db.getclass(props[attr].classname)
                                parent = ClassWrapper(self._clw._dbw, cl)

                                if isinstance(value, types.StringType):
                                    value = [value]

                                value = ListWrapper(parent, value)

                                value = [ str(nodeid) for nodeid in value.getnodeids() ]

                            elif isinstance(props[attr], hyperdb.Interval) \
                            and not isinstance(value, date.Interval):
                                value = date.Interval(value)

                            elif isinstance(props[attr], hyperdb.Date) \
                            and not isinstance(value, date.Date):
                                value = date.Date(value)

                            self._cl.set(str(self._id), **{attr:value})

                        else:
                            raise ValueError, \
                            "This node is marked as readonly and can't \
                            be assigned and value"

                    # not a member or class property
                    else:
                        raise AttributeError, \
                        "'%s' is not a member of object 'NodeWrapper'"%attr

        def __iter__(self):
            """\
            Create iteration of all properties in this node. The iteration contains
            tuples with the property name on the first index and the value on the
            second index. For linked properties an instance to a NodeWrapper is
            used and not the value.
            """

            return iter( [(prop, self.__getattr__(prop)) for prop in self._cl.getprops()] )

        def __hash__(self):
            """\
            Node identifier.
            """

            return self._id

        def __eq__(self, other):
            """\
            Are these nodes equal?
            """

            same_class = isinstance(other, NodeWrapper) \
                         and self._cl.classname == other._cl.classname

            if not other is None:
                other = self._clw.getid(other)

            return same_class and (other == self._id)

        def __ne__(self, other):
            """\
            Are these nodes different?
            """

            return not self.__eq__(other)

        def __nonzero__(self):
            """\
            Truth value testing.
            """

            return not self._id is None

        def plain(self, property=None):
            """\
            Return the property in a nice way. Links are resolved.
            """

            if not property is None:
                value = self.__getattr__(property)

                if isinstance(value, types.ListType):
                    list = []
                    for one in value:
                        label = one._clw.getlabel()

                        if isNode(one):
                            list.append( getattr(one, label) )
                        else:
                            list.append(one)
                    value = ', '.join( [str(item) for item in list] )

                elif isinstance(value, types.NoneType):
                    value = ''

                elif isNode(value):
                    label = value._clw.getlabel()
                    value = getattr(value, label)
            else:
                label = self._clw.getlabel()
                value = self.__getattr__(label)

            if value is None:
                value = ''

            return value

        def get(self, propname):
            """\
            Get the value of a property on an existing node of this class.

            'propname' must be the name of a property of this class or a
            KeyError is raised.
            """

            if self._id is None:
                raise NotImplementedError, \
                "This method is not implemented for phantom nodes"

            else:
                return self._clw.get(self._id, propname)

        def set(self, **propvalues):
            """\
            Modify a property on an existing node of this class.

            Each key in 'propvalues' must be the name of a property of this
            class or a KeyError is raised.

            All values in 'propvalues' must be acceptable types for their
            corresponding properties or a TypeError is raised.

            If the value of the key property is set, it must not collide with
            other key strings or a ValueError is raised.

            If the value of a Link or Multilink property contains an invalid
            node id, a ValueError is raised.
            """

            if self._id is None:
                raise NotImplementedError, \
                "This method is not implemented for phantom nodes"

            else:
                self._clw.set(self._id, propvalues)

        def is_retired(self):
            """\
            Return true if the node is retired.
            """

            if self._id is None:
                raise NotImplementedError, \
                "This method is not implemented for phantom nodes"

            else:
                return self._clw.is_retired(self._id)

        def safeget(self, propname, default=None):
            """\
            Safely get the value of a property on an existing node of this class.

            Return 'default' if the node doesn't exist.
            """

            if self._id is None:
                raise NotImplementedError, \
                "This method is not implemented for phantom nodes"

            else:
                return self._clw.safeget(self._id, propname, default)

        def pop(self):
            """\
            Get node 'key' and remove (retire) it from this class.
            """

            if self._id is None:
                raise NotImplementedError, \
                "This method is not implemented for phantom nodes"

            else:
                return self._clw.pop(self._id)

        def resurrect(self):
            """\
            Bring any deleted (retired) node back alive.
            """

            if self._id is None:
                raise NotImplementedError, \
                "This method is not implemented for phantom nodes"

            else:
                self._clw.resurrect(self._id)

    class ListWrapper(types.ListType):
        """\
        List object to give easier access to NodeWrapper's.

        Introduction
        ============
        This wrapper almost fully supports all available operations for
        Python's list object. The difference is that it is meant to be used
        in ClassWrapper's and NodeWrapper's.

        Initialization
        ==============
        The wrapper can be initialized as follows:
        1.  lw = dbwrapper.ClassWrapper(<ClassWrapper>)
            This is the simplest form. The wrapper will be created to hold
            only NodeWrapper objects from class <ClassWrapper>.
            The list will be empty.

        2.  lw = dbwrapper.ClassWrapper(<ClassWrapper>, [<key>|<nodeid>|<NodeWrapper>])
            With this form you will fill the list with initial data.
            The data must be nodes in <ClassWrapper> and may be addressed
            by either key (in case the class has a key column), or by node id
            (as integer or string), or by a NodeWrapper object.

        For both forms an additional parameter can be set to tell the dbwrapper
        that all NodeWrapper classes in the ListClass are either read-only or not.
        Default they are set to not read-only, meaning all nodes in the list
        may be changed.

        Several methods in the ClassWrapper and the NodeWrapper do return
        ListWrapper objects. In general these methods are all the search
        methods in the ClassWrapper and the 'get' method in the NodeWrapper.

        Default the list won't accept duplicate nodes. If you try to add a node
        which is already present, a ValueError will be raised. This behavior
        can be omitted by setting ListWrapper member 'unique' to False.
        If False, then duplicate nodes are accepted. Switching member
        'unique' back to True while there are duplicate nodes will raise
        a ValueError too.
        """

        class RegExpClass:
            """\
            A class with regular expression methods that will filter nodes
            from hyperdb.Class. The methods in this class are one to one
            with the regular expression methods in python module 're'.
            """

            def __init__(self, owner):
                """\
                Initialize class

                "owner" must be of type ListWrapper
                """

                if not isinstance(owner, ListWrapper):
                    raise TypeError, \
                    "Owner object isn't an instance of 'ListWrapper'"

                self._lw = owner

            def __repr__(self):
                """\
                Slightly more useful representation
                """

                return '''<dbwrapper.ListWrapper.RegExpClass '%s'>'''% \
                                                        self._lw._clw._cl

            def __str__(self):
                """\
                Slightly more useful representation
                """

                return self.__repr__()

            def match(self, pattern, flags=0, column=None):
                """\
                Find all nodes in the class that have a result when
                applying the "pattern" at the start of the string in
                "column", returning a list object with tuples pairs.

                The first index of each tuple contains a
                dbwraper.NodeWrapper object of a node that matches
                with "pattern".

                The second index in the tuples holds the match object.

                "flags" may have the same flags as used/defined in
                python module 're'. These flags are also available
                as members of module 'dbwrapper'.
                (e.g. dbwrapper.IGNORECASE)

                "column" can be used to perform a search on an
                other column than the column returned by
                ClassWrapper.getlabel().
                """

                return self._lw._clw.re.match(pattern, flags, column, nodelist=self._lw)

            def search(self, pattern, flags=0, column=None):
                """\
                Find all nodes in the class that have a result when
                scanning through the string in "column" looking for a
                match to the "pattern", returning a list object with
                tuples pairs.

                The first index of each tuple contains a
                dbwraper.NodeWrapper object of a node that matches
                with "pattern".

                The second index in the tuples holds the search object.

                "flags" may have the same flags as used/defined in
                python module 're'. These flags are also available
                as members of module 'dbwrapper'.
                (e.g. dbwrapper.IGNORECASE)

                "column" can be used to perform a search on an
                other column than the column returned by
                ClassWrapper.getlabel().
                """

                return self._lw._clw.re.search(pattern, flags, column, nodelist=self._lw)

            def findall(self, pattern, flags=0, column=None):
                """\
                Return all nodes in class that have "pattern" matches
                in the strings in "column". Returned is a list of tuple
                pairs.

                The first tuple index contains a NodeWrapper object of
                a matching node.

                The second tuple index contains a list of all
                non-overlapping matches in the node's "column" string.

                If one or more groups are present in the "pattern", the
                second tuple index will contain a list of groups; this
                will be a list of tuples if the "pattern" has more than
                one group.

                "flags" may have the same flags as used/defined in
                python module 're'. These flags are also available
                as members of module 'dbwrapper'.
                (e.g. dbwrapper.IGNORECASE)

                "column" can be used to perform a search on an
                other column than the column returned by
                ClassWrapper.getlabel().
                """

                return self._lw._clw.re.findall(pattern, flags, column, nodelist=self._lw)

        def __init__(self, owner, readonly=False, *list):
            """\
            Create a list object for dbwrapper.

            "owner" must be a 'ClassWrapper' object. Only nodes present
            in 'ClassWrapper' will be allowed in the list.

            "readonly" can be set to prevent that properties of the nodes
            in the list can be changed. The list content can always be
            changed.

            "list" can be a list of any node specification like key,
            nodeid or NodeClass object.
            """

            if not isClass(owner):
                raise TypeError, \
                "owner object isn't an instance of 'ClassWrapper'"

            self.__list    = []
            self._clw      = owner
            self.__regexp  = False
            self.re        = self.RegExpClass(self)
            self.unique    = True

            if isinstance(readonly, types.ListType) \
            or isinstance(readonly, types.TupleType):
                self._readonly = False
                list = readonly
            else:
                self._readonly = readonly

            if list:
                if len(list) == 1 \
                and isinstance(list[0], types.ListType):
                    list = list[0]

                self.__iadd__(list)

        def __repr__(self):
            """\
            Slightly more useful representation
            """

            return str(self.__list)

        def __str__(self):
            """\
            Slightly more useful representation
            """

            return self.__repr__()

        def __getitem__(self, index):
            """\
            Retrieve an item at position 'index'.
            """

            return self.__list[index]

        def __setitem__(self, index, value):
            """\
            Assign 'value' to the item at position 'index'.
            """

            self.__inner_set(index, value)

        def __delitem__(self, index):
            """\
            Delete the item at position 'index'.
            """

            del self.__list[index]

        def __setattr__(self, attr, value):
            """\
            Set any attribute.

            This method prevents that member 'unique' can be set to True
            while the content in the list is not unique.
            """

            if attr == 'unique' \
            and value \
            and self.__dict__.has_key(attr) \
            and not self.unique:
                for index, node1 in enumerate(self.__list):
                    if self.__regexp:
                        node1 = node1[0]

                    for node2 in self.__list[index+1:]:
                        if self.__regexp:
                            node2 = node2[0]

                        if node1 == node2:
                            raise ValueError, \
                            "Can't set list to unique. Node '%s' has more \
                            than one instances in list."%node1

            self.__dict__[attr] = value

        def __len__(self):
            """\
            Number of items in list.
            """

            return len(self.__list)

        def __isub__(self, other):
            """\
            Remove items from list.
            """

            if not isinstance(other, types.ListType) \
            and not isinstance(other, types.TupleType):
                other = [ other ]

            for node in other:
                self.remove(node)

            return self

        def __iadd__(self, other):
            """\
            Add items to list.
            """

            if not isinstance(other, types.ListType) \
            and not isinstance(other, types.TupleType):
                other = [ other ]

            for node in other:
                self.append(node)

            return self

        def __iter__(self):
            """\
            Step trough the list.
            """

            return iter(self.__list)

        def __getslice__(self, start, stop):
            """\
            Get a piece of the list.
            """

            return self.__list[start:stop]

        def __setslice__(self, start, stop, sequence):
            """\
            Set a piece of the list.
            """

            if isinstance(sequence, types.ListType) \
            or isinstance(sequence, types.TupleType):
                for index in range(start, stop):
                    if index < len(sequence):
                        self.__setitem__(index, sequence[index])
                    else:
                        del self.__list[stop-1]
            else:
                del self.__list[start:stop]
                self.insert(start, sequence)

        def __delslice__(self, start, stop):
            """\
            Delete a piece of the list.
            """

            del self.__list[start:stop]

        def __contains__(self, value):
            """\
            Determine if the list has a given node.
            """

            try:
                index = self.index(value)

            except:
                found = False

            else:
                found = (index > -1)

            return found

        def __inner_set(self, index, value):
            """\
            Handles the 'set' operation of all data changing methods.
            """

            regexp = False

            if isinstance(value, types.TupleType):
                if len(value) == 2:
                    # regulare expression list
                    node = value[0]
                    regexp = True
                else:
                    node = None
            else:
                node = value

            if not isNode(node):
                try:
                    node = NodeWrapper(self._clw, node, self._readonly)

                except:
                    raise TypeError, \
                    "Value isn't an instance of 'NodeWrapper'"

            if node._cl.classname != self._clw._cl.classname:
                raise TypeError, \
                "Node '%s' isn't member of class '%s'"% \
                (node, self._clw._cl.classname)

            if self.unique:
                try:
                    list_index = self.index(node)

                except ValueError:
                    pass

                else:
                    if list_index != index:
                        raise ValueError, \
                        "Node '%s' already present in unique list."%node

            if len(self.__list) == 0 \
            or (len(self.__list) == 1 and self.__list[index] == None):
                self.__regexp = regexp

            if self.__regexp:
                if regexp:
                    self.__list[index] = (node, value[1])
                else:
                    self.__list[index] = (node, None)
            else:
                if regexp:
                    self.__list[index] = node
                else:
                    self.__list[index] = node

        def append(self, value):
            """\
            Append a node to the list.
            """

            self.insert(len(self.__list), value)

        def insert(self, index, value):
            """\
            Insert a node at position 'index'.
            """

            self.__list.insert(index, None)

            try:
                self.__inner_set(index, value)

            except:
                del self.__list[index]
                raise

        def extend(self, other):
            """\
            Extend the list with the content of an other list.
            """

            self.__iadd__(other)

        def remove(self, value):
            """\
            Remove the first node in the list that matches value.
            """

            index = self.index(value)

            del self.__list[index]

        def count(self, value):
            """\
            Return the number of occurrences of value.
            """

            value = NodeWrapper(self._clw, value)

            if not isNode(value):
                raise TypeError, \
                "Value isn't an instance of 'NodeWrapper'"

            occurrens = 0
            for index, node in enumerate(self.__list):
                if self.__regexp:
                    # regulare expression list
                    node = node[0]

                if node == value:
                    occurrens += 1

            return occurrens

        def pop(self, index):
            """\
            Return the node at position 'index' and remove it from the list.
            """

            value = self.__list[index]
            del self.__list[index]
            return value

        def sort(self):
            """\
            Sort all nodes in the list. The sorting will be done on column
            'order'. If the class has  no column 'order', then the node id
            is used.
            """

            def inner_sort(self, prop, index1, index2):
                if self.__regexp:
                    # regulare expression list
                    node1 = self.__list[index1][0]
                    node2 = self.__list[index2][0]
                else:
                    node1 = self.__list[index1]
                    node2 = self.__list[index2]

                value1 = int(getattr(node1, prop))
                value2 = int(getattr(node2, prop))

                if value1 > value2:
                    temp = self.__list[index2]
                    self.__list[index2] = self.__list[index1]
                    self.__list[index1] = temp

                    return True

                else:
                    return False

            if self.__list:
                props = self._clw._cl.getprops()

                if props.has_key('order'):
                    prop = 'order'
                else:
                    prop = 'id'

                swap = True
                while swap:
                    swap = False

                    for index in range(0, len(self.__list)-1):
                        swap = swap or inner_sort(self, prop, index, index+1)

        def reverse(self):
            """\
            Reverse all nodes in the list.
            """

            self.__list.reverse()

        def index(self, value, start=None, stop=None):
            """\
            Return the index position of the first node that matches value.
            'start' and 'stop' can be used to search trough a slice of the list.
            """

            try:
                value = NodeWrapper(self._clw, value)

            except IndexError:
                raise ValueError, \
                "index(%s): %s not in list"%(value, value)

            if not isNode(value):
                raise TypeError, \
                "Value isn't an instance of 'NodeWrapper'"

            if start is None:
                start = 0

            if stop is None:
                stop = len(self.__list)

            index = 0
            for index, node in enumerate(self.__list[start:stop]):
                if self.__regexp \
                and isinstance(node, types.TupleType):
                    # regulare expression list
                    node = node[0]

                if value == node:
                    return index

            raise ValueError, \
            "index(%s): %s not in list"%(value, value)

        def get(self, key):
            """\
            Return a node indexed by 'key'.

            "key" identifies the node.
            It can have one of the next types:
                1 - an integer
                    The 'key' will be treated as the id of the node
                    and used for the indexing.
                2 - a string
                    The 'key' will be treated as key argument of the
                    node and used to lookup the node id.
                    If the lookup action raises a KeyError and 'key'
                    only contains digits, the integer value of 'key'
                    will be used as the node id.
                    In both cases the node id is used for the indexing.
                3 - an instance of NodeWrapper
                    The node id will be obtained from the NodeWrapper
                    class and used for the indexing.
                4 - None type
                    This is a special option implemented to make it
                    easy usable in detectors.
            """

            index = self.index(key)

            return self.__list[index]

        def getnodeids(self):
            """\
            Return a (normal) list containing all node ids (as integer) of all
            nodes in this list class.
            """

            ids = []
            for node in self.__list:
                if self.__regexp:
                    # regulare expression list
                    node = node[0]

                ids.append(node._id)

            return ids

        def filter(self, search_matches, filterspec, sort=(None,None), group=(None,None)):
            """\
            Return a list of the NodeWrappers of the active nodes in this class
            that match the 'filter' spec, sorted by the group spec and then the
            sort spec.

            "filterspec" is {propname: value(s)}

            "sort" and "group" are (dir, prop) where dir is '+', '-' or None
            and prop is a prop name or None

            "search_matches" is {nodeid: marker}

            The filter must match all properties specified - but if the
            property value to match is a list, any one of the values in the
            list may match for that property to match.
            """

            return self._clw.filter(search_matches, filterspec, sort, group, nodelist=self)

        def search(self, value, column=None):
            """\
            Lookup all nodes that have a 100% match with 'value' and
            return them in a list.

            "value" is the pattern to look for.

            "column" can be set to any string property. A TypeError is
            raised if 'column' isn't a string property. The label column
            will be used if 'column' is omitted.
            """

            return self._clw.search(value, column, nodelist=self)

        def simple(self):
            """\
            Converts the list content to the simple format. This method can be
            used to get rid of the regular expression object when a list was
            created by a regular expression search.
            """

            if self.__regexp:
                for index, node in enumerate(self.__list):
                    self.__list[index] = node[0]

                self.__regexp = False