diff roundup/hyperdb.py @ 1356:83f33642d220 maint-0.5

[[Metadata associated with this commit was garbled during conversion from CVS to Subversion.]]
author Richard Jones <richard@users.sourceforge.net>
date Thu, 09 Jan 2003 22:59:22 +0000
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/roundup/hyperdb.py	Thu Jan 09 22:59:22 2003 +0000
@@ -0,0 +1,619 @@
+#
+# Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
+# This module is free software, and you may redistribute it and/or modify
+# under the same terms as Python, so long as this copyright message and
+# disclaimer are retained in their original form.
+#
+# IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
+# DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
+# OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+# BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
+# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+# 
+# $Id: hyperdb.py,v 1.84 2002-10-07 00:52:51 richard Exp $
+
+"""
+Hyperdatabase implementation, especially field types.
+"""
+
+# standard python modules
+import sys, os, time, re
+
+# roundup modules
+import date, password
+
+# configure up the DEBUG and TRACE captures
+class Sink:
+    def write(self, content):
+        pass
+DEBUG = os.environ.get('HYPERDBDEBUG', '')
+if DEBUG and __debug__:
+    if DEBUG == 'stdout':
+        DEBUG = sys.stdout
+    else:
+        DEBUG = open(DEBUG, 'a')
+else:
+    DEBUG = Sink()
+TRACE = os.environ.get('HYPERDBTRACE', '')
+if TRACE and __debug__:
+    if TRACE == 'stdout':
+        TRACE = sys.stdout
+    else:
+        TRACE = open(TRACE, 'w')
+else:
+    TRACE = Sink()
+def traceMark():
+    print >>TRACE, '**MARK', time.ctime()
+del Sink
+
+#
+# Types
+#
+class String:
+    """An object designating a String property."""
+    def __init__(self, indexme='no'):
+        self.indexme = indexme == 'yes'
+    def __repr__(self):
+        ' more useful for dumps '
+        return '<%s>'%self.__class__
+
+class Password:
+    """An object designating a Password property."""
+    def __repr__(self):
+        ' more useful for dumps '
+        return '<%s>'%self.__class__
+
+class Date:
+    """An object designating a Date property."""
+    def __repr__(self):
+        ' more useful for dumps '
+        return '<%s>'%self.__class__
+
+class Interval:
+    """An object designating an Interval property."""
+    def __repr__(self):
+        ' more useful for dumps '
+        return '<%s>'%self.__class__
+
+class Link:
+    """An object designating a Link property that links to a
+       node in a specified class."""
+    def __init__(self, classname, do_journal='yes'):
+        ''' Default is to not journal link and unlink events
+        '''
+        self.classname = classname
+        self.do_journal = do_journal == 'yes'
+    def __repr__(self):
+        ' more useful for dumps '
+        return '<%s to "%s">'%(self.__class__, self.classname)
+
+class Multilink:
+    """An object designating a Multilink property that links
+       to nodes in a specified class.
+
+       "classname" indicates the class to link to
+
+       "do_journal" indicates whether the linked-to nodes should have
+                    'link' and 'unlink' events placed in their journal
+    """
+    def __init__(self, classname, do_journal='yes'):
+        ''' Default is to not journal link and unlink events
+        '''
+        self.classname = classname
+        self.do_journal = do_journal == 'yes'
+    def __repr__(self):
+        ' more useful for dumps '
+        return '<%s to "%s">'%(self.__class__, self.classname)
+
+class Boolean:
+    """An object designating a boolean property"""
+    def __repr__(self):
+        'more useful for dumps'
+        return '<%s>' % self.__class__
+    
+class Number:
+    """An object designating a numeric property"""
+    def __repr__(self):
+        'more useful for dumps'
+        return '<%s>' % self.__class__
+#
+# Support for splitting designators
+#
+class DesignatorError(ValueError):
+    pass
+def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
+    ''' Take a foo123 and return ('foo', 123)
+    '''
+    m = dre.match(designator)
+    if m is None:
+        raise DesignatorError, '"%s" not a node designator'%designator
+    return m.group(1), m.group(2)
+
+#
+# the base Database class
+#
+class DatabaseError(ValueError):
+    '''Error to be raised when there is some problem in the database code
+    '''
+    pass
+class Database:
+    '''A database for storing records containing flexible data types.
+
+This class defines a hyperdatabase storage layer, which the Classes use to
+store their data.
+
+
+Transactions
+------------
+The Database should support transactions through the commit() and
+rollback() methods. All other Database methods should be transaction-aware,
+using data from the current transaction before looking up the database.
+
+An implementation must provide an override for the get() method so that the
+in-database value is returned in preference to the in-transaction value.
+This is necessary to determine if any values have changed during a
+transaction.
+
+
+Implementation
+--------------
+
+All methods except __repr__ and getnode must be implemented by a
+concrete backend Class.
+
+'''
+
+    # flag to set on retired entries
+    RETIRED_FLAG = '__hyperdb_retired'
+
+    def __init__(self, config, journaltag=None):
+        """Open a hyperdatabase given a specifier to some storage.
+
+        The 'storagelocator' is obtained from config.DATABASE.
+        The meaning of 'storagelocator' depends on the particular
+        implementation of the hyperdatabase.  It could be a file name,
+        a directory path, a socket descriptor for a connection to a
+        database over the network, etc.
+
+        The 'journaltag' is a token that will be attached to the journal
+        entries for any edits done on the database.  If 'journaltag' is
+        None, the database is opened in read-only mode: the Class.create(),
+        Class.set(), and Class.retire() methods are disabled.
+        """
+        raise NotImplementedError
+
+    def post_init(self):
+        """Called once the schema initialisation has finished."""
+        raise NotImplementedError
+
+    def __getattr__(self, classname):
+        """A convenient way of calling self.getclass(classname)."""
+        raise NotImplementedError
+
+    def addclass(self, cl):
+        '''Add a Class to the hyperdatabase.
+        '''
+        raise NotImplementedError
+
+    def getclasses(self):
+        """Return a list of the names of all existing classes."""
+        raise NotImplementedError
+
+    def getclass(self, classname):
+        """Get the Class object representing a particular class.
+
+        If 'classname' is not a valid class name, a KeyError is raised.
+        """
+        raise NotImplementedError
+
+    def clear(self):
+        '''Delete all database contents.
+        '''
+        raise NotImplementedError
+
+    def getclassdb(self, classname, mode='r'):
+        '''Obtain a connection to the class db that will be used for
+           multiple actions.
+        '''
+        raise NotImplementedError
+
+    def addnode(self, classname, nodeid, node):
+        '''Add the specified node to its class's db.
+        '''
+        raise NotImplementedError
+
+    def serialise(self, classname, node):
+        '''Copy the node contents, converting non-marshallable data into
+           marshallable data.
+        '''
+        return node
+
+    def setnode(self, classname, nodeid, node):
+        '''Change the specified node.
+        '''
+        raise NotImplementedError
+
+    def unserialise(self, classname, node):
+        '''Decode the marshalled node data
+        '''
+        return node
+
+    def getnode(self, classname, nodeid, db=None, cache=1):
+        '''Get a node from the database.
+        '''
+        raise NotImplementedError
+
+    def hasnode(self, classname, nodeid, db=None):
+        '''Determine if the database has a given node.
+        '''
+        raise NotImplementedError
+
+    def countnodes(self, classname, db=None):
+        '''Count the number of nodes that exist for a particular Class.
+        '''
+        raise NotImplementedError
+
+    def getnodeids(self, classname, db=None):
+        '''Retrieve all the ids of the nodes for a particular Class.
+        '''
+        raise NotImplementedError
+
+    def storefile(self, classname, nodeid, property, content):
+        '''Store the content of the file in the database.
+        
+           The property may be None, in which case the filename does not
+           indicate which property is being saved.
+        '''
+        raise NotImplementedError
+
+    def getfile(self, classname, nodeid, property):
+        '''Store the content of the file in the database.
+        '''
+        raise NotImplementedError
+
+    def addjournal(self, classname, nodeid, action, params):
+        ''' Journal the Action
+        'action' may be:
+
+            'create' or 'set' -- 'params' is a dictionary of property values
+            'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
+            'retire' -- 'params' is None
+        '''
+        raise NotImplementedError
+
+    def getjournal(self, classname, nodeid):
+        ''' get the journal for id
+        '''
+        raise NotImplementedError
+
+    def pack(self, pack_before):
+        ''' pack the database
+        '''
+        raise NotImplementedError
+
+    def commit(self):
+        ''' Commit the current transactions.
+
+        Save all data changed since the database was opened or since the
+        last commit() or rollback().
+        '''
+        raise NotImplementedError
+
+    def rollback(self):
+        ''' Reverse all actions from the current transaction.
+
+        Undo all the changes made since the database was opened or the last
+        commit() or rollback() was performed.
+        '''
+        raise NotImplementedError
+
+#
+# The base Class class
+#
+class Class:
+    """ The handle to a particular class of nodes in a hyperdatabase.
+        
+        All methods except __repr__ and getnode must be implemented by a
+        concrete backend Class.
+    """
+
+    def __init__(self, db, classname, **properties):
+        """Create a new class with a given name and property specification.
+
+        'classname' must not collide with the name of an existing class,
+        or a ValueError is raised.  The keyword arguments in 'properties'
+        must map names to property objects, or a TypeError is raised.
+        """
+        raise NotImplementedError
+
+    def __repr__(self):
+        '''Slightly more useful representation
+        '''
+        return '<hyperdb.Class "%s">'%self.classname
+
+    # Editing nodes:
+
+    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.
+        """
+        raise NotImplementedError
+
+    _marker = []
+    def get(self, nodeid, propname, default=_marker, cache=1):
+        """Get the value of a property on an existing node of this class.
+
+        'nodeid' must be the id of an existing node of this class or an
+        IndexError is raised.  'propname' must be the name of a property
+        of this class or a KeyError is raised.
+
+        'cache' indicates whether the transaction cache should be queried
+        for the node. If the node has been modified and you need to
+        determine what its values prior to modification are, you need to
+        set cache=0.
+        """
+        raise NotImplementedError
+
+    def getnode(self, nodeid, cache=1):
+        ''' Return a convenience wrapper for the node.
+
+        'nodeid' must be the id of an existing node of this class or an
+        IndexError is raised.
+
+        'cache' indicates whether the transaction cache should be queried
+        for the node. If the node has been modified and you need to
+        determine what its values prior to modification are, you need to
+        set cache=0.
+        '''
+        return Node(self, nodeid, cache=cache)
+
+    def set(self, nodeid, **propvalues):
+        """Modify a property on an existing node of this class.
+        
+        'nodeid' must be the id of an existing node of this class or an
+        IndexError is raised.
+
+        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.
+        """
+        raise NotImplementedError
+
+    def retire(self, nodeid):
+        """Retire a node.
+        
+        The properties on the node remain available from the get() method,
+        and the node's id is never reused.
+        
+        Retired nodes are not returned by the find(), list(), or lookup()
+        methods, and other nodes may reuse the values of their key properties.
+        """
+        raise NotImplementedError
+
+    def is_retired(self, nodeid):
+        '''Return true if the node is rerired
+        '''
+        raise NotImplementedError
+
+    def destroy(self, nodeid):
+        """Destroy a node.
+        
+        WARNING: this method should never be used except in extremely rare
+                 situations where there could never be links to the node being
+                 deleted
+        WARNING: use retire() instead
+        WARNING: the properties of this node will not be available ever again
+        WARNING: really, use retire() instead
+
+        Well, I think that's enough warnings. This method exists mostly to
+        support the session storage of the cgi interface.
+
+        The node is completely removed from the hyperdb, including all journal
+        entries. It will no longer be available, and will generally break code
+        if there are any references to the node.
+        """
+
+    def history(self, nodeid):
+        """Retrieve the journal of edits on a particular node.
+
+        'nodeid' must be the id of an existing node of this class or an
+        IndexError is raised.
+
+        The returned list contains tuples of the form
+
+            (date, tag, action, params)
+
+        'date' is a Timestamp object specifying the time of the change and
+        'tag' is the journaltag specified when the database was opened.
+        """
+        raise NotImplementedError
+
+    # Locating nodes:
+    def hasnode(self, nodeid):
+        '''Determine if the given nodeid actually exists
+        '''
+        raise NotImplementedError
+
+    def setkey(self, propname):
+        """Select a String property of this class to be the key property.
+
+        'propname' must be the name of a String property of this class or
+        None, or a TypeError is raised.  The values of the key property on
+        all existing nodes must be unique or a ValueError is raised.
+        """
+        raise NotImplementedError
+
+    def getkey(self):
+        """Return the name of the key property for this class or None."""
+        raise NotImplementedError
+
+    def labelprop(self, default_to_id=0):
+        ''' Return the property name for a label for the given node.
+
+        This method attempts to generate a consistent label for the node.
+        It tries the following in order:
+            1. key property
+            2. "name" property
+            3. "title" property
+            4. first property from the sorted property name list
+        '''
+        raise NotImplementedError
+
+    def lookup(self, keyvalue):
+        """Locate a particular node by its key property and return its id.
+
+        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.
+        """
+        raise NotImplementedError
+
+    def find(self, **propspec):
+        """Get the ids of nodes in this class which link to the given nodes.
+
+        'propspec' consists of keyword args propname={nodeid:1,}   
+        '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.
+
+        Any node in this class whose 'propname' property links to any of the
+        nodeids 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':1,'3':1}, files={'7':1})
+        """
+        raise NotImplementedError
+
+    def filter(self, search_matches, filterspec, sort=(None,None),
+            group=(None,None)):
+        ''' Return a list of the ids 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 specificed - 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.
+        '''
+        raise NotImplementedError
+
+    def count(self):
+        """Get the number of nodes in this class.
+
+        If the returned integer is 'numnodes', the ids of all the nodes
+        in this class run from 1 to numnodes, and numnodes+1 will be the
+        id of the next node to be created in this class.
+        """
+        raise NotImplementedError
+
+    # Manipulating properties:
+    def getprops(self, protected=1):
+        """Return a dictionary mapping property names to property objects.
+           If the "protected" flag is true, we include protected properties -
+           those which may not be modified.
+        """
+        raise NotImplementedError
+
+    def addprop(self, **properties):
+        """Add properties to this class.
+
+        The keyword arguments in 'properties' must map names to property
+        objects, or a TypeError is raised.  None of the keys in 'properties'
+        may collide with the names of existing properties, or a ValueError
+        is raised before any properties have been added.
+        """
+        raise NotImplementedError
+
+    def index(self, nodeid):
+        '''Add (or refresh) the node to search indexes
+        '''
+        raise NotImplementedError
+
+class Node:
+    ''' A convenience wrapper for the given node
+    '''
+    def __init__(self, cl, nodeid, cache=1):
+        self.__dict__['cl'] = cl
+        self.__dict__['nodeid'] = nodeid
+        self.__dict__['cache'] = cache
+    def keys(self, protected=1):
+        return self.cl.getprops(protected=protected).keys()
+    def values(self, protected=1):
+        l = []
+        for name in self.cl.getprops(protected=protected).keys():
+            l.append(self.cl.get(self.nodeid, name, cache=self.cache))
+        return l
+    def items(self, protected=1):
+        l = []
+        for name in self.cl.getprops(protected=protected).keys():
+            l.append((name, self.cl.get(self.nodeid, name, cache=self.cache)))
+        return l
+    def has_key(self, name):
+        return self.cl.getprops().has_key(name)
+    def __getattr__(self, name):
+        if self.__dict__.has_key(name):
+            return self.__dict__[name]
+        try:
+            return self.cl.get(self.nodeid, name, cache=self.cache)
+        except KeyError, value:
+            # we trap this but re-raise it as AttributeError - all other
+            # exceptions should pass through untrapped
+            pass
+        # nope, no such attribute
+        raise AttributeError, str(value)
+    def __getitem__(self, name):
+        return self.cl.get(self.nodeid, name, cache=self.cache)
+    def __setattr__(self, name, value):
+        try:
+            return self.cl.set(self.nodeid, **{name: value})
+        except KeyError, value:
+            raise AttributeError, str(value)
+    def __setitem__(self, name, value):
+        self.cl.set(self.nodeid, **{name: value})
+    def history(self):
+        return self.cl.history(self.nodeid)
+    def retire(self):
+        return self.cl.retire(self.nodeid)
+
+
+def Choice(name, db, *options):
+    '''Quick helper to create a simple class with choices
+    '''
+    cl = Class(db, name, name=String(), order=String())
+    for i in range(len(options)):
+        cl.create(name=options[i], order=i)
+    return hyperdb.Link(name)
+
+# vim: set filetype=python ts=4 sw=4 et si

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