See
My Coding Style for explanation of, well, my coding style...
BaseDatabaseConnection is a nominal abstract class that defines standard functionality for objects that can connect to and execute queries against a back-end database system (MySQL, PostgreSQL, etc.). As such, it has to encapsulate or provide the following items:
Connection
(Property):
- The current active connection to the back-end database;
Server
(Property):
- The location of the database server (machine-name or -address);
Database
(Property):
- The name of the database that the connection will be made to on the specified server;
User
(Property):
- The user-name that will be used to connect to the specified database;
Password
(Property):
- The password that will be used to connect to the specified database;
Execute
(Method):
- A mechanism that allows the execution of queries against the database-connection; and
Call
(Method):
- A mechanism that allows the execution of stored procedures against the database connection.
The separation of general query and stored-procedure call may not be needed in many cases, though the apparent leading Python/MySQL library (
MySQLdb) provides separate mechanisms for each, presumably for a good reason. I'm going to assume that there's a good reason, at any rate, and maintain that separation.
The
Execute
and
Call
methods, assuming that they return anything from the database, will return a list of result-sets, each of which is in turn a list of row-dictionaries. Each of
those will have a key/value pair where the key is the field-name, and the value is the value from the row for that field. Though creating another class to handle the result-sets, rows and fields/values is certainly possible, it feels a bit like sandblasting a soup-cracker for the purposes of the current project.
Perhaps doubly so, in that I'm intending to cache results in memory anyway (
BaseFilesystemItemCache-derived objects will handle that).
class BaseDatabaseConnection( object ):
"""Nominal abstract class, provides baseline functionality and interface requirements for objects that can connect to and execute queries, procedures, and/or functions against a back-end data-source."""
###########################
# Class Attributes #
###########################
_databaseNameCheck = re.compile( '[- \\.A-Za-z0-9]+' )
_passwordCheck = re.compile( '[- \\.A-Za-z0-9]+' )
_serverNameCheck = re.compile( '[- \\.A-Za-z0-9]+' )
_userNameCheck = re.compile( '[- \\.A-Za-z0-9]+' )
###########################
# Class Property Getters #
###########################
def _GetConnection( self ):
"""Abstract property - Gets a usable connection to the database using the object's connection properties.
Raises RuntimeError if the connection fails."""
raise NotImplementedError( '%s.Connection error: Connection has not been implemented as defined by BaseDatabaseConnection.' % ( self.__class__.__name__ ) )
def _GetDatabase( self ):
"""Gets or sets the name of the database that the connection will be made to.
Raises ConnectionChangeError if the property is changed after it has been set.
Raises TypeError if the property value being set is not a string.
Raises ValueError if the property value being set contains invalid characters (\\n, \\t, \\r).
"""
return self._database
def _GetPassword( self ):
"""Gets or sets the password for the user under whose authentication/authorization credentials the connection will run.
Raises TypeError if the value being set is not a string.
"""
return self._server
def _GetServer( self ):
"""Gets or sets the server that the connection will be made to.
Raises ConnectionChangeError if the property is changed after it has been set.
Raises TypeError if the property value being set is not a string.
"""
return self._server
def _GetUser( self ):
"""Gets or sets the user under whose authentication/authorization credentials the connection will run.
Raises TypeError if the value being set is not a string.
Raises ValueError if the property value being set contains invalid characters (line-breaking characters, \\t, anything outside the [ \\.a-zA-Z0-9] range).
"""
return self._user
###########################
# Class Property Setters #
###########################
def _SetDatabase( self, value ):
if self._database != None:
raise ConnectionChangeError( '%s.Database error: Changes to the Database property after it has been set are not allowed.' % ( ) )
if type( value ) != types.StringType:
raise TypeError( '%s.Database error: %s is not a string.' % ( self.__class__.__name__, value ) )
checkedValue = self._databaseNameCheck.sub( '', value )
if checkedValue != '':
raise ValueError( '%s.Database error: "%s" contains invalid/illegal characters (%s).' % ( self.__class__.__name__, value, checkedValue ) )
self._database = value
def _SetPassword( self, value ):
if type( value ) != types.StringType:
raise TypeError( '%s.Password error: %s is not a string.' % ( self.__class__.__name__, value ) )
checkedValue = self._passwordCheck.sub( '', value )
if checkedValue != '':
raise ValueError( '%s.Password error: "%s" contains invalid/illegal characters (%s).' % ( self.__class__.__name__, value, checkedValue ) )
self._password = value
def _SetServer( self, value ):
if self._server != None:
raise ConnectionChangeError( '%s.Server error: Changes to the Database property after it has been set are not allowed.' % ( self.__class__.__name)) ) )
if type( value ) != types.StringType:
raise TypeError( '%s.Server error: %s is not a string.' % ( self.__class__.__name__, value ) )
checkedValue = self._serverNameCheck.sub( '', value )
if checkedValue != '':
raise ValueError( '%s.Server error: "%s" contains invalid/illegal characters (%s).' % ( self.__class__.__name__, value, checkedValue ) )
self._server = value
def _SetUser( self, value ):
if type( value ) != types.StringType:
raise TypeError( '%s.User error: %s is not a string.' % ( self.__class__.__name__, value ) )
checkedValue = self._userNameCheck.sub( '', value )
if checkedValue != '':
raise ValueError( '%s.User error: "%s" contains invalid/illegal characters (%s).' % ( self.__class__.__name__, value, checkedValue ) )
self._server = value
###########################
# Class Property Deleters #
###########################
def _DelDatabase( self ):
raise NotImplementedError( '%s.Database error: the Database property cannot be deleted.' % ( self.__class__.__name__ ) )
def _DelPassword( self ):
raise NotImplementedError( '%s.Password error: the Password property cannot be deleted.' % ( self.__class__.__name__ ) )
def _DelServer( self ):
raise NotImplementedError( '%s.Server error: the Server property cannot be deleted.' % ( self.__class__.__name__ ) )
def _DelUser( self ):
raise NotImplementedError( '%s.User error: the User property cannot be deleted.' % ( self.__class__.__name__ ) )
###########################
# Class Properties #
###########################
Database = property( _GetDatabase, _SetDatabase, _DelDatabase, _GetDatabase.__doc__ )
Password = property( _GetPassword, _SetPassword, _DelPassword, _GetPassword.__doc__ )
Server = property( _GetServer, _SetServer, _DelServer, _GetServer.__doc__ )
User = property( _GetUser, _SetUser, _DelUser, _GetUser.__doc__ )
###########################
# Object Constructor #
###########################
def __init__( self, **kwargs ):
"""Object constructor.
Recognized keywords are:
database ... The name of the database the connection will be made to.
password ... The password to use for the database connection.
server ..... The server (name or IP-address) that the connection will be made to.
user ....... The user to use for the database connection.
Raises NotImplementedError if an attempt is made to instantiate the class.
Raises TypeError if any keyword-value is of an invalid type.
Raises ValueError if any keyword-value is invalid.
Raises RuntimeError if any other exception is raised by the creation process.
"""
if self.__class__ == BaseDatabaseConnection:
raise NotImplementedError( 'BaseDatabaseConnection is nominally an abstract class, and cannot be instantiated' )
# Set default values for attributes:
self._connection = None
self._database = None
self._password = None
self._server = None
self._user = None
kwargKeys = kwargs.keys()
# Set database value, if one was supplied
if 'database' in kwargKeys:
try:
self._SetDatabase( kwargs[ 'database' ] )
except TypeError, error:
raise TypeError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
except ValueError, error:
raise ValueError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
except Exception, error:
raise RuntimeError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
# Set password value, if one was supplied
if 'password' in kwargKeys:
try:
self._SetPassword( kwargs[ 'password' ] )
except TypeError, error:
raise TypeError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
except ValueError, error:
raise ValueError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
except Exception, error:
raise RuntimeError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
# Set server value, if one was supplied
if 'server' in kwargKeys:
try:
self._SetServer( kwargs[ 'server' ] )
except TypeError, error:
raise TypeError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
except ValueError, error:
raise ValueError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
except Exception, error:
raise RuntimeError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
# Set user value, if one was supplied
if 'user' in kwargKeys:
try:
self._SetUser( kwargs[ 'user' ] )
except TypeError, error:
raise TypeError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
except ValueError, error:
raise ValueError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
except Exception, error:
raise RuntimeError( '%s Error: Could not create an instance of %s: %s', % ( self.__class__.__name__, self.__class__.__name__, error ) )
###########################
# Object Destructor #
###########################
###########################
# Class Methods #
###########################
def Call( self, procedureName, *args ):
"""Abstract method - Calls a stored procedure on the connected database and returns the results, if any.
Returns a list of lists of dictionary objects, where each dictionary represents a single row in the results, and each list of dictionaries represents a single result-set from the results."""
raise NotImplementedError( '%s.Call Error: Call has not been overridden in from it\'s definition in BaseDatabaseConnection' % ( self.__class__.__name__ ) )
def Execute( self, sqlString ):
"""Abstract method - Executes a query against the connected database and returns the results, if any.
Returns a list of lists of dictionary objects, where each dictionary represents a single row in the results, and each list of dictionaries represents a single result-set from the results."""
raise NotImplementedError( '%s.Execute Error: Execute has not been overridden in from it\'s definition in BaseDatabaseConnection' % ( self.__class__.__name__ ) )
###########################
# Static Class Methods #
###########################
pass
__all__ += [ 'BaseDatabaseConnection' ]
Commentary
Most of this, I would hope, is fairly straightforward - the interface provided is fairly simple, with only five public property and two public method members that need to be dealt with. Of those, four of the property-members are concrete, and are simple string-value storage items, used to generate a connection to the back-end database. It is worth noting, though, that the
Server
and
Database
properties can only be set once. This restriction is in place to prevent accidental runtime changes for a database connection that would change which database the connection is pointed at - the risk, if those could be changed arbitrarily at runtime, is that the connection might read from one database and write to another, making the virtual filesystem's data invalid.
The
Connection
property is abstract, and so needs to be implemented on each derived class. Unlike the concrete properties, there is no associated internal storage attribute defined. The design/intention behind the structure of
Connection
is that derived classes would lazily instantiate a connection when the property is requested, using the other (concrete) properties to create the applicable connection object. I also expect that once a connection is instantiated behind the
Connection
property, the logic of the property-getter will be written in such a way as to check for an existing and usable connection before creating a new one. On the surface, that probably sounds like just another way of saying it's lazily instantiated, but there is a potential significant (and subtle) difference. My expectation is that a connection object can be created and used, but may be left in a state where it cannot be re-used. Assuming that there is a way to detect the "usability" of the underlying connection object, if it's still usable, it can just be returned and reused. If it's
not reusable for whatever reason, a new one would be created and returned instead.
I also toyed with the idea of adding a
Queue
property, that would allow queries that don't need to be executed until object-destruction occurs to be batched together for one run against the database. For this project, that feels like it might be overkill, so I'm leaving it out of the mix, at least for the time being.
One of the reasons that I'm setting the
Queue
aside for now is that the
Call
and
Execute
methods are intended to actually
run the back-end database-calls/queries when they are called. If the
Queue
property were implemented, those methods would potentially have to return something - the SQL to be executed, or some sort of function/argument reference-set - in order to
be queue-able. More complexity than feels necessary at this time.