Suppression build/
authorCyril Robert <Cyril Robert cyrilrbt@gmail.com>
Thu, 27 May 2010 14:44:29 +0000 (10:44 -0400)
committerCyril Robert <Cyril Robert cyrilrbt@gmail.com>
Thu, 27 May 2010 14:44:29 +0000 (10:44 -0400)
108 files changed:
build/lib/roundup/__init__.py [deleted file]
build/lib/roundup/actions.py [deleted file]
build/lib/roundup/admin.py [deleted file]
build/lib/roundup/anypy/__init__.py [deleted file]
build/lib/roundup/anypy/cookie_.py [deleted file]
build/lib/roundup/anypy/dbm_.py [deleted file]
build/lib/roundup/anypy/hashlib_.py [deleted file]
build/lib/roundup/anypy/http_.py [deleted file]
build/lib/roundup/anypy/io_.py [deleted file]
build/lib/roundup/anypy/sets_.py [deleted file]
build/lib/roundup/anypy/urllib_.py [deleted file]
build/lib/roundup/backends/__init__.py [deleted file]
build/lib/roundup/backends/back_anydbm.py [deleted file]
build/lib/roundup/backends/back_mysql.py [deleted file]
build/lib/roundup/backends/back_postgresql.py [deleted file]
build/lib/roundup/backends/back_sqlite.py [deleted file]
build/lib/roundup/backends/back_tsearch2.py [deleted file]
build/lib/roundup/backends/blobfiles.py [deleted file]
build/lib/roundup/backends/indexer_common.py [deleted file]
build/lib/roundup/backends/indexer_dbm.py [deleted file]
build/lib/roundup/backends/indexer_rdbms.py [deleted file]
build/lib/roundup/backends/indexer_xapian.py [deleted file]
build/lib/roundup/backends/locking.py [deleted file]
build/lib/roundup/backends/portalocker.py [deleted file]
build/lib/roundup/backends/rdbms_common.py [deleted file]
build/lib/roundup/backends/sessions_dbm.py [deleted file]
build/lib/roundup/backends/sessions_rdbms.py [deleted file]
build/lib/roundup/backends/tsearch2_setup.py [deleted file]
build/lib/roundup/cgi/MultiMapping.py [deleted file]
build/lib/roundup/cgi/PageTemplates/Expressions.py [deleted file]
build/lib/roundup/cgi/PageTemplates/GlobalTranslationService.py [deleted file]
build/lib/roundup/cgi/PageTemplates/MultiMapping.py [deleted file]
build/lib/roundup/cgi/PageTemplates/PageTemplate.py [deleted file]
build/lib/roundup/cgi/PageTemplates/PathIterator.py [deleted file]
build/lib/roundup/cgi/PageTemplates/PythonExpr.py [deleted file]
build/lib/roundup/cgi/PageTemplates/TALES.py [deleted file]
build/lib/roundup/cgi/PageTemplates/__init__.py [deleted file]
build/lib/roundup/cgi/TAL/DummyEngine.py [deleted file]
build/lib/roundup/cgi/TAL/HTMLParser.py [deleted file]
build/lib/roundup/cgi/TAL/HTMLTALParser.py [deleted file]
build/lib/roundup/cgi/TAL/TALDefs.py [deleted file]
build/lib/roundup/cgi/TAL/TALGenerator.py [deleted file]
build/lib/roundup/cgi/TAL/TALInterpreter.py [deleted file]
build/lib/roundup/cgi/TAL/TALParser.py [deleted file]
build/lib/roundup/cgi/TAL/TranslationContext.py [deleted file]
build/lib/roundup/cgi/TAL/XMLParser.py [deleted file]
build/lib/roundup/cgi/TAL/__init__.py [deleted file]
build/lib/roundup/cgi/TAL/markupbase.py [deleted file]
build/lib/roundup/cgi/TAL/talgettext.py [deleted file]
build/lib/roundup/cgi/TranslationService.py [deleted file]
build/lib/roundup/cgi/ZTUtils/Batch.py [deleted file]
build/lib/roundup/cgi/ZTUtils/Iterator.py [deleted file]
build/lib/roundup/cgi/ZTUtils/__init__.py [deleted file]
build/lib/roundup/cgi/__init__.py [deleted file]
build/lib/roundup/cgi/accept_language.py [deleted file]
build/lib/roundup/cgi/actions.py [deleted file]
build/lib/roundup/cgi/apache.py [deleted file]
build/lib/roundup/cgi/cgitb.py [deleted file]
build/lib/roundup/cgi/client.py [deleted file]
build/lib/roundup/cgi/exceptions.py [deleted file]
build/lib/roundup/cgi/form_parser.py [deleted file]
build/lib/roundup/cgi/templating.py [deleted file]
build/lib/roundup/cgi/wsgi_handler.py [deleted file]
build/lib/roundup/cgi/zLOG.py [deleted file]
build/lib/roundup/configuration.py [deleted file]
build/lib/roundup/date.py [deleted file]
build/lib/roundup/demo.py [deleted file]
build/lib/roundup/exceptions.py [deleted file]
build/lib/roundup/hyperdb.py [deleted file]
build/lib/roundup/i18n.py [deleted file]
build/lib/roundup/init.py [deleted file]
build/lib/roundup/install_util.py [deleted file]
build/lib/roundup/instance.py [deleted file]
build/lib/roundup/mailer.py [deleted file]
build/lib/roundup/mailgw.py [deleted file]
build/lib/roundup/msgfmt.py [deleted file]
build/lib/roundup/password.py [deleted file]
build/lib/roundup/rfc2822.py [deleted file]
build/lib/roundup/roundupdb.py [deleted file]
build/lib/roundup/scripts/__init__.py [deleted file]
build/lib/roundup/scripts/roundup_admin.py [deleted file]
build/lib/roundup/scripts/roundup_demo.py [deleted file]
build/lib/roundup/scripts/roundup_gettext.py [deleted file]
build/lib/roundup/scripts/roundup_mailgw.py [deleted file]
build/lib/roundup/scripts/roundup_server.py [deleted file]
build/lib/roundup/scripts/roundup_xmlrpc_server.py [deleted file]
build/lib/roundup/security.py [deleted file]
build/lib/roundup/support.py [deleted file]
build/lib/roundup/token.py [deleted file]
build/lib/roundup/version_check.py [deleted file]
build/lib/roundup/xmlrpc.py [deleted file]
build/scripts-2.6/roundup-admin [deleted file]
build/scripts-2.6/roundup-demo [deleted file]
build/scripts-2.6/roundup-gettext [deleted file]
build/scripts-2.6/roundup-mailgw [deleted file]
build/scripts-2.6/roundup-server [deleted file]
build/scripts-2.6/roundup-xmlrpc-server [deleted file]
build/share/locale/de/LC_MESSAGES/roundup.mo [deleted file]
build/share/locale/en/LC_MESSAGES/roundup.mo [deleted file]
build/share/locale/es/LC_MESSAGES/roundup.mo [deleted file]
build/share/locale/fr/LC_MESSAGES/roundup.mo [deleted file]
build/share/locale/hu/LC_MESSAGES/roundup.mo [deleted file]
build/share/locale/it/LC_MESSAGES/roundup.mo [deleted file]
build/share/locale/ja/LC_MESSAGES/roundup.mo [deleted file]
build/share/locale/lt/LC_MESSAGES/roundup.mo [deleted file]
build/share/locale/ru/LC_MESSAGES/roundup.mo [deleted file]
build/share/locale/zh_CN/LC_MESSAGES/roundup.mo [deleted file]
build/share/locale/zh_TW/LC_MESSAGES/roundup.mo [deleted file]

diff --git a/build/lib/roundup/__init__.py b/build/lib/roundup/__init__.py
deleted file mode 100644 (file)
index f646c11..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-#
-# 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: __init__.py,v 1.54 2008-09-01 01:58:32 richard Exp $
-
-'''Roundup - issue tracking for knowledge workers.
-
-This is a simple-to-use and -install issue-tracking system with
-command-line, web and e-mail interfaces.
-
-Roundup manages a number of issues (with properties such as
-"description", "priority", and so on) and provides the ability to (a) submit
-new issues, (b) find and edit existing issues, and (c) discuss issues with
-other participants. The system will facilitate communication among the
-participants by managing discussions and notifying interested parties when
-issues are edited.
-
-Roundup's structure is that of a cake::
-
-  _________________________________________________________________________
- |  E-mail Client   |   Web Browser   |   Detector Scripts   |    Shell    |
- |------------------+-----------------+----------------------+-------------|
- |   E-mail User    |    Web User     |      Detector        |   Command   |
- |-------------------------------------------------------------------------|
- |                         Roundup Database Layer                          |
- |-------------------------------------------------------------------------|
- |                          Hyperdatabase Layer                            |
- |-------------------------------------------------------------------------|
- |                             Storage Layer                               |
-  -------------------------------------------------------------------------
-
-1. The first layer represents the users (chocolate).
-2. The second layer is the Roundup interface to the users (vanilla).
-3. The third and fourth layers are the internal Roundup database storage
-   mechanisms (strawberry).
-4. The final, lowest layer is the underlying database storage (rum).
-
-These are implemented in the code in the following manner::
-
-  E-mail User: roundup-mailgw and roundup.mailgw
-     Web User: cgi-bin/roundup.cgi or roundup-server over
-               roundup.cgi.client and roundup.cgi.template
-     Detector: roundup.roundupdb and templates/<template>/detectors
-      Command: roundup-admin
-   Roundup DB: roundup.roundupdb
-     Hyper DB: roundup.hyperdb, roundup.date
-      Storage: roundup.backends.*
-
-Additionally, there is a directory of unit tests in "test".
-
-For more information, see the original overview and specification documents
-written by Ka-Ping Yee in the "doc" directory. If nothing else, it has a
-much prettier cake :)
-'''
-__docformat__ = 'restructuredtext'
-
-__version__ = '1.5.0'
-
-# vim: set filetype=python ts=4 sw=4 et si
diff --git a/build/lib/roundup/actions.py b/build/lib/roundup/actions.py
deleted file mode 100644 (file)
index e3bbec5..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-#
-# Copyright (C) 2009 Stefan Seefeld
-# All rights reserved.
-# For license terms see the file COPYING.txt.
-#
-
-from roundup.exceptions import *
-from roundup import hyperdb
-from roundup.i18n import _
-
-class Action:
-    def __init__(self, db, translator):
-        self.db = db
-        self.translator = translator
-
-    def handle(self, *args):
-        """Action handler procedure"""
-        raise NotImplementedError
-
-    def execute(self, *args):
-        """Execute the action specified by this object."""
-
-        self.permission(*args)
-        return self.handle(*args)
-
-
-    def permission(self, *args):
-        """Check whether the user has permission to execute this action.
-
-        If not, raise Unauthorised."""
-
-        pass
-
-
-    def gettext(self, msgid):
-        """Return the localized translation of msgid"""
-        return self.translator.gettext(msgid)
-
-
-    _ = gettext
-
-
-class Retire(Action):
-
-    def handle(self, designator):
-
-        classname, itemid = hyperdb.splitDesignator(designator)
-
-        # make sure we don't try to retire admin or anonymous
-        if (classname == 'user' and
-            self.db.user.get(itemid, 'username') in ('admin', 'anonymous')):
-            raise ValueError(self._(
-                'You may not retire the admin or anonymous user'))
-
-        # do the retire
-        self.db.getclass(classname).retire(itemid)
-        self.db.commit()
-
-
-    def permission(self, designator):
-
-        classname, itemid = hyperdb.splitDesignator(designator)
-
-        if not self.db.security.hasPermission('Edit', self.db.getuid(),
-                                              classname=classname, itemid=itemid):
-            raise Unauthorised(self._('You do not have permission to '
-                                      'retire the %(classname)s class.')%classname)
-            
diff --git a/build/lib/roundup/admin.py b/build/lib/roundup/admin.py
deleted file mode 100644 (file)
index 511046d..0000000
+++ /dev/null
@@ -1,1588 +0,0 @@
-#! /usr/bin/env python
-#
-# 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.
-#
-
-"""Administration commands for maintaining Roundup trackers.
-"""
-__docformat__ = 'restructuredtext'
-
-import csv, getopt, getpass, os, re, shutil, sys, UserDict, operator
-
-from roundup import date, hyperdb, roundupdb, init, password, token
-from roundup import __version__ as roundup_version
-import roundup.instance
-from roundup.configuration import CoreConfig
-from roundup.i18n import _
-from roundup.exceptions import UsageError
-
-class CommandDict(UserDict.UserDict):
-    """Simple dictionary that lets us do lookups using partial keys.
-
-    Original code submitted by Engelbert Gruber.
-    """
-    _marker = []
-    def get(self, key, default=_marker):
-        if key in self.data:
-            return [(key, self.data[key])]
-        keylist = sorted(self.data)
-        l = []
-        for ki in keylist:
-            if ki.startswith(key):
-                l.append((ki, self.data[ki]))
-        if not l and default is self._marker:
-            raise KeyError(key)
-        return l
-
-class AdminTool:
-    """ A collection of methods used in maintaining Roundup trackers.
-
-        Typically these methods are accessed through the roundup-admin
-        script. The main() method provided on this class gives the main
-        loop for the roundup-admin script.
-
-        Actions are defined by do_*() methods, with help for the action
-        given in the method docstring.
-
-        Additional help may be supplied by help_*() methods.
-    """
-    def __init__(self):
-        self.commands = CommandDict()
-        for k in AdminTool.__dict__:
-            if k[:3] == 'do_':
-                self.commands[k[3:]] = getattr(self, k)
-        self.help = {}
-        for k in AdminTool.__dict__:
-            if k[:5] == 'help_':
-                self.help[k[5:]] = getattr(self, k)
-        self.tracker_home = ''
-        self.db = None
-        self.db_uncommitted = False
-
-    def get_class(self, classname):
-        """Get the class - raise an exception if it doesn't exist.
-        """
-        try:
-            return self.db.getclass(classname)
-        except KeyError:
-            raise UsageError(_('no such class "%(classname)s"')%locals())
-
-    def props_from_args(self, args):
-        """ Produce a dictionary of prop: value from the args list.
-
-            The args list is specified as ``prop=value prop=value ...``.
-        """
-        props = {}
-        for arg in args:
-            if arg.find('=') == -1:
-                raise UsageError(_('argument "%(arg)s" not propname=value'
-                    )%locals())
-            l = arg.split('=')
-            if len(l) < 2:
-                raise UsageError(_('argument "%(arg)s" not propname=value'
-                    )%locals())
-            key, value = l[0], '='.join(l[1:])
-            if value:
-                props[key] = value
-            else:
-                props[key] = None
-        return props
-
-    def usage(self, message=''):
-        """ Display a simple usage message.
-        """
-        if message:
-            message = _('Problem: %(message)s\n\n')%locals()
-        print _("""%(message)sUsage: roundup-admin [options] [<command> <arguments>]
-
-Options:
- -i instance home  -- specify the issue tracker "home directory" to administer
- -u                -- the user[:password] to use for commands
- -d                -- print full designators not just class id numbers
- -c                -- when outputting lists of data, comma-separate them.
-                      Same as '-S ","'.
- -S <string>       -- when outputting lists of data, string-separate them
- -s                -- when outputting lists of data, space-separate them.
-                      Same as '-S " "'.
- -V                -- be verbose when importing
- -v                -- report Roundup and Python versions (and quit)
-
- Only one of -s, -c or -S can be specified.
-
-Help:
- roundup-admin -h
- roundup-admin help                       -- this help
- roundup-admin help <command>             -- command-specific help
- roundup-admin help all                   -- all available help
-""")%locals()
-        self.help_commands()
-
-    def help_commands(self):
-        """List the commands available with their help summary.
-        """
-        print _('Commands:'),
-        commands = ['']
-        for command in self.commands.itervalues():
-            h = _(command.__doc__).split('\n')[0]
-            commands.append(' '+h[7:])
-        commands.sort()
-        commands.append(_(
-"""Commands may be abbreviated as long as the abbreviation
-matches only one command, e.g. l == li == lis == list."""))
-        print '\n'.join(commands)
-        print
-
-    def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
-        """ Produce an HTML command list.
-        """
-        commands = sorted(self.commands.itervalues(),
-            operator.attrgetter('__name__'))
-        for command in commands:
-            h = _(command.__doc__).split('\n')
-            name = command.__name__[3:]
-            usage = h[0]
-            print """
-<tr><td valign=top><strong>%(name)s</strong></td>
-    <td><tt>%(usage)s</tt><p>
-<pre>""" % locals()
-            indent = indent_re.match(h[3])
-            if indent: indent = len(indent.group(1))
-            for line in h[3:]:
-                if indent:
-                    print line[indent:]
-                else:
-                    print line
-            print '</pre></td></tr>\n'
-
-    def help_all(self):
-        print _("""
-All commands (except help) require a tracker specifier. This is just
-the path to the roundup tracker you're working with. A roundup tracker
-is where roundup keeps the database and configuration file that defines
-an issue tracker. It may be thought of as the issue tracker's "home
-directory". It may be specified in the environment variable TRACKER_HOME
-or on the command line as "-i tracker".
-
-A designator is a classname and a nodeid concatenated, eg. bug1, user10, ...
-
-Property values are represented as strings in command arguments and in the
-printed results:
- . Strings are, well, strings.
- . Date values are printed in the full date format in the local time zone,
-   and accepted in the full format or any of the partial formats explained
-   below.
- . Link values are printed as node designators. When given as an argument,
-   node designators and key strings are both accepted.
- . Multilink values are printed as lists of node designators joined
-   by commas.  When given as an argument, node designators and key
-   strings are both accepted; an empty string, a single node, or a list
-   of nodes joined by commas is accepted.
-
-When property values must contain spaces, just surround the value with
-quotes, either ' or ". A single space may also be backslash-quoted. If a
-value must contain a quote character, it must be backslash-quoted or inside
-quotes. Examples:
-           hello world      (2 tokens: hello, world)
-           "hello world"    (1 token: hello world)
-           "Roch'e" Compaan (2 tokens: Roch'e Compaan)
-           Roch\\'e Compaan  (2 tokens: Roch'e Compaan)
-           address="1 2 3"  (1 token: address=1 2 3)
-           \\\\               (1 token: \\)
-           \\n\\r\\t           (1 token: a newline, carriage-return and tab)
-
-When multiple nodes are specified to the roundup get or roundup set
-commands, the specified properties are retrieved or set on all the listed
-nodes.
-
-When multiple results are returned by the roundup get or roundup find
-commands, they are printed one per line (default) or joined by commas (with
-the -c) option.
-
-Where the command changes data, a login name/password is required. The
-login may be specified as either "name" or "name:password".
- . ROUNDUP_LOGIN environment variable
- . the -u command-line option
-If either the name or password is not supplied, they are obtained from the
-command-line.
-
-Date format examples:
-  "2000-04-17.03:45" means <Date 2000-04-17.08:45:00>
-  "2000-04-17" means <Date 2000-04-17.00:00:00>
-  "01-25" means <Date yyyy-01-25.00:00:00>
-  "08-13.22:13" means <Date yyyy-08-14.03:13:00>
-  "11-07.09:32:43" means <Date yyyy-11-07.14:32:43>
-  "14:25" means <Date yyyy-mm-dd.19:25:00>
-  "8:47:11" means <Date yyyy-mm-dd.13:47:11>
-  "." means "right now"
-
-Command help:
-""")
-        for name, command in self.commands.items():
-            print _('%s:')%name
-            print '   ', _(command.__doc__)
-
-    def do_help(self, args, nl_re=re.compile('[\r\n]'),
-            indent_re=re.compile(r'^(\s+)\S+')):
-        ''"""Usage: help topic
-        Give help about topic.
-
-        commands  -- list commands
-        <command> -- help specific to a command
-        initopts  -- init command options
-        all       -- all available help
-        """
-        if len(args)>0:
-            topic = args[0]
-        else:
-            topic = 'help'
-
-
-        # try help_ methods
-        if topic in self.help:
-            self.help[topic]()
-            return 0
-
-        # try command docstrings
-        try:
-            l = self.commands.get(topic)
-        except KeyError:
-            print _('Sorry, no help for "%(topic)s"')%locals()
-            return 1
-
-        # display the help for each match, removing the docsring indent
-        for name, help in l:
-            lines = nl_re.split(_(help.__doc__))
-            print lines[0]
-            indent = indent_re.match(lines[1])
-            if indent: indent = len(indent.group(1))
-            for line in lines[1:]:
-                if indent:
-                    print line[indent:]
-                else:
-                    print line
-        return 0
-
-    def listTemplates(self):
-        """ List all the available templates.
-
-        Look in the following places, where the later rules take precedence:
-
-         1. <roundup.admin.__file__>/../../share/roundup/templates/*
-            this is where they will be if we installed an egg via easy_install
-         2. <prefix>/share/roundup/templates/*
-            this should be the standard place to find them when Roundup is
-            installed
-         3. <roundup.admin.__file__>/../templates/*
-            this will be used if Roundup's run in the distro (aka. source)
-            directory
-         4. <current working dir>/*
-            this is for when someone unpacks a 3rd-party template
-         5. <current working dir>
-            this is for someone who "cd"s to the 3rd-party template dir
-        """
-        # OK, try <prefix>/share/roundup/templates
-        #     and <egg-directory>/share/roundup/templates
-        # -- this module (roundup.admin) will be installed in something
-        # like:
-        #    /usr/lib/python2.5/site-packages/roundup/admin.py  (5 dirs up)
-        #    c:\python25\lib\site-packages\roundup\admin.py     (4 dirs up)
-        #    /usr/lib/python2.5/site-packages/roundup-1.3.3-py2.5-egg/roundup/admin.py
-        #    (2 dirs up)
-        #
-        # we're interested in where the directory containing "share" is
-        templates = {}
-        for N in 2, 4, 5:
-            path = __file__
-            # move up N elements in the path
-            for i in range(N):
-                path = os.path.dirname(path)
-            tdir = os.path.join(path, 'share', 'roundup', 'templates')
-            if os.path.isdir(tdir):
-                templates = init.listTemplates(tdir)
-                break
-
-        # OK, now try as if we're in the roundup source distribution
-        # directory, so this module will be in .../roundup-*/roundup/admin.py
-        # and we're interested in the .../roundup-*/ part.
-        path = __file__
-        for i in range(2):
-            path = os.path.dirname(path)
-        tdir = os.path.join(path, 'templates')
-        if os.path.isdir(tdir):
-            templates.update(init.listTemplates(tdir))
-
-        # Try subdirs of the current dir
-        templates.update(init.listTemplates(os.getcwd()))
-
-        # Finally, try the current directory as a template
-        template = init.loadTemplateInfo(os.getcwd())
-        if template:
-            templates[template['name']] = template
-
-        return templates
-
-    def help_initopts(self):
-        templates = self.listTemplates()
-        print _('Templates:'), ', '.join(templates)
-        import roundup.backends
-        backends = roundup.backends.list_backends()
-        print _('Back ends:'), ', '.join(backends)
-
-    def do_install(self, tracker_home, args):
-        ''"""Usage: install [template [backend [key=val[,key=val]]]]
-        Install a new Roundup tracker.
-
-        The command will prompt for the tracker home directory
-        (if not supplied through TRACKER_HOME or the -i option).
-        The template and backend may be specified on the command-line
-        as arguments, in that order.
-
-        Command line arguments following the backend allows you to
-        pass initial values for config options.  For example, passing
-        "web_http_auth=no,rdbms_user=dinsdale" will override defaults
-        for options http_auth in section [web] and user in section [rdbms].
-        Please be careful to not use spaces in this argument! (Enclose
-        whole argument in quotes if you need spaces in option value).
-
-        The initialise command must be called after this command in order
-        to initialise the tracker's database. You may edit the tracker's
-        initial database contents before running that command by editing
-        the tracker's dbinit.py module init() function.
-
-        See also initopts help.
-        """
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-
-        # make sure the tracker home can be created
-        tracker_home = os.path.abspath(tracker_home)
-        parent = os.path.split(tracker_home)[0]
-        if not os.path.exists(parent):
-            raise UsageError(_('Instance home parent directory "%(parent)s"'
-                ' does not exist')%locals())
-
-        config_ini_file = os.path.join(tracker_home, CoreConfig.INI_FILE)
-        # check for both old- and new-style configs
-        if list(filter(os.path.exists, [config_ini_file,
-                os.path.join(tracker_home, 'config.py')])):
-            ok = raw_input(_(
-"""WARNING: There appears to be a tracker in "%(tracker_home)s"!
-If you re-install it, you will lose all the data!
-Erase it? Y/N: """) % locals())
-            if ok.strip().lower() != 'y':
-                return 0
-
-            # clear it out so the install isn't confused
-            shutil.rmtree(tracker_home)
-
-        # select template
-        templates = self.listTemplates()
-        template = len(args) > 1 and args[1] or ''
-        if template not in templates:
-            print _('Templates:'), ', '.join(templates)
-        while template not in templates:
-            template = raw_input(_('Select template [classic]: ')).strip()
-            if not template:
-                template = 'classic'
-
-        # select hyperdb backend
-        import roundup.backends
-        backends = roundup.backends.list_backends()
-        backend = len(args) > 2 and args[2] or ''
-        if backend not in backends:
-            print _('Back ends:'), ', '.join(backends)
-        while backend not in backends:
-            backend = raw_input(_('Select backend [anydbm]: ')).strip()
-            if not backend:
-                backend = 'anydbm'
-        # XXX perform a unit test based on the user's selections
-
-        # Process configuration file definitions
-        if len(args) > 3:
-            try:
-                defns = dict([item.split("=") for item in args[3].split(",")])
-            except:
-                print _('Error in configuration settings: "%s"') % args[3]
-                raise
-        else:
-            defns = {}
-
-        # install!
-        init.install(tracker_home, templates[template]['path'], settings=defns)
-        init.write_select_db(tracker_home, backend)
-
-        print _("""
----------------------------------------------------------------------------
- You should now edit the tracker configuration file:
-   %(config_file)s""") % {"config_file": config_ini_file}
-
-        # find list of options that need manual adjustments
-        # XXX config._get_unset_options() is marked as private
-        #   (leading underscore).  make it public or don't care?
-        need_set = CoreConfig(tracker_home)._get_unset_options()
-        if need_set:
-            print _(" ... at a minimum, you must set following options:")
-            for section in need_set:
-                print "   [%s]: %s" % (section, ", ".join(need_set[section]))
-
-        # note about schema modifications
-        print _("""
- If you wish to modify the database schema,
- you should also edit the schema file:
-   %(database_config_file)s
- You may also change the database initialisation file:
-   %(database_init_file)s
- ... see the documentation on customizing for more information.
-
- You MUST run the "roundup-admin initialise" command once you've performed
- the above steps.
----------------------------------------------------------------------------
-""") % {
-    'database_config_file': os.path.join(tracker_home, 'schema.py'),
-    'database_init_file': os.path.join(tracker_home, 'initial_data.py'),
-}
-        return 0
-
-    def do_genconfig(self, args):
-        ''"""Usage: genconfig <filename>
-        Generate a new tracker config file (ini style) with default values
-        in <filename>.
-        """
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-        config = CoreConfig()
-        config.save(args[0])
-
-    def do_initialise(self, tracker_home, args):
-        ''"""Usage: initialise [adminpw]
-        Initialise a new Roundup tracker.
-
-        The administrator details will be set at this step.
-
-        Execute the tracker's initialisation function dbinit.init()
-        """
-        # password
-        if len(args) > 1:
-            adminpw = args[1]
-        else:
-            adminpw = ''
-            confirm = 'x'
-            while adminpw != confirm:
-                adminpw = getpass.getpass(_('Admin Password: '))
-                confirm = getpass.getpass(_('       Confirm: '))
-
-        # make sure the tracker home is installed
-        if not os.path.exists(tracker_home):
-            raise UsageError(_('Instance home does not exist')%locals())
-        try:
-            tracker = roundup.instance.open(tracker_home)
-        except roundup.instance.TrackerError:
-            raise UsageError(_('Instance has not been installed')%locals())
-
-        # is there already a database?
-        if tracker.exists():
-            ok = raw_input(_(
-"""WARNING: The database is already initialised!
-If you re-initialise it, you will lose all the data!
-Erase it? Y/N: """))
-            if ok.strip().lower() != 'y':
-                return 0
-
-            backend = tracker.get_backend_name()
-
-            # nuke it
-            tracker.nuke()
-
-            # re-write the backend select file
-            init.write_select_db(tracker_home, backend, tracker.config.DATABASE)
-
-        # GO
-        tracker.init(password.Password(adminpw))
-
-        return 0
-
-
-    def do_get(self, args):
-        ''"""Usage: get property designator[,designator]*
-        Get the given property of one or more designator(s).
-
-        A designator is a classname and a nodeid concatenated,
-        eg. bug1, user10, ...
-
-        Retrieves the property value of the nodes specified
-        by the designators.
-        """
-        if len(args) < 2:
-            raise UsageError(_('Not enough arguments supplied'))
-        propname = args[0]
-        designators = args[1].split(',')
-        l = []
-        for designator in designators:
-            # decode the node designator
-            try:
-                classname, nodeid = hyperdb.splitDesignator(designator)
-            except hyperdb.DesignatorError, message:
-                raise UsageError(message)
-
-            # get the class
-            cl = self.get_class(classname)
-            try:
-                id=[]
-                if self.separator:
-                    if self.print_designator:
-                        # see if property is a link or multilink for
-                        # which getting a desginator make sense.
-                        # Algorithm: Get the properties of the
-                        #     current designator's class. (cl.getprops)
-                        # get the property object for the property the
-                        #     user requested (properties[propname])
-                        # verify its type (isinstance...)
-                        # raise error if not link/multilink
-                        # get class name for link/multilink property
-                        # do the get on the designators
-                        # append the new designators
-                        # print
-                        properties = cl.getprops()
-                        property = properties[propname]
-                        if not (isinstance(property, hyperdb.Multilink) or
-                          isinstance(property, hyperdb.Link)):
-                            raise UsageError(_('property %s is not of type'
-                                ' Multilink or Link so -d flag does not '
-                                'apply.')%propname)
-                        propclassname = self.db.getclass(property.classname).classname
-                        id = cl.get(nodeid, propname)
-                        for i in id:
-                            l.append(propclassname + i)
-                    else:
-                        id = cl.get(nodeid, propname)
-                        for i in id:
-                            l.append(i)
-                else:
-                    if self.print_designator:
-                        properties = cl.getprops()
-                        property = properties[propname]
-                        if not (isinstance(property, hyperdb.Multilink) or
-                          isinstance(property, hyperdb.Link)):
-                            raise UsageError(_('property %s is not of type'
-                                ' Multilink or Link so -d flag does not '
-                                'apply.')%propname)
-                        propclassname = self.db.getclass(property.classname).classname
-                        id = cl.get(nodeid, propname)
-                        for i in id:
-                            print propclassname + i
-                    else:
-                        print cl.get(nodeid, propname)
-            except IndexError:
-                raise UsageError(_('no such %(classname)s node '
-                    '"%(nodeid)s"')%locals())
-            except KeyError:
-                raise UsageError(_('no such %(classname)s property '
-                    '"%(propname)s"')%locals())
-        if self.separator:
-            print self.separator.join(l)
-
-        return 0
-
-
-    def do_set(self, args):
-        ''"""Usage: set items property=value property=value ...
-        Set the given properties of one or more items(s).
-
-        The items are specified as a class or as a comma-separated
-        list of item designators (ie "designator[,designator,...]").
-
-        A designator is a classname and a nodeid concatenated,
-        eg. bug1, user10, ...
-
-        This command sets the properties to the values for all designators
-        given. If the value is missing (ie. "property=") then the property
-        is un-set. If the property is a multilink, you specify the linked
-        ids for the multilink as comma-separated numbers (ie "1,2,3").
-        """
-        if len(args) < 2:
-            raise UsageError(_('Not enough arguments supplied'))
-        from roundup import hyperdb
-
-        designators = args[0].split(',')
-        if len(designators) == 1:
-            designator = designators[0]
-            try:
-                designator = hyperdb.splitDesignator(designator)
-                designators = [designator]
-            except hyperdb.DesignatorError:
-                cl = self.get_class(designator)
-                designators = [(designator, x) for x in cl.list()]
-        else:
-            try:
-                designators = [hyperdb.splitDesignator(x) for x in designators]
-            except hyperdb.DesignatorError, message:
-                raise UsageError(message)
-
-        # get the props from the args
-        props = self.props_from_args(args[1:])
-
-        # now do the set for all the nodes
-        for classname, itemid in designators:
-            cl = self.get_class(classname)
-
-            properties = cl.getprops()
-            for key, value in props.items():
-                try:
-                    props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid,
-                        key, value)
-                except hyperdb.HyperdbValueError, message:
-                    raise UsageError(message)
-
-            # try the set
-            try:
-                cl.set(itemid, **props)
-            except (TypeError, IndexError, ValueError), message:
-                import traceback; traceback.print_exc()
-                raise UsageError(message)
-        self.db_uncommitted = True
-        return 0
-
-    def do_find(self, args):
-        ''"""Usage: find classname propname=value ...
-        Find the nodes of the given class with a given link property value.
-
-        Find the nodes of the given class with a given link property value.
-        The value may be either the nodeid of the linked node, or its key
-        value.
-        """
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-        classname = args[0]
-        # get the class
-        cl = self.get_class(classname)
-
-        # handle the propname=value argument
-        props = self.props_from_args(args[1:])
-
-        # convert the user-input value to a value used for find()
-        for propname, value in props.iteritems():
-            if ',' in value:
-                values = value.split(',')
-            else:
-                values = [value]
-            d = props[propname] = {}
-            for value in values:
-                value = hyperdb.rawToHyperdb(self.db, cl, None, propname, value)
-                if isinstance(value, list):
-                    for entry in value:
-                        d[entry] = 1
-                else:
-                    d[value] = 1
-
-        # now do the find
-        try:
-            id = []
-            designator = []
-            if self.separator:
-                if self.print_designator:
-                    id = cl.find(**props)
-                    for i in id:
-                        designator.append(classname + i)
-                    print self.separator.join(designator)
-                else:
-                    print self.separator.join(cl.find(**props))
-
-            else:
-                if self.print_designator:
-                    id = cl.find(**props)
-                    for i in id:
-                        designator.append(classname + i)
-                    print designator
-                else:
-                    print cl.find(**props)
-        except KeyError:
-            raise UsageError(_('%(classname)s has no property '
-                '"%(propname)s"')%locals())
-        except (ValueError, TypeError), message:
-            raise UsageError(message)
-        return 0
-
-    def do_specification(self, args):
-        ''"""Usage: specification classname
-        Show the properties for a classname.
-
-        This lists the properties for a given class.
-        """
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-        classname = args[0]
-        # get the class
-        cl = self.get_class(classname)
-
-        # get the key property
-        keyprop = cl.getkey()
-        for key in cl.properties:
-            value = cl.properties[key]
-            if keyprop == key:
-                print _('%(key)s: %(value)s (key property)')%locals()
-            else:
-                print _('%(key)s: %(value)s')%locals()
-
-    def do_display(self, args):
-        ''"""Usage: display designator[,designator]*
-        Show the property values for the given node(s).
-
-        A designator is a classname and a nodeid concatenated,
-        eg. bug1, user10, ...
-
-        This lists the properties and their associated values for the given
-        node.
-        """
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-
-        # decode the node designator
-        for designator in args[0].split(','):
-            try:
-                classname, nodeid = hyperdb.splitDesignator(designator)
-            except hyperdb.DesignatorError, message:
-                raise UsageError(message)
-
-            # get the class
-            cl = self.get_class(classname)
-
-            # display the values
-            keys = sorted(cl.properties)
-            for key in keys:
-                value = cl.get(nodeid, key)
-                print _('%(key)s: %(value)s')%locals()
-
-    def do_create(self, args):
-        ''"""Usage: create classname property=value ...
-        Create a new entry of a given class.
-
-        This creates a new entry of the given class using the property
-        name=value arguments provided on the command line after the "create"
-        command.
-        """
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-        from roundup import hyperdb
-
-        classname = args[0]
-
-        # get the class
-        cl = self.get_class(classname)
-
-        # now do a create
-        props = {}
-        properties = cl.getprops(protected = 0)
-        if len(args) == 1:
-            # ask for the properties
-            for key in properties:
-                if key == 'id': continue
-                value = properties[key]
-                name = value.__class__.__name__
-                if isinstance(value , hyperdb.Password):
-                    again = None
-                    while value != again:
-                        value = getpass.getpass(_('%(propname)s (Password): ')%{
-                            'propname': key.capitalize()})
-                        again = getpass.getpass(_('   %(propname)s (Again): ')%{
-                            'propname': key.capitalize()})
-                        if value != again: print _('Sorry, try again...')
-                    if value:
-                        props[key] = value
-                else:
-                    value = raw_input(_('%(propname)s (%(proptype)s): ')%{
-                        'propname': key.capitalize(), 'proptype': name})
-                    if value:
-                        props[key] = value
-        else:
-            props = self.props_from_args(args[1:])
-
-        # convert types
-        for propname in props:
-            try:
-                props[propname] = hyperdb.rawToHyperdb(self.db, cl, None,
-                    propname, props[propname])
-            except hyperdb.HyperdbValueError, message:
-                raise UsageError(message)
-
-        # check for the key property
-        propname = cl.getkey()
-        if propname and propname not in props:
-            raise UsageError(_('you must provide the "%(propname)s" '
-                'property.')%locals())
-
-        # do the actual create
-        try:
-            print cl.create(**props)
-        except (TypeError, IndexError, ValueError), message:
-            raise UsageError(message)
-        self.db_uncommitted = True
-        return 0
-
-    def do_list(self, args):
-        ''"""Usage: list classname [property]
-        List the instances of a class.
-
-        Lists all instances of the given class. If the property is not
-        specified, the  "label" property is used. The label property is
-        tried in order: the key, "name", "title" and then the first
-        property, alphabetically.
-
-        With -c, -S or -s print a list of item id's if no property
-        specified.  If property specified, print list of that property
-        for every class instance.
-        """
-        if len(args) > 2:
-            raise UsageError(_('Too many arguments supplied'))
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-        classname = args[0]
-        
-        # get the class
-        cl = self.get_class(classname)
-
-        # figure the property
-        if len(args) > 1:
-            propname = args[1]
-        else:
-            propname = cl.labelprop()
-
-        if self.separator:
-            if len(args) == 2:
-                # create a list of propnames since user specified propname
-                proplist=[]
-                for nodeid in cl.list():
-                    try:
-                        proplist.append(cl.get(nodeid, propname))
-                    except KeyError:
-                        raise UsageError(_('%(classname)s has no property '
-                            '"%(propname)s"')%locals())
-                print self.separator.join(proplist)
-            else:
-                # create a list of index id's since user didn't specify
-                # otherwise
-                print self.separator.join(cl.list())
-        else:
-            for nodeid in cl.list():
-                try:
-                    value = cl.get(nodeid, propname)
-                except KeyError:
-                    raise UsageError(_('%(classname)s has no property '
-                        '"%(propname)s"')%locals())
-                print _('%(nodeid)4s: %(value)s')%locals()
-        return 0
-
-    def do_table(self, args):
-        ''"""Usage: table classname [property[,property]*]
-        List the instances of a class in tabular form.
-
-        Lists all instances of the given class. If the properties are not
-        specified, all properties are displayed. By default, the column
-        widths are the width of the largest value. The width may be
-        explicitly defined by defining the property as "name:width".
-        For example::
-
-          roundup> table priority id,name:10
-          Id Name
-          1  fatal-bug
-          2  bug
-          3  usability
-          4  feature
-
-        Also to make the width of the column the width of the label,
-        leave a trailing : without a width on the property. For example::
-
-          roundup> table priority id,name:
-          Id Name
-          1  fata
-          2  bug
-          3  usab
-          4  feat
-
-        will result in a the 4 character wide "Name" column.
-        """
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-        classname = args[0]
-
-        # get the class
-        cl = self.get_class(classname)
-
-        # figure the property names to display
-        if len(args) > 1:
-            prop_names = args[1].split(',')
-            all_props = cl.getprops()
-            for spec in prop_names:
-                if ':' in spec:
-                    try:
-                        propname, width = spec.split(':')
-                    except (ValueError, TypeError):
-                        raise UsageError(_('"%(spec)s" not '
-                            'name:width')%locals())
-                else:
-                    propname = spec
-                if propname not in all_props:
-                    raise UsageError(_('%(classname)s has no property '
-                        '"%(propname)s"')%locals())
-        else:
-            prop_names = cl.getprops()
-
-        # now figure column widths
-        props = []
-        for spec in prop_names:
-            if ':' in spec:
-                name, width = spec.split(':')
-                if width == '':
-                    props.append((name, len(spec)))
-                else:
-                    props.append((name, int(width)))
-            else:
-               # this is going to be slow
-               maxlen = len(spec)
-               for nodeid in cl.list():
-                   curlen = len(str(cl.get(nodeid, spec)))
-                   if curlen > maxlen:
-                       maxlen = curlen
-               props.append((spec, maxlen))
-
-        # now display the heading
-        print ' '.join([name.capitalize().ljust(width) for name,width in props])
-
-        # and the table data
-        for nodeid in cl.list():
-            l = []
-            for name, width in props:
-                if name != 'id':
-                    try:
-                        value = str(cl.get(nodeid, name))
-                    except KeyError:
-                        # we already checked if the property is valid - a
-                        # KeyError here means the node just doesn't have a
-                        # value for it
-                        value = ''
-                else:
-                    value = str(nodeid)
-                f = '%%-%ds'%width
-                l.append(f%value[:width])
-            print ' '.join(l)
-        return 0
-
-    def do_history(self, args):
-        ''"""Usage: history designator
-        Show the history entries of a designator.
-
-        A designator is a classname and a nodeid concatenated,
-        eg. bug1, user10, ...
-
-        Lists the journal entries for the node identified by the designator.
-        """
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-        try:
-            classname, nodeid = hyperdb.splitDesignator(args[0])
-        except hyperdb.DesignatorError, message:
-            raise UsageError(message)
-
-        try:
-            print self.db.getclass(classname).history(nodeid)
-        except KeyError:
-            raise UsageError(_('no such class "%(classname)s"')%locals())
-        except IndexError:
-            raise UsageError(_('no such %(classname)s node '
-                '"%(nodeid)s"')%locals())
-        return 0
-
-    def do_commit(self, args):
-        ''"""Usage: commit
-        Commit changes made to the database during an interactive session.
-
-        The changes made during an interactive session are not
-        automatically written to the database - they must be committed
-        using this command.
-
-        One-off commands on the command-line are automatically committed if
-        they are successful.
-        """
-        self.db.commit()
-        self.db_uncommitted = False
-        return 0
-
-    def do_rollback(self, args):
-        ''"""Usage: rollback
-        Undo all changes that are pending commit to the database.
-
-        The changes made during an interactive session are not
-        automatically written to the database - they must be committed
-        manually. This command undoes all those changes, so a commit
-        immediately after would make no changes to the database.
-        """
-        self.db.rollback()
-        self.db_uncommitted = False
-        return 0
-
-    def do_retire(self, args):
-        ''"""Usage: retire designator[,designator]*
-        Retire the node specified by designator.
-
-        A designator is a classname and a nodeid concatenated,
-        eg. bug1, user10, ...
-
-        This action indicates that a particular node is not to be retrieved
-        by the list or find commands, and its key value may be re-used.
-        """
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-        designators = args[0].split(',')
-        for designator in designators:
-            try:
-                classname, nodeid = hyperdb.splitDesignator(designator)
-            except hyperdb.DesignatorError, message:
-                raise UsageError(message)
-            try:
-                self.db.getclass(classname).retire(nodeid)
-            except KeyError:
-                raise UsageError(_('no such class "%(classname)s"')%locals())
-            except IndexError:
-                raise UsageError(_('no such %(classname)s node '
-                    '"%(nodeid)s"')%locals())
-        self.db_uncommitted = True
-        return 0
-
-    def do_restore(self, args):
-        ''"""Usage: restore designator[,designator]*
-        Restore the retired node specified by designator.
-
-        A designator is a classname and a nodeid concatenated,
-        eg. bug1, user10, ...
-
-        The given nodes will become available for users again.
-        """
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-        designators = args[0].split(',')
-        for designator in designators:
-            try:
-                classname, nodeid = hyperdb.splitDesignator(designator)
-            except hyperdb.DesignatorError, message:
-                raise UsageError(message)
-            try:
-                self.db.getclass(classname).restore(nodeid)
-            except KeyError:
-                raise UsageError(_('no such class "%(classname)s"')%locals())
-            except IndexError:
-                raise UsageError(_('no such %(classname)s node '
-                    '"%(nodeid)s"')%locals())
-        self.db_uncommitted = True
-        return 0
-
-    def do_export(self, args, export_files=True):
-        ''"""Usage: export [[-]class[,class]] export_dir
-        Export the database to colon-separated-value files.
-        To exclude the files (e.g. for the msg or file class),
-        use the exporttables command.
-
-        Optionally limit the export to just the named classes
-        or exclude the named classes, if the 1st argument starts with '-'.
-
-        This action exports the current data from the database into
-        colon-separated-value files that are placed in the nominated
-        destination directory.
-        """
-        # grab the directory to export to
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-
-        dir = args[-1]
-
-        # get the list of classes to export
-        if len(args) == 2:
-            if args[0].startswith('-'):
-                classes = [ c for c in self.db.classes
-                            if not c in args[0][1:].split(',') ]
-            else:
-                classes = args[0].split(',')
-        else:
-            classes = self.db.classes
-
-        class colon_separated(csv.excel):
-            delimiter = ':'
-
-        # make sure target dir exists
-        if not os.path.exists(dir):
-            os.makedirs(dir)
-
-        # maximum csv field length exceeding configured size?
-        max_len = self.db.config.CSV_FIELD_SIZE
-
-        # do all the classes specified
-        for classname in classes:
-            cl = self.get_class(classname)
-
-            if not export_files and hasattr(cl, 'export_files'):
-                sys.stdout.write('Exporting %s WITHOUT the files\r\n'%
-                    classname)
-
-            f = open(os.path.join(dir, classname+'.csv'), 'wb')
-            writer = csv.writer(f, colon_separated)
-
-            properties = cl.getprops()
-            propnames = cl.export_propnames()
-            fields = propnames[:]
-            fields.append('is retired')
-            writer.writerow(fields)
-
-            # all nodes for this class
-            for nodeid in cl.getnodeids():
-                if self.verbose:
-                    sys.stdout.write('\rExporting %s - %s'%(classname, nodeid))
-                    sys.stdout.flush()
-                node = cl.getnode(nodeid)
-                exp = cl.export_list(propnames, nodeid)
-                lensum = sum ([len (repr(node[p])) for p in propnames])
-                # for a safe upper bound of field length we add
-                # difference between CSV len and sum of all field lengths
-                d = sum ([len(x) for x in exp]) - lensum
-                assert (d > 0)
-                for p in propnames:
-                    ll = len(repr(node[p])) + d
-                    if ll > max_len:
-                        max_len = ll
-                writer.writerow(exp)
-                if export_files and hasattr(cl, 'export_files'):
-                    cl.export_files(dir, nodeid)
-
-            # close this file
-            f.close()
-
-            # export the journals
-            jf = open(os.path.join(dir, classname+'-journals.csv'), 'wb')
-            if self.verbose:
-                sys.stdout.write("\nExporting Journal for %s\n" % classname)
-                sys.stdout.flush()
-            journals = csv.writer(jf, colon_separated)
-            for row in cl.export_journals():
-                journals.writerow(row)
-            jf.close()
-        if max_len > self.db.config.CSV_FIELD_SIZE:
-            print >> sys.stderr, \
-                "Warning: config csv_field_size should be at least %s"%max_len
-        return 0
-
-    def do_exporttables(self, args):
-        ''"""Usage: exporttables [[-]class[,class]] export_dir
-        Export the database to colon-separated-value files, excluding the
-        files below $TRACKER_HOME/db/files/ (which can be archived separately).
-        To include the files, use the export command.
-
-        Optionally limit the export to just the named classes
-        or exclude the named classes, if the 1st argument starts with '-'.
-
-        This action exports the current data from the database into
-        colon-separated-value files that are placed in the nominated
-        destination directory.
-        """
-        return self.do_export(args, export_files=False)
-
-    def do_import(self, args):
-        ''"""Usage: import import_dir
-        Import a database from the directory containing CSV files,
-        two per class to import.
-
-        The files used in the import are:
-
-        <class>.csv
-          This must define the same properties as the class (including
-          having a "header" line with those property names.)
-        <class>-journals.csv
-          This defines the journals for the items being imported.
-
-        The imported nodes will have the same nodeid as defined in the
-        import file, thus replacing any existing content.
-
-        The new nodes are added to the existing database - if you want to
-        create a new database using the imported data, then create a new
-        database (or, tediously, retire all the old data.)
-        """
-        if len(args) < 1:
-            raise UsageError(_('Not enough arguments supplied'))
-        from roundup import hyperdb
-
-        if hasattr (csv, 'field_size_limit'):
-            csv.field_size_limit(self.db.config.CSV_FIELD_SIZE)
-
-        # directory to import from
-        dir = args[0]
-
-        class colon_separated(csv.excel):
-            delimiter = ':'
-
-        # import all the files
-        for file in os.listdir(dir):
-            classname, ext = os.path.splitext(file)
-            # we only care about CSV files
-            if ext != '.csv' or classname.endswith('-journals'):
-                continue
-
-            cl = self.get_class(classname)
-
-            # ensure that the properties and the CSV file headings match
-            f = open(os.path.join(dir, file), 'r')
-            reader = csv.reader(f, colon_separated)
-            file_props = None
-            maxid = 1
-            # loop through the file and create a node for each entry
-            for n, r in enumerate(reader):
-                if file_props is None:
-                    file_props = r
-                    continue
-
-                if self.verbose:
-                    sys.stdout.write('\rImporting %s - %s'%(classname, n))
-                    sys.stdout.flush()
-
-                # do the import and figure the current highest nodeid
-                nodeid = cl.import_list(file_props, r)
-                if hasattr(cl, 'import_files'):
-                    cl.import_files(dir, nodeid)
-                maxid = max(maxid, int(nodeid))
-
-            # (print to sys.stdout here to allow tests to squash it .. ugh)
-            print >> sys.stdout
-
-            f.close()
-
-            # import the journals
-            f = open(os.path.join(args[0], classname + '-journals.csv'), 'r')
-            reader = csv.reader(f, colon_separated)
-            cl.import_journals(reader)
-            f.close()
-
-            # (print to sys.stdout here to allow tests to squash it .. ugh)
-            print >> sys.stdout, 'setting', classname, maxid+1
-
-            # set the id counter
-            self.db.setid(classname, str(maxid+1))
-
-        self.db_uncommitted = True
-        return 0
-
-    def do_pack(self, args):
-        ''"""Usage: pack period | date
-
-        Remove journal entries older than a period of time specified or
-        before a certain date.
-
-        A period is specified using the suffixes "y", "m", and "d". The
-        suffix "w" (for "week") means 7 days.
-
-              "3y" means three years
-              "2y 1m" means two years and one month
-              "1m 25d" means one month and 25 days
-              "2w 3d" means two weeks and three days
-
-        Date format is "YYYY-MM-DD" eg:
-            2001-01-01
-
-        """
-        if len(args) != 1:
-            raise UsageError(_('Not enough arguments supplied'))
-
-        # are we dealing with a period or a date
-        value = args[0]
-        date_re = re.compile(r"""
-              (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
-              (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
-              """, re.VERBOSE)
-        m = date_re.match(value)
-        if not m:
-            raise ValueError(_('Invalid format'))
-        m = m.groupdict()
-        if m['period']:
-            pack_before = date.Date(". - %s"%value)
-        elif m['date']:
-            pack_before = date.Date(value)
-        self.db.pack(pack_before)
-        self.db_uncommitted = True
-        return 0
-
-    def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')):
-        ''"""Usage: reindex [classname|designator]*
-        Re-generate a tracker's search indexes.
-
-        This will re-generate the search indexes for a tracker.
-        This will typically happen automatically.
-        """
-        if args:
-            for arg in args:
-                m = desre.match(arg)
-                if m:
-                    cl = self.get_class(m.group(1))
-                    try:
-                        cl.index(m.group(2))
-                    except IndexError:
-                        raise UsageError(_('no such item "%(designator)s"')%{
-                            'designator': arg})
-                else:
-                    cl = self.get_class(arg)
-                    self.db.reindex(arg)
-        else:
-            self.db.reindex(show_progress=True)
-        return 0
-
-    def do_security(self, args):
-        ''"""Usage: security [Role name]
-        Display the Permissions available to one or all Roles.
-        """
-        if len(args) == 1:
-            role = args[0]
-            try:
-                roles = [(args[0], self.db.security.role[args[0]])]
-            except KeyError:
-                print _('No such Role "%(role)s"')%locals()
-                return 1
-        else:
-            roles = list(self.db.security.role.items())
-            role = self.db.config.NEW_WEB_USER_ROLES
-            if ',' in role:
-                print _('New Web users get the Roles "%(role)s"')%locals()
-            else:
-                print _('New Web users get the Role "%(role)s"')%locals()
-            role = self.db.config.NEW_EMAIL_USER_ROLES
-            if ',' in role:
-                print _('New Email users get the Roles "%(role)s"')%locals()
-            else:
-                print _('New Email users get the Role "%(role)s"')%locals()
-        roles.sort()
-        for rolename, role in roles:
-            print _('Role "%(name)s":')%role.__dict__
-            for permission in role.permissions:
-                d = permission.__dict__
-                if permission.klass:
-                    if permission.properties:
-                        print _(' %(description)s (%(name)s for "%(klass)s"'
-                          ': %(properties)s only)')%d
-                    else:
-                        print _(' %(description)s (%(name)s for "%(klass)s" '
-                            'only)')%d
-                else:
-                    print _(' %(description)s (%(name)s)')%d
-        return 0
-
-
-    def do_migrate(self, args):
-        ''"""Usage: migrate
-        Update a tracker's database to be compatible with the Roundup
-        codebase.
-
-        You should run the "migrate" command for your tracker once you've
-        installed the latest codebase. 
-
-        Do this before you use the web, command-line or mail interface and
-        before any users access the tracker.
-
-        This command will respond with either "Tracker updated" (if you've
-        not previously run it on an RDBMS backend) or "No migration action
-        required" (if you have run it, or have used another interface to the
-        tracker, or possibly because you are using anydbm).
-
-        It's safe to run this even if it's not required, so just get into
-        the habit.
-        """
-        if getattr(self.db, 'db_version_updated'):
-            print _('Tracker updated')
-            self.db_uncommitted = True
-        else:
-            print _('No migration action required')
-        return 0
-
-    def run_command(self, args):
-        """Run a single command
-        """
-        command = args[0]
-
-        # handle help now
-        if command == 'help':
-            if len(args)>1:
-                self.do_help(args[1:])
-                return 0
-            self.do_help(['help'])
-            return 0
-        if command == 'morehelp':
-            self.do_help(['help'])
-            self.help_commands()
-            self.help_all()
-            return 0
-        if command == 'config':
-            self.do_config(args[1:])
-            return 0
-
-        # figure what the command is
-        try:
-            functions = self.commands.get(command)
-        except KeyError:
-            # not a valid command
-            print _('Unknown command "%(command)s" ("help commands" for a '
-                'list)')%locals()
-            return 1
-
-        # check for multiple matches
-        if len(functions) > 1:
-            print _('Multiple commands match "%(command)s": %(list)s')%{'command':
-                command, 'list': ', '.join([i[0] for i in functions])}
-            return 1
-        command, function = functions[0]
-
-        # make sure we have a tracker_home
-        while not self.tracker_home:
-            self.tracker_home = raw_input(_('Enter tracker home: ')).strip()
-
-        # before we open the db, we may be doing an install or init
-        if command == 'initialise':
-            try:
-                return self.do_initialise(self.tracker_home, args)
-            except UsageError, message:
-                print _('Error: %(message)s')%locals()
-                return 1
-        elif command == 'install':
-            try:
-                return self.do_install(self.tracker_home, args)
-            except UsageError, message:
-                print _('Error: %(message)s')%locals()
-                return 1
-
-        # get the tracker
-        try:
-            tracker = roundup.instance.open(self.tracker_home)
-        except ValueError, message:
-            self.tracker_home = ''
-            print _("Error: Couldn't open tracker: %(message)s")%locals()
-            return 1
-
-        # only open the database once!
-        if not self.db:
-            self.db = tracker.open('admin')
-
-        # do the command
-        ret = 0
-        try:
-            ret = function(args[1:])
-        except UsageError, message:
-            print _('Error: %(message)s')%locals()
-            print
-            print function.__doc__
-            ret = 1
-        except:
-            import traceback
-            traceback.print_exc()
-            ret = 1
-        return ret
-
-    def interactive(self):
-        """Run in an interactive mode
-        """
-        print _('Roundup %s ready for input.\nType "help" for help.'
-            % roundup_version)
-        try:
-            import readline
-        except ImportError:
-            print _('Note: command history and editing not available')
-
-        while 1:
-            try:
-                command = raw_input(_('roundup> '))
-            except EOFError:
-                print _('exit...')
-                break
-            if not command: continue
-            args = token.token_split(command)
-            if not args: continue
-            if args[0] in ('quit', 'exit'): break
-            self.run_command(args)
-
-        # exit.. check for transactions
-        if self.db and self.db_uncommitted:
-            commit = raw_input(_('There are unsaved changes. Commit them (y/N)? '))
-            if commit and commit[0].lower() == 'y':
-                self.db.commit()
-        return 0
-
-    def main(self):
-        try:
-            opts, args = getopt.getopt(sys.argv[1:], 'i:u:hcdsS:vV')
-        except getopt.GetoptError, e:
-            self.usage(str(e))
-            return 1
-
-        # handle command-line args
-        self.tracker_home = os.environ.get('TRACKER_HOME', '')
-        # TODO: reinstate the user/password stuff (-u arg too)
-        name = password = ''
-        if 'ROUNDUP_LOGIN' in os.environ:
-            l = os.environ['ROUNDUP_LOGIN'].split(':')
-            name = l[0]
-            if len(l) > 1:
-                password = l[1]
-        self.separator = None
-        self.print_designator = 0
-        self.verbose = 0
-        for opt, arg in opts:
-            if opt == '-h':
-                self.usage()
-                return 0
-            elif opt == '-v':
-                print '%s (python %s)'%(roundup_version, sys.version.split()[0])
-                return 0
-            elif opt == '-V':
-                self.verbose = 1
-            elif opt == '-i':
-                self.tracker_home = arg
-            elif opt == '-c':
-                if self.separator != None:
-                    self.usage('Only one of -c, -S and -s may be specified')
-                    return 1
-                self.separator = ','
-            elif opt == '-S':
-                if self.separator != None:
-                    self.usage('Only one of -c, -S and -s may be specified')
-                    return 1
-                self.separator = arg
-            elif opt == '-s':
-                if self.separator != None:
-                    self.usage('Only one of -c, -S and -s may be specified')
-                    return 1
-                self.separator = ' '
-            elif opt == '-d':
-                self.print_designator = 1
-
-        # if no command - go interactive
-        # wrap in a try/finally so we always close off the db
-        ret = 0
-        try:
-            if not args:
-                self.interactive()
-            else:
-                ret = self.run_command(args)
-                if self.db: self.db.commit()
-            return ret
-        finally:
-            if self.db:
-                self.db.close()
-
-if __name__ == '__main__':
-    tool = AdminTool()
-    sys.exit(tool.main())
-
-# vim: set filetype=python sts=4 sw=4 et si :
diff --git a/build/lib/roundup/anypy/__init__.py b/build/lib/roundup/anypy/__init__.py
deleted file mode 100644 (file)
index 21eee1e..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-roundup.anypy - compatibility layer for any Python 2.3+
-"""
-VERSION = '.'.join(map(str,
-                       (0,
-                        1,  # hashlib_, sets_
-                        )))
diff --git a/build/lib/roundup/anypy/cookie_.py b/build/lib/roundup/anypy/cookie_.py
deleted file mode 100644 (file)
index 44bf93a..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-
-try:
-    from http import cookies as Cookie
-    from http.cookies import CookieError, BaseCookie, SimpleCookie
-    from http.cookies import _getdate as get_cookie_date
-except:
-    from Cookie import CookieError, BaseCookie, SimpleCookie
-    from Cookie import _getdate as get_cookie_date
diff --git a/build/lib/roundup/anypy/dbm_.py b/build/lib/roundup/anypy/dbm_.py
deleted file mode 100644 (file)
index 093a2f1..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-# In Python 3 the "anydbm" module was renamed to be "dbm" which is now a
-# package containing the various implementations. The "wichdb" module's
-# whichdb() function was moved to the new "dbm" module.
-
-try:
-    # old school first because <3 had a "dbm" module too...
-    import anydbm
-    from whichdb import whichdb
-except ImportError:
-    # python 3+
-    import dbm as anydbm
-    whichdb = anydbm.whichdb
diff --git a/build/lib/roundup/anypy/hashlib_.py b/build/lib/roundup/anypy/hashlib_.py
deleted file mode 100644 (file)
index 9365b97..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-"""
-anypy.hashlib_: encapsulation of hashlib/md5/sha1/sha
-"""
-
-try:
-    from hashlib import md5, sha1 # new in Python 2.5
-except ImportError:
-    from md5 import md5           # deprecated in Python 2.6
-    from sha import sha as sha1   # deprecated in Python 2.6
-
-# vim: ts=8 sts=4 sw=4 si
diff --git a/build/lib/roundup/anypy/http_.py b/build/lib/roundup/anypy/http_.py
deleted file mode 100644 (file)
index a7f5238..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-try:
-    from http import client
-except:
-    import httplib as client
-
diff --git a/build/lib/roundup/anypy/io_.py b/build/lib/roundup/anypy/io_.py
deleted file mode 100644 (file)
index 86f8023..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-
-try:
-    from io import StringIO
-except:
-    from StringIO import StringIO
-
diff --git a/build/lib/roundup/anypy/sets_.py b/build/lib/roundup/anypy/sets_.py
deleted file mode 100644 (file)
index e1f2f30..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-anypy.sets_: sets compatibility module
-
-uses the built-in type 'set' if available, and thus avoids
-deprecation warnings. Simple usage:
-
-Change all
-    from sets import Set
-to
-    from roundup.anypy.sets_ import set
-
-and use 'set' instead of 'Set'.
-To avoid unnecessary imports, you can:
-
-    try:
-        set
-    except NameError:
-        from roundup.anypy.sets_ import set
-
-see:
-http://docs.python.org/library/sets.html#comparison-to-the-built-in-set-types
-
-"""
-
-try:
-    set = set                     # built-in since Python 2.4
-except NameError, TypeError:
-    from sets import Set as set   # deprecated as of Python 2.6
-
-# vim: ts=8 sts=4 sw=4 si et
diff --git a/build/lib/roundup/anypy/urllib_.py b/build/lib/roundup/anypy/urllib_.py
deleted file mode 100644 (file)
index d1a8a4b..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-
-try:
-    from urllib.parse import quote, urlparse
-except:
-    from urllib import quote
-    from urlparse import urlparse
diff --git a/build/lib/roundup/backends/__init__.py b/build/lib/roundup/backends/__init__.py
deleted file mode 100644 (file)
index 1fc7948..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-#
-# 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: __init__.py,v 1.40 2007-11-07 20:47:12 richard Exp $
-
-'''Container for the hyperdb storage backend implementations.
-'''
-__docformat__ = 'restructuredtext'
-
-import sys
-
-# These names are used to suppress import errors.
-# If get_backend raises an ImportError with appropriate
-# module name, have_backend quietly returns False.
-# Otherwise the error is reraised.
-_modules = {
-    'mysql': ('MySQLdb',),
-    'postgresql': ('psycopg',),
-    'tsearch2': ('psycopg',),
-    'sqlite': ('pysqlite', 'pysqlite2', 'sqlite3', '_sqlite3', 'sqlite'),
-}
-
-def get_backend(name):
-    '''Get a specific backend by name.'''
-    vars = globals()
-    # if requested backend has been imported yet, return current instance
-    if name in vars:
-        return vars[name]
-    # import the backend module
-    module_name = 'back_%s' % name
-    try:
-        module = __import__(module_name, vars)
-    except:
-        # import failed, but in versions prior to 2.4, a (broken)
-        # module is left in sys.modules and package globals;
-        # subsequent imports would succeed and get the broken module.
-        # This no longer happens in Python 2.4 and later.
-        if sys.version_info < (2, 4):
-            del sys.modules['.'.join((__name__, module_name))]
-            del vars[module_name]
-        raise
-    else:
-        vars[name] = module
-        return module
-
-def have_backend(name):
-    '''Is backend "name" available?'''
-    if name == 'tsearch2':
-        # currently not working
-        return 0
-    try:
-        get_backend(name)
-        return 1
-    except ImportError, e:
-        for name in _modules.get(name, (name,)):
-            if str(e).startswith('No module named %s'%name):
-                return 0
-        raise
-    return 0
-
-def list_backends():
-    '''List all available backend names.
-
-    This function has side-effect of registering backward-compatible
-    globals for all available backends.
-
-    '''
-    l = []
-    for name in 'anydbm', 'mysql', 'sqlite', 'postgresql':
-        if have_backend(name):
-            l.append(name)
-    return l
-
-# vim: set filetype=python sts=4 sw=4 et si :
diff --git a/build/lib/roundup/backends/back_anydbm.py b/build/lib/roundup/backends/back_anydbm.py
deleted file mode 100644 (file)
index baa389b..0000000
+++ /dev/null
@@ -1,2169 +0,0 @@
-#
-# 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.
-#
-"""This module defines a backend that saves the hyperdatabase in a
-database chosen by anydbm. It is guaranteed to always be available in python
-versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
-serious bugs, and is not available)
-"""
-__docformat__ = 'restructuredtext'
-
-import os, marshal, re, weakref, string, copy, time, shutil, logging
-
-from roundup.anypy.dbm_ import anydbm, whichdb
-
-from roundup import hyperdb, date, password, roundupdb, security, support
-from roundup.support import reversed
-from roundup.backends import locking
-from roundup.i18n import _
-
-from roundup.backends.blobfiles import FileStorage
-from roundup.backends.sessions_dbm import Sessions, OneTimeKeys
-
-try:
-    from roundup.backends.indexer_xapian import Indexer
-except ImportError:
-    from roundup.backends.indexer_dbm import Indexer
-
-def db_exists(config):
-    # check for the user db
-    for db in 'nodes.user nodes.user.db'.split():
-        if os.path.exists(os.path.join(config.DATABASE, db)):
-            return 1
-    return 0
-
-def db_nuke(config):
-    shutil.rmtree(config.DATABASE)
-
-#
-# Now the database
-#
-class Database(FileStorage, hyperdb.Database, roundupdb.Database):
-    """A database for storing records containing flexible data types.
-
-    Transaction stuff TODO:
-
-    - check the timestamp of the class file and nuke the cache if it's
-      modified. Do some sort of conflict checking on the dirty stuff.
-    - perhaps detect write collisions (related to above)?
-    """
-    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(), Class.retire(), and Class.restore() methods are
-        disabled.
-        """
-        FileStorage.__init__(self, config.UMASK)
-        self.config, self.journaltag = config, journaltag
-        self.dir = config.DATABASE
-        self.classes = {}
-        self.cache = {}         # cache of nodes loaded or created
-        self.stats = {'cache_hits': 0, 'cache_misses': 0, 'get_items': 0,
-            'filtering': 0}
-        self.dirtynodes = {}    # keep track of the dirty nodes by class
-        self.newnodes = {}      # keep track of the new nodes by class
-        self.destroyednodes = {}# keep track of the destroyed nodes by class
-        self.transactions = []
-        self.indexer = Indexer(self)
-        self.security = security.Security(self)
-        os.umask(config.UMASK)
-
-        # lock it
-        lockfilenm = os.path.join(self.dir, 'lock')
-        self.lockfile = locking.acquire_lock(lockfilenm)
-        self.lockfile.write(str(os.getpid()))
-        self.lockfile.flush()
-
-    def post_init(self):
-        """Called once the schema initialisation has finished.
-        """
-        # reindex the db if necessary
-        if self.indexer.should_reindex():
-            self.reindex()
-
-    def refresh_database(self):
-        """Rebuild the database
-        """
-        self.reindex()
-
-    def getSessionManager(self):
-        return Sessions(self)
-
-    def getOTKManager(self):
-        return OneTimeKeys(self)
-
-    def reindex(self, classname=None, show_progress=False):
-        if classname:
-            classes = [self.getclass(classname)]
-        else:
-            classes = self.classes.values()
-        for klass in classes:
-            if show_progress:
-                for nodeid in support.Progress('Reindex %s'%klass.classname,
-                        klass.list()):
-                    klass.index(nodeid)
-            else:
-                for nodeid in klass.list():
-                    klass.index(nodeid)
-        self.indexer.save_index()
-
-    def __repr__(self):
-        return '<back_anydbm instance at %x>'%id(self)
-
-    #
-    # Classes
-    #
-    def __getattr__(self, classname):
-        """A convenient way of calling self.getclass(classname)."""
-        if classname in self.classes:
-            return self.classes[classname]
-        raise AttributeError, classname
-
-    def addclass(self, cl):
-        cn = cl.classname
-        if cn in self.classes:
-            raise ValueError, cn
-        self.classes[cn] = cl
-
-        # add default Edit and View permissions
-        self.security.addPermission(name="Create", klass=cn,
-            description="User is allowed to create "+cn)
-        self.security.addPermission(name="Edit", klass=cn,
-            description="User is allowed to edit "+cn)
-        self.security.addPermission(name="View", klass=cn,
-            description="User is allowed to access "+cn)
-
-    def getclasses(self):
-        """Return a list of the names of all existing classes."""
-        return sorted(self.classes)
-
-    def getclass(self, classname):
-        """Get the Class object representing a particular class.
-
-        If 'classname' is not a valid class name, a KeyError is raised.
-        """
-        try:
-            return self.classes[classname]
-        except KeyError:
-            raise KeyError('There is no class called "%s"'%classname)
-
-    #
-    # Class DBs
-    #
-    def clear(self):
-        """Delete all database contents
-        """
-        logging.getLogger('hyperdb').info('clear')
-        for cn in self.classes:
-            for dummy in 'nodes', 'journals':
-                path = os.path.join(self.dir, 'journals.%s'%cn)
-                if os.path.exists(path):
-                    os.remove(path)
-                elif os.path.exists(path+'.db'):    # dbm appends .db
-                    os.remove(path+'.db')
-        # reset id sequences
-        path = os.path.join(os.getcwd(), self.dir, '_ids')
-        if os.path.exists(path):
-            os.remove(path)
-        elif os.path.exists(path+'.db'):    # dbm appends .db
-            os.remove(path+'.db')
-
-    def getclassdb(self, classname, mode='r'):
-        """ grab a connection to the class db that will be used for
-            multiple actions
-        """
-        return self.opendb('nodes.%s'%classname, mode)
-
-    def determine_db_type(self, path):
-        """ determine which DB wrote the class file
-        """
-        db_type = ''
-        if os.path.exists(path):
-            db_type = whichdb(path)
-            if not db_type:
-                raise hyperdb.DatabaseError(_("Couldn't identify database type"))
-        elif os.path.exists(path+'.db'):
-            # if the path ends in '.db', it's a dbm database, whether
-            # anydbm says it's dbhash or not!
-            db_type = 'dbm'
-        return db_type
-
-    def opendb(self, name, mode):
-        """Low-level database opener that gets around anydbm/dbm
-           eccentricities.
-        """
-        # figure the class db type
-        path = os.path.join(os.getcwd(), self.dir, name)
-        db_type = self.determine_db_type(path)
-
-        # new database? let anydbm pick the best dbm
-        # in Python 3+ the "dbm" ("anydbm" to us) module already uses the
-        # whichdb() function to do this
-        if not db_type or hasattr(anydbm, 'whichdb'):
-            if __debug__:
-                logging.getLogger('hyperdb').debug(
-                    "opendb anydbm.open(%r, 'c')"%path)
-            return anydbm.open(path, 'c')
-
-        # in Python <3 it anydbm was a little dumb so manually open the
-        # database with the correct module
-        try:
-            dbm = __import__(db_type)
-        except ImportError:
-            raise hyperdb.DatabaseError(_("Couldn't open database - the "
-                "required module '%s' is not available")%db_type)
-        if __debug__:
-            logging.getLogger('hyperdb').debug(
-                "opendb %r.open(%r, %r)"%(db_type, path, mode))
-        return dbm.open(path, mode)
-
-    #
-    # Node IDs
-    #
-    def newid(self, classname):
-        """ Generate a new id for the given class
-        """
-        # open the ids DB - create if if doesn't exist
-        db = self.opendb('_ids', 'c')
-        if classname in db:
-            newid = db[classname] = str(int(db[classname]) + 1)
-        else:
-            # the count() bit is transitional - older dbs won't start at 1
-            newid = str(self.getclass(classname).count()+1)
-            db[classname] = newid
-        db.close()
-        return newid
-
-    def setid(self, classname, setid):
-        """ Set the id counter: used during import of database
-        """
-        # open the ids DB - create if if doesn't exist
-        db = self.opendb('_ids', 'c')
-        db[classname] = str(setid)
-        db.close()
-
-    #
-    # Nodes
-    #
-    def addnode(self, classname, nodeid, node):
-        """ add the specified node to its class's db
-        """
-        # we'll be supplied these props if we're doing an import
-        if 'creator' not in node:
-            # add in the "calculated" properties (dupe so we don't affect
-            # calling code's node assumptions)
-            node = node.copy()
-            node['creator'] = self.getuid()
-            node['actor'] = self.getuid()
-            node['creation'] = node['activity'] = date.Date()
-
-        self.newnodes.setdefault(classname, {})[nodeid] = 1
-        self.cache.setdefault(classname, {})[nodeid] = node
-        self.savenode(classname, nodeid, node)
-
-    def setnode(self, classname, nodeid, node):
-        """ change the specified node
-        """
-        self.dirtynodes.setdefault(classname, {})[nodeid] = 1
-
-        # can't set without having already loaded the node
-        self.cache[classname][nodeid] = node
-        self.savenode(classname, nodeid, node)
-
-    def savenode(self, classname, nodeid, node):
-        """ perform the saving of data specified by the set/addnode
-        """
-        if __debug__:
-            logging.getLogger('hyperdb').debug('save %s%s %r'%(classname, nodeid, node))
-        self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
-
-    def getnode(self, classname, nodeid, db=None, cache=1):
-        """ get a node from the database
-
-            Note the "cache" parameter is not used, and exists purely for
-            backward compatibility!
-        """
-        # try the cache
-        cache_dict = self.cache.setdefault(classname, {})
-        if nodeid in cache_dict:
-            if __debug__:
-                logging.getLogger('hyperdb').debug('get %s%s cached'%(classname, nodeid))
-                self.stats['cache_hits'] += 1
-            return cache_dict[nodeid]
-
-        if __debug__:
-            self.stats['cache_misses'] += 1
-            start_t = time.time()
-            logging.getLogger('hyperdb').debug('get %s%s'%(classname, nodeid))
-
-        # get from the database and save in the cache
-        if db is None:
-            db = self.getclassdb(classname)
-        if nodeid not in db:
-            raise IndexError("no such %s %s"%(classname, nodeid))
-
-        # check the uncommitted, destroyed nodes
-        if (classname in self.destroyednodes and
-                nodeid in self.destroyednodes[classname]):
-            raise IndexError("no such %s %s"%(classname, nodeid))
-
-        # decode
-        res = marshal.loads(db[nodeid])
-
-        # reverse the serialisation
-        res = self.unserialise(classname, res)
-
-        # store off in the cache dict
-        if cache:
-            cache_dict[nodeid] = res
-
-        if __debug__:
-            self.stats['get_items'] += (time.time() - start_t)
-
-        return res
-
-    def destroynode(self, classname, nodeid):
-        """Remove a node from the database. Called exclusively by the
-           destroy() method on Class.
-        """
-        logging.getLogger('hyperdb').info('destroy %s%s'%(classname, nodeid))
-
-        # remove from cache and newnodes if it's there
-        if (classname in self.cache and nodeid in self.cache[classname]):
-            del self.cache[classname][nodeid]
-        if (classname in self.newnodes and nodeid in self.newnodes[classname]):
-            del self.newnodes[classname][nodeid]
-
-        # see if there's any obvious commit actions that we should get rid of
-        for entry in self.transactions[:]:
-            if entry[1][:2] == (classname, nodeid):
-                self.transactions.remove(entry)
-
-        # add to the destroyednodes map
-        self.destroyednodes.setdefault(classname, {})[nodeid] = 1
-
-        # add the destroy commit action
-        self.transactions.append((self.doDestroyNode, (classname, nodeid)))
-        self.transactions.append((FileStorage.destroy, (self, classname, nodeid)))
-
-    def serialise(self, classname, node):
-        """Copy the node contents, converting non-marshallable data into
-           marshallable data.
-        """
-        properties = self.getclass(classname).getprops()
-        d = {}
-        for k, v in node.iteritems():
-            if k == self.RETIRED_FLAG:
-                d[k] = v
-                continue
-
-            # if the property doesn't exist then we really don't care
-            if k not in properties:
-                continue
-
-            # get the property spec
-            prop = properties[k]
-
-            if isinstance(prop, hyperdb.Password) and v is not None:
-                d[k] = str(v)
-            elif isinstance(prop, hyperdb.Date) and v is not None:
-                d[k] = v.serialise()
-            elif isinstance(prop, hyperdb.Interval) and v is not None:
-                d[k] = v.serialise()
-            else:
-                d[k] = v
-        return d
-
-    def unserialise(self, classname, node):
-        """Decode the marshalled node data
-        """
-        properties = self.getclass(classname).getprops()
-        d = {}
-        for k, v in node.iteritems():
-            # if the property doesn't exist, or is the "retired" flag then
-            # it won't be in the properties dict
-            if k not in properties:
-                d[k] = v
-                continue
-
-            # get the property spec
-            prop = properties[k]
-
-            if isinstance(prop, hyperdb.Date) and v is not None:
-                d[k] = date.Date(v)
-            elif isinstance(prop, hyperdb.Interval) and v is not None:
-                d[k] = date.Interval(v)
-            elif isinstance(prop, hyperdb.Password) and v is not None:
-                p = password.Password()
-                p.unpack(v)
-                d[k] = p
-            else:
-                d[k] = v
-        return d
-
-    def hasnode(self, classname, nodeid, db=None):
-        """ determine if the database has a given node
-        """
-        # try the cache
-        cache = self.cache.setdefault(classname, {})
-        if nodeid in cache:
-            return 1
-
-        # not in the cache - check the database
-        if db is None:
-            db = self.getclassdb(classname)
-        return nodeid in db
-
-    def countnodes(self, classname, db=None):
-        count = 0
-
-        # include the uncommitted nodes
-        if classname in self.newnodes:
-            count += len(self.newnodes[classname])
-        if classname in self.destroyednodes:
-            count -= len(self.destroyednodes[classname])
-
-        # and count those in the DB
-        if db is None:
-            db = self.getclassdb(classname)
-        return count + len(db)
-
-
-    #
-    # Files - special node properties
-    # inherited from FileStorage
-
-    #
-    # Journal
-    #
-    def addjournal(self, classname, nodeid, action, params, creator=None,
-            creation=None):
-        """ 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
-
-            'creator' -- the user performing the action, which defaults to
-            the current user.
-        """
-        if __debug__:
-            logging.getLogger('hyperdb').debug('addjournal %s%s %s %r %s %r'%(classname,
-                nodeid, action, params, creator, creation))
-        if creator is None:
-            creator = self.getuid()
-        self.transactions.append((self.doSaveJournal, (classname, nodeid,
-            action, params, creator, creation)))
-
-    def setjournal(self, classname, nodeid, journal):
-        """Set the journal to the "journal" list."""
-        if __debug__:
-            logging.getLogger('hyperdb').debug('setjournal %s%s %r'%(classname,
-                nodeid, journal))
-        self.transactions.append((self.doSetJournal, (classname, nodeid,
-            journal)))
-
-    def getjournal(self, classname, nodeid):
-        """ get the journal for id
-
-            Raise IndexError if the node doesn't exist (as per history()'s
-            API)
-        """
-        # our journal result
-        res = []
-
-        # add any journal entries for transactions not committed to the
-        # database
-        for method, args in self.transactions:
-            if method != self.doSaveJournal:
-                continue
-            (cache_classname, cache_nodeid, cache_action, cache_params,
-                cache_creator, cache_creation) = args
-            if cache_classname == classname and cache_nodeid == nodeid:
-                if not cache_creator:
-                    cache_creator = self.getuid()
-                if not cache_creation:
-                    cache_creation = date.Date()
-                res.append((cache_nodeid, cache_creation, cache_creator,
-                    cache_action, cache_params))
-
-        # attempt to open the journal - in some rare cases, the journal may
-        # not exist
-        try:
-            db = self.opendb('journals.%s'%classname, 'r')
-        except anydbm.error, error:
-            if str(error) == "need 'c' or 'n' flag to open new db":
-                raise IndexError('no such %s %s'%(classname, nodeid))
-            elif error.args[0] != 2:
-                # this isn't a "not found" error, be alarmed!
-                raise
-            if res:
-                # we have unsaved journal entries, return them
-                return res
-            raise IndexError('no such %s %s'%(classname, nodeid))
-        try:
-            journal = marshal.loads(db[nodeid])
-        except KeyError:
-            db.close()
-            if res:
-                # we have some unsaved journal entries, be happy!
-                return res
-            raise IndexError('no such %s %s'%(classname, nodeid))
-        db.close()
-
-        # add all the saved journal entries for this node
-        for nodeid, date_stamp, user, action, params in journal:
-            res.append((nodeid, date.Date(date_stamp), user, action, params))
-        return res
-
-    def pack(self, pack_before):
-        """ Delete all journal entries except "create" before 'pack_before'.
-        """
-        pack_before = pack_before.serialise()
-        for classname in self.getclasses():
-            packed = 0
-            # get the journal db
-            db_name = 'journals.%s'%classname
-            path = os.path.join(os.getcwd(), self.dir, classname)
-            db_type = self.determine_db_type(path)
-            db = self.opendb(db_name, 'w')
-
-            for key in db:
-                # get the journal for this db entry
-                journal = marshal.loads(db[key])
-                l = []
-                last_set_entry = None
-                for entry in journal:
-                    # unpack the entry
-                    (nodeid, date_stamp, self.journaltag, action,
-                        params) = entry
-                    # if the entry is after the pack date, _or_ the initial
-                    # create entry, then it stays
-                    if date_stamp > pack_before or action == 'create':
-                        l.append(entry)
-                    else:
-                        packed += 1
-                db[key] = marshal.dumps(l)
-
-                logging.getLogger('hyperdb').info('packed %d %s items'%(packed,
-                    classname))
-
-            if db_type == 'gdbm':
-                db.reorganize()
-            db.close()
-
-
-    #
-    # Basic transaction support
-    #
-    def commit(self, fail_ok=False):
-        """ Commit the current transactions.
-
-        Save all data changed since the database was opened or since the
-        last commit() or rollback().
-
-        fail_ok indicates that the commit is allowed to fail. This is used
-        in the web interface when committing cleaning of the session
-        database. We don't care if there's a concurrency issue there.
-
-        The only backend this seems to affect is postgres.
-        """
-        logging.getLogger('hyperdb').info('commit %s transactions'%(
-            len(self.transactions)))
-
-        # keep a handle to all the database files opened
-        self.databases = {}
-
-        try:
-            # now, do all the transactions
-            reindex = {}
-            for method, args in self.transactions:
-                reindex[method(*args)] = 1
-        finally:
-            # make sure we close all the database files
-            for db in self.databases.itervalues():
-                db.close()
-            del self.databases
-
-        # clear the transactions list now so the blobfile implementation
-        # doesn't think there's still pending file commits when it tries
-        # to access the file data
-        self.transactions = []
-
-        # reindex the nodes that request it
-        for classname, nodeid in [k for k in reindex if k]:
-            self.getclass(classname).index(nodeid)
-
-        # save the indexer state
-        self.indexer.save_index()
-
-        self.clearCache()
-
-    def clearCache(self):
-        # all transactions committed, back to normal
-        self.cache = {}
-        self.dirtynodes = {}
-        self.newnodes = {}
-        self.destroyednodes = {}
-        self.transactions = []
-
-    def getCachedClassDB(self, classname):
-        """ get the class db, looking in our cache of databases for commit
-        """
-        # get the database handle
-        db_name = 'nodes.%s'%classname
-        if db_name not in self.databases:
-            self.databases[db_name] = self.getclassdb(classname, 'c')
-        return self.databases[db_name]
-
-    def doSaveNode(self, classname, nodeid, node):
-        db = self.getCachedClassDB(classname)
-
-        # now save the marshalled data
-        db[nodeid] = marshal.dumps(self.serialise(classname, node))
-
-        # return the classname, nodeid so we reindex this content
-        return (classname, nodeid)
-
-    def getCachedJournalDB(self, classname):
-        """ get the journal db, looking in our cache of databases for commit
-        """
-        # get the database handle
-        db_name = 'journals.%s'%classname
-        if db_name not in self.databases:
-            self.databases[db_name] = self.opendb(db_name, 'c')
-        return self.databases[db_name]
-
-    def doSaveJournal(self, classname, nodeid, action, params, creator,
-            creation):
-        # serialise the parameters now if necessary
-        if isinstance(params, type({})):
-            if action in ('set', 'create'):
-                params = self.serialise(classname, params)
-
-        # handle supply of the special journalling parameters (usually
-        # supplied on importing an existing database)
-        journaltag = creator
-        if creation:
-            journaldate = creation.serialise()
-        else:
-            journaldate = date.Date().serialise()
-
-        # create the journal entry
-        entry = (nodeid, journaldate, journaltag, action, params)
-
-        db = self.getCachedJournalDB(classname)
-
-        # now insert the journal entry
-        if nodeid in db:
-            # append to existing
-            s = db[nodeid]
-            l = marshal.loads(s)
-            l.append(entry)
-        else:
-            l = [entry]
-
-        db[nodeid] = marshal.dumps(l)
-
-    def doSetJournal(self, classname, nodeid, journal):
-        l = []
-        for nodeid, journaldate, journaltag, action, params in journal:
-            # serialise the parameters now if necessary
-            if isinstance(params, type({})):
-                if action in ('set', 'create'):
-                    params = self.serialise(classname, params)
-            journaldate = journaldate.serialise()
-            l.append((nodeid, journaldate, journaltag, action, params))
-        db = self.getCachedJournalDB(classname)
-        db[nodeid] = marshal.dumps(l)
-
-    def doDestroyNode(self, classname, nodeid):
-        # delete from the class database
-        db = self.getCachedClassDB(classname)
-        if nodeid in db:
-            del db[nodeid]
-
-        # delete from the database
-        db = self.getCachedJournalDB(classname)
-        if nodeid in db:
-            del db[nodeid]
-
-    def rollback(self):
-        """ Reverse all actions from the current transaction.
-        """
-        logging.getLogger('hyperdb').info('rollback %s transactions'%(
-            len(self.transactions)))
-
-        for method, args in self.transactions:
-            # delete temporary files
-            if method == self.doStoreFile:
-                self.rollbackStoreFile(*args)
-        self.cache = {}
-        self.dirtynodes = {}
-        self.newnodes = {}
-        self.destroyednodes = {}
-        self.transactions = []
-
-    def close(self):
-        """ Nothing to do
-        """
-        if self.lockfile is not None:
-            locking.release_lock(self.lockfile)
-            self.lockfile.close()
-            self.lockfile = None
-
-_marker = []
-class Class(hyperdb.Class):
-    """The handle to a particular class of nodes in a hyperdatabase."""
-
-    def enableJournalling(self):
-        """Turn journalling on for this class
-        """
-        self.do_journal = 1
-
-    def disableJournalling(self):
-        """Turn journalling off for this class
-        """
-        self.do_journal = 0
-
-    # 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.
-
-        These operations trigger detectors and can be vetoed.  Attempts
-        to modify the "creation" or "activity" properties cause a KeyError.
-        """
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError(_('Database open read-only'))
-        self.fireAuditors('create', None, propvalues)
-        newid = self.create_inner(**propvalues)
-        self.fireReactors('create', newid, None)
-        return newid
-
-    def create_inner(self, **propvalues):
-        """ Called by create, in-between the audit and react calls.
-        """
-        if 'id' in propvalues:
-            raise KeyError('"id" is reserved')
-
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError(_('Database open read-only'))
-
-        if 'creation' in propvalues or 'activity' in propvalues:
-            raise KeyError('"creation" and "activity" are reserved')
-        # new node's id
-        newid = self.db.newid(self.classname)
-
-        # validate propvalues
-        num_re = re.compile('^\d+$')
-        for key, value in propvalues.iteritems():
-            if key == self.key:
-                try:
-                    self.lookup(value)
-                except KeyError:
-                    pass
-                else:
-                    raise ValueError('node with key "%s" exists'%value)
-
-            # try to handle this property
-            try:
-                prop = self.properties[key]
-            except KeyError:
-                raise KeyError('"%s" has no property "%s"'%(self.classname,
-                    key))
-
-            if value is not None and isinstance(prop, hyperdb.Link):
-                if type(value) != type(''):
-                    raise ValueError('link value must be String')
-                link_class = self.properties[key].classname
-                # if it isn't a number, it's a key
-                if not num_re.match(value):
-                    try:
-                        value = self.db.classes[link_class].lookup(value)
-                    except (TypeError, KeyError):
-                        raise IndexError('new property "%s": %s not a %s'%(
-                            key, value, link_class))
-                elif not self.db.getclass(link_class).hasnode(value):
-                    raise IndexError('%s has no node %s'%(link_class,
-                        value))
-
-                # save off the value
-                propvalues[key] = value
-
-                # register the link with the newly linked node
-                if self.do_journal and self.properties[key].do_journal:
-                    self.db.addjournal(link_class, value, 'link',
-                        (self.classname, newid, key))
-
-            elif isinstance(prop, hyperdb.Multilink):
-                if value is None:
-                    value = []
-                if not hasattr(value, '__iter__'):
-                    raise TypeError('new property "%s" not an iterable of ids'%key)
-
-                # clean up and validate the list of links
-                link_class = self.properties[key].classname
-                l = []
-                for entry in value:
-                    if type(entry) != type(''):
-                        raise ValueError('"%s" multilink value (%r) '\
-                            'must contain Strings'%(key, value))
-                    # if it isn't a number, it's a key
-                    if not num_re.match(entry):
-                        try:
-                            entry = self.db.classes[link_class].lookup(entry)
-                        except (TypeError, KeyError):
-                            raise IndexError('new property "%s": %s not a %s'%(
-                                key, entry, self.properties[key].classname))
-                    l.append(entry)
-                value = l
-                propvalues[key] = value
-
-                # handle additions
-                for nodeid in value:
-                    if not self.db.getclass(link_class).hasnode(nodeid):
-                        raise IndexError('%s has no node %s'%(link_class,
-                            nodeid))
-                    # register the link with the newly linked node
-                    if self.do_journal and self.properties[key].do_journal:
-                        self.db.addjournal(link_class, nodeid, 'link',
-                            (self.classname, newid, key))
-
-            elif isinstance(prop, hyperdb.String):
-                if type(value) != type('') and type(value) != type(u''):
-                    raise TypeError('new property "%s" not a string'%key)
-                if prop.indexme:
-                    self.db.indexer.add_text((self.classname, newid, key),
-                        value)
-
-            elif isinstance(prop, hyperdb.Password):
-                if not isinstance(value, password.Password):
-                    raise TypeError('new property "%s" not a Password'%key)
-
-            elif isinstance(prop, hyperdb.Date):
-                if value is not None and not isinstance(value, date.Date):
-                    raise TypeError('new property "%s" not a Date'%key)
-
-            elif isinstance(prop, hyperdb.Interval):
-                if value is not None and not isinstance(value, date.Interval):
-                    raise TypeError('new property "%s" not an Interval'%key)
-
-            elif value is not None and isinstance(prop, hyperdb.Number):
-                try:
-                    float(value)
-                except ValueError:
-                    raise TypeError('new property "%s" not numeric'%key)
-
-            elif value is not None and isinstance(prop, hyperdb.Boolean):
-                try:
-                    int(value)
-                except ValueError:
-                    raise TypeError('new property "%s" not boolean'%key)
-
-        # make sure there's data where there needs to be
-        for key, prop in self.properties.iteritems():
-            if key in propvalues:
-                continue
-            if key == self.key:
-                raise ValueError('key property "%s" is required'%key)
-            if isinstance(prop, hyperdb.Multilink):
-                propvalues[key] = []
-
-        # done
-        self.db.addnode(self.classname, newid, propvalues)
-        if self.do_journal:
-            self.db.addjournal(self.classname, newid, 'create', {})
-
-        return newid
-
-    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' exists for backward compatibility, and is not used.
-
-        Attempts to get the "creation" or "activity" properties should
-        do the right thing.
-        """
-        if propname == 'id':
-            return nodeid
-
-        # get the node's dict
-        d = self.db.getnode(self.classname, nodeid)
-
-        # check for one of the special props
-        if propname == 'creation':
-            if 'creation' in d:
-                return d['creation']
-            if not self.do_journal:
-                raise ValueError('Journalling is disabled for this class')
-            journal = self.db.getjournal(self.classname, nodeid)
-            if journal:
-                return journal[0][1]
-            else:
-                # on the strange chance that there's no journal
-                return date.Date()
-        if propname == 'activity':
-            if 'activity' in d:
-                return d['activity']
-            if not self.do_journal:
-                raise ValueError('Journalling is disabled for this class')
-            journal = self.db.getjournal(self.classname, nodeid)
-            if journal:
-                return self.db.getjournal(self.classname, nodeid)[-1][1]
-            else:
-                # on the strange chance that there's no journal
-                return date.Date()
-        if propname == 'creator':
-            if 'creator' in d:
-                return d['creator']
-            if not self.do_journal:
-                raise ValueError('Journalling is disabled for this class')
-            journal = self.db.getjournal(self.classname, nodeid)
-            if journal:
-                num_re = re.compile('^\d+$')
-                value = journal[0][2]
-                if num_re.match(value):
-                    return value
-                else:
-                    # old-style "username" journal tag
-                    try:
-                        return self.db.user.lookup(value)
-                    except KeyError:
-                        # user's been retired, return admin
-                        return '1'
-            else:
-                return self.db.getuid()
-        if propname == 'actor':
-            if 'actor' in d:
-                return d['actor']
-            if not self.do_journal:
-                raise ValueError('Journalling is disabled for this class')
-            journal = self.db.getjournal(self.classname, nodeid)
-            if journal:
-                num_re = re.compile('^\d+$')
-                value = journal[-1][2]
-                if num_re.match(value):
-                    return value
-                else:
-                    # old-style "username" journal tag
-                    try:
-                        return self.db.user.lookup(value)
-                    except KeyError:
-                        # user's been retired, return admin
-                        return '1'
-            else:
-                return self.db.getuid()
-
-        # get the property (raises KeyErorr if invalid)
-        prop = self.properties[propname]
-
-        if propname not in d:
-            if default is _marker:
-                if isinstance(prop, hyperdb.Multilink):
-                    return []
-                else:
-                    return None
-            else:
-                return default
-
-        # return a dupe of the list so code doesn't get confused
-        if isinstance(prop, hyperdb.Multilink):
-            return d[propname][:]
-
-        return d[propname]
-
-    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.
-
-        These operations trigger detectors and can be vetoed.  Attempts
-        to modify the "creation" or "activity" properties cause a KeyError.
-        """
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError(_('Database open read-only'))
-
-        self.fireAuditors('set', nodeid, propvalues)
-        oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
-        for name, prop in self.getprops(protected=0).iteritems():
-            if name in oldvalues:
-                continue
-            if isinstance(prop, hyperdb.Multilink):
-                oldvalues[name] = []
-            else:
-                oldvalues[name] = None
-        propvalues = self.set_inner(nodeid, **propvalues)
-        self.fireReactors('set', nodeid, oldvalues)
-        return propvalues
-
-    def set_inner(self, nodeid, **propvalues):
-        """ Called by set, in-between the audit and react calls.
-        """
-        if not propvalues:
-            return propvalues
-
-        if 'creation' in propvalues or 'activity' in propvalues:
-            raise KeyError, '"creation" and "activity" are reserved'
-
-        if 'id' in propvalues:
-            raise KeyError, '"id" is reserved'
-
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError(_('Database open read-only'))
-
-        node = self.db.getnode(self.classname, nodeid)
-        if self.db.RETIRED_FLAG in node:
-            raise IndexError
-        num_re = re.compile('^\d+$')
-
-        # if the journal value is to be different, store it in here
-        journalvalues = {}
-
-        # list() propvalues 'cos it might be modified by the loop
-        for propname, value in list(propvalues.items()):
-            # check to make sure we're not duplicating an existing key
-            if propname == self.key and node[propname] != value:
-                try:
-                    self.lookup(value)
-                except KeyError:
-                    pass
-                else:
-                    raise ValueError('node with key "%s" exists'%value)
-
-            # this will raise the KeyError if the property isn't valid
-            # ... we don't use getprops() here because we only care about
-            # the writeable properties.
-            try:
-                prop = self.properties[propname]
-            except KeyError:
-                raise KeyError('"%s" has no property named "%s"'%(
-                    self.classname, propname))
-
-            # if the value's the same as the existing value, no sense in
-            # doing anything
-            current = node.get(propname, None)
-            if value == current:
-                del propvalues[propname]
-                continue
-            journalvalues[propname] = current
-
-            # do stuff based on the prop type
-            if isinstance(prop, hyperdb.Link):
-                link_class = prop.classname
-                # if it isn't a number, it's a key
-                if value is not None and not isinstance(value, type('')):
-                    raise ValueError('property "%s" link value be a string'%(
-                        propname))
-                if isinstance(value, type('')) and not num_re.match(value):
-                    try:
-                        value = self.db.classes[link_class].lookup(value)
-                    except (TypeError, KeyError):
-                        raise IndexError('new property "%s": %s not a %s'%(
-                            propname, value, prop.classname))
-
-                if (value is not None and
-                        not self.db.getclass(link_class).hasnode(value)):
-                    raise IndexError('%s has no node %s'%(link_class,
-                        value))
-
-                if self.do_journal and prop.do_journal:
-                    # register the unlink with the old linked node
-                    if propname in node and node[propname] is not None:
-                        self.db.addjournal(link_class, node[propname], 'unlink',
-                            (self.classname, nodeid, propname))
-
-                    # register the link with the newly linked node
-                    if value is not None:
-                        self.db.addjournal(link_class, value, 'link',
-                            (self.classname, nodeid, propname))
-
-            elif isinstance(prop, hyperdb.Multilink):
-                if value is None:
-                    value = []
-                if not hasattr(value, '__iter__'):
-                    raise TypeError('new property "%s" not an iterable of'
-                        ' ids'%propname)
-                link_class = self.properties[propname].classname
-                l = []
-                for entry in value:
-                    # if it isn't a number, it's a key
-                    if type(entry) != type(''):
-                        raise ValueError('new property "%s" link value '
-                            'must be a string'%propname)
-                    if not num_re.match(entry):
-                        try:
-                            entry = self.db.classes[link_class].lookup(entry)
-                        except (TypeError, KeyError):
-                            raise IndexError('new property "%s": %s not a %s'%(
-                                propname, entry,
-                                self.properties[propname].classname))
-                    l.append(entry)
-                value = l
-                propvalues[propname] = value
-
-                # figure the journal entry for this property
-                add = []
-                remove = []
-
-                # handle removals
-                if propname in node:
-                    l = node[propname]
-                else:
-                    l = []
-                for id in l[:]:
-                    if id in value:
-                        continue
-                    # register the unlink with the old linked node
-                    if self.do_journal and self.properties[propname].do_journal:
-                        self.db.addjournal(link_class, id, 'unlink',
-                            (self.classname, nodeid, propname))
-                    l.remove(id)
-                    remove.append(id)
-
-                # handle additions
-                for id in value:
-                    if not self.db.getclass(link_class).hasnode(id):
-                        raise IndexError('%s has no node %s'%(link_class,
-                            id))
-                    if id in l:
-                        continue
-                    # register the link with the newly linked node
-                    if self.do_journal and self.properties[propname].do_journal:
-                        self.db.addjournal(link_class, id, 'link',
-                            (self.classname, nodeid, propname))
-                    l.append(id)
-                    add.append(id)
-
-                # figure the journal entry
-                l = []
-                if add:
-                    l.append(('+', add))
-                if remove:
-                    l.append(('-', remove))
-                if l:
-                    journalvalues[propname] = tuple(l)
-
-            elif isinstance(prop, hyperdb.String):
-                if value is not None and type(value) != type('') and type(value) != type(u''):
-                    raise TypeError('new property "%s" not a '
-                        'string'%propname)
-                if prop.indexme:
-                    self.db.indexer.add_text((self.classname, nodeid, propname),
-                        value)
-
-            elif isinstance(prop, hyperdb.Password):
-                if not isinstance(value, password.Password):
-                    raise TypeError('new property "%s" not a '
-                        'Password'%propname)
-                propvalues[propname] = value
-
-            elif value is not None and isinstance(prop, hyperdb.Date):
-                if not isinstance(value, date.Date):
-                    raise TypeError('new property "%s" not a '
-                        'Date'%propname)
-                propvalues[propname] = value
-
-            elif value is not None and isinstance(prop, hyperdb.Interval):
-                if not isinstance(value, date.Interval):
-                    raise TypeError('new property "%s" not an '
-                        'Interval'%propname)
-                propvalues[propname] = value
-
-            elif value is not None and isinstance(prop, hyperdb.Number):
-                try:
-                    float(value)
-                except ValueError:
-                    raise TypeError('new property "%s" not '
-                        'numeric'%propname)
-
-            elif value is not None and isinstance(prop, hyperdb.Boolean):
-                try:
-                    int(value)
-                except ValueError:
-                    raise TypeError('new property "%s" not '
-                        'boolean'%propname)
-
-            node[propname] = value
-
-        # nothing to do?
-        if not propvalues:
-            return propvalues
-
-        # update the activity time
-        node['activity'] = date.Date()
-        node['actor'] = self.db.getuid()
-
-        # do the set, and journal it
-        self.db.setnode(self.classname, nodeid, node)
-
-        if self.do_journal:
-            self.db.addjournal(self.classname, nodeid, 'set', journalvalues)
-
-        return propvalues
-
-    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.
-
-        These operations trigger detectors and can be vetoed.  Attempts
-        to modify the "creation" or "activity" properties cause a KeyError.
-        """
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError(_('Database open read-only'))
-
-        self.fireAuditors('retire', nodeid, None)
-
-        node = self.db.getnode(self.classname, nodeid)
-        node[self.db.RETIRED_FLAG] = 1
-        self.db.setnode(self.classname, nodeid, node)
-        if self.do_journal:
-            self.db.addjournal(self.classname, nodeid, 'retired', None)
-
-        self.fireReactors('retire', nodeid, None)
-
-    def restore(self, nodeid):
-        """Restpre a retired node.
-
-        Make node available for all operations like it was before retirement.
-        """
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError(_('Database open read-only'))
-
-        node = self.db.getnode(self.classname, nodeid)
-        # check if key property was overrided
-        key = self.getkey()
-        try:
-            id = self.lookup(node[key])
-        except KeyError:
-            pass
-        else:
-            raise KeyError("Key property (%s) of retired node clashes "
-                "with existing one (%s)" % (key, node[key]))
-        # Now we can safely restore node
-        self.fireAuditors('restore', nodeid, None)
-        del node[self.db.RETIRED_FLAG]
-        self.db.setnode(self.classname, nodeid, node)
-        if self.do_journal:
-            self.db.addjournal(self.classname, nodeid, 'restored', None)
-
-        self.fireReactors('restore', nodeid, None)
-
-    def is_retired(self, nodeid, cldb=None):
-        """Return true if the node is retired.
-        """
-        node = self.db.getnode(self.classname, nodeid, cldb)
-        if self.db.RETIRED_FLAG in node:
-            return 1
-        return 0
-
-    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.
-        """
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError(_('Database open read-only'))
-        self.db.destroynode(self.classname, nodeid)
-
-    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
-
-            (nodeid, 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.
-        """
-        if not self.do_journal:
-            raise ValueError('Journalling is disabled for this class')
-        return self.db.getjournal(self.classname, nodeid)
-
-    # Locating nodes:
-    def hasnode(self, nodeid):
-        """Determine if the given nodeid actually exists
-        """
-        return self.db.hasnode(self.classname, nodeid)
-
-    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. If the
-        property doesn't exist, KeyError is raised.
-        """
-        prop = self.getprops()[propname]
-        if not isinstance(prop, hyperdb.String):
-            raise TypeError('key properties must be String')
-        self.key = propname
-
-    def getkey(self):
-        """Return the name of the key property for this class or None."""
-        return self.key
-
-    # TODO: set up a separate index db file for this? profile?
-    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.
-        """
-        if not self.key:
-            raise TypeError('No key property set for '
-                'class %s'%self.classname)
-        cldb = self.db.getclassdb(self.classname)
-        try:
-            for nodeid in self.getnodeids(cldb):
-                node = self.db.getnode(self.classname, nodeid, cldb)
-                if self.db.RETIRED_FLAG in node:
-                    continue
-                if self.key not in node:
-                    continue
-                if node[self.key] == keyvalue:
-                    return nodeid
-        finally:
-            cldb.close()
-        raise KeyError('No key (%s) value "%s" for "%s"'%(self.key,
-            keyvalue, self.classname))
-
-    # change from spec - allows multiple props to match
-    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 or
-                   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. Examples::
-
-            db.issue.find(messages='1')
-            db.issue.find(messages={'1':1,'3':1}, files={'7':1})
-        """
-        for propname, itemids in propspec.iteritems():
-            # check the prop is OK
-            prop = self.properties[propname]
-            if not isinstance(prop, hyperdb.Link) and not isinstance(prop, hyperdb.Multilink):
-                raise TypeError("'%s' not a Link/Multilink "
-                    "property"%propname)
-
-        # ok, now do the find
-        cldb = self.db.getclassdb(self.classname)
-        l = []
-        try:
-            for id in self.getnodeids(db=cldb):
-                item = self.db.getnode(self.classname, id, db=cldb)
-                if self.db.RETIRED_FLAG in item:
-                    continue
-                for propname, itemids in propspec.iteritems():
-                    if type(itemids) is not type({}):
-                        itemids = {itemids:1}
-
-                    # special case if the item doesn't have this property
-                    if propname not in item:
-                        if None in itemids:
-                            l.append(id)
-                            break
-                        continue
-
-                    # grab the property definition and its value on this item
-                    prop = self.properties[propname]
-                    value = item[propname]
-                    if isinstance(prop, hyperdb.Link) and value in itemids:
-                        l.append(id)
-                        break
-                    elif isinstance(prop, hyperdb.Multilink):
-                        hit = 0
-                        for v in value:
-                            if v in itemids:
-                                l.append(id)
-                                hit = 1
-                                break
-                        if hit:
-                            break
-        finally:
-            cldb.close()
-        return l
-
-    def stringFind(self, **requirements):
-        """Locate a particular node by matching a set of its String
-        properties in a caseless search.
-
-        If the property is not a String property, a TypeError is raised.
-
-        The return is a list of the id of all nodes that match.
-        """
-        for propname in requirements:
-            prop = self.properties[propname]
-            if not isinstance(prop, hyperdb.String):
-                raise TypeError("'%s' not a String property"%propname)
-            requirements[propname] = requirements[propname].lower()
-        l = []
-        cldb = self.db.getclassdb(self.classname)
-        try:
-            for nodeid in self.getnodeids(cldb):
-                node = self.db.getnode(self.classname, nodeid, cldb)
-                if self.db.RETIRED_FLAG in node:
-                    continue
-                for key, value in requirements.iteritems():
-                    if key not in node:
-                        break
-                    if node[key] is None or node[key].lower() != value:
-                        break
-                else:
-                    l.append(nodeid)
-        finally:
-            cldb.close()
-        return l
-
-    def list(self):
-        """ Return a list of the ids of the active nodes in this class.
-        """
-        l = []
-        cn = self.classname
-        cldb = self.db.getclassdb(cn)
-        try:
-            for nodeid in self.getnodeids(cldb):
-                node = self.db.getnode(cn, nodeid, cldb)
-                if self.db.RETIRED_FLAG in node:
-                    continue
-                l.append(nodeid)
-        finally:
-            cldb.close()
-        l.sort()
-        return l
-
-    def getnodeids(self, db=None, retired=None):
-        """ Return a list of ALL nodeids
-
-            Set retired=None to get all nodes. Otherwise it'll get all the
-            retired or non-retired nodes, depending on the flag.
-        """
-        res = []
-
-        # start off with the new nodes
-        if self.classname in self.db.newnodes:
-            res.extend(self.db.newnodes[self.classname])
-
-        must_close = False
-        if db is None:
-            db = self.db.getclassdb(self.classname)
-            must_close = True
-        try:
-            res.extend(db)
-
-            # remove the uncommitted, destroyed nodes
-            if self.classname in self.db.destroyednodes:
-                for nodeid in self.db.destroyednodes[self.classname]:
-                    if nodeid in db:
-                        res.remove(nodeid)
-
-            # check retired flag
-            if retired is False or retired is True:
-                l = []
-                for nodeid in res:
-                    node = self.db.getnode(self.classname, nodeid, db)
-                    is_ret = self.db.RETIRED_FLAG in node
-                    if retired == is_ret:
-                        l.append(nodeid)
-                res = l
-        finally:
-            if must_close:
-                db.close()
-        return res
-
-    def _filter(self, search_matches, filterspec, proptree,
-            num_re = re.compile('^\d+$')):
-        """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 a sequence type or None
-
-        The filter must match all properties specificed. If the property
-        value to match is a list:
-
-        1. String properties must match all elements in the list, and
-        2. Other properties must match any of the elements in the list.
-        """
-        if __debug__:
-            start_t = time.time()
-
-        cn = self.classname
-
-        # optimise filterspec
-        l = []
-        props = self.getprops()
-        LINK = 'spec:link'
-        MULTILINK = 'spec:multilink'
-        STRING = 'spec:string'
-        DATE = 'spec:date'
-        INTERVAL = 'spec:interval'
-        OTHER = 'spec:other'
-
-        for k, v in filterspec.iteritems():
-            propclass = props[k]
-            if isinstance(propclass, hyperdb.Link):
-                if type(v) is not type([]):
-                    v = [v]
-                u = []
-                for entry in v:
-                    # the value -1 is a special "not set" sentinel
-                    if entry == '-1':
-                        entry = None
-                    u.append(entry)
-                l.append((LINK, k, u))
-            elif isinstance(propclass, hyperdb.Multilink):
-                # the value -1 is a special "not set" sentinel
-                if v in ('-1', ['-1']):
-                    v = []
-                elif type(v) is not type([]):
-                    v = [v]
-                l.append((MULTILINK, k, v))
-            elif isinstance(propclass, hyperdb.String) and k != 'id':
-                if type(v) is not type([]):
-                    v = [v]
-                for v in v:
-                    # simple glob searching
-                    v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
-                    v = v.replace('?', '.')
-                    v = v.replace('*', '.*?')
-                    l.append((STRING, k, re.compile(v, re.I)))
-            elif isinstance(propclass, hyperdb.Date):
-                try:
-                    date_rng = propclass.range_from_raw(v, self.db)
-                    l.append((DATE, k, date_rng))
-                except ValueError:
-                    # If range creation fails - ignore that search parameter
-                    pass
-            elif isinstance(propclass, hyperdb.Interval):
-                try:
-                    intv_rng = date.Range(v, date.Interval)
-                    l.append((INTERVAL, k, intv_rng))
-                except ValueError:
-                    # If range creation fails - ignore that search parameter
-                    pass
-
-            elif isinstance(propclass, hyperdb.Boolean):
-                if type(v) == type(""):
-                    v = v.split(',')
-                if type(v) != type([]):
-                    v = [v]
-                bv = []
-                for val in v:
-                    if type(val) is type(''):
-                        bv.append(propclass.from_raw (val))
-                    else:
-                        bv.append(val)
-                l.append((OTHER, k, bv))
-
-            elif k == 'id':
-                if type(v) != type([]):
-                    v = v.split(',')
-                l.append((OTHER, k, [str(int(val)) for val in v]))
-
-            elif isinstance(propclass, hyperdb.Number):
-                if type(v) != type([]):
-                    try :
-                        v = v.split(',')
-                    except AttributeError :
-                        v = [v]
-                l.append((OTHER, k, [float(val) for val in v]))
-
-        filterspec = l
-        
-        # now, find all the nodes that are active and pass filtering
-        matches = []
-        cldb = self.db.getclassdb(cn)
-        t = 0
-        try:
-            # TODO: only full-scan once (use items())
-            for nodeid in self.getnodeids(cldb):
-                node = self.db.getnode(cn, nodeid, cldb)
-                if self.db.RETIRED_FLAG in node:
-                    continue
-                # apply filter
-                for t, k, v in filterspec:
-                    # handle the id prop
-                    if k == 'id':
-                        if nodeid not in v:
-                            break
-                        continue
-
-                    # get the node value
-                    nv = node.get(k, None)
-
-                    match = 0
-
-                    # now apply the property filter
-                    if t == LINK:
-                        # link - if this node's property doesn't appear in the
-                        # filterspec's nodeid list, skip it
-                        match = nv in v
-                    elif t == MULTILINK:
-                        # multilink - if any of the nodeids required by the
-                        # filterspec aren't in this node's property, then skip
-                        # it
-                        nv = node.get(k, [])
-
-                        # check for matching the absence of multilink values
-                        if not v:
-                            match = not nv
-                        else:
-                            # othewise, make sure this node has each of the
-                            # required values
-                            for want in v:
-                                if want in nv:
-                                    match = 1
-                                    break
-                    elif t == STRING:
-                        if nv is None:
-                            nv = ''
-                        # RE search
-                        match = v.search(nv)
-                    elif t == DATE or t == INTERVAL:
-                        if nv is None:
-                            match = v is None
-                        else:
-                            if v.to_value:
-                                if v.from_value <= nv and v.to_value >= nv:
-                                    match = 1
-                            else:
-                                if v.from_value <= nv:
-                                    match = 1
-                    elif t == OTHER:
-                        # straight value comparison for the other types
-                        match = nv in v
-                    if not match:
-                        break
-                else:
-                    matches.append([nodeid, node])
-
-            # filter based on full text search
-            if search_matches is not None:
-                k = []
-                for v in matches:
-                    if v[0] in search_matches:
-                        k.append(v)
-                matches = k
-
-            # add sorting information to the proptree
-            JPROPS = {'actor':1, 'activity':1, 'creator':1, 'creation':1}
-            children = []
-            if proptree:
-                children = proptree.sortable_children()
-            for pt in children:
-                dir = pt.sort_direction
-                prop = pt.name
-                assert (dir and prop)
-                propclass = props[prop]
-                pt.sort_ids = []
-                is_pointer = isinstance(propclass,(hyperdb.Link,
-                    hyperdb.Multilink))
-                if not is_pointer:
-                    pt.sort_result = []
-                try:
-                    # cache the opened link class db, if needed.
-                    lcldb = None
-                    # cache the linked class items too
-                    lcache = {}
-
-                    for entry in matches:
-                        itemid = entry[-2]
-                        item = entry[-1]
-                        # handle the properties that might be "faked"
-                        # also, handle possible missing properties
-                        try:
-                            v = item[prop]
-                        except KeyError:
-                            if prop in JPROPS:
-                                # force lookup of the special journal prop
-                                v = self.get(itemid, prop)
-                            else:
-                                # the node doesn't have a value for this
-                                # property
-                                v = None
-                                if isinstance(propclass, hyperdb.Multilink):
-                                    v = []
-                                if prop == 'id':
-                                    v = int (itemid)
-                                pt.sort_ids.append(v)
-                                if not is_pointer:
-                                    pt.sort_result.append(v)
-                                continue
-
-                        # missing (None) values are always sorted first
-                        if v is None:
-                            pt.sort_ids.append(v)
-                            if not is_pointer:
-                                pt.sort_result.append(v)
-                            continue
-
-                        if isinstance(propclass, hyperdb.Link):
-                            lcn = propclass.classname
-                            link = self.db.classes[lcn]
-                            key = link.orderprop()
-                            child = pt.propdict[key]
-                            if key!='id':
-                                if v not in lcache:
-                                    # open the link class db if it's not already
-                                    if lcldb is None:
-                                        lcldb = self.db.getclassdb(lcn)
-                                    lcache[v] = self.db.getnode(lcn, v, lcldb)
-                                r = lcache[v][key]
-                                child.propdict[key].sort_ids.append(r)
-                            else:
-                                child.propdict[key].sort_ids.append(v)
-                        pt.sort_ids.append(v)
-                        if not is_pointer:
-                            r = propclass.sort_repr(pt.parent.cls, v, pt.name)
-                            pt.sort_result.append(r)
-                finally:
-                    # if we opened the link class db, close it now
-                    if lcldb is not None:
-                        lcldb.close()
-                del lcache
-        finally:
-            cldb.close()
-
-        # pull the id out of the individual entries
-        matches = [entry[-2] for entry in matches]
-        if __debug__:
-            self.db.stats['filtering'] += (time.time() - start_t)
-        return matches
-
-    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.
-        """
-        return self.db.countnodes(self.classname)
-
-    # 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.
-
-           In addition to the actual properties on the node, these
-           methods provide the "creation" and "activity" properties. If the
-           "protected" flag is true, we include protected properties - those
-           which may not be modified.
-        """
-        d = self.properties.copy()
-        if protected:
-            d['id'] = hyperdb.String()
-            d['creation'] = hyperdb.Date()
-            d['activity'] = hyperdb.Date()
-            d['creator'] = hyperdb.Link('user')
-            d['actor'] = hyperdb.Link('user')
-        return d
-
-    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.
-        """
-        for key in properties:
-            if key in self.properties:
-                raise ValueError(key)
-        self.properties.update(properties)
-
-    def index(self, nodeid):
-        """ Add (or refresh) the node to search indexes """
-        # find all the String properties that have indexme
-        for prop, propclass in self.getprops().iteritems():
-            if isinstance(propclass, hyperdb.String) and propclass.indexme:
-                # index them under (classname, nodeid, property)
-                try:
-                    value = str(self.get(nodeid, prop))
-                except IndexError:
-                    # node has been destroyed
-                    continue
-                self.db.indexer.add_text((self.classname, nodeid, prop), value)
-
-    #
-    # import / export support
-    #
-    def export_list(self, propnames, nodeid):
-        """ Export a node - generate a list of CSV-able data in the order
-            specified by propnames for the given node.
-        """
-        properties = self.getprops()
-        l = []
-        for prop in propnames:
-            proptype = properties[prop]
-            value = self.get(nodeid, prop)
-            # "marshal" data where needed
-            if value is None:
-                pass
-            elif isinstance(proptype, hyperdb.Date):
-                value = value.get_tuple()
-            elif isinstance(proptype, hyperdb.Interval):
-                value = value.get_tuple()
-            elif isinstance(proptype, hyperdb.Password):
-                value = str(value)
-            l.append(repr(value))
-
-        # append retired flag
-        l.append(repr(self.is_retired(nodeid)))
-
-        return l
-
-    def import_list(self, propnames, proplist):
-        """ Import a node - all information including "id" is present and
-            should not be sanity checked. Triggers are not triggered. The
-            journal should be initialised using the "creator" and "created"
-            information.
-
-            Return the nodeid of the node imported.
-        """
-        if self.db.journaltag is None:
-            raise hyperdb.DatabaseError(_('Database open read-only'))
-        properties = self.getprops()
-
-        # make the new node's property map
-        d = {}
-        newid = None
-        for i in range(len(propnames)):
-            # Figure the property for this column
-            propname = propnames[i]
-
-            # Use eval to reverse the repr() used to output the CSV
-            value = eval(proplist[i])
-
-            # "unmarshal" where necessary
-            if propname == 'id':
-                newid = value
-                continue
-            elif propname == 'is retired':
-                # is the item retired?
-                if int(value):
-                    d[self.db.RETIRED_FLAG] = 1
-                continue
-            elif value is None:
-                d[propname] = None
-                continue
-
-            prop = properties[propname]
-            if isinstance(prop, hyperdb.Date):
-                value = date.Date(value)
-            elif isinstance(prop, hyperdb.Interval):
-                value = date.Interval(value)
-            elif isinstance(prop, hyperdb.Password):
-                pwd = password.Password()
-                pwd.unpack(value)
-                value = pwd
-            d[propname] = value
-
-        # get a new id if necessary
-        if newid is None:
-            newid = self.db.newid(self.classname)
-
-        # add the node and journal
-        self.db.addnode(self.classname, newid, d)
-        return newid
-
-    def export_journals(self):
-        """Export a class's journal - generate a list of lists of
-        CSV-able data:
-
-            nodeid, date, user, action, params
-
-        No heading here - the columns are fixed.
-        """
-        properties = self.getprops()
-        r = []
-        for nodeid in self.getnodeids():
-            for nodeid, date, user, action, params in self.history(nodeid):
-                date = date.get_tuple()
-                if action == 'set':
-                    export_data = {}
-                    for propname, value in params.iteritems():
-                        if propname not in properties:
-                            # property no longer in the schema
-                            continue
-
-                        prop = properties[propname]
-                        # make sure the params are eval()'able
-                        if value is None:
-                            pass
-                        elif isinstance(prop, hyperdb.Date):
-                            # this is a hack - some dates are stored as strings
-                            if not isinstance(value, type('')):
-                                value = value.get_tuple()
-                        elif isinstance(prop, hyperdb.Interval):
-                            # hack too - some intervals are stored as strings
-                            if not isinstance(value, type('')):
-                                value = value.get_tuple()
-                        elif isinstance(prop, hyperdb.Password):
-                            value = str(value)
-                        export_data[propname] = value
-                    params = export_data
-                r.append([repr(nodeid), repr(date), repr(user),
-                    repr(action), repr(params)])
-        return r
-
-    def import_journals(self, entries):
-        """Import a class's journal.
-
-        Uses setjournal() to set the journal for each item."""
-        properties = self.getprops()
-        d = {}
-        for l in entries:
-            nodeid, jdate, user, action, params = tuple(map(eval, l))
-            r = d.setdefault(nodeid, [])
-            if action == 'set':
-                for propname, value in params.iteritems():
-                    prop = properties[propname]
-                    if value is None:
-                        pass
-                    elif isinstance(prop, hyperdb.Date):
-                        value = date.Date(value)
-                    elif isinstance(prop, hyperdb.Interval):
-                        value = date.Interval(value)
-                    elif isinstance(prop, hyperdb.Password):
-                        pwd = password.Password()
-                        pwd.unpack(value)
-                        value = pwd
-                    params[propname] = value
-            r.append((nodeid, date.Date(jdate), user, action, params))
-
-        for nodeid, l in d.iteritems():
-            self.db.setjournal(self.classname, nodeid, l)
-
-class FileClass(hyperdb.FileClass, Class):
-    """This class defines a large chunk of data. To support this, it has a
-       mandatory String property "content" which is typically saved off
-       externally to the hyperdb.
-
-       The default MIME type of this data is defined by the
-       "default_mime_type" class attribute, which may be overridden by each
-       node if the class defines a "type" String property.
-    """
-    def __init__(self, db, classname, **properties):
-        """The newly-created class automatically includes the "content"
-        and "type" properties.
-        """
-        if 'content' not in properties:
-            properties['content'] = hyperdb.String(indexme='yes')
-        if 'type' not in properties:
-            properties['type'] = hyperdb.String()
-        Class.__init__(self, db, classname, **properties)
-
-    def create(self, **propvalues):
-        """ Snarf the "content" propvalue and store in a file
-        """
-        # we need to fire the auditors now, or the content property won't
-        # be in propvalues for the auditors to play with
-        self.fireAuditors('create', None, propvalues)
-
-        # now remove the content property so it's not stored in the db
-        content = propvalues['content']
-        del propvalues['content']
-
-        # make sure we have a MIME type
-        mime_type = propvalues.get('type', self.default_mime_type)
-
-        # do the database create
-        newid = self.create_inner(**propvalues)
-
-        # store off the content as a file
-        self.db.storefile(self.classname, newid, None, content)
-
-        # fire reactors
-        self.fireReactors('create', newid, None)
-
-        return newid
-
-    def get(self, nodeid, propname, default=_marker, cache=1):
-        """ Trap the content propname and get it from the file
-
-        'cache' exists for backwards compatibility, and is not used.
-        """
-        poss_msg = 'Possibly an access right configuration problem.'
-        if propname == 'content':
-            try:
-                return self.db.getfile(self.classname, nodeid, None)
-            except IOError, strerror:
-                # XXX by catching this we don't see an error in the log.
-                return 'ERROR reading file: %s%s\n%s\n%s'%(
-                        self.classname, nodeid, poss_msg, strerror)
-        if default is not _marker:
-            return Class.get(self, nodeid, propname, default)
-        else:
-            return Class.get(self, nodeid, propname)
-
-    def set(self, itemid, **propvalues):
-        """ Snarf the "content" propvalue and update it in a file
-        """
-        self.fireAuditors('set', itemid, propvalues)
-
-        # create the oldvalues dict - fill in any missing values
-        oldvalues = copy.deepcopy(self.db.getnode(self.classname, itemid))
-        for name, prop in self.getprops(protected=0).iteritems():
-            if name in oldvalues:
-                continue
-            if isinstance(prop, hyperdb.Multilink):
-                oldvalues[name] = []
-            else:
-                oldvalues[name] = None
-
-        # now remove the content property so it's not stored in the db
-        content = None
-        if 'content' in propvalues:
-            content = propvalues['content']
-            del propvalues['content']
-
-        # do the database update
-        propvalues = self.set_inner(itemid, **propvalues)
-
-        # do content?
-        if content:
-            # store and possibly index
-            self.db.storefile(self.classname, itemid, None, content)
-            if self.properties['content'].indexme:
-                mime_type = self.get(itemid, 'type', self.default_mime_type)
-                self.db.indexer.add_text((self.classname, itemid, 'content'),
-                    content, mime_type)
-            propvalues['content'] = content
-
-        # fire reactors
-        self.fireReactors('set', itemid, oldvalues)
-        return propvalues
-
-    def index(self, nodeid):
-        """ Add (or refresh) the node to search indexes.
-
-        Use the content-type property for the content property.
-        """
-        # find all the String properties that have indexme
-        for prop, propclass in self.getprops().iteritems():
-            if prop == 'content' and propclass.indexme:
-                mime_type = self.get(nodeid, 'type', self.default_mime_type)
-                self.db.indexer.add_text((self.classname, nodeid, 'content'),
-                    str(self.get(nodeid, 'content')), mime_type)
-            elif isinstance(propclass, hyperdb.String) and propclass.indexme:
-                # index them under (classname, nodeid, property)
-                try:
-                    value = str(self.get(nodeid, prop))
-                except IndexError:
-                    # node has been destroyed
-                    continue
-                self.db.indexer.add_text((self.classname, nodeid, prop), value)
-
-# deviation from spec - was called ItemClass
-class IssueClass(Class, roundupdb.IssueClass):
-    # Overridden methods:
-    def __init__(self, db, classname, **properties):
-        """The newly-created class automatically includes the "messages",
-        "files", "nosy", and "superseder" properties.  If the 'properties'
-        dictionary attempts to specify any of these properties or a
-        "creation" or "activity" property, a ValueError is raised.
-        """
-        if 'title' not in properties:
-            properties['title'] = hyperdb.String(indexme='yes')
-        if 'messages' not in properties:
-            properties['messages'] = hyperdb.Multilink("msg")
-        if 'files' not in properties:
-            properties['files'] = hyperdb.Multilink("file")
-        if 'nosy' not in properties:
-            # note: journalling is turned off as it really just wastes
-            # space. this behaviour may be overridden in an instance
-            properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
-        if 'superseder' not in properties:
-            properties['superseder'] = hyperdb.Multilink(classname)
-        Class.__init__(self, db, classname, **properties)
-
-# vim: set et sts=4 sw=4 :
diff --git a/build/lib/roundup/backends/back_mysql.py b/build/lib/roundup/backends/back_mysql.py
deleted file mode 100644 (file)
index 2bc4f13..0000000
+++ /dev/null
@@ -1,646 +0,0 @@
-#
-# Copyright (c) 2003 Martynas Sklyzmantas, Andrey Lebedev <andrey@micro.lt>
-#
-# 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.
-#
-
-'''This module defines a backend implementation for MySQL.
-
-
-How to implement AUTO_INCREMENT:
-
-mysql> create table foo (num integer auto_increment primary key, name
-varchar(255)) AUTO_INCREMENT=1 ENGINE=InnoDB;
-
-ql> insert into foo (name) values ('foo5');
-Query OK, 1 row affected (0.00 sec)
-
-mysql> SELECT num FROM foo WHERE num IS NULL;
-+-----+
-| num |
-+-----+
-|   4 |
-+-----+
-1 row in set (0.00 sec)
-
-mysql> SELECT num FROM foo WHERE num IS NULL;
-Empty set (0.00 sec)
-
-NOTE: we don't need an index on the id column if it's PRIMARY KEY
-
-'''
-__docformat__ = 'restructuredtext'
-
-from roundup.backends.rdbms_common import *
-from roundup.backends import rdbms_common
-import MySQLdb
-import os, shutil
-from MySQLdb.constants import ER
-import logging
-
-def connection_dict(config, dbnamestr=None):
-    d = rdbms_common.connection_dict(config, dbnamestr)
-    if d.has_key('password'):
-        d['passwd'] = d['password']
-        del d['password']
-    if d.has_key('port'):
-        d['port'] = int(d['port'])
-    return d
-
-def db_nuke(config):
-    """Clear all database contents and drop database itself"""
-    if db_exists(config):
-        kwargs = connection_dict(config)
-        conn = MySQLdb.connect(**kwargs)
-        try:
-            conn.select_db(config.RDBMS_NAME)
-        except:
-            # no, it doesn't exist
-            pass
-        else:
-            cursor = conn.cursor()
-            cursor.execute("SHOW TABLES")
-            tables = cursor.fetchall()
-            # stupid MySQL bug requires us to drop all the tables first
-            for table in tables:
-                command = 'DROP TABLE `%s`'%table[0]
-                logging.debug(command)
-                cursor.execute(command)
-            command = "DROP DATABASE %s"%config.RDBMS_NAME
-            logging.info(command)
-            cursor.execute(command)
-            conn.commit()
-        conn.close()
-
-    if os.path.exists(config.DATABASE):
-        shutil.rmtree(config.DATABASE)
-
-def db_create(config):
-    """Create the database."""
-    kwargs = connection_dict(config)
-    conn = MySQLdb.connect(**kwargs)
-    cursor = conn.cursor()
-    command = "CREATE DATABASE %s"%config.RDBMS_NAME
-    logging.info(command)
-    cursor.execute(command)
-    conn.commit()
-    conn.close()
-
-def db_exists(config):
-    """Check if database already exists."""
-    kwargs = connection_dict(config)
-    conn = MySQLdb.connect(**kwargs)
-    try:
-        try:
-            conn.select_db(config.RDBMS_NAME)
-        except MySQLdb.OperationalError:
-            return 0
-    finally:
-        conn.close()
-    return 1
-
-
-class Database(Database):
-    arg = '%s'
-
-    # used by some code to switch styles of query
-    implements_intersect = 0
-
-    # Backend for MySQL to use.
-    # InnoDB is faster, but if you're running <4.0.16 then you'll need to
-    # use BDB to pass all unit tests.
-    mysql_backend = 'InnoDB'
-    #mysql_backend = 'BDB'
-
-    hyperdb_to_sql_datatypes = {
-        hyperdb.String : 'TEXT',
-        hyperdb.Date   : 'DATETIME',
-        hyperdb.Link   : 'INTEGER',
-        hyperdb.Interval  : 'VARCHAR(255)',
-        hyperdb.Password  : 'VARCHAR(255)',
-        hyperdb.Boolean   : 'BOOL',
-        hyperdb.Number    : 'REAL',
-    }
-
-    hyperdb_to_sql_value = {
-        hyperdb.String : str,
-        # no fractional seconds for MySQL
-        hyperdb.Date   : lambda x: x.formal(sep=' '),
-        hyperdb.Link   : int,
-        hyperdb.Interval  : str,
-        hyperdb.Password  : str,
-        hyperdb.Boolean   : int,
-        hyperdb.Number    : lambda x: x,
-        hyperdb.Multilink : lambda x: x,    # used in journal marshalling
-    }
-
-    def sql_open_connection(self):
-        kwargs = connection_dict(self.config, 'db')
-        self.log_info('open database %r'%(kwargs['db'],))
-        try:
-            conn = MySQLdb.connect(**kwargs)
-        except MySQLdb.OperationalError, message:
-            raise DatabaseError, message
-        cursor = conn.cursor()
-        cursor.execute("SET AUTOCOMMIT=0")
-        cursor.execute("START TRANSACTION")
-        return (conn, cursor)
-
-    def open_connection(self):
-        # make sure the database actually exists
-        if not db_exists(self.config):
-            db_create(self.config)
-
-        self.conn, self.cursor = self.sql_open_connection()
-
-        try:
-            self.load_dbschema()
-        except MySQLdb.OperationalError, message:
-            if message[0] != ER.NO_DB_ERROR:
-                raise
-        except MySQLdb.ProgrammingError, message:
-            if message[0] != ER.NO_SUCH_TABLE:
-                raise DatabaseError, message
-            self.init_dbschema()
-            self.sql("CREATE TABLE `schema` (`schema` TEXT) ENGINE=%s"%
-                self.mysql_backend)
-            self.sql('''CREATE TABLE ids (name VARCHAR(255),
-                num INTEGER) ENGINE=%s'''%self.mysql_backend)
-            self.sql('create index ids_name_idx on ids(name)')
-            self.create_version_2_tables()
-
-    def load_dbschema(self):
-        ''' Load the schema definition that the database currently implements
-        '''
-        self.cursor.execute('select `schema` from `schema`')
-        schema = self.cursor.fetchone()
-        if schema:
-            self.database_schema = eval(schema[0])
-        else:
-            self.database_schema = {}
-
-    def save_dbschema(self):
-        ''' Save the schema definition that the database currently implements
-        '''
-        s = repr(self.database_schema)
-        self.sql('delete from `schema`')
-        self.sql('insert into `schema` values (%s)', (s,))
-
-    def create_version_2_tables(self):
-        # OTK store
-        self.sql('''CREATE TABLE otks (otk_key VARCHAR(255),
-            otk_value TEXT, otk_time FLOAT(20))
-            ENGINE=%s'''%self.mysql_backend)
-        self.sql('CREATE INDEX otks_key_idx ON otks(otk_key)')
-
-        # Sessions store
-        self.sql('''CREATE TABLE sessions (session_key VARCHAR(255),
-            session_time FLOAT(20), session_value TEXT)
-            ENGINE=%s'''%self.mysql_backend)
-        self.sql('''CREATE INDEX sessions_key_idx ON
-            sessions(session_key)''')
-
-        # full-text indexing store
-        self.sql('''CREATE TABLE __textids (_class VARCHAR(255),
-            _itemid VARCHAR(255), _prop VARCHAR(255), _textid INT)
-            ENGINE=%s'''%self.mysql_backend)
-        self.sql('''CREATE TABLE __words (_word VARCHAR(30),
-            _textid INT) ENGINE=%s'''%self.mysql_backend)
-        self.sql('CREATE INDEX words_word_ids ON __words(_word)')
-        self.sql('CREATE INDEX words_by_id ON __words (_textid)')
-        self.sql('CREATE UNIQUE INDEX __textids_by_props ON '
-                 '__textids (_class, _itemid, _prop)')
-        sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
-        self.sql(sql, ('__textids', 1))
-
-    def add_new_columns_v2(self):
-        '''While we're adding the actor column, we need to update the
-        tables to have the correct datatypes.'''
-        for klass in self.classes.values():
-            cn = klass.classname
-            properties = klass.getprops()
-            old_spec = self.database_schema['tables'][cn]
-
-            # figure the non-Multilink properties to copy over
-            propnames = ['activity', 'creation', 'creator']
-
-            # figure actions based on data type
-            for name, s_prop in old_spec[1]:
-                # s_prop is a repr() string of a hyperdb type object
-                if s_prop.find('Multilink') == -1:
-                    if properties.has_key(name):
-                        propnames.append(name)
-                    continue
-                tn = '%s_%s'%(cn, name)
-
-                if properties.has_key(name):
-                    # grabe the current values
-                    sql = 'select linkid, nodeid from %s'%tn
-                    self.sql(sql)
-                    rows = self.cursor.fetchall()
-
-                # drop the old table
-                self.drop_multilink_table_indexes(cn, name)
-                sql = 'drop table %s'%tn
-                self.sql(sql)
-
-                if properties.has_key(name):
-                    # re-create and populate the new table
-                    self.create_multilink_table(klass, name)
-                    sql = '''insert into %s (linkid, nodeid) values
-                        (%s, %s)'''%(tn, self.arg, self.arg)
-                    for linkid, nodeid in rows:
-                        self.sql(sql, (int(linkid), int(nodeid)))
-
-            # figure the column names to fetch
-            fetch = ['_%s'%name for name in propnames]
-
-            # select the data out of the old table
-            fetch.append('id')
-            fetch.append('__retired__')
-            fetchcols = ','.join(fetch)
-            sql = 'select %s from _%s'%(fetchcols, cn)
-            self.sql(sql)
-
-            # unserialise the old data
-            olddata = []
-            propnames = propnames + ['id', '__retired__']
-            cols = []
-            first = 1
-            for entry in self.cursor.fetchall():
-                l = []
-                olddata.append(l)
-                for i in range(len(propnames)):
-                    name = propnames[i]
-                    v = entry[i]
-
-                    if name in ('id', '__retired__'):
-                        if first:
-                            cols.append(name)
-                        l.append(int(v))
-                        continue
-                    if first:
-                        cols.append('_' + name)
-                    prop = properties[name]
-                    if isinstance(prop, Date) and v is not None:
-                        v = date.Date(v)
-                    elif isinstance(prop, Interval) and v is not None:
-                        v = date.Interval(v)
-                    elif isinstance(prop, Password) and v is not None:
-                        v = password.Password(encrypted=v)
-                    elif (isinstance(prop, Boolean) or
-                            isinstance(prop, Number)) and v is not None:
-                        v = float(v)
-
-                    # convert to new MySQL data type
-                    prop = properties[name]
-                    if v is not None:
-                        e = self.to_sql_value(prop.__class__)(v)
-                    else:
-                        e = None
-                    l.append(e)
-
-                    # Intervals store the seconds value too
-                    if isinstance(prop, Interval):
-                        if first:
-                            cols.append('__' + name + '_int__')
-                        if v is not None:
-                            l.append(v.as_seconds())
-                        else:
-                            l.append(e)
-                first = 0
-
-            self.drop_class_table_indexes(cn, old_spec[0])
-
-            # drop the old table
-            self.sql('drop table _%s'%cn)
-
-            # create the new table
-            self.create_class_table(klass)
-
-            # do the insert of the old data
-            args = ','.join([self.arg for x in cols])
-            cols = ','.join(cols)
-            sql = 'insert into _%s (%s) values (%s)'%(cn, cols, args)
-            for entry in olddata:
-                self.sql(sql, tuple(entry))
-
-            # now load up the old journal data to migrate it
-            cols = ','.join('nodeid date tag action params'.split())
-            sql = 'select %s from %s__journal'%(cols, cn)
-            self.sql(sql)
-
-            # data conversions
-            olddata = []
-            for nodeid, journaldate, journaltag, action, params in \
-                    self.cursor.fetchall():
-                #nodeid = int(nodeid)
-                journaldate = date.Date(journaldate)
-                #params = eval(params)
-                olddata.append((nodeid, journaldate, journaltag, action,
-                    params))
-
-            # drop journal table and indexes
-            self.drop_journal_table_indexes(cn)
-            sql = 'drop table %s__journal'%cn
-            self.sql(sql)
-
-            # re-create journal table
-            self.create_journal_table(klass)
-            dc = self.to_sql_value(hyperdb.Date)
-            for nodeid, journaldate, journaltag, action, params in olddata:
-                self.save_journal(cn, cols, nodeid, dc(journaldate),
-                    journaltag, action, params)
-
-            # make sure the normal schema update code doesn't try to
-            # change things
-            self.database_schema['tables'][cn] = klass.schema()
-
-    def fix_version_2_tables(self):
-        # Convert journal date column to TIMESTAMP, params column to TEXT
-        self._convert_journal_tables()
-
-        # Convert all String properties to TEXT
-        self._convert_string_properties()
-
-    def __repr__(self):
-        return '<myroundsql 0x%x>'%id(self)
-
-    def sql_fetchone(self):
-        return self.cursor.fetchone()
-
-    def sql_fetchall(self):
-        return self.cursor.fetchall()
-
-    def sql_index_exists(self, table_name, index_name):
-        self.sql('show index from %s'%table_name)
-        for index in self.cursor.fetchall():
-            if index[2] == index_name:
-                return 1
-        return 0
-
-    def create_class_table(self, spec, create_sequence=1):
-        cols, mls = self.determine_columns(spec.properties.items())
-
-        # add on our special columns
-        cols.append(('id', 'INTEGER PRIMARY KEY'))
-        cols.append(('__retired__', 'INTEGER DEFAULT 0'))
-
-        # create the base table
-        scols = ','.join(['%s %s'%x for x in cols])
-        sql = 'create table _%s (%s) ENGINE=%s'%(spec.classname, scols,
-            self.mysql_backend)
-        self.sql(sql)
-
-        self.create_class_table_indexes(spec)
-        return cols, mls
-
-    def create_class_table_indexes(self, spec):
-        ''' create the class table for the given spec
-        '''
-        # create __retired__ index
-        index_sql2 = 'create index _%s_retired_idx on _%s(__retired__)'%(
-                        spec.classname, spec.classname)
-        self.sql(index_sql2)
-
-        # create index for key property
-        if spec.key:
-            if isinstance(spec.properties[spec.key], String):
-                idx = spec.key + '(255)'
-            else:
-                idx = spec.key
-            index_sql3 = 'create index _%s_%s_idx on _%s(_%s)'%(
-                        spec.classname, spec.key,
-                        spec.classname, idx)
-            self.sql(index_sql3)
-
-        # TODO: create indexes on (selected?) Link property columns, as
-        # they're more likely to be used for lookup
-
-    def add_class_key_required_unique_constraint(self, cn, key):
-        # mysql requires sizes on TEXT indexes
-        prop = self.classes[cn].getprops()[key]
-        if isinstance(prop, String):
-            sql = '''create unique index _%s_key_retired_idx
-                on _%s(__retired__, _%s(255))'''%(cn, cn, key)
-        else:
-            sql = '''create unique index _%s_key_retired_idx
-                on _%s(__retired__, _%s)'''%(cn, cn, key)
-        self.sql(sql)
-
-    def create_class_table_key_index(self, cn, key):
-        # mysql requires sizes on TEXT indexes
-        prop = self.classes[cn].getprops()[key]
-        if isinstance(prop, String):
-            sql = 'create index _%s_%s_idx on _%s(_%s(255))'%(cn, key, cn, key)
-        else:
-            sql = 'create index _%s_%s_idx on _%s(_%s)'%(cn, key, cn, key)
-        self.sql(sql)
-
-    def drop_class_table_indexes(self, cn, key):
-        # drop the old table indexes first
-        l = ['_%s_id_idx'%cn, '_%s_retired_idx'%cn]
-        if key:
-            l.append('_%s_%s_idx'%(cn, key))
-
-        table_name = '_%s'%cn
-        for index_name in l:
-            if not self.sql_index_exists(table_name, index_name):
-                continue
-            index_sql = 'drop index %s on %s'%(index_name, table_name)
-            self.sql(index_sql)
-
-    def create_journal_table(self, spec):
-        ''' create the journal table for a class given the spec and
-            already-determined cols
-        '''
-        # journal table
-        cols = ','.join(['%s varchar'%x
-            for x in 'nodeid date tag action params'.split()])
-        sql = '''create table %s__journal (
-            nodeid integer, date datetime, tag varchar(255),
-            action varchar(255), params text) ENGINE=%s'''%(
-            spec.classname, self.mysql_backend)
-        self.sql(sql)
-        self.create_journal_table_indexes(spec)
-
-    def drop_journal_table_indexes(self, classname):
-        index_name = '%s_journ_idx'%classname
-        if not self.sql_index_exists('%s__journal'%classname, index_name):
-            return
-        index_sql = 'drop index %s on %s__journal'%(index_name, classname)
-        self.sql(index_sql)
-
-    def create_multilink_table(self, spec, ml):
-        sql = '''CREATE TABLE `%s_%s` (linkid VARCHAR(255),
-            nodeid VARCHAR(255)) ENGINE=%s'''%(spec.classname, ml,
-                self.mysql_backend)
-        self.sql(sql)
-        self.create_multilink_table_indexes(spec, ml)
-
-    def drop_multilink_table_indexes(self, classname, ml):
-        l = [
-            '%s_%s_l_idx'%(classname, ml),
-            '%s_%s_n_idx'%(classname, ml)
-        ]
-        table_name = '%s_%s'%(classname, ml)
-        for index_name in l:
-            if not self.sql_index_exists(table_name, index_name):
-                continue
-            sql = 'drop index %s on %s'%(index_name, table_name)
-            self.sql(sql)
-
-    def drop_class_table_key_index(self, cn, key):
-        table_name = '_%s'%cn
-        index_name = '_%s_%s_idx'%(cn, key)
-        if not self.sql_index_exists(table_name, index_name):
-            return
-        sql = 'drop index %s on %s'%(index_name, table_name)
-        self.sql(sql)
-
-    # old-skool id generation
-    def newid(self, classname):
-        ''' Generate a new id for the given class
-        '''
-        # get the next ID - "FOR UPDATE" will lock the row for us
-        sql = 'select num from ids where name=%s FOR UPDATE'%self.arg
-        self.sql(sql, (classname, ))
-        newid = int(self.cursor.fetchone()[0])
-
-        # update the counter
-        sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
-        vals = (int(newid)+1, classname)
-        self.sql(sql, vals)
-
-        # return as string
-        return str(newid)
-
-    def setid(self, classname, setid):
-        ''' Set the id counter: used during import of database
-
-        We add one to make it behave like the seqeunces in postgres.
-        '''
-        sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
-        vals = (int(setid)+1, classname)
-        self.sql(sql, vals)
-
-    def clear(self):
-        rdbms_common.Database.clear(self)
-
-        # set the id counters to 0 (setid adds one) so we start at 1
-        for cn in self.classes.keys():
-            self.setid(cn, 0)
-
-    def create_class(self, spec):
-        rdbms_common.Database.create_class(self, spec)
-        sql = 'insert into ids (name, num) values (%s, %s)'
-        vals = (spec.classname, 1)
-        self.sql(sql, vals)
-
-    def sql_commit(self, fail_ok=False):
-        ''' Actually commit to the database.
-        '''
-        self.log_info('commit')
-
-        # MySQL commits don't seem to ever fail, the latest update winning.
-        # makes you wonder why they have transactions...
-        self.conn.commit()
-
-        # open a new cursor for subsequent work
-        self.cursor = self.conn.cursor()
-
-        # make sure we're in a new transaction and not autocommitting
-        self.sql("SET AUTOCOMMIT=0")
-        self.sql("START TRANSACTION")
-
-    def sql_close(self):
-        self.log_info('close')
-        try:
-            self.conn.close()
-        except MySQLdb.ProgrammingError, message:
-            if str(message) != 'closing a closed connection':
-                raise
-
-class MysqlClass:
-    def _subselect(self, classname, multilink_table):
-        ''' "I can't believe it's not a toy RDBMS"
-           see, even toy RDBMSes like gadfly and sqlite can do sub-selects...
-        '''
-        self.db.sql('select nodeid from %s'%multilink_table)
-        s = ','.join([x[0] for x in self.db.sql_fetchall()])
-        return '_%s.id not in (%s)'%(classname, s)
-
-    def create_inner(self, **propvalues):
-        try:
-            return rdbms_common.Class.create_inner(self, **propvalues)
-        except MySQLdb.IntegrityError, e:
-            self._handle_integrity_error(e, propvalues)
-
-    def set_inner(self, nodeid, **propvalues):
-        try:
-            return rdbms_common.Class.set_inner(self, nodeid,
-                                                **propvalues)
-        except MySQLdb.IntegrityError, e:
-            self._handle_integrity_error(e, propvalues)
-
-    def _handle_integrity_error(self, e, propvalues):
-        ''' Handle a MySQL IntegrityError.
-
-        If the error is recognized, then it may be converted into an
-        alternative exception.  Otherwise, it is raised unchanged from
-        this function.'''
-
-        # There are checks in create_inner/set_inner to see if a node
-        # is being created with the same key as an existing node.
-        # But, there is a race condition -- we may pass those checks,
-        # only to find out that a parallel session has created the
-        # node by by the time we actually issue the SQL command to
-        # create the node.  Fortunately, MySQL gives us a unique error
-        # code for this situation, so we can detect it here and handle
-        # it appropriately.
-        # 
-        # The details of the race condition are as follows, where
-        # "X" is a classname, and the term "thread" is meant to
-        # refer generically to both threads and processes:
-        #
-        # Thread A                    Thread B
-        # --------                    --------
-        #                             read table for X
-        # create new X object
-        # commit
-        #                             create new X object
-        #
-        # In Thread B, the check in create_inner does not notice that
-        # the new X object is a duplicate of that committed in Thread
-        # A because MySQL's default "consistent nonlocking read"
-        # behavior means that Thread B sees a snapshot of the database
-        # at the point at which its transaction began -- which was
-        # before Thread A created the object.  However, the attempt
-        # to *write* to the table for X, creating a duplicate entry,
-        # triggers an error at the point of the write.
-        #
-        # If both A and B's transaction begins with creating a new X
-        # object, then this bug cannot occur because creating the
-        # object requires getting a new ID, and newid() locks the id
-        # table until the transaction is committed or rolledback.  So,
-        # B will block until A's commit is complete, and will not
-        # actually get its snapshot until A's transaction completes.
-        # But, if the transaction has begun prior to calling newid,
-        # then the snapshot has already been established.
-        if e[0] == ER.DUP_ENTRY:
-            key = propvalues[self.key]
-            raise ValueError, 'node with key "%s" exists' % key
-        # We don't know what this exception is; reraise it.
-        raise
-        
-
-class Class(MysqlClass, rdbms_common.Class):
-    pass
-class IssueClass(MysqlClass, rdbms_common.IssueClass):
-    pass
-class FileClass(MysqlClass, rdbms_common.FileClass):
-    pass
-
-# vim: set et sts=4 sw=4 :
diff --git a/build/lib/roundup/backends/back_postgresql.py b/build/lib/roundup/backends/back_postgresql.py
deleted file mode 100644 (file)
index e066083..0000000
+++ /dev/null
@@ -1,288 +0,0 @@
-#$Id: back_postgresql.py,v 1.44 2008-08-07 05:50:03 richard Exp $
-#
-# Copyright (c) 2003 Martynas Sklyzmantas, Andrey Lebedev <andrey@micro.lt>
-#
-# 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.
-#
-'''Postgresql backend via psycopg for Roundup.'''
-__docformat__ = 'restructuredtext'
-
-import os, shutil, time
-try:
-    import psycopg
-    from psycopg import QuotedString
-    from psycopg import ProgrammingError
-except:
-    from psycopg2 import psycopg1 as psycopg
-    from psycopg2.extensions import QuotedString
-    from psycopg2.psycopg1 import ProgrammingError
-import logging
-
-from roundup import hyperdb, date
-from roundup.backends import rdbms_common
-from roundup.backends import sessions_rdbms
-
-def connection_dict(config, dbnamestr=None):
-    ''' read_default_group is MySQL-specific, ignore it '''
-    d = rdbms_common.connection_dict(config, dbnamestr)
-    if 'read_default_group' in d:
-        del d['read_default_group']
-    if 'read_default_file' in d:
-        del d['read_default_file']
-    return d
-
-def db_create(config):
-    """Clear all database contents and drop database itself"""
-    command = "CREATE DATABASE %s WITH ENCODING='UNICODE'"%config.RDBMS_NAME
-    logging.getLogger('hyperdb').info(command)
-    db_command(config, command)
-
-def db_nuke(config, fail_ok=0):
-    """Clear all database contents and drop database itself"""
-    command = 'DROP DATABASE %s'% config.RDBMS_NAME
-    logging.getLogger('hyperdb').info(command)
-    db_command(config, command)
-
-    if os.path.exists(config.DATABASE):
-        shutil.rmtree(config.DATABASE)
-
-def db_command(config, command):
-    '''Perform some sort of database-level command. Retry 10 times if we
-    fail by conflicting with another user.
-    '''
-    template1 = connection_dict(config)
-    template1['database'] = 'template1'
-
-    try:
-        conn = psycopg.connect(**template1)
-    except psycopg.OperationalError, message:
-        raise hyperdb.DatabaseError(message)
-
-    conn.set_isolation_level(0)
-    cursor = conn.cursor()
-    try:
-        for n in range(10):
-            if pg_command(cursor, command):
-                return
-    finally:
-        conn.close()
-    raise RuntimeError('10 attempts to create database failed')
-
-def pg_command(cursor, command):
-    '''Execute the postgresql command, which may be blocked by some other
-    user connecting to the database, and return a true value if it succeeds.
-
-    If there is a concurrent update, retry the command.
-    '''
-    try:
-        cursor.execute(command)
-    except psycopg.ProgrammingError, err:
-        response = str(err).split('\n')[0]
-        if response.find('FATAL') != -1:
-            raise RuntimeError(response)
-        else:
-            msgs = [
-                'is being accessed by other users',
-                'could not serialize access due to concurrent update',
-            ]
-            can_retry = 0
-            for msg in msgs:
-                if response.find(msg) == -1:
-                    can_retry = 1
-            if can_retry:
-                time.sleep(1)
-                return 0
-            raise RuntimeError(response)
-    return 1
-
-def db_exists(config):
-    """Check if database already exists"""
-    db = connection_dict(config, 'database')
-    try:
-        conn = psycopg.connect(**db)
-        conn.close()
-        return 1
-    except:
-        return 0
-
-class Sessions(sessions_rdbms.Sessions):
-    def set(self, *args, **kwargs):
-        try:
-            sessions_rdbms.Sessions.set(self, *args, **kwargs)
-        except ProgrammingError, err:
-            response = str(err).split('\n')[0]
-            if -1 != response.find('ERROR') and \
-               -1 != response.find('could not serialize access due to concurrent update'):
-                # another client just updated, and we're running on
-                # serializable isolation.
-                # see http://www.postgresql.org/docs/7.4/interactive/transaction-iso.html
-                self.db.rollback()
-
-class Database(rdbms_common.Database):
-    arg = '%s'
-
-    # used by some code to switch styles of query
-    implements_intersect = 1
-
-    def getSessionManager(self):
-        return Sessions(self)
-
-    def sql_open_connection(self):
-        db = connection_dict(self.config, 'database')
-        logging.getLogger('hyperdb').info('open database %r'%db['database'])
-        try:
-            conn = psycopg.connect(**db)
-        except psycopg.OperationalError, message:
-            raise hyperdb.DatabaseError(message)
-
-        cursor = conn.cursor()
-
-        return (conn, cursor)
-
-    def open_connection(self):
-        if not db_exists(self.config):
-            db_create(self.config)
-
-        self.conn, self.cursor = self.sql_open_connection()
-
-        try:
-            self.load_dbschema()
-        except psycopg.ProgrammingError, message:
-            if str(message).find('schema') == -1:
-                raise
-            self.rollback()
-            self.init_dbschema()
-            self.sql("CREATE TABLE schema (schema TEXT)")
-            self.sql("CREATE TABLE dual (dummy integer)")
-            self.sql("insert into dual values (1)")
-            self.create_version_2_tables()
-
-    def create_version_2_tables(self):
-        # OTK store
-        self.sql('''CREATE TABLE otks (otk_key VARCHAR(255),
-            otk_value TEXT, otk_time REAL)''')
-        self.sql('CREATE INDEX otks_key_idx ON otks(otk_key)')
-
-        # Sessions store
-        self.sql('''CREATE TABLE sessions (
-            session_key VARCHAR(255), session_time REAL,
-            session_value TEXT)''')
-        self.sql('''CREATE INDEX sessions_key_idx ON
-            sessions(session_key)''')
-
-        # full-text indexing store
-        self.sql('CREATE SEQUENCE ___textids_ids')
-        self.sql('''CREATE TABLE __textids (
-            _textid integer primary key, _class VARCHAR(255),
-            _itemid VARCHAR(255), _prop VARCHAR(255))''')
-        self.sql('''CREATE TABLE __words (_word VARCHAR(30),
-            _textid integer)''')
-        self.sql('CREATE INDEX words_word_idx ON __words(_word)')
-        self.sql('CREATE INDEX words_by_id ON __words (_textid)')
-        self.sql('CREATE UNIQUE INDEX __textids_by_props ON '
-                 '__textids (_class, _itemid, _prop)')
-
-    def fix_version_2_tables(self):
-        # Convert journal date column to TIMESTAMP, params column to TEXT
-        self._convert_journal_tables()
-
-        # Convert all String properties to TEXT
-        self._convert_string_properties()
-
-        # convert session / OTK *_time columns to REAL
-        for name in ('otk', 'session'):
-            self.sql('drop index %ss_key_idx'%name)
-            self.sql('drop table %ss'%name)
-            self.sql('''CREATE TABLE %ss (%s_key VARCHAR(255),
-                %s_value VARCHAR(255), %s_time REAL)'''%(name, name, name,
-                name))
-            self.sql('CREATE INDEX %ss_key_idx ON %ss(%s_key)'%(name, name,
-                name))
-
-    def fix_version_3_tables(self):
-        rdbms_common.Database.fix_version_3_tables(self)
-        self.sql('''CREATE INDEX words_both_idx ON public.__words
-            USING btree (_word, _textid)''')
-
-    def add_actor_column(self):
-        # update existing tables to have the new actor column
-        tables = self.database_schema['tables']
-        for name in tables:
-            self.sql('ALTER TABLE _%s add __actor VARCHAR(255)'%name)
-
-    def __repr__(self):
-        return '<roundpsycopgsql 0x%x>' % id(self)
-
-    def sql_commit(self, fail_ok=False):
-        ''' Actually commit to the database.
-        '''
-        logging.getLogger('hyperdb').info('commit')
-
-        try:
-            self.conn.commit()
-        except psycopg.ProgrammingError, message:
-            # we've been instructed that this commit is allowed to fail
-            if fail_ok and str(message).endswith('could not serialize '
-                    'access due to concurrent update'):
-                logging.getLogger('hyperdb').info('commit FAILED, but fail_ok')
-            else:
-                raise
-
-        # open a new cursor for subsequent work
-        self.cursor = self.conn.cursor()
-
-    def sql_stringquote(self, value):
-        ''' psycopg.QuotedString returns a "buffer" object with the
-            single-quotes around it... '''
-        return str(QuotedString(str(value)))[1:-1]
-
-    def sql_index_exists(self, table_name, index_name):
-        sql = 'select count(*) from pg_indexes where ' \
-            'tablename=%s and indexname=%s'%(self.arg, self.arg)
-        self.sql(sql, (table_name, index_name))
-        return self.cursor.fetchone()[0]
-
-    def create_class_table(self, spec, create_sequence=1):
-        if create_sequence:
-            sql = 'CREATE SEQUENCE _%s_ids'%spec.classname
-            self.sql(sql)
-
-        return rdbms_common.Database.create_class_table(self, spec)
-
-    def drop_class_table(self, cn):
-        sql = 'drop table _%s'%cn
-        self.sql(sql)
-
-        sql = 'drop sequence _%s_ids'%cn
-        self.sql(sql)
-
-    def newid(self, classname):
-        sql = "select nextval('_%s_ids') from dual"%classname
-        self.sql(sql)
-        return str(self.cursor.fetchone()[0])
-
-    def setid(self, classname, setid):
-        sql = "select setval('_%s_ids', %s) from dual"%(classname, int(setid))
-        self.sql(sql)
-
-    def clear(self):
-        rdbms_common.Database.clear(self)
-
-        # reset the sequences
-        for cn in self.classes:
-            self.cursor.execute('DROP SEQUENCE _%s_ids'%cn)
-            self.cursor.execute('CREATE SEQUENCE _%s_ids'%cn)
-
-class PostgresqlClass:
-    order_by_null_values = '(%s is not NULL)'
-
-class Class(PostgresqlClass, rdbms_common.Class):
-    pass
-class IssueClass(PostgresqlClass, rdbms_common.IssueClass):
-    pass
-class FileClass(PostgresqlClass, rdbms_common.FileClass):
-    pass
-
-# vim: set et sts=4 sw=4 :
diff --git a/build/lib/roundup/backends/back_sqlite.py b/build/lib/roundup/backends/back_sqlite.py
deleted file mode 100644 (file)
index c947eb3..0000000
+++ /dev/null
@@ -1,410 +0,0 @@
-"""Implements a backend for SQLite.
-
-See https://pysqlite.sourceforge.net/ for pysqlite info
-
-
-NOTE: we use the rdbms_common table creation methods which define datatypes
-for the columns, but sqlite IGNORES these specifications.
-"""
-__docformat__ = 'restructuredtext'
-
-import os, base64, marshal, shutil, time, logging
-
-from roundup import hyperdb, date, password
-from roundup.backends import rdbms_common
-sqlite_version = None
-try:
-    import sqlite3 as sqlite
-    sqlite_version = 3
-except ImportError:
-    try:
-        from pysqlite2 import dbapi2 as sqlite
-        if sqlite.version_info < (2,1,0):
-            raise ValueError('pysqlite2 minimum version is 2.1.0+ '
-                '- %s found'%sqlite.version)
-        sqlite_version = 2
-    except ImportError:
-        import sqlite
-        sqlite_version = 1
-
-def db_exists(config):
-    return os.path.exists(os.path.join(config.DATABASE, 'db'))
-
-def db_nuke(config):
-    shutil.rmtree(config.DATABASE)
-
-class Database(rdbms_common.Database):
-    # char to use for positional arguments
-    if sqlite_version in (2,3):
-        arg = '?'
-    else:
-        arg = '%s'
-
-    # used by some code to switch styles of query
-    implements_intersect = 1
-
-    hyperdb_to_sql_datatypes = {
-        hyperdb.String : 'VARCHAR(255)',
-        hyperdb.Date   : 'VARCHAR(30)',
-        hyperdb.Link   : 'INTEGER',
-        hyperdb.Interval  : 'VARCHAR(255)',
-        hyperdb.Password  : 'VARCHAR(255)',
-        hyperdb.Boolean   : 'BOOLEAN',
-        hyperdb.Number    : 'REAL',
-    }
-    hyperdb_to_sql_value = {
-        hyperdb.String : str,
-        hyperdb.Date   : lambda x: x.serialise(),
-        hyperdb.Link   : int,
-        hyperdb.Interval  : str,
-        hyperdb.Password  : str,
-        hyperdb.Boolean   : int,
-        hyperdb.Number    : lambda x: x,
-        hyperdb.Multilink : lambda x: x,    # used in journal marshalling
-    }
-    sql_to_hyperdb_value = {
-        hyperdb.String : lambda x: isinstance(x, unicode) and x.encode('utf8') or str(x),
-        hyperdb.Date   : lambda x: date.Date(str(x)),
-        hyperdb.Link   : str, # XXX numeric ids
-        hyperdb.Interval  : date.Interval,
-        hyperdb.Password  : lambda x: password.Password(encrypted=x),
-        hyperdb.Boolean   : int,
-        hyperdb.Number    : rdbms_common._num_cvt,
-        hyperdb.Multilink : lambda x: x,    # used in journal marshalling
-    }
-
-    def sqlite_busy_handler(self, data, table, count):
-        """invoked whenever SQLite tries to access a database that is locked"""
-        if count == 1:
-            # use a 30 second timeout (extraordinarily generous)
-            # for handling locked database
-            self._busy_handler_endtime = time.time() + 30
-        elif time.time() > self._busy_handler_endtime:
-            # timeout expired - no more retries
-            return 0
-        # sleep adaptively as retry count grows,
-        # starting from about half a second
-        time_to_sleep = 0.01 * (2 << min(5, count))
-        time.sleep(time_to_sleep)
-        return 1
-
-    def sql_open_connection(self):
-        """Open a standard, non-autocommitting connection.
-
-        pysqlite will automatically BEGIN TRANSACTION for us.
-        """
-        # make sure the database directory exists
-        # database itself will be created by sqlite if needed
-        if not os.path.isdir(self.config.DATABASE):
-            os.makedirs(self.config.DATABASE)
-
-        db = os.path.join(self.config.DATABASE, 'db')
-        logging.getLogger('hyperdb').info('open database %r'%db)
-        # set a 30 second timeout (extraordinarily generous) for handling
-        # locked database
-        if sqlite_version == 1:
-            conn = sqlite.connect(db=db)
-            conn.db.sqlite_busy_handler(self.sqlite_busy_handler)
-        else:
-            conn = sqlite.connect(db, timeout=30)
-            conn.row_factory = sqlite.Row
-
-        # pysqlite2 / sqlite3 want us to store Unicode in the db but
-        # that's not what's been done historically and it's definitely
-        # not what the other backends do, so we'll stick with UTF-8
-        if sqlite_version in (2, 3):
-            conn.text_factory = str
-
-        cursor = conn.cursor()
-        return (conn, cursor)
-
-    def open_connection(self):
-        # ensure files are group readable and writable
-        os.umask(self.config.UMASK)
-
-        (self.conn, self.cursor) = self.sql_open_connection()
-
-        try:
-            self.load_dbschema()
-        except sqlite.DatabaseError, error:
-            if str(error) != 'no such table: schema':
-                raise
-            self.init_dbschema()
-            self.sql('create table schema (schema varchar)')
-            self.sql('create table ids (name varchar, num integer)')
-            self.sql('create index ids_name_idx on ids(name)')
-            self.create_version_2_tables()
-
-    def create_version_2_tables(self):
-        self.sql('create table otks (otk_key varchar, '
-            'otk_value varchar, otk_time integer)')
-        self.sql('create index otks_key_idx on otks(otk_key)')
-        self.sql('create table sessions (session_key varchar, '
-            'session_time integer, session_value varchar)')
-        self.sql('create index sessions_key_idx on '
-                'sessions(session_key)')
-
-        # full-text indexing store
-        self.sql('CREATE TABLE __textids (_class varchar, '
-            '_itemid varchar, _prop varchar, _textid integer primary key) ')
-        self.sql('CREATE TABLE __words (_word varchar, '
-            '_textid integer)')
-        self.sql('CREATE INDEX words_word_ids ON __words(_word)')
-        self.sql('CREATE INDEX words_by_id ON __words (_textid)')
-        self.sql('CREATE UNIQUE INDEX __textids_by_props ON '
-                 '__textids (_class, _itemid, _prop)')
-        sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
-        self.sql(sql, ('__textids', 1))
-
-    def add_new_columns_v2(self):
-        # update existing tables to have the new actor column
-        tables = self.database_schema['tables']
-        for classname, spec in self.classes.items():
-            if classname in tables:
-                dbspec = tables[classname]
-                self.update_class(spec, dbspec, force=1, adding_v2=1)
-                # we've updated - don't try again
-                tables[classname] = spec.schema()
-
-    def fix_version_3_tables(self):
-        # NOOP - no restriction on column length here
-        pass
-
-    def update_class(self, spec, old_spec, force=0, adding_v2=0):
-        """ Determine the differences between the current spec and the
-            database version of the spec, and update where necessary.
-
-            If 'force' is true, update the database anyway.
-
-            SQLite doesn't have ALTER TABLE, so we have to copy and
-            regenerate the tables with the new schema.
-        """
-        new_spec = spec.schema()
-        new_spec[1].sort()
-        old_spec[1].sort()
-        if not force and new_spec == old_spec:
-            # no changes
-            return 0
-
-        logging.getLogger('hyperdb').info('update_class %s'%spec.classname)
-
-        # detect multilinks that have been removed, and drop their table
-        old_has = {}
-        for name, prop in old_spec[1]:
-            old_has[name] = 1
-            if name in spec.properties or not isinstance(prop, hyperdb.Multilink):
-                continue
-            # it's a multilink, and it's been removed - drop the old
-            # table. First drop indexes.
-            self.drop_multilink_table_indexes(spec.classname, name)
-            sql = 'drop table %s_%s'%(spec.classname, prop)
-            self.sql(sql)
-
-        # now figure how we populate the new table
-        if adding_v2:
-            fetch = ['_activity', '_creation', '_creator']
-        else:
-            fetch = ['_actor', '_activity', '_creation', '_creator']
-        properties = spec.getprops()
-        for propname,x in new_spec[1]:
-            prop = properties[propname]
-            if isinstance(prop, hyperdb.Multilink):
-                if propname not in old_has:
-                    # we need to create the new table
-                    self.create_multilink_table(spec, propname)
-                elif force:
-                    tn = '%s_%s'%(spec.classname, propname)
-                    # grabe the current values
-                    sql = 'select linkid, nodeid from %s'%tn
-                    self.sql(sql)
-                    rows = self.cursor.fetchall()
-
-                    # drop the old table
-                    self.drop_multilink_table_indexes(spec.classname, propname)
-                    sql = 'drop table %s'%tn
-                    self.sql(sql)
-
-                    # re-create and populate the new table
-                    self.create_multilink_table(spec, propname)
-                    sql = """insert into %s (linkid, nodeid) values
-                        (%s, %s)"""%(tn, self.arg, self.arg)
-                    for linkid, nodeid in rows:
-                        self.sql(sql, (int(linkid), int(nodeid)))
-            elif propname in old_has:
-                # we copy this col over from the old table
-                fetch.append('_'+propname)
-
-        # select the data out of the old table
-        fetch.append('id')
-        fetch.append('__retired__')
-        fetchcols = ','.join(fetch)
-        cn = spec.classname
-        sql = 'select %s from _%s'%(fetchcols, cn)
-        self.sql(sql)
-        olddata = self.cursor.fetchall()
-
-        # TODO: update all the other index dropping code
-        self.drop_class_table_indexes(cn, old_spec[0])
-
-        # drop the old table
-        self.sql('drop table _%s'%cn)
-
-        # create the new table
-        self.create_class_table(spec)
-
-        if olddata:
-            inscols = ['id', '_actor', '_activity', '_creation', '_creator']
-            for propname,x in new_spec[1]:
-                prop = properties[propname]
-                if isinstance(prop, hyperdb.Multilink):
-                    continue
-                elif isinstance(prop, hyperdb.Interval):
-                    inscols.append('_'+propname)
-                    inscols.append('__'+propname+'_int__')
-                elif propname in old_has:
-                    # we copy this col over from the old table
-                    inscols.append('_'+propname)
-
-            # do the insert of the old data - the new columns will have
-            # NULL values
-            args = ','.join([self.arg for x in inscols])
-            cols = ','.join(inscols)
-            sql = 'insert into _%s (%s) values (%s)'%(cn, cols, args)
-            for entry in olddata:
-                d = []
-                for name in inscols:
-                    # generate the new value for the Interval int column
-                    if name.endswith('_int__'):
-                        name = name[2:-6]
-                        if sqlite_version in (2,3):
-                            try:
-                                v = hyperdb.Interval(entry[name]).as_seconds()
-                            except IndexError:
-                                v = None
-                        elif name in entry:
-                            v = hyperdb.Interval(entry[name]).as_seconds()
-                        else:
-                            v = None
-                    elif sqlite_version in (2,3):
-                        try:
-                            v = entry[name]
-                        except IndexError:
-                            v = None
-                    elif (sqlite_version == 1 and name in entry):
-                        v = entry[name]
-                    else:
-                        v = None
-                    d.append(v)
-                self.sql(sql, tuple(d))
-
-        return 1
-
-    def sql_close(self):
-        """ Squash any error caused by us already having closed the
-            connection.
-        """
-        try:
-            self.conn.close()
-        except sqlite.ProgrammingError, value:
-            if str(value) != 'close failed - Connection is closed.':
-                raise
-
-    def sql_rollback(self):
-        """ Squash any error caused by us having closed the connection (and
-            therefore not having anything to roll back)
-        """
-        try:
-            self.conn.rollback()
-        except sqlite.ProgrammingError, value:
-            if str(value) != 'rollback failed - Connection is closed.':
-                raise
-
-    def __repr__(self):
-        return '<roundlite 0x%x>'%id(self)
-
-    def sql_commit(self, fail_ok=False):
-        """ Actually commit to the database.
-
-            Ignore errors if there's nothing to commit.
-        """
-        try:
-            self.conn.commit()
-        except sqlite.DatabaseError, error:
-            if str(error) != 'cannot commit - no transaction is active':
-                raise
-        # open a new cursor for subsequent work
-        self.cursor = self.conn.cursor()
-
-    def sql_index_exists(self, table_name, index_name):
-        self.sql('pragma index_list(%s)'%table_name)
-        for entry in self.cursor.fetchall():
-            if entry[1] == index_name:
-                return 1
-        return 0
-
-    # old-skool id generation
-    def newid(self, classname):
-        """ Generate a new id for the given class
-        """
-        # get the next ID
-        sql = 'select num from ids where name=%s'%self.arg
-        self.sql(sql, (classname, ))
-        newid = int(self.cursor.fetchone()[0])
-
-        # update the counter
-        sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
-        vals = (int(newid)+1, classname)
-        self.sql(sql, vals)
-
-        # return as string
-        return str(newid)
-
-    def setid(self, classname, setid):
-        """ Set the id counter: used during import of database
-
-        We add one to make it behave like the sequences in postgres.
-        """
-        sql = 'update ids set num=%s where name=%s'%(self.arg, self.arg)
-        vals = (int(setid)+1, classname)
-        self.sql(sql, vals)
-
-    def clear(self):
-        rdbms_common.Database.clear(self)
-        # set the id counters to 0 (setid adds one) so we start at 1
-        for cn in self.classes.keys():
-            self.setid(cn, 0)
-
-    def create_class(self, spec):
-        rdbms_common.Database.create_class(self, spec)
-        sql = 'insert into ids (name, num) values (%s, %s)'%(self.arg, self.arg)
-        vals = (spec.classname, 1)
-        self.sql(sql, vals)
-
-    if sqlite_version in (2,3):
-        def load_journal(self, classname, cols, nodeid):
-            """We need to turn the sqlite3.Row into a tuple so it can be
-            unpacked"""
-            l = rdbms_common.Database.load_journal(self,
-                classname, cols, nodeid)
-            cols = range(5)
-            return [[row[col] for col in cols] for row in l]
-
-class sqliteClass:
-    def filter(self, search_matches, filterspec, sort=(None,None),
-            group=(None,None)):
-        """ If there's NO matches to a fetch, sqlite returns NULL
-            instead of nothing
-        """
-        return [f for f in rdbms_common.Class.filter(self, search_matches,
-            filterspec, sort=sort, group=group) if f]
-
-class Class(sqliteClass, rdbms_common.Class):
-    pass
-
-class IssueClass(sqliteClass, rdbms_common.IssueClass):
-    pass
-
-class FileClass(sqliteClass, rdbms_common.FileClass):
-    pass
-
-# vim: set et sts=4 sw=4 :
diff --git a/build/lib/roundup/backends/back_tsearch2.py b/build/lib/roundup/backends/back_tsearch2.py
deleted file mode 100644 (file)
index 0eb3992..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-#$Id: back_tsearch2.py,v 1.9 2005-01-08 16:16:59 jlgijsbers Exp $
-
-# Note: this backend is EXPERIMENTAL. Do not use if you value your data.
-import re
-
-import psycopg
-
-from roundup import hyperdb
-from roundup.support import ensureParentsExist
-from roundup.backends import back_postgresql, tsearch2_setup, indexer_rdbms
-from roundup.backends.back_postgresql import db_create, db_nuke, db_command
-from roundup.backends.back_postgresql import pg_command, db_exists, Class, IssueClass, FileClass
-from roundup.backends.indexer_common import _isLink, Indexer
-
-# XXX: Should probably be on the Class class.
-def _indexedProps(spec):
-    """Get a list of properties to be indexed on 'spec'."""
-    return [prop for prop, propclass in spec.getprops().items()
-            if isinstance(propclass, hyperdb.String) and propclass.indexme]
-
-def _getQueryDict(spec):
-    """Get a convenience dictionary for creating tsearch2 indexes."""
-    query_dict = {'classname': spec.classname,
-                  'indexedColumns': ['_' + prop for prop in _indexedProps(spec)]}
-    query_dict['tablename'] = "_%(classname)s" % query_dict
-    query_dict['triggername'] = "%(tablename)s_tsvectorupdate" % query_dict
-    return query_dict
-
-class Database(back_postgresql.Database):
-    def __init__(self, config, journaltag=None):
-        back_postgresql.Database.__init__(self, config, journaltag)
-        self.indexer = Indexer(self)
-    
-    def create_version_2_tables(self):
-        back_postgresql.Database.create_version_2_tables(self)
-        tsearch2_setup.setup(self.cursor)    
-
-    def create_class_table_indexes(self, spec):
-        back_postgresql.Database.create_class_table_indexes(self, spec)
-        self.cursor.execute("""CREATE INDEX _%(classname)s_idxFTI_idx
-                               ON %(tablename)s USING gist(idxFTI);""" %
-                            _getQueryDict(spec))
-
-        self.create_tsearch2_trigger(spec)
-
-    def create_tsearch2_trigger(self, spec):
-        d = _getQueryDict(spec)
-        if d['indexedColumns']:
-            
-            d['joined'] = " || ' ' ||".join(d['indexedColumns'])
-            query = """UPDATE %(tablename)s
-                       SET idxFTI = to_tsvector('default', %(joined)s)""" % d
-            self.cursor.execute(query)
-
-            d['joined'] = ", ".join(d['indexedColumns']) 
-            query = """CREATE TRIGGER %(triggername)s
-                       BEFORE UPDATE OR INSERT ON %(tablename)s
-                       FOR EACH ROW EXECUTE PROCEDURE
-                       tsearch2(idxFTI, %(joined)s);""" % d
-            self.cursor.execute(query)
-
-    def drop_tsearch2_trigger(self, spec):
-        # Check whether the trigger exists before trying to drop it.
-        query_dict = _getQueryDict(spec)
-        self.sql("""SELECT tgname FROM pg_catalog.pg_trigger
-                    WHERE tgname = '%(triggername)s'""" % query_dict)
-        if self.cursor.fetchall():
-            self.sql("""DROP TRIGGER %(triggername)s ON %(tablename)s""" %
-                     query_dict)
-
-    def update_class(self, spec, old_spec, force=0):
-        result = back_postgresql.Database.update_class(self, spec, old_spec, force)
-
-        # Drop trigger...
-        self.drop_tsearch2_trigger(spec)
-
-        # and recreate if necessary.
-        self.create_tsearch2_trigger(spec)
-
-        return result
-
-    def determine_all_columns(self, spec):
-        cols, mls = back_postgresql.Database.determine_all_columns(self, spec)
-        cols.append(('idxFTI', 'tsvector'))
-        return cols, mls
-        
-class Indexer(Indexer):
-    def __init__(self, db):
-        self.db = db
-
-    # This indexer never needs to reindex.
-    def should_reindex(self):
-        return 0
-
-    def getHits(self, search_terms, klass):
-        return self.find(search_terms, klass)    
-    
-    def find(self, search_terms, klass):
-        if not search_terms:
-            return None
-
-        hits = self.tsearchQuery(klass.classname, search_terms)
-        designator_propname = {}
-
-        for nm, propclass in klass.getprops().items():
-            if _isLink(propclass):
-                hits.extend(self.tsearchQuery(propclass.classname, search_terms))
-
-        return hits
-
-    def tsearchQuery(self, classname, search_terms):
-        query = """SELECT id FROM _%(classname)s
-                   WHERE idxFTI @@ to_tsquery('default', '%(terms)s')"""                    
-        
-        query = query % {'classname': classname,
-                         'terms': ' & '.join(search_terms)}
-        self.db.cursor.execute(query)
-        klass = self.db.getclass(classname)
-        nodeids = [str(row[0]) for row in self.db.cursor.fetchall()]
-
-        # filter out files without text/plain mime type
-        # XXX: files without text/plain shouldn't be indexed at all, we
-        # should take care of this in the trigger
-        if klass.getprops().has_key('type'):
-            nodeids = [nodeid for nodeid in nodeids
-                       if klass.get(nodeid, 'type') == 'text/plain']
-
-        # XXX: We haven't implemented property-level search, so I'm just faking
-        # it here with a property named 'XXX'. We still need to fix the other
-        # backends and indexer_common.Indexer.search to only want to unpack two
-        # values.
-        return [(classname, nodeid, 'XXX') for nodeid in nodeids]
-
-    # These only exist to satisfy the interface that's expected from indexers.
-    def force_reindex(self):
-        pass
-
-    def add_text(self, identifier, text, mime_type=None):
-        pass
-
-    def close(self):
-        pass
-
-class FileClass(hyperdb.FileClass, Class):
-    '''This class defines a large chunk of data. To support this, it has a
-       mandatory String property "content" which is typically saved off
-       externally to the hyperdb.
-
-       However, this implementation just stores it in the hyperdb.
-    '''
-    def __init__(self, db, classname, **properties):
-        '''The newly-created class automatically includes the "content" property.,
-        '''
-        properties['content'] = hyperdb.String(indexme='yes')
-        Class.__init__(self, db, classname, **properties)
-
-    default_mime_type = 'text/plain'
-    def create(self, **propvalues):
-        # figure the mime type
-        if self.getprops().has_key('type') and not propvalues.get('type'):
-            propvalues['type'] = self.default_mime_type
-        return Class.create(self, **propvalues)
-
-    def export_files(self, dirname, nodeid):
-        dest = self.exportFilename(dirname, nodeid)
-        ensureParentsExist(dest)
-        fp = open(dest, "w")
-        fp.write(self.get(nodeid, "content", default=''))
-        fp.close()
-
-    def import_files(self, dirname, nodeid):
-        source = self.exportFilename(dirname, nodeid)
-
-        fp = open(source, "r")
-        # Use Database.setnode instead of self.set or self.set_inner here, as
-        # Database.setnode doesn't update the "activity" or "actor" properties.
-        self.db.setnode(self.classname, nodeid, values={'content': fp.read()})
-        fp.close()
diff --git a/build/lib/roundup/backends/blobfiles.py b/build/lib/roundup/backends/blobfiles.py
deleted file mode 100644 (file)
index 0e4d9f0..0000000
+++ /dev/null
@@ -1,406 +0,0 @@
-#
-# 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.
-#
-"""This module exports file storage for roundup backends.
-Files are stored into a directory hierarchy.
-"""
-__docformat__ = 'restructuredtext'
-
-import os
-
-def files_in_dir(dir):
-    if not os.path.exists(dir):
-        return 0
-    num_files = 0
-    for dir_entry in os.listdir(dir):
-        full_filename = os.path.join(dir,dir_entry)
-        if os.path.isfile(full_filename):
-            num_files = num_files + 1
-        elif os.path.isdir(full_filename):
-            num_files = num_files + files_in_dir(full_filename)
-    return num_files
-
-class FileStorage:
-    """Store files in some directory structure
-
-    Some databases do not permit the storage of arbitrary data (i.e.,
-    file content).  And, some database schema explicitly store file
-    content in the fielsystem.  In particular, if a class defines a
-    'filename' property, it is assumed that the data is stored in the
-    indicated file, outside of whatever database Roundup is otherwise
-    using.
-
-    In these situations, it is difficult to maintain the transactional
-    abstractions used elsewhere in Roundup.  In particular, if a
-    file's content is edited, but then the containing transaction is
-    not committed, we do not want to commit the edit.  Similarly, we
-    would like to guarantee that if a transaction is committed to the
-    database, then the edit has in fact taken place.
-
-    This class provides an approximation of these transactional
-    requirements.
-
-    For classes that do not have a 'filename' property, the file name
-    used to store the file's content is a deterministic function of
-    the classname and nodeid for the file.  The 'filename' function
-    computes this name.  The name will contain directories and
-    subdirectories, but, suppose, for the purposes of what follows,
-    that the filename is 'file'.
-
-    Edit Procotol
-    -------------
-    
-    When a file is created or edited, the following protocol is used:
-
-    1. The new content of the file is placed in 'file.tmp'.
-
-    2. A transaction is recored in 'self.transactions' referencing the
-       'doStoreFile' method of this class.
-
-    3. At some subsequent point, the database 'commit' function is
-       called.  This function first performs a traditional database
-       commit (for example, by issuing a SQL command to commit the
-       current transaction), and, then, runs the transactions recored
-       in 'self.transactions'.
-
-    4. The 'doStoreFile' method renames the 'file.tmp' to 'file'.
-
-    If Step 3 never occurs, but, instead, the database 'rollback'
-    method is called, then that method, after rolling back the
-    database transaction, calls 'rollbackStoreFile', which removes
-    'file.tmp'.
-
-    Race Condition
-    --------------
-
-    If two Roundup instances (say, the mail gateway and a web client,
-    or two web clients running with a multi-process server) attempt
-    edits at the same time, both will write to 'file.tmp', and the
-    results will be indeterminate.
-    
-    Crash Analysis
-    --------------
-    
-    There are several situations that may occur if a crash (whether
-    because the machine crashes, because an unhandled Python exception
-    is raised, or because the Python process is killed) occurs.
-    
-    Complexity ensues because backuping up an RDBMS is generally more
-    complex than simply copying a file.  Instead, some command is run
-    which stores a snapshot of the database in a file.  So, if you
-    back up the database to a file, and then back up the filesystem,
-    it is likely that further database transactions have occurred
-    between the point of database backup and the point of filesystem
-    backup.
-
-    For the purposes, of this analysis, we assume that the filesystem
-    backup occurred after the database backup.  Furthermore, we assume
-    that filesystem backups are atomic; i.e., the at the filesystem is
-    not being modified during the backup.
-
-    1. Neither the 'commit' nor 'rollback' methods on the database are
-       ever called.
-
-       In this case, the '.tmp' file should be ignored as the
-       transaction was not committed.
-
-    2. The 'commit' method is called.  Subsequently, the machine
-       crashes, and is restored from backups.
-
-       The most recent filesystem backup and the most recent database
-       backup are not in general from the same instant in time.
-
-       This problem means that we can never be sure after a crash if
-       the contents of a file are what we intend.  It is always
-       possible that an edit was made to the file that is not
-       reflected in the filesystem.
-
-    3. A crash occurs between the point of the database commit and the
-       call to 'doStoreFile'.
-
-       If only one of 'file' and 'file.tmp' exists, then that
-       version should be used.  However, if both 'file' and 'file.tmp'
-       exist, there is no way to know which version to use.
-
-    Reading the File
-    ----------------
-
-    When determining the content of the file, we use the following
-    algorithm:
-
-    1. If 'self.transactions' reflects an edit of the file, then use
-       'file.tmp'.
-
-       We know that an edit to the file is in process so 'file.tmp' is
-       the right choice.  If 'file.tmp' does not exist, raise an
-       exception; something has removed the content of the file while
-       we are in the process of editing it.
-
-    2. Otherwise, if 'file.tmp' exists, and 'file' does not, use
-       'file.tmp'.
-
-       We know that the file is supposed to exist because there is a
-       reference to it in the database.  Since 'file' does not exist,
-       we assume that Crash 3 occurred during the initial creation of
-       the file.
-
-    3. Otherwise, use 'file'.
-
-       If 'file.tmp' is not present, this is obviously the best we can
-       do.  This is always the right answer unless Crash 2 occurred,
-       in which case the contents of 'file' may be newer than they
-       were at the point of database backup.
-
-       If 'file.tmp' is present, we know that we are not actively
-       editing the file.  The possibilities are:
-
-       a. Crash 1 has occurred.  In this case, using 'file' is the
-          right answer, so we will have chosen correctly.
-
-       b. Crash 3 has occurred.  In this case, 'file.tmp' is the right
-          answer, so we will have chosen incorrectly.  However, 'file'
-          was at least a previously committed value.
-
-    Future Improvements
-    -------------------
-
-    One approach would be to take advantage of databases which do
-    allow the storage of arbitary date.  For example, MySQL provides
-    the HUGE BLOB datatype for storing up to 4GB of data.
-
-    Another approach would be to store a version ('v') in the actual
-    database and name files 'file.v'.  Then, the editing protocol
-    would become:
-
-    1. Generate a new version 'v', guaranteed to be different from all
-       other versions ever used by the database.  (The version need
-       not be in any particular sequence; a UUID would be fine.)
-
-    2. Store the content in 'file.v'.
-
-    3. Update the database to indicate that the version of the node is
-       'v'.
-
-    Now, if the transaction is committed, the database will refer to
-    'file.v', where the content exists.  If the transaction is rolled
-    back, or not committed, 'file.v' will never be referenced.  In the
-    event of a crash, under the assumptions above, there may be
-    'file.v' files that are not referenced by the database, but the
-    database will be consistent, so long as unreferenced 'file.v'
-    files are never removed until after the database has been backed
-    up.
-    """    
-
-    tempext = '.tmp'
-    """The suffix added to files indicating that they are uncommitted."""
-    
-    def __init__(self, umask):
-        self.umask = umask
-
-    def subdirFilename(self, classname, nodeid, property=None):
-        """Determine what the filename and subdir for nodeid + classname is."""
-        if property:
-            name = '%s%s.%s'%(classname, nodeid, property)
-        else:
-            # roundupdb.FileClass never specified the property name, so don't
-            # include it
-            name = '%s%s'%(classname, nodeid)
-
-        # have a separate subdir for every thousand messages
-        subdir = str(int(nodeid) / 1000)
-        return os.path.join(subdir, name)
-
-    def _tempfile(self, filename):
-        """Return a temporary filename.
-
-        'filename' -- The name of the eventual destination file."""
-
-        return filename + self.tempext
-
-    def _editInProgress(self, classname, nodeid, property):
-        """Return true if the file indicated is being edited.
-
-        returns -- True if the current transaction includes an edit to
-        the file indicated."""
-
-        for method, args in self.transactions:
-            if (method == self.doStoreFile and
-                args == (classname, nodeid, property)):
-                return True
-
-        return False
-    
-
-    def filename(self, classname, nodeid, property=None, create=0):
-        """Determine what the filename for the given node and optionally
-        property is.
-
-        Try a variety of different filenames - the file could be in the
-        usual place, or it could be in a temp file pre-commit *or* it
-        could be in an old-style, backwards-compatible flat directory.
-        """
-        filename  = os.path.join(self.dir, 'files', classname,
-                                 self.subdirFilename(classname, nodeid, property))
-        # If the caller is going to create the file, return the
-        # post-commit filename.  It is the callers responsibility to
-        # add self.tempext when actually creating the file.
-        if create:
-            return filename
-
-        tempfile = self._tempfile(filename)
-
-        # If an edit to this file is in progress, then return the name
-        # of the temporary file containing the edited content.
-        if self._editInProgress(classname, nodeid, property):
-            if not os.path.exists(tempfile):
-                raise IOError('content file for %s not found'%tempfile)
-            return tempfile
-
-        if os.path.exists(filename):
-            return filename
-
-        # Otherwise, if the temporary file exists, then the probable 
-        # explanation is that a crash occurred between the point that
-        # the database entry recording the creation of the file
-        # occured and the point at which the file was renamed from the
-        # temporary name to the final name.
-        if os.path.exists(tempfile):
-            try:
-                # Clean up, by performing the commit now.
-                os.rename(tempfile, filename)
-            except:
-                pass
-            # If two Roundup clients both try to rename the file
-            # at the same time, only one of them will succeed.
-            # So, tolerate such an error -- but no other.
-            if not os.path.exists(filename):
-                raise IOError('content file for %s not found'%filename)
-            return filename
-
-        # ok, try flat (very old-style)
-        if property:
-            filename = os.path.join(self.dir, 'files', '%s%s.%s'%(classname,
-                nodeid, property))
-        else:
-            filename = os.path.join(self.dir, 'files', '%s%s'%(classname,
-                nodeid))
-        if os.path.exists(filename):
-            return filename
-
-        # file just ain't there
-        raise IOError('content file for %s not found'%filename)
-
-    def filesize(self, classname, nodeid, property=None, create=0):
-        filename = self.filename(classname, nodeid, property, create)
-        return os.path.getsize(filename)
-
-    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.
-        """
-        # determine the name of the file to write to
-        name = self.filename(classname, nodeid, property, create=1)
-
-        # make sure the file storage dir exists
-        if not os.path.exists(os.path.dirname(name)):
-            os.makedirs(os.path.dirname(name))
-
-        # save to a temp file
-        name = self._tempfile(name)
-
-        # make sure we don't register the rename action more than once
-        if not self._editInProgress(classname, nodeid, property):
-            # save off the rename action
-            self.transactions.append((self.doStoreFile, (classname, nodeid,
-                property)))
-        # always set umask before writing to make sure we have the proper one
-        # in multi-tracker (i.e. multi-umask) or modpython scenarios
-        # the umask may have changed since last we set it.
-        os.umask(self.umask)
-        open(name, 'wb').write(content)
-
-    def getfile(self, classname, nodeid, property):
-        """Get the content of the file in the database.
-        """
-        filename = self.filename(classname, nodeid, property)
-
-        f = open(filename, 'rb')
-        try:
-            # snarf the contents and make sure we close the file
-            return f.read()
-        finally:
-            f.close()
-
-    def numfiles(self):
-        """Get number of files in storage, even across subdirectories.
-        """
-        files_dir = os.path.join(self.dir, 'files')
-        return files_in_dir(files_dir)
-
-    def doStoreFile(self, classname, nodeid, property, **databases):
-        """Store the file as part of a transaction commit.
-        """
-        # determine the name of the file to write to
-        name = self.filename(classname, nodeid, property, 1)
-
-        # the file is currently ".tmp" - move it to its real name to commit
-        if name.endswith(self.tempext):
-            # creation
-            dstname = os.path.splitext(name)[0]
-        else:
-            # edit operation
-            dstname = name
-            name = self._tempfile(name)
-
-        # content is being updated (and some platforms, eg. win32, won't
-        # let us rename over the top of the old file)
-        if os.path.exists(dstname):
-            os.remove(dstname)
-
-        os.rename(name, dstname)
-
-        # return the classname, nodeid so we reindex this content
-        return (classname, nodeid)
-
-    def rollbackStoreFile(self, classname, nodeid, property, **databases):
-        """Remove the temp file as a part of a rollback
-        """
-        # determine the name of the file to delete
-        name = self.filename(classname, nodeid, property)
-        if not name.endswith(self.tempext):
-            name += self.tempext
-        os.remove(name)
-
-    def isStoreFile(self, classname, nodeid):
-        """See if there is actually any FileStorage for this node.
-           Is there a better way than using self.filename?
-        """
-        try:
-            fname = self.filename(classname, nodeid)
-            return True
-        except IOError:
-            return False
-
-    def destroy(self, classname, nodeid):
-        """If there is actually FileStorage for this node
-           remove it from the filesystem
-        """
-        if self.isStoreFile(classname, nodeid):
-            os.remove(self.filename(classname, nodeid))
-
-# vim: set filetype=python ts=4 sw=4 et si
diff --git a/build/lib/roundup/backends/indexer_common.py b/build/lib/roundup/backends/indexer_common.py
deleted file mode 100644 (file)
index b342612..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-#$Id: indexer_common.py,v 1.11 2008-09-11 19:41:07 schlatterbeck Exp $
-import re
-# Python 2.3 ... 2.6 compatibility:
-from roundup.anypy.sets_ import set
-
-from roundup import hyperdb
-
-STOPWORDS = [
-    "A", "AND", "ARE", "AS", "AT", "BE", "BUT", "BY",
-    "FOR", "IF", "IN", "INTO", "IS", "IT",
-    "NO", "NOT", "OF", "ON", "OR", "SUCH",
-    "THAT", "THE", "THEIR", "THEN", "THERE", "THESE",
-    "THEY", "THIS", "TO", "WAS", "WILL", "WITH"
-]
-
-def _isLink(propclass):
-    return (isinstance(propclass, hyperdb.Link) or
-            isinstance(propclass, hyperdb.Multilink))
-
-class Indexer:
-    def __init__(self, db):
-        self.stopwords = set(STOPWORDS)
-        for word in db.config[('main', 'indexer_stopwords')]:
-            self.stopwords.add(word)
-        # Do not index anything longer than 25 characters since that'll be
-        # gibberish (encoded text or somesuch) or shorter than 2 characters
-        self.minlength = 2
-        self.maxlength = 25
-
-    def is_stopword(self, word):
-        return word in self.stopwords
-
-    def getHits(self, search_terms, klass):
-        return self.find(search_terms)
-
-    def search(self, search_terms, klass, ignore={}):
-        """Display search results looking for [search, terms] associated
-        with the hyperdb Class "klass". Ignore hits on {class: property}.
-        """
-        # do the index lookup
-        hits = self.getHits(search_terms, klass)
-        if not hits:
-            return {}
-
-        designator_propname = {}
-        for nm, propclass in klass.getprops().iteritems():
-            if _isLink(propclass):
-                designator_propname.setdefault(propclass.classname,
-                    []).append(nm)
-
-        # build a dictionary of nodes and their associated messages
-        # and files
-        nodeids = {}      # this is the answer
-        propspec = {}     # used to do the klass.find
-        for l in designator_propname.itervalues():
-            for propname in l:
-                propspec[propname] = {}  # used as a set (value doesn't matter)
-
-        # don't unpack hits entries as sqlite3's Row can't be unpacked :(
-        for entry in hits:
-            # skip this result if we don't care about this class/property
-            classname = entry[0]
-            property = entry[2]
-            if (classname, property) in ignore:
-                continue
-
-            # if it's a property on klass, it's easy
-            # (make sure the nodeid is str() not unicode() as returned by some
-            # backends as that can cause problems down the track)
-            nodeid = str(entry[1])
-            if classname == klass.classname:
-                if nodeid not in nodeids:
-                    nodeids[nodeid] = {}
-                continue
-
-            # make sure the class is a linked one, otherwise ignore
-            if classname not in designator_propname:
-                continue
-
-            # it's a linked class - set up to do the klass.find
-            for linkprop in designator_propname[classname]:
-                propspec[linkprop][nodeid] = 1
-
-        # retain only the meaningful entries
-        for propname, idset in list(propspec.items()):
-            if not idset:
-                del propspec[propname]
-
-        # klass.find tells me the klass nodeids the linked nodes relate to
-        propdefs = klass.getprops()
-        for resid in klass.find(**propspec):
-            resid = str(resid)
-            if resid in nodeids:
-                continue # we ignore duplicate resids
-            nodeids[resid] = {}
-            node_dict = nodeids[resid]
-            # now figure out where it came from
-            for linkprop in propspec:
-                v = klass.get(resid, linkprop)
-                # the link might be a Link so deal with a single result or None
-                if isinstance(propdefs[linkprop], hyperdb.Link):
-                    if v is None: continue
-                    v = [v]
-                for nodeid in v:
-                    if nodeid in propspec[linkprop]:
-                        # OK, this node[propname] has a winner
-                        if linkprop not in node_dict:
-                            node_dict[linkprop] = [nodeid]
-                        else:
-                            node_dict[linkprop].append(nodeid)
-        return nodeids
-
diff --git a/build/lib/roundup/backends/indexer_dbm.py b/build/lib/roundup/backends/indexer_dbm.py
deleted file mode 100644 (file)
index 11ed64f..0000000
+++ /dev/null
@@ -1,292 +0,0 @@
-#
-# This module is derived from the module described at:
-#   http://gnosis.cx/publish/programming/charming_python_15.txt
-# 
-# Author: David Mertz (mertz@gnosis.cx)
-# Thanks to: Pat Knight (p.knight@ktgroup.co.uk)
-#            Gregory Popovitch (greg@gpy.com)
-# 
-# The original module was released under this license, and remains under
-# it:
-#
-#     This file is released to the public domain.  I (dqm) would
-#     appreciate it if you choose to keep derived works under terms
-#     that promote freedom, but obviously am giving up any rights
-#     to compel such.
-# 
-#$Id: indexer_dbm.py,v 1.9 2006-04-27 05:48:26 richard Exp $
-'''This module provides an indexer class, RoundupIndexer, that stores text
-indices in a roundup instance.  This class makes searching the content of
-messages, string properties and text files possible.
-'''
-__docformat__ = 'restructuredtext'
-
-import os, shutil, re, mimetypes, marshal, zlib, errno
-from roundup.hyperdb import Link, Multilink
-from roundup.backends.indexer_common import Indexer as IndexerBase
-
-class Indexer(IndexerBase):
-    '''Indexes information from roundup's hyperdb to allow efficient
-    searching.
-
-    Three structures are created by the indexer::
-
-          files   {identifier: (fileid, wordcount)}
-          words   {word: {fileid: count}}
-          fileids {fileid: identifier}
-
-    where identifier is (classname, nodeid, propertyname)
-    '''
-    def __init__(self, db):
-        IndexerBase.__init__(self, db)
-        self.indexdb_path = os.path.join(db.config.DATABASE, 'indexes')
-        self.indexdb = os.path.join(self.indexdb_path, 'index.db')
-        self.reindex = 0
-        self.quiet = 9
-        self.changed = 0
-
-        # see if we need to reindex because of a change in code
-        version = os.path.join(self.indexdb_path, 'version')
-        if (not os.path.exists(self.indexdb_path) or
-                not os.path.exists(version)):
-            # for now the file itself is a flag
-            self.force_reindex()
-        elif os.path.exists(version):
-            version = open(version).read()
-            # check the value and reindex if it's not the latest
-            if version.strip() != '1':
-                self.force_reindex()
-
-    def force_reindex(self):
-        '''Force a reindex condition
-        '''
-        if os.path.exists(self.indexdb_path):
-            shutil.rmtree(self.indexdb_path)
-        os.makedirs(self.indexdb_path)
-        os.chmod(self.indexdb_path, 0775)
-        open(os.path.join(self.indexdb_path, 'version'), 'w').write('1\n')
-        self.reindex = 1
-        self.changed = 1
-
-    def should_reindex(self):
-        '''Should we reindex?
-        '''
-        return self.reindex
-
-    def add_text(self, identifier, text, mime_type='text/plain'):
-        '''Add some text associated with the (classname, nodeid, property)
-        identifier.
-        '''
-        # make sure the index is loaded
-        self.load_index()
-
-        # remove old entries for this identifier
-        if identifier in self.files:
-            self.purge_entry(identifier)
-
-        # split into words
-        words = self.splitter(text, mime_type)
-
-        # Find new file index, and assign it to identifier
-        # (_TOP uses trick of negative to avoid conflict with file index)
-        self.files['_TOP'] = (self.files['_TOP'][0]-1, None)
-        file_index = abs(self.files['_TOP'][0])
-        self.files[identifier] = (file_index, len(words))
-        self.fileids[file_index] = identifier
-
-        # find the unique words
-        filedict = {}
-        for word in words:
-            if self.is_stopword(word):
-                continue
-            if word in filedict:
-                filedict[word] = filedict[word]+1
-            else:
-                filedict[word] = 1
-
-        # now add to the totals
-        for word in filedict:
-            # each word has a dict of {identifier: count}
-            if word in self.words:
-                entry = self.words[word]
-            else:
-                # new word
-                entry = {}
-                self.words[word] = entry
-
-            # make a reference to the file for this word
-            entry[file_index] = filedict[word]
-
-        # save needed
-        self.changed = 1
-
-    def splitter(self, text, ftype):
-        '''Split the contents of a text string into a list of 'words'
-        '''
-        if ftype == 'text/plain':
-            words = self.text_splitter(text)
-        else:
-            return []
-        return words
-
-    def text_splitter(self, text):
-        """Split text/plain string into a list of words
-        """
-        # case insensitive
-        text = str(text).upper()
-
-        # Split the raw text
-        return re.findall(r'\b\w{%d,%d}\b' % (self.minlength, self.maxlength),
-                          text)
-
-    # we override this to ignore too short and too long words
-    # and also to fix a bug - the (fail) case.
-    def find(self, wordlist):
-        '''Locate files that match ALL the words in wordlist
-        '''
-        if not hasattr(self, 'words'):
-            self.load_index()
-        self.load_index(wordlist=wordlist)
-        entries = {}
-        hits = None
-        for word in wordlist:
-            if not self.minlength <= len(word) <= self.maxlength:
-                # word outside the bounds of what we index - ignore
-                continue
-            word = word.upper()
-            if self.is_stopword(word):
-                continue
-            entry = self.words.get(word)    # For each word, get index
-            entries[word] = entry           #   of matching files
-            if not entry:                   # Nothing for this one word (fail)
-                return {}
-            if hits is None:
-                hits = {}
-                for k in entry:
-                    if k not in self.fileids:
-                        raise ValueError('Index is corrupted: re-generate it')
-                    hits[k] = self.fileids[k]
-            else:
-                # Eliminate hits for every non-match
-                for fileid in list(hits):
-                    if fileid not in entry:
-                        del hits[fileid]
-        if hits is None:
-            return {}
-        return list(hits.values())
-
-    segments = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ#_-!"
-    def load_index(self, reload=0, wordlist=None):
-        # Unless reload is indicated, do not load twice
-        if self.index_loaded() and not reload:
-            return 0
-
-        # Ok, now let's actually load it
-        db = {'WORDS': {}, 'FILES': {'_TOP':(0,None)}, 'FILEIDS': {}}
-
-        # Identify the relevant word-dictionary segments
-        if not wordlist:
-            segments = self.segments
-        else:
-            segments = ['-','#']
-            for word in wordlist:
-                segments.append(word[0].upper())
-
-        # Load the segments
-        for segment in segments:
-            try:
-                f = open(self.indexdb + segment, 'rb')
-            except IOError, error:
-                # probably just nonexistent segment index file
-                if error.errno != errno.ENOENT: raise
-            else:
-                pickle_str = zlib.decompress(f.read())
-                f.close()
-                dbslice = marshal.loads(pickle_str)
-                if dbslice.get('WORDS'):
-                    # if it has some words, add them
-                    for word, entry in dbslice['WORDS'].iteritems():
-                        db['WORDS'][word] = entry
-                if dbslice.get('FILES'):
-                    # if it has some files, add them
-                    db['FILES'] = dbslice['FILES']
-                if dbslice.get('FILEIDS'):
-                    # if it has fileids, add them
-                    db['FILEIDS'] = dbslice['FILEIDS']
-
-        self.words = db['WORDS']
-        self.files = db['FILES']
-        self.fileids = db['FILEIDS']
-        self.changed = 0
-
-    def save_index(self):
-        # only save if the index is loaded and changed
-        if not self.index_loaded() or not self.changed:
-            return
-
-        # brutal space saver... delete all the small segments
-        for segment in self.segments:
-            try:
-                os.remove(self.indexdb + segment)
-            except OSError, error:
-                # probably just nonexistent segment index file
-                if error.errno != errno.ENOENT: raise
-
-        # First write the much simpler filename/fileid dictionaries
-        dbfil = {'WORDS':None, 'FILES':self.files, 'FILEIDS':self.fileids}
-        open(self.indexdb+'-','wb').write(zlib.compress(marshal.dumps(dbfil)))
-
-        # The hard part is splitting the word dictionary up, of course
-        letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ#_"
-        segdicts = {}                           # Need batch of empty dicts
-        for segment in letters:
-            segdicts[segment] = {}
-        for word, entry in self.words.iteritems():  # Split into segment dicts
-            initchar = word[0].upper()
-            segdicts[initchar][word] = entry
-
-        # save
-        for initchar in letters:
-            db = {'WORDS':segdicts[initchar], 'FILES':None, 'FILEIDS':None}
-            pickle_str = marshal.dumps(db)
-            filename = self.indexdb + initchar
-            pickle_fh = open(filename, 'wb')
-            pickle_fh.write(zlib.compress(pickle_str))
-            os.chmod(filename, 0664)
-
-        # save done
-        self.changed = 0
-
-    def purge_entry(self, identifier):
-        '''Remove a file from file index and word index
-        '''
-        self.load_index()
-
-        if identifier not in self.files:
-            return
-
-        file_index = self.files[identifier][0]
-        del self.files[identifier]
-        del self.fileids[file_index]
-
-        # The much harder part, cleanup the word index
-        for key, occurs in self.words.iteritems():
-            if file_index in occurs:
-                del occurs[file_index]
-
-        # save needed
-        self.changed = 1
-
-    def index_loaded(self):
-        return (hasattr(self,'fileids') and hasattr(self,'files') and
-            hasattr(self,'words'))
-
-    def rollback(self):
-        ''' load last saved index info. '''
-        self.load_index(reload=1)
-
-    def close(self):
-        pass
-
-
-# vim: set filetype=python ts=4 sw=4 et si
diff --git a/build/lib/roundup/backends/indexer_rdbms.py b/build/lib/roundup/backends/indexer_rdbms.py
deleted file mode 100644 (file)
index a5886ff..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-#$Id: indexer_rdbms.py,v 1.18 2008-09-01 00:43:02 richard Exp $
-""" This implements the full-text indexer over two RDBMS tables. The first
-is a mapping of words to occurance IDs. The second maps the IDs to (Class,
-propname, itemid) instances.
-"""
-import re
-# Python 2.3 ... 2.6 compatibility:
-from roundup.anypy.sets_ import set
-
-from roundup.backends.indexer_common import Indexer as IndexerBase
-
-class Indexer(IndexerBase):
-    def __init__(self, db):
-        IndexerBase.__init__(self, db)
-        self.db = db
-        self.reindex = 0
-
-    def close(self):
-        """close the indexing database"""
-        # just nuke the circular reference
-        self.db = None
-
-    def save_index(self):
-        """Save the changes to the index."""
-        # not necessary - the RDBMS connection will handle this for us
-        pass
-
-    def force_reindex(self):
-        """Force a reindexing of the database.  This essentially
-        empties the tables ids and index and sets a flag so
-        that the databases are reindexed"""
-        self.reindex = 1
-
-    def should_reindex(self):
-        """returns True if the indexes need to be rebuilt"""
-        return self.reindex
-
-    def add_text(self, identifier, text, mime_type='text/plain'):
-        """ "identifier" is  (classname, itemid, property) """
-        if mime_type != 'text/plain':
-            return
-
-        # Ensure all elements of the identifier are strings 'cos the itemid
-        # column is varchar even if item ids may be numbers elsewhere in the
-        # code. ugh.
-        identifier = tuple(map(str, identifier))
-
-        # first, find the id of the (classname, itemid, property)
-        a = self.db.arg
-        sql = 'select _textid from __textids where _class=%s and '\
-            '_itemid=%s and _prop=%s'%(a, a, a)
-        self.db.cursor.execute(sql, identifier)
-        r = self.db.cursor.fetchone()
-        if not r:
-            # not previously indexed
-            id = self.db.newid('__textids')
-            sql = 'insert into __textids (_textid, _class, _itemid, _prop)'\
-                ' values (%s, %s, %s, %s)'%(a, a, a, a)
-            self.db.cursor.execute(sql, (id, ) + identifier)
-        else:
-            id = int(r[0])
-            # clear out any existing indexed values
-            sql = 'delete from __words where _textid=%s'%a
-            self.db.cursor.execute(sql, (id, ))
-
-        # ok, find all the unique words in the text
-        if not isinstance(text, unicode):
-            text = unicode(text, "utf-8", "replace")
-        text = text.upper()
-        wordlist = [w.encode("utf-8")
-                    for w in re.findall(r'(?u)\b\w{%d,%d}\b'
-                                        % (self.minlength, self.maxlength), text)]
-        words = set()
-        for word in wordlist:
-            if self.is_stopword(word): continue
-            words.add(word)
-
-        # for each word, add an entry in the db
-        sql = 'insert into __words (_word, _textid) values (%s, %s)'%(a, a)
-        words = [(word, id) for word in words]
-        self.db.cursor.executemany(sql, words)
-
-    def find(self, wordlist):
-        """look up all the words in the wordlist.
-        If none are found return an empty dictionary
-        * more rules here
-        """
-        if not wordlist:
-            return []
-
-        l = [word.upper() for word in wordlist
-             if self.minlength <= len(word) <= self.maxlength]
-        l = [word for word in l if not self.is_stopword(word)]
-
-        if not l:
-            return []
-
-        if self.db.implements_intersect:
-            # simple AND search
-            sql = 'select distinct(_textid) from __words where _word=%s'%self.db.arg
-            sql = '\nINTERSECT\n'.join([sql]*len(l))
-            self.db.cursor.execute(sql, tuple(l))
-            r = self.db.cursor.fetchall()
-            if not r:
-                return []
-            a = ','.join([self.db.arg] * len(r))
-            sql = 'select _class, _itemid, _prop from __textids '\
-                'where _textid in (%s)'%a
-            self.db.cursor.execute(sql, tuple([int(row[0]) for row in r]))
-
-        else:
-            # A more complex version for MySQL since it doesn't implement INTERSECT
-
-            # Construct SQL statement to join __words table to itself
-            # multiple times.
-            sql = """select distinct(__words1._textid)
-                        from __words as __words1 %s
-                        where __words1._word=%s %s"""
-
-            join_tmpl = ' left join __words as __words%d using (_textid) \n'
-            match_tmpl = ' and __words%d._word=%s \n'
-
-            join_list = []
-            match_list = []
-            for n in xrange(len(l) - 1):
-                join_list.append(join_tmpl % (n + 2))
-                match_list.append(match_tmpl % (n + 2, self.db.arg))
-
-            sql = sql%(' '.join(join_list), self.db.arg, ' '.join(match_list))
-            self.db.cursor.execute(sql, l)
-
-            r = [x[0] for x in self.db.cursor.fetchall()]
-            if not r:
-                return []
-
-            a = ','.join([self.db.arg] * len(r))
-            sql = 'select _class, _itemid, _prop from __textids '\
-                'where _textid in (%s)'%a
-
-            self.db.cursor.execute(sql, tuple(map(int, r)))
-
-        return self.db.cursor.fetchall()
-
diff --git a/build/lib/roundup/backends/indexer_xapian.py b/build/lib/roundup/backends/indexer_xapian.py
deleted file mode 100644 (file)
index 38a7f2e..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-#$Id: indexer_xapian.py,v 1.6 2007-10-25 07:02:42 richard Exp $
-''' This implements the full-text indexer using the Xapian indexer.
-'''
-import re, os
-
-import xapian
-
-from roundup.backends.indexer_common import Indexer as IndexerBase
-
-# TODO: we need to delete documents when a property is *reindexed*
-
-class Indexer(IndexerBase):
-    def __init__(self, db):
-        IndexerBase.__init__(self, db)
-        self.db_path = db.config.DATABASE
-        self.reindex = 0
-        self.transaction_active = False
-
-    def _get_database(self):
-        index = os.path.join(self.db_path, 'text-index')
-        return xapian.WritableDatabase(index, xapian.DB_CREATE_OR_OPEN)
-
-    def save_index(self):
-        '''Save the changes to the index.'''
-        if not self.transaction_active:
-            return
-        # XXX: Xapian databases don't actually implement transactions yet
-        database = self._get_database()
-        database.commit_transaction()
-        self.transaction_active = False
-
-    def close(self):
-        '''close the indexing database'''
-        pass
-
-    def rollback(self):
-        if not self.transaction_active:
-            return
-        # XXX: Xapian databases don't actually implement transactions yet
-        database = self._get_database()
-        database.cancel_transaction()
-        self.transaction_active = False
-
-    def force_reindex(self):
-        '''Force a reindexing of the database.  This essentially
-        empties the tables ids and index and sets a flag so
-        that the databases are reindexed'''
-        self.reindex = 1
-
-    def should_reindex(self):
-        '''returns True if the indexes need to be rebuilt'''
-        return self.reindex
-
-    def add_text(self, identifier, text, mime_type='text/plain'):
-        ''' "identifier" is  (classname, itemid, property) '''
-        if mime_type != 'text/plain':
-            return
-        if not text: text = ''
-
-        # open the database and start a transaction if needed
-        database = self._get_database()
-        # XXX: Xapian databases don't actually implement transactions yet
-        #if not self.transaction_active:
-            #database.begin_transaction()
-            #self.transaction_active = True
-
-        # TODO: allow configuration of other languages
-        stemmer = xapian.Stem("english")
-
-        # We use the identifier twice: once in the actual "text" being
-        # indexed so we can search on it, and again as the "data" being
-        # indexed so we know what we're matching when we get results
-        identifier = '%s:%s:%s'%identifier
-
-        # see if the id is in the database
-        enquire = xapian.Enquire(database)
-        query = xapian.Query(xapian.Query.OP_AND, [identifier])
-        enquire.set_query(query)
-        matches = enquire.get_mset(0, 10)
-        if matches.size():      # would it killya to implement __len__()??
-            b = matches.begin()
-            docid = b.get_docid()
-        else:
-            docid = None
-
-        # create the new document
-        doc = xapian.Document()
-        doc.set_data(identifier)
-        doc.add_posting(identifier, 0)
-
-        for match in re.finditer(r'\b\w{%d,%d}\b'
-                                 % (self.minlength, self.maxlength),
-                                 text.upper()):
-            word = match.group(0)
-            if self.is_stopword(word):
-                continue
-            term = stemmer(word)
-            doc.add_posting(term, match.start(0))
-        if docid:
-            database.replace_document(docid, doc)
-        else:
-            database.add_document(doc)
-
-    def find(self, wordlist):
-        '''look up all the words in the wordlist.
-        If none are found return an empty dictionary
-        * more rules here
-        '''
-        if not wordlist:
-            return {}
-
-        database = self._get_database()
-
-        enquire = xapian.Enquire(database)
-        stemmer = xapian.Stem("english")
-        terms = []
-        for term in [word.upper() for word in wordlist
-                          if self.minlength <= len(word) <= self.maxlength]:
-            if not self.is_stopword(term):
-                terms.append(stemmer(term))
-        query = xapian.Query(xapian.Query.OP_AND, terms)
-
-        enquire.set_query(query)
-        matches = enquire.get_mset(0, 10)
-
-        return [tuple(m[xapian.MSET_DOCUMENT].get_data().split(':'))
-            for m in matches]
-
diff --git a/build/lib/roundup/backends/locking.py b/build/lib/roundup/backends/locking.py
deleted file mode 100644 (file)
index 5702609..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-#! /usr/bin/env python
-# Copyright (c) 2002 ekit.com Inc (http://www.ekit-inc.com/)
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-#   The above copyright notice and this permission notice shall be included in
-#   all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-
-# $Id: locking.py,v 1.8 2004-02-11 23:55:09 richard Exp $
-
-'''This module provides a generic interface to acquire and release
-exclusive access to a file.
-
-It should work on Unix and Windows.
-'''
-__docformat__ = 'restructuredtext'
-
-from roundup.backends import portalocker
-
-def acquire_lock(path, block=1):
-    '''Acquire a lock for the given path
-    '''
-    file = open(path, 'w')
-    if block:
-        portalocker.lock(file, portalocker.LOCK_EX)
-    else:
-        portalocker.lock(file, portalocker.LOCK_EX|portalocker.LOCK_NB)
-    return file
-
-def release_lock(file):
-    '''Release our lock on the given path
-    '''
-    portalocker.unlock(file)
diff --git a/build/lib/roundup/backends/portalocker.py b/build/lib/roundup/backends/portalocker.py
deleted file mode 100644 (file)
index 18b2748..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-# portalocker.py - Cross-platform (posix/nt) API for flock-style file locking.
-#                  Requires python 1.5.2 or better.
-
-# ID line added by richard for Roundup file tracking
-# $Id: portalocker.py,v 1.9 2006-09-09 05:42:45 richard Exp $
-
-"""Cross-platform (posix/nt) API for flock-style file locking.
-
-Synopsis::
-
-   import portalocker
-   file = open("somefile", "r+")
-   portalocker.lock(file, portalocker.LOCK_EX)
-   file.seek(12)
-   file.write("foo")
-   file.close()
-
-If you know what you're doing, you may choose to::
-
-   portalocker.unlock(file)
-
-before closing the file, but why?
-
-Methods::
-
-   lock( file, flags )
-   unlock( file )
-
-Constants::
-
-   LOCK_EX
-   LOCK_SH
-   LOCK_NB
-
-I learned the win32 technique for locking files from sample code
-provided by John Nielsen <nielsenjf@my-deja.com> in the documentation
-that accompanies the win32 modules.
-
-:Author: Jonathan Feinberg <jdf@pobox.com>
-:Version: Id: portalocker.py,v 1.3 2001/05/29 18:47:55 Administrator Exp 
-          **un-cvsified by richard so the version doesn't change**
-"""
-__docformat__ = 'restructuredtext'
-
-import os
-
-if os.name == 'nt':
-    import win32con
-    import win32file
-    import pywintypes
-    LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK
-    LOCK_SH = 0 # the default
-    LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY
-    # is there any reason not to reuse the following structure?
-    __overlapped = pywintypes.OVERLAPPED()
-elif os.name == 'posix':
-    import fcntl
-    LOCK_EX = fcntl.LOCK_EX
-    LOCK_SH = fcntl.LOCK_SH
-    LOCK_NB = fcntl.LOCK_NB
-else:
-    raise RuntimeError("PortaLocker only defined for nt and posix platforms")
-
-if os.name == 'nt':
-    # eugh, we want 0xffff0000 here, but python 2.3 won't let us :(
-    FFFF0000 = -65536
-    def lock(file, flags):
-        hfile = win32file._get_osfhandle(file.fileno())
-        # LockFileEx is not supported on all Win32 platforms (Win95, Win98,
-        # WinME).
-        # If it's not supported, win32file will raise an exception.
-        # Try LockFileEx first, as it has more functionality and handles
-        # blocking locks more efficiently.
-        try:
-            win32file.LockFileEx(hfile, flags, 0, FFFF0000, __overlapped)
-        except win32file.error, e:
-            import winerror
-            # Propagate upwards all exceptions other than not-implemented.
-            if e[0] != winerror.ERROR_CALL_NOT_IMPLEMENTED:
-                raise e
-            
-            # LockFileEx is not supported. Use LockFile.
-            # LockFile does not support shared locking -- always exclusive.
-            # Care: the low/high length params are reversed compared to
-            # LockFileEx.
-            if not flags & LOCK_EX:
-                import warnings
-                warnings.warn("PortaLocker does not support shared "
-                    "locking on Win9x", RuntimeWarning)
-            # LockFile only supports immediate-fail locking.
-            if flags & LOCK_NB:
-                win32file.LockFile(hfile, 0, 0, FFFF0000, 0)
-            else:
-                # Emulate a blocking lock with a polling loop.
-                import time
-                while 1:
-                    # Attempt a lock.
-                    try:
-                        win32file.LockFile(hfile, 0, 0, FFFF0000, 0)
-                        break
-                    except win32file.error, e:
-                        # Propagate upwards all exceptions other than lock
-                        # violation.
-                        if e[0] != winerror.ERROR_LOCK_VIOLATION:
-                            raise e
-                    # Sleep and poll again.
-                    time.sleep(0.1)
-        # TODO: should this return the result of the lock?
-                    
-    def unlock(file):
-        hfile = win32file._get_osfhandle(file.fileno())
-        # UnlockFileEx is not supported on all Win32 platforms (Win95, Win98,
-        # WinME).
-        # If it's not supported, win32file will raise an api_error exception.
-        try:
-            win32file.UnlockFileEx(hfile, 0, FFFF0000, __overlapped)
-        except win32file.error, e:
-            import winerror
-            # Propagate upwards all exceptions other than not-implemented.
-            if e[0] != winerror.ERROR_CALL_NOT_IMPLEMENTED:
-                raise e
-            
-            # UnlockFileEx is not supported. Use UnlockFile.
-            # Care: the low/high length params are reversed compared to
-            # UnLockFileEx.
-            win32file.UnlockFile(hfile, 0, 0, FFFF0000, 0)
-
-elif os.name =='posix':
-    def lock(file, flags):
-        fcntl.flock(file.fileno(), flags)
-        # TODO: should this return the result of the lock?
-
-    def unlock(file):
-        fcntl.flock(file.fileno(), fcntl.LOCK_UN)
-
-if __name__ == '__main__':
-    from time import time, strftime, localtime
-    import sys
-
-    log = open('log.txt', "a+")
-    lock(log, LOCK_EX)
-
-    timestamp = strftime("%m/%d/%Y %H:%M:%S\n", localtime(time()))
-    log.write( timestamp )
-
-    print "Wrote lines. Hit enter to release lock."
-    dummy = sys.stdin.readline()
-
-    log.close()
-
diff --git a/build/lib/roundup/backends/rdbms_common.py b/build/lib/roundup/backends/rdbms_common.py
deleted file mode 100644 (file)
index 6942a78..0000000
+++ /dev/null
@@ -1,2814 +0,0 @@
-#
-# 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.
-#
-""" Relational database (SQL) backend common code.
-
-Basics:
-
-- map roundup classes to relational tables
-- automatically detect schema changes and modify the table schemas
-  appropriately (we store the "database version" of the schema in the
-  database itself as the only row of the "schema" table)
-- multilinks (which represent a many-to-many relationship) are handled through
-  intermediate tables
-- journals are stored adjunct to the per-class tables
-- table names and columns have "_" prepended so the names can't clash with
-  restricted names (like "order")
-- retirement is determined by the __retired__ column being > 0
-
-Database-specific changes may generally be pushed out to the overridable
-sql_* methods, since everything else should be fairly generic. There's
-probably a bit of work to be done if a database is used that actually
-honors column typing, since the initial databases don't (sqlite stores
-everything as a string.)
-
-The schema of the hyperdb being mapped to the database is stored in the
-database itself as a repr()'ed dictionary of information about each Class
-that maps to a table. If that information differs from the hyperdb schema,
-then we update it. We also store in the schema dict a version which
-allows us to upgrade the database schema when necessary. See upgrade_db().
-
-To force a unqiueness constraint on the key properties we put the item
-id into the __retired__ column duing retirement (so it's 0 for "active"
-items) and place a unqiueness constraint on key + __retired__. This is
-particularly important for the users class where multiple users may
-try to have the same username, with potentially many retired users with
-the same name.
-"""
-__docformat__ = 'restructuredtext'
-
-# standard python modules
-import sys, os, time, re, errno, weakref, copy, logging
-
-# roundup modules
-from roundup import hyperdb, date, password, roundupdb, security, support
-from roundup.hyperdb import String, Password, Date, Interval, Link, \
-    Multilink, DatabaseError, Boolean, Number, Node
-from roundup.backends import locking
-from roundup.support import reversed
-from roundup.i18n import _
-
-# support
-from roundup.backends.blobfiles import FileStorage
-try:
-    from roundup.backends.indexer_xapian import Indexer
-except ImportError:
-    from roundup.backends.indexer_rdbms import Indexer
-from roundup.backends.sessions_rdbms import Sessions, OneTimeKeys
-from roundup.date import Range
-
-# dummy value meaning "argument not passed"
-_marker = []
-
-def _num_cvt(num):
-    num = str(num)
-    try:
-        return int(num)
-    except:
-        return float(num)
-
-def _bool_cvt(value):
-    if value in ('TRUE', 'FALSE'):
-        return {'TRUE': 1, 'FALSE': 0}[value]
-    # assume it's a number returned from the db API
-    return int(value)
-
-def connection_dict(config, dbnamestr=None):
-    """ Used by Postgresql and MySQL to detemine the keyword args for
-    opening the database connection."""
-    d = { }
-    if dbnamestr:
-        d[dbnamestr] = config.RDBMS_NAME
-    for name in ('host', 'port', 'password', 'user', 'read_default_group',
-            'read_default_file'):
-        cvar = 'RDBMS_'+name.upper()
-        if config[cvar] is not None:
-            d[name] = config[cvar]
-    return d
-
-class Database(FileStorage, hyperdb.Database, roundupdb.Database):
-    """ Wrapper around an SQL database that presents a hyperdb interface.
-
-        - some functionality is specific to the actual SQL database, hence
-          the sql_* methods that are NotImplemented
-        - we keep a cache of the latest N row fetches (where N is configurable).
-    """
-    def __init__(self, config, journaltag=None):
-        """ Open the database and load the schema from it.
-        """
-        FileStorage.__init__(self, config.UMASK)
-        self.config, self.journaltag = config, journaltag
-        self.dir = config.DATABASE
-        self.classes = {}
-        self.indexer = Indexer(self)
-        self.security = security.Security(self)
-
-        # additional transaction support for external files and the like
-        self.transactions = []
-
-        # keep a cache of the N most recently retrieved rows of any kind
-        # (classname, nodeid) = row
-        self.cache_size = config.RDBMS_CACHE_SIZE
-        self.cache = {}
-        self.cache_lru = []
-        self.stats = {'cache_hits': 0, 'cache_misses': 0, 'get_items': 0,
-            'filtering': 0}
-
-        # database lock
-        self.lockfile = None
-
-        # open a connection to the database, creating the "conn" attribute
-        self.open_connection()
-
-    def clearCache(self):
-        self.cache = {}
-        self.cache_lru = []
-
-    def getSessionManager(self):
-        return Sessions(self)
-
-    def getOTKManager(self):
-        return OneTimeKeys(self)
-
-    def open_connection(self):
-        """ Open a connection to the database, creating it if necessary.
-
-            Must call self.load_dbschema()
-        """
-        raise NotImplemented
-
-    def sql(self, sql, args=None):
-        """ Execute the sql with the optional args.
-        """
-        self.log_debug('SQL %r %r'%(sql, args))
-        if args:
-            self.cursor.execute(sql, args)
-        else:
-            self.cursor.execute(sql)
-
-    def sql_fetchone(self):
-        """ Fetch a single row. If there's nothing to fetch, return None.
-        """
-        return self.cursor.fetchone()
-
-    def sql_fetchall(self):
-        """ Fetch all rows. If there's nothing to fetch, return [].
-        """
-        return self.cursor.fetchall()
-
-    def sql_stringquote(self, value):
-        """ Quote the string so it's safe to put in the 'sql quotes'
-        """
-        return re.sub("'", "''", str(value))
-
-    def init_dbschema(self):
-        self.database_schema = {
-            'version': self.current_db_version,
-            'tables': {}
-        }
-
-    def load_dbschema(self):
-        """ Load the schema definition that the database currently implements
-        """
-        self.cursor.execute('select schema from schema')
-        schema = self.cursor.fetchone()
-        if schema:
-            self.database_schema = eval(schema[0])
-        else:
-            self.database_schema = {}
-
-    def save_dbschema(self):
-        """ Save the schema definition that the database currently implements
-        """
-        s = repr(self.database_schema)
-        self.sql('delete from schema')
-        self.sql('insert into schema values (%s)'%self.arg, (s,))
-
-    def post_init(self):
-        """ Called once the schema initialisation has finished.
-
-            We should now confirm that the schema defined by our "classes"
-            attribute actually matches the schema in the database.
-        """
-        save = 0
-
-        # handle changes in the schema
-        tables = self.database_schema['tables']
-        for classname, spec in self.classes.iteritems():
-            if classname in tables:
-                dbspec = tables[classname]
-                if self.update_class(spec, dbspec):
-                    tables[classname] = spec.schema()
-                    save = 1
-            else:
-                self.create_class(spec)
-                tables[classname] = spec.schema()
-                save = 1
-
-        for classname, spec in list(tables.items()):
-            if classname not in self.classes:
-                self.drop_class(classname, tables[classname])
-                del tables[classname]
-                save = 1
-
-        # now upgrade the database for column type changes, new internal
-        # tables, etc.
-        save = save | self.upgrade_db()
-
-        # update the database version of the schema
-        if save:
-            self.save_dbschema()
-
-        # reindex the db if necessary
-        if self.indexer.should_reindex():
-            self.reindex()
-
-        # commit
-        self.sql_commit()
-
-    # update this number when we need to make changes to the SQL structure
-    # of the backen database
-    current_db_version = 5
-    db_version_updated = False
-    def upgrade_db(self):
-        """ Update the SQL database to reflect changes in the backend code.
-
-            Return boolean whether we need to save the schema.
-        """
-        version = self.database_schema.get('version', 1)
-        if version > self.current_db_version:
-            raise DatabaseError('attempting to run rev %d DATABASE with rev '
-                '%d CODE!'%(version, self.current_db_version))
-        if version == self.current_db_version:
-            # nothing to do
-            return 0
-
-        if version < 2:
-            self.log_info('upgrade to version 2')
-            # change the schema structure
-            self.database_schema = {'tables': self.database_schema}
-
-            # version 1 didn't have the actor column (note that in
-            # MySQL this will also transition the tables to typed columns)
-            self.add_new_columns_v2()
-
-            # version 1 doesn't have the OTK, session and indexing in the
-            # database
-            self.create_version_2_tables()
-
-        if version < 3:
-            self.log_info('upgrade to version 3')
-            self.fix_version_2_tables()
-
-        if version < 4:
-            self.fix_version_3_tables()
-
-        if version < 5:
-            self.fix_version_4_tables()
-
-        self.database_schema['version'] = self.current_db_version
-        self.db_version_updated = True
-        return 1
-
-    def fix_version_3_tables(self):
-        # drop the shorter VARCHAR OTK column and add a new TEXT one
-        for name in ('otk', 'session'):
-            self.sql('DELETE FROM %ss'%name)
-            self.sql('ALTER TABLE %ss DROP %s_value'%(name, name))
-            self.sql('ALTER TABLE %ss ADD %s_value TEXT'%(name, name))
-
-    def fix_version_2_tables(self):
-        # Default (used by sqlite): NOOP
-        pass
-
-    def fix_version_4_tables(self):
-        # note this is an explicit call now
-        c = self.cursor
-        for cn, klass in self.classes.iteritems():
-            c.execute('select id from _%s where __retired__<>0'%(cn,))
-            for (id,) in c.fetchall():
-                c.execute('update _%s set __retired__=%s where id=%s'%(cn,
-                    self.arg, self.arg), (id, id))
-
-            if klass.key:
-                self.add_class_key_required_unique_constraint(cn, klass.key)
-
-    def _convert_journal_tables(self):
-        """Get current journal table contents, drop the table and re-create"""
-        c = self.cursor
-        cols = ','.join('nodeid date tag action params'.split())
-        for klass in self.classes.itervalues():
-            # slurp and drop
-            sql = 'select %s from %s__journal order by date'%(cols,
-                klass.classname)
-            c.execute(sql)
-            contents = c.fetchall()
-            self.drop_journal_table_indexes(klass.classname)
-            c.execute('drop table %s__journal'%klass.classname)
-
-            # re-create and re-populate
-            self.create_journal_table(klass)
-            a = self.arg
-            sql = 'insert into %s__journal (%s) values (%s,%s,%s,%s,%s)'%(
-                klass.classname, cols, a, a, a, a, a)
-            for row in contents:
-                # no data conversion needed
-                self.cursor.execute(sql, row)
-
-    def _convert_string_properties(self):
-        """Get current Class tables that contain String properties, and
-        convert the VARCHAR columns to TEXT"""
-        c = self.cursor
-        for klass in self.classes.itervalues():
-            # slurp and drop
-            cols, mls = self.determine_columns(list(klass.properties.iteritems()))
-            scols = ','.join([i[0] for i in cols])
-            sql = 'select id,%s from _%s'%(scols, klass.classname)
-            c.execute(sql)
-            contents = c.fetchall()
-            self.drop_class_table_indexes(klass.classname, klass.getkey())
-            c.execute('drop table _%s'%klass.classname)
-
-            # re-create and re-populate
-            self.create_class_table(klass, create_sequence=0)
-            a = ','.join([self.arg for i in range(len(cols)+1)])
-            sql = 'insert into _%s (id,%s) values (%s)'%(klass.classname,
-                scols, a)
-            for row in contents:
-                l = []
-                for entry in row:
-                    # mysql will already be a string - psql needs "help"
-                    if entry is not None and not isinstance(entry, type('')):
-                        entry = str(entry)
-                    l.append(entry)
-                self.cursor.execute(sql, l)
-
-    def refresh_database(self):
-        self.post_init()
-
-
-    def reindex(self, classname=None, show_progress=False):
-        if classname:
-            classes = [self.getclass(classname)]
-        else:
-            classes = list(self.classes.itervalues())
-        for klass in classes:
-            if show_progress:
-                for nodeid in support.Progress('Reindex %s'%klass.classname,
-                        klass.list()):
-                    klass.index(nodeid)
-            else:
-                for nodeid in klass.list():
-                    klass.index(nodeid)
-        self.indexer.save_index()
-
-    hyperdb_to_sql_datatypes = {
-        hyperdb.String : 'TEXT',
-        hyperdb.Date   : 'TIMESTAMP',
-        hyperdb.Link   : 'INTEGER',
-        hyperdb.Interval  : 'VARCHAR(255)',
-        hyperdb.Password  : 'VARCHAR(255)',
-        hyperdb.Boolean   : 'BOOLEAN',
-        hyperdb.Number    : 'REAL',
-    }
-
-    def hyperdb_to_sql_datatype(self, propclass):
-
-        datatype = self.hyperdb_to_sql_datatypes.get(propclass)
-        if datatype:
-            return datatype
-        
-        for k, v in self.hyperdb_to_sql_datatypes.iteritems():
-            if issubclass(propclass, k):
-                return v
-
-        raise ValueError('%r is not a hyperdb property class' % propclass)
-    
-    def determine_columns(self, properties):
-        """ Figure the column names and multilink properties from the spec
-
-            "properties" is a list of (name, prop) where prop may be an
-            instance of a hyperdb "type" _or_ a string repr of that type.
-        """
-        cols = [
-            ('_actor', self.hyperdb_to_sql_datatype(hyperdb.Link)),
-            ('_activity', self.hyperdb_to_sql_datatype(hyperdb.Date)),
-