"""This class abstracts and assists in database connectivity."""
__docformat__ = "restructuredtext"
# Created: Fri Nov 3 11:32:55 PST 2000
# Author: Shannon -jj Behrens
# Email: jjinux@users.sourceforge.net
#
# Copyright (c) Shannon -jj Behrens. All rights reserved.
from aquarium.util.AquariumClass import AquariumClass
from aquarium.util.AutoLoader import AutoLoader
import aquarium.conf.AquariumProperties as properties
class DatabaseAssistant(AquariumClass, AutoLoader):
"""This class abstracts and assists in database connectivity.
The DatabaseAssistant (or DatabaseAdministrator or DatabaseAutoloader
or DatabaseAbstractor or DatabaseAdaptor) helps non-database modules
work with the database. This is what I have envisioned for database
access in Aquarium: I'd like screen modules, etc. to be able to
say (for example)::
myWhatever = self._ctx.dba.SomeTable.someQueryTask(someParams)
This means a couple things. The only classes that should ever have SQL
in them or reference the python database module (as defined in
AquariumProperties) are database modules (i.e. modules located in the
database directory). This will create greater flexibility (or at least
ease the pain) when it is time to change database types or schemas.
Next, each set of related tasks (usually associated with a particular
table in the database) will each be accessible via one class that
basically provides an API for interaction with that table. Last, all
the management of importing the various "table classes" should be taken
care of by Aquarium (via this class or one of its subclasses). This
means that whatever class ``dba`` belongs to, in the above example, must
be able to transparently catch the call to ``SomeTable``, import the
``SomeTable`` class, instantiate it, and then call the ``someQueryTask``
method. Naturally, we'll be caching the class once it's instantiated.
Furthermore, ``dba`` should also provide any functions that must be called
by non-database modules (i.e. it should be an adaptor for the Python
database modules). For instance, it should provide a ``connect`` function
because this function must be called by a non-database module.
Here's how I see the above taking place. This class will define a
base class ``DatabaseAssistant``. This base class (or one of its
subclasses) will be responsible for the following:
* Using the AquariumProperties to import the correct Python database
module(s) and save a reference in ``ctx.db``.
* Abstracting any functions defined in the Python database module(s)
that need to be called by modules other than database modules
(hence the funny naming convention for the methods in this class):
* Mixing in the aquarium.util.AutoLoader_ class in order to do dynamic
imports of the "table classes."
Once the ``DatabaseAssistant`` it taken care of, then you can define a
bunch of "table classes" (that decend from aquarium.util.AquariumClass_)
that use the database assistant as well as the Python database module to
implement a bunch of functions relating to a particular set of tasks
(usually associated with a particular table).
The following private variables are used:
_connection
This is our connection to the database.
.. _aquarium.util.AutoLoader: aquarium.util.AutoLoader.AutoLoader-class.html
.. _aquarium.util.AquariumClass:
aquarium.util.AquariumClass.AquariumClass-class.html
"""
def __init__(self, ctx):
"""Initialize private variables. Set ``ctx.db``.
Notice that Python database modules don't contain exactly
one class (they don't use classes at all), which is a bit
different from Aquarium modules.
"""
AquariumClass.__init__(self, ctx)
module = properties.PYTHON_DATABASE_MODULE
exec("import %s" % module)
ctx.db = eval(module)
def connect(self):
"""Connect to the database.
Will automatically call close for any previous connections.
"""
ctx = self._ctx
if hasattr(self, "_connection"):
self._close()
self._connection = ctx.db.connect(
*properties.DATABASE_CONNECTION_ARGS,
**properties.DATABASE_CONNECTION_KWARGS)
def cursor(self):
"""Get a cursor object.
You'll get a ``NameError`` exception if you try to call this
before a connection has been made. That's probably good
enough.
"""
return self._connection.cursor()
def fetchonedict(self, cursor, force=True):
"""Fetch one row as a dict.
If force is True and there are no more rows, I'll raise a
MissingRecordError. Otherwise (if force is False and there are no more
rows), I'll simply return None. Note, database modules don't *have* to
use this function, I'm just providing it for convenience.
"""
row = cursor.fetchone()
if not row:
if force:
raise MissingRecordError
else:
return None
fieldCount = len(cursor.description)
retDict = {}
for i in range(0, fieldCount):
# Note that the 0'th element of each tuple in
# cursor.description is the col name.
retDict[cursor.description[i][0]] = row[i]
return retDict
def fetchalldicts(self, cursor):
"""Fetch all rows and return a list of dicts.
This function will return ``[]`` if there are no more rows
available. Note, database modules don't *have* to use
this function, I'm just providing it for convenience.
"""
return list(self.fetchalldictsgenerator(cursor))
def fetchalldictsgenerator(self, cursor):
"""This is a generator for fetching all rows as dicts.
Note, database modules don't *have* to use this function, I'm just
providing it for convenience.
"""
try:
while True:
yield self.fetchonedict(cursor)
except MissingRecordError:
pass
def close(self):
"""Close the connection do the database.
This function is safe to call twice.
"""
if hasattr(self, "_connection"):
self._connection.close()
del(self._connection)
def commit(self):
"""Commit the current transaction.
If the database doesn't support transactions, I'll do nothing.
"""
if hasattr(self._connection, "commit"):
self._connection.commit()
def rollback(self):
"""Rollback the current transaction.
If the database doesn't support transactions, I'll do nothing.
"""
if hasattr(self._connection, "rollback"):
self._connection.rollback()
def __del__(self):
"""Rollback anything uncommitted. Close the connection."""
if not hasattr(self, "_connection"):
return
self.rollback()
self.close()
class MissingRecordError(LookupError):
"""A database record that was expected to exist doesn't actually exist."""
def __init__(self, *args, **kargs):
"""Provide a default error message."""
if not args:
args = (self.__init__.__doc__,)
LookupError.__init__(self, *args, **kargs)
|