795 lines
28 KiB
Python
795 lines
28 KiB
Python
|
"""adodbapi.apibase - A python DB API 2.0 (PEP 249) interface to Microsoft ADO
|
||
|
|
||
|
Copyright (C) 2002 Henrik Ekelund, version 2.1 by Vernon Cole
|
||
|
* http://sourceforge.net/projects/pywin32
|
||
|
* http://sourceforge.net/projects/adodbapi
|
||
|
"""
|
||
|
|
||
|
import datetime
|
||
|
import decimal
|
||
|
import numbers
|
||
|
import sys
|
||
|
import time
|
||
|
|
||
|
# noinspection PyUnresolvedReferences
|
||
|
from . import ado_consts as adc
|
||
|
|
||
|
verbose = False # debugging flag
|
||
|
|
||
|
onIronPython = sys.platform == "cli"
|
||
|
if onIronPython: # we need type definitions for odd data we may need to convert
|
||
|
# noinspection PyUnresolvedReferences
|
||
|
from System import DateTime, DBNull
|
||
|
|
||
|
NullTypes = (type(None), DBNull)
|
||
|
else:
|
||
|
DateTime = type(NotImplemented) # should never be seen on win32
|
||
|
NullTypes = type(None)
|
||
|
|
||
|
# --- define objects to smooth out Python3 <-> Python 2.x differences
|
||
|
unicodeType = str
|
||
|
longType = int
|
||
|
StringTypes = str
|
||
|
makeByteBuffer = bytes
|
||
|
memoryViewType = memoryview
|
||
|
_BaseException = Exception
|
||
|
|
||
|
try: # jdhardy -- handle bytes under IronPython & Py3
|
||
|
bytes
|
||
|
except NameError:
|
||
|
bytes = str # define it for old Pythons
|
||
|
|
||
|
|
||
|
# ------- Error handlers ------
|
||
|
def standardErrorHandler(connection, cursor, errorclass, errorvalue):
|
||
|
err = (errorclass, errorvalue)
|
||
|
try:
|
||
|
connection.messages.append(err)
|
||
|
except:
|
||
|
pass
|
||
|
if cursor is not None:
|
||
|
try:
|
||
|
cursor.messages.append(err)
|
||
|
except:
|
||
|
pass
|
||
|
raise errorclass(errorvalue)
|
||
|
|
||
|
|
||
|
# Note: _BaseException is defined differently between Python 2.x and 3.x
|
||
|
class Error(_BaseException):
|
||
|
pass # Exception that is the base class of all other error
|
||
|
# exceptions. You can use this to catch all errors with one
|
||
|
# single 'except' statement. Warnings are not considered
|
||
|
# errors and thus should not use this class as base. It must
|
||
|
# be a subclass of the Python StandardError (defined in the
|
||
|
# module exceptions).
|
||
|
|
||
|
|
||
|
class Warning(_BaseException):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class InterfaceError(Error):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class DatabaseError(Error):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class InternalError(DatabaseError):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class OperationalError(DatabaseError):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class ProgrammingError(DatabaseError):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class IntegrityError(DatabaseError):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class DataError(DatabaseError):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class NotSupportedError(DatabaseError):
|
||
|
pass
|
||
|
|
||
|
|
||
|
class FetchFailedError(OperationalError):
|
||
|
"""
|
||
|
Error is used by RawStoredProcedureQuerySet to determine when a fetch
|
||
|
failed due to a connection being closed or there is no record set
|
||
|
returned. (Non-standard, added especially for django)
|
||
|
"""
|
||
|
|
||
|
pass
|
||
|
|
||
|
|
||
|
# # # # # ----- Type Objects and Constructors ----- # # # # #
|
||
|
# Many databases need to have the input in a particular format for binding to an operation's input parameters.
|
||
|
# For example, if an input is destined for a DATE column, then it must be bound to the database in a particular
|
||
|
# string format. Similar problems exist for "Row ID" columns or large binary items (e.g. blobs or RAW columns).
|
||
|
# This presents problems for Python since the parameters to the executeXXX() method are untyped.
|
||
|
# When the database module sees a Python string object, it doesn't know if it should be bound as a simple CHAR
|
||
|
# column, as a raw BINARY item, or as a DATE.
|
||
|
#
|
||
|
# To overcome this problem, a module must provide the constructors defined below to create objects that can
|
||
|
# hold special values. When passed to the cursor methods, the module can then detect the proper type of
|
||
|
# the input parameter and bind it accordingly.
|
||
|
|
||
|
# A Cursor Object's description attribute returns information about each of the result columns of a query.
|
||
|
# The type_code must compare equal to one of Type Objects defined below. Type Objects may be equal to more than
|
||
|
# one type code (e.g. DATETIME could be equal to the type codes for date, time and timestamp columns;
|
||
|
# see the Implementation Hints below for details).
|
||
|
|
||
|
# SQL NULL values are represented by the Python None singleton on input and output.
|
||
|
|
||
|
# Note: Usage of Unix ticks for database interfacing can cause troubles because of the limited date range they cover.
|
||
|
|
||
|
|
||
|
# def Date(year,month,day):
|
||
|
# "This function constructs an object holding a date value. "
|
||
|
# return dateconverter.date(year,month,day) #dateconverter.Date(year,month,day)
|
||
|
#
|
||
|
# def Time(hour,minute,second):
|
||
|
# "This function constructs an object holding a time value. "
|
||
|
# return dateconverter.time(hour, minute, second) # dateconverter.Time(hour,minute,second)
|
||
|
#
|
||
|
# def Timestamp(year,month,day,hour,minute,second):
|
||
|
# "This function constructs an object holding a time stamp value. "
|
||
|
# return dateconverter.datetime(year,month,day,hour,minute,second)
|
||
|
#
|
||
|
# def DateFromTicks(ticks):
|
||
|
# """This function constructs an object holding a date value from the given ticks value
|
||
|
# (number of seconds since the epoch; see the documentation of the standard Python time module for details). """
|
||
|
# return Date(*time.gmtime(ticks)[:3])
|
||
|
#
|
||
|
# def TimeFromTicks(ticks):
|
||
|
# """This function constructs an object holding a time value from the given ticks value
|
||
|
# (number of seconds since the epoch; see the documentation of the standard Python time module for details). """
|
||
|
# return Time(*time.gmtime(ticks)[3:6])
|
||
|
#
|
||
|
# def TimestampFromTicks(ticks):
|
||
|
# """This function constructs an object holding a time stamp value from the given
|
||
|
# ticks value (number of seconds since the epoch;
|
||
|
# see the documentation of the standard Python time module for details). """
|
||
|
# return Timestamp(*time.gmtime(ticks)[:6])
|
||
|
#
|
||
|
# def Binary(aString):
|
||
|
# """This function constructs an object capable of holding a binary (long) string value. """
|
||
|
# b = makeByteBuffer(aString)
|
||
|
# return b
|
||
|
# ----- Time converters ----------------------------------------------
|
||
|
class TimeConverter(object): # this is a generic time converter skeleton
|
||
|
def __init__(self): # the details will be filled in by instances
|
||
|
self._ordinal_1899_12_31 = datetime.date(1899, 12, 31).toordinal() - 1
|
||
|
# Use cls.types to compare if an input parameter is a datetime
|
||
|
self.types = {
|
||
|
type(self.Date(2000, 1, 1)),
|
||
|
type(self.Time(12, 1, 1)),
|
||
|
type(self.Timestamp(2000, 1, 1, 12, 1, 1)),
|
||
|
datetime.datetime,
|
||
|
datetime.time,
|
||
|
datetime.date,
|
||
|
}
|
||
|
|
||
|
def COMDate(self, obj):
|
||
|
"""Returns a ComDate from a date-time"""
|
||
|
try: # most likely a datetime
|
||
|
tt = obj.timetuple()
|
||
|
|
||
|
try:
|
||
|
ms = obj.microsecond
|
||
|
except:
|
||
|
ms = 0
|
||
|
return self.ComDateFromTuple(tt, ms)
|
||
|
except: # might be a tuple
|
||
|
try:
|
||
|
return self.ComDateFromTuple(obj)
|
||
|
except: # try an mxdate
|
||
|
try:
|
||
|
return obj.COMDate()
|
||
|
except:
|
||
|
raise ValueError('Cannot convert "%s" to COMdate.' % repr(obj))
|
||
|
|
||
|
def ComDateFromTuple(self, t, microseconds=0):
|
||
|
d = datetime.date(t[0], t[1], t[2])
|
||
|
integerPart = d.toordinal() - self._ordinal_1899_12_31
|
||
|
ms = (t[3] * 3600 + t[4] * 60 + t[5]) * 1000000 + microseconds
|
||
|
fractPart = float(ms) / 86400000000.0
|
||
|
return integerPart + fractPart
|
||
|
|
||
|
def DateObjectFromCOMDate(self, comDate):
|
||
|
"Returns an object of the wanted type from a ComDate"
|
||
|
raise NotImplementedError # "Abstract class"
|
||
|
|
||
|
def Date(self, year, month, day):
|
||
|
"This function constructs an object holding a date value."
|
||
|
raise NotImplementedError # "Abstract class"
|
||
|
|
||
|
def Time(self, hour, minute, second):
|
||
|
"This function constructs an object holding a time value."
|
||
|
raise NotImplementedError # "Abstract class"
|
||
|
|
||
|
def Timestamp(self, year, month, day, hour, minute, second):
|
||
|
"This function constructs an object holding a time stamp value."
|
||
|
raise NotImplementedError # "Abstract class"
|
||
|
# all purpose date to ISO format converter
|
||
|
|
||
|
def DateObjectToIsoFormatString(self, obj):
|
||
|
"This function should return a string in the format 'YYYY-MM-dd HH:MM:SS:ms' (ms optional)"
|
||
|
try: # most likely, a datetime.datetime
|
||
|
s = obj.isoformat(" ")
|
||
|
except (TypeError, AttributeError):
|
||
|
if isinstance(obj, datetime.date):
|
||
|
s = obj.isoformat() + " 00:00:00" # return exact midnight
|
||
|
else:
|
||
|
try: # maybe it has a strftime method, like mx
|
||
|
s = obj.strftime("%Y-%m-%d %H:%M:%S")
|
||
|
except AttributeError:
|
||
|
try: # but may be time.struct_time
|
||
|
s = time.strftime("%Y-%m-%d %H:%M:%S", obj)
|
||
|
except:
|
||
|
raise ValueError('Cannot convert "%s" to isoformat' % repr(obj))
|
||
|
return s
|
||
|
|
||
|
|
||
|
# -- Optional: if mx extensions are installed you may use mxDateTime ----
|
||
|
try:
|
||
|
import mx.DateTime
|
||
|
|
||
|
mxDateTime = True
|
||
|
except:
|
||
|
mxDateTime = False
|
||
|
if mxDateTime:
|
||
|
|
||
|
class mxDateTimeConverter(TimeConverter): # used optionally if installed
|
||
|
def __init__(self):
|
||
|
TimeConverter.__init__(self)
|
||
|
self.types.add(type(mx.DateTime))
|
||
|
|
||
|
def DateObjectFromCOMDate(self, comDate):
|
||
|
return mx.DateTime.DateTimeFromCOMDate(comDate)
|
||
|
|
||
|
def Date(self, year, month, day):
|
||
|
return mx.DateTime.Date(year, month, day)
|
||
|
|
||
|
def Time(self, hour, minute, second):
|
||
|
return mx.DateTime.Time(hour, minute, second)
|
||
|
|
||
|
def Timestamp(self, year, month, day, hour, minute, second):
|
||
|
return mx.DateTime.Timestamp(year, month, day, hour, minute, second)
|
||
|
|
||
|
else:
|
||
|
|
||
|
class mxDateTimeConverter(TimeConverter):
|
||
|
pass # if no mx is installed
|
||
|
|
||
|
|
||
|
class pythonDateTimeConverter(TimeConverter): # standard since Python 2.3
|
||
|
def __init__(self):
|
||
|
TimeConverter.__init__(self)
|
||
|
|
||
|
def DateObjectFromCOMDate(self, comDate):
|
||
|
if isinstance(comDate, datetime.datetime):
|
||
|
odn = comDate.toordinal()
|
||
|
tim = comDate.time()
|
||
|
new = datetime.datetime.combine(datetime.datetime.fromordinal(odn), tim)
|
||
|
return new
|
||
|
# return comDate.replace(tzinfo=None) # make non aware
|
||
|
elif isinstance(comDate, DateTime):
|
||
|
fComDate = comDate.ToOADate() # ironPython clr Date/Time
|
||
|
else:
|
||
|
fComDate = float(comDate) # ComDate is number of days since 1899-12-31
|
||
|
integerPart = int(fComDate)
|
||
|
floatpart = fComDate - integerPart
|
||
|
##if floatpart == 0.0:
|
||
|
## return datetime.date.fromordinal(integerPart + self._ordinal_1899_12_31)
|
||
|
dte = datetime.datetime.fromordinal(
|
||
|
integerPart + self._ordinal_1899_12_31
|
||
|
) + datetime.timedelta(milliseconds=floatpart * 86400000)
|
||
|
# millisecondsperday=86400000 # 24*60*60*1000
|
||
|
return dte
|
||
|
|
||
|
def Date(self, year, month, day):
|
||
|
return datetime.date(year, month, day)
|
||
|
|
||
|
def Time(self, hour, minute, second):
|
||
|
return datetime.time(hour, minute, second)
|
||
|
|
||
|
def Timestamp(self, year, month, day, hour, minute, second):
|
||
|
return datetime.datetime(year, month, day, hour, minute, second)
|
||
|
|
||
|
|
||
|
class pythonTimeConverter(TimeConverter): # the old, ?nix type date and time
|
||
|
def __init__(self): # caution: this Class gets confised by timezones and DST
|
||
|
TimeConverter.__init__(self)
|
||
|
self.types.add(time.struct_time)
|
||
|
|
||
|
def DateObjectFromCOMDate(self, comDate):
|
||
|
"Returns ticks since 1970"
|
||
|
if isinstance(comDate, datetime.datetime):
|
||
|
return comDate.timetuple()
|
||
|
elif isinstance(comDate, DateTime): # ironPython clr date/time
|
||
|
fcomDate = comDate.ToOADate()
|
||
|
else:
|
||
|
fcomDate = float(comDate)
|
||
|
secondsperday = 86400 # 24*60*60
|
||
|
# ComDate is number of days since 1899-12-31, gmtime epoch is 1970-1-1 = 25569 days
|
||
|
t = time.gmtime(secondsperday * (fcomDate - 25569.0))
|
||
|
return t # year,month,day,hour,minute,second,weekday,julianday,daylightsaving=t
|
||
|
|
||
|
def Date(self, year, month, day):
|
||
|
return self.Timestamp(year, month, day, 0, 0, 0)
|
||
|
|
||
|
def Time(self, hour, minute, second):
|
||
|
return time.gmtime((hour * 60 + minute) * 60 + second)
|
||
|
|
||
|
def Timestamp(self, year, month, day, hour, minute, second):
|
||
|
return time.localtime(
|
||
|
time.mktime((year, month, day, hour, minute, second, 0, 0, -1))
|
||
|
)
|
||
|
|
||
|
|
||
|
base_dateconverter = pythonDateTimeConverter()
|
||
|
|
||
|
# ------ DB API required module attributes ---------------------
|
||
|
threadsafety = 1 # TODO -- find out whether this module is actually BETTER than 1.
|
||
|
|
||
|
apilevel = "2.0" # String constant stating the supported DB API level.
|
||
|
|
||
|
paramstyle = "qmark" # the default parameter style
|
||
|
|
||
|
# ------ control for an extension which may become part of DB API 3.0 ---
|
||
|
accepted_paramstyles = ("qmark", "named", "format", "pyformat", "dynamic")
|
||
|
|
||
|
# ------------------------------------------------------------------------------------------
|
||
|
# define similar types for generic conversion routines
|
||
|
adoIntegerTypes = (
|
||
|
adc.adInteger,
|
||
|
adc.adSmallInt,
|
||
|
adc.adTinyInt,
|
||
|
adc.adUnsignedInt,
|
||
|
adc.adUnsignedSmallInt,
|
||
|
adc.adUnsignedTinyInt,
|
||
|
adc.adBoolean,
|
||
|
adc.adError,
|
||
|
) # max 32 bits
|
||
|
adoRowIdTypes = (adc.adChapter,) # v2.1 Rose
|
||
|
adoLongTypes = (adc.adBigInt, adc.adFileTime, adc.adUnsignedBigInt)
|
||
|
adoExactNumericTypes = (
|
||
|
adc.adDecimal,
|
||
|
adc.adNumeric,
|
||
|
adc.adVarNumeric,
|
||
|
adc.adCurrency,
|
||
|
) # v2.3 Cole
|
||
|
adoApproximateNumericTypes = (adc.adDouble, adc.adSingle) # v2.1 Cole
|
||
|
adoStringTypes = (
|
||
|
adc.adBSTR,
|
||
|
adc.adChar,
|
||
|
adc.adLongVarChar,
|
||
|
adc.adLongVarWChar,
|
||
|
adc.adVarChar,
|
||
|
adc.adVarWChar,
|
||
|
adc.adWChar,
|
||
|
)
|
||
|
adoBinaryTypes = (adc.adBinary, adc.adLongVarBinary, adc.adVarBinary)
|
||
|
adoDateTimeTypes = (adc.adDBTime, adc.adDBTimeStamp, adc.adDate, adc.adDBDate)
|
||
|
adoRemainingTypes = (
|
||
|
adc.adEmpty,
|
||
|
adc.adIDispatch,
|
||
|
adc.adIUnknown,
|
||
|
adc.adPropVariant,
|
||
|
adc.adArray,
|
||
|
adc.adUserDefined,
|
||
|
adc.adVariant,
|
||
|
adc.adGUID,
|
||
|
)
|
||
|
|
||
|
|
||
|
# this class is a trick to determine whether a type is a member of a related group of types. see PEP notes
|
||
|
class DBAPITypeObject(object):
|
||
|
def __init__(self, valuesTuple):
|
||
|
self.values = frozenset(valuesTuple)
|
||
|
|
||
|
def __eq__(self, other):
|
||
|
return other in self.values
|
||
|
|
||
|
def __ne__(self, other):
|
||
|
return other not in self.values
|
||
|
|
||
|
|
||
|
"""This type object is used to describe columns in a database that are string-based (e.g. CHAR). """
|
||
|
STRING = DBAPITypeObject(adoStringTypes)
|
||
|
|
||
|
"""This type object is used to describe (long) binary columns in a database (e.g. LONG, RAW, BLOBs). """
|
||
|
BINARY = DBAPITypeObject(adoBinaryTypes)
|
||
|
|
||
|
"""This type object is used to describe numeric columns in a database. """
|
||
|
NUMBER = DBAPITypeObject(
|
||
|
adoIntegerTypes + adoLongTypes + adoExactNumericTypes + adoApproximateNumericTypes
|
||
|
)
|
||
|
|
||
|
"""This type object is used to describe date/time columns in a database. """
|
||
|
|
||
|
DATETIME = DBAPITypeObject(adoDateTimeTypes)
|
||
|
"""This type object is used to describe the "Row ID" column in a database. """
|
||
|
ROWID = DBAPITypeObject(adoRowIdTypes)
|
||
|
|
||
|
OTHER = DBAPITypeObject(adoRemainingTypes)
|
||
|
|
||
|
# ------- utilities for translating python data types to ADO data types ---------------------------------
|
||
|
typeMap = {
|
||
|
memoryViewType: adc.adVarBinary,
|
||
|
float: adc.adDouble,
|
||
|
type(None): adc.adEmpty,
|
||
|
str: adc.adBSTR,
|
||
|
bool: adc.adBoolean, # v2.1 Cole
|
||
|
decimal.Decimal: adc.adDecimal,
|
||
|
int: adc.adBigInt,
|
||
|
bytes: adc.adVarBinary,
|
||
|
}
|
||
|
|
||
|
|
||
|
def pyTypeToADOType(d):
|
||
|
tp = type(d)
|
||
|
try:
|
||
|
return typeMap[tp]
|
||
|
except KeyError: # The type was not defined in the pre-computed Type table
|
||
|
from . import dateconverter
|
||
|
|
||
|
if (
|
||
|
tp in dateconverter.types
|
||
|
): # maybe it is one of our supported Date/Time types
|
||
|
return adc.adDate
|
||
|
# otherwise, attempt to discern the type by probing the data object itself -- to handle duck typing
|
||
|
if isinstance(d, StringTypes):
|
||
|
return adc.adBSTR
|
||
|
if isinstance(d, numbers.Integral):
|
||
|
return adc.adBigInt
|
||
|
if isinstance(d, numbers.Real):
|
||
|
return adc.adDouble
|
||
|
raise DataError('cannot convert "%s" (type=%s) to ADO' % (repr(d), tp))
|
||
|
|
||
|
|
||
|
# # # # # # # # # # # # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
|
# functions to convert database values to Python objects
|
||
|
# ------------------------------------------------------------------------
|
||
|
# variant type : function converting variant to Python value
|
||
|
def variantConvertDate(v):
|
||
|
from . import dateconverter # this function only called when adodbapi is running
|
||
|
|
||
|
return dateconverter.DateObjectFromCOMDate(v)
|
||
|
|
||
|
|
||
|
def cvtString(variant): # use to get old action of adodbapi v1 if desired
|
||
|
if onIronPython:
|
||
|
try:
|
||
|
return variant.ToString()
|
||
|
except:
|
||
|
pass
|
||
|
return str(variant)
|
||
|
|
||
|
|
||
|
def cvtDecimal(variant): # better name
|
||
|
return _convertNumberWithCulture(variant, decimal.Decimal)
|
||
|
|
||
|
|
||
|
def cvtNumeric(variant): # older name - don't break old code
|
||
|
return cvtDecimal(variant)
|
||
|
|
||
|
|
||
|
def cvtFloat(variant):
|
||
|
return _convertNumberWithCulture(variant, float)
|
||
|
|
||
|
|
||
|
def _convertNumberWithCulture(variant, f):
|
||
|
try:
|
||
|
return f(variant)
|
||
|
except (ValueError, TypeError, decimal.InvalidOperation):
|
||
|
try:
|
||
|
europeVsUS = str(variant).replace(",", ".")
|
||
|
return f(europeVsUS)
|
||
|
except (ValueError, TypeError, decimal.InvalidOperation):
|
||
|
pass
|
||
|
|
||
|
|
||
|
def cvtInt(variant):
|
||
|
return int(variant)
|
||
|
|
||
|
|
||
|
def cvtLong(variant): # only important in old versions where long and int differ
|
||
|
return int(variant)
|
||
|
|
||
|
|
||
|
def cvtBuffer(variant):
|
||
|
return bytes(variant)
|
||
|
|
||
|
|
||
|
def cvtUnicode(variant):
|
||
|
return str(variant)
|
||
|
|
||
|
|
||
|
def identity(x):
|
||
|
return x
|
||
|
|
||
|
|
||
|
def cvtUnusual(variant):
|
||
|
if verbose > 1:
|
||
|
sys.stderr.write("Conversion called for Unusual data=%s\n" % repr(variant))
|
||
|
if isinstance(variant, DateTime): # COMdate or System.Date
|
||
|
from .adodbapi import ( # this will only be called when adodbapi is in use, and very rarely
|
||
|
dateconverter,
|
||
|
)
|
||
|
|
||
|
return dateconverter.DateObjectFromCOMDate(variant)
|
||
|
return variant # cannot find conversion function -- just give the data to the user
|
||
|
|
||
|
|
||
|
def convert_to_python(variant, func): # convert DB value into Python value
|
||
|
if isinstance(variant, NullTypes): # IronPython Null or None
|
||
|
return None
|
||
|
return func(variant) # call the appropriate conversion function
|
||
|
|
||
|
|
||
|
class MultiMap(dict): # builds a dictionary from {(sequence,of,keys) : function}
|
||
|
"""A dictionary of ado.type : function -- but you can set multiple items by passing a sequence of keys"""
|
||
|
|
||
|
# useful for defining conversion functions for groups of similar data types.
|
||
|
def __init__(self, aDict):
|
||
|
for k, v in list(aDict.items()):
|
||
|
self[k] = v # we must call __setitem__
|
||
|
|
||
|
def __setitem__(self, adoType, cvtFn):
|
||
|
"set a single item, or a whole sequence of items"
|
||
|
try: # user passed us a sequence, set them individually
|
||
|
for type in adoType:
|
||
|
dict.__setitem__(self, type, cvtFn)
|
||
|
except TypeError: # a single value fails attempt to iterate
|
||
|
dict.__setitem__(self, adoType, cvtFn)
|
||
|
|
||
|
|
||
|
# initialize variantConversions dictionary used to convert SQL to Python
|
||
|
# this is the dictionary of default conversion functions, built by the class above.
|
||
|
# this becomes a class attribute for the Connection, and that attribute is used
|
||
|
# to build the list of column conversion functions for the Cursor
|
||
|
variantConversions = MultiMap(
|
||
|
{
|
||
|
adoDateTimeTypes: variantConvertDate,
|
||
|
adoApproximateNumericTypes: cvtFloat,
|
||
|
adoExactNumericTypes: cvtDecimal, # use to force decimal rather than unicode
|
||
|
adoLongTypes: cvtLong,
|
||
|
adoIntegerTypes: cvtInt,
|
||
|
adoRowIdTypes: cvtInt,
|
||
|
adoStringTypes: identity,
|
||
|
adoBinaryTypes: cvtBuffer,
|
||
|
adoRemainingTypes: cvtUnusual,
|
||
|
}
|
||
|
)
|
||
|
|
||
|
# # # # # classes to emulate the result of cursor.fetchxxx() as a sequence of sequences # # # # #
|
||
|
# "an ENUM of how my low level records are laid out"
|
||
|
RS_WIN_32, RS_ARRAY, RS_REMOTE = list(range(1, 4))
|
||
|
|
||
|
|
||
|
class SQLrow(object): # a single database row
|
||
|
# class to emulate a sequence, so that a column may be retrieved by either number or name
|
||
|
def __init__(self, rows, index): # "rows" is an _SQLrows object, index is which row
|
||
|
self.rows = rows # parent 'fetch' container object
|
||
|
self.index = index # my row number within parent
|
||
|
|
||
|
def __getattr__(self, name): # used for row.columnName type of value access
|
||
|
try:
|
||
|
return self._getValue(self.rows.columnNames[name.lower()])
|
||
|
except KeyError:
|
||
|
raise AttributeError('Unknown column name "{}"'.format(name))
|
||
|
|
||
|
def _getValue(self, key): # key must be an integer
|
||
|
if (
|
||
|
self.rows.recordset_format == RS_ARRAY
|
||
|
): # retrieve from two-dimensional array
|
||
|
v = self.rows.ado_results[key, self.index]
|
||
|
elif self.rows.recordset_format == RS_REMOTE:
|
||
|
v = self.rows.ado_results[self.index][key]
|
||
|
else: # pywin32 - retrieve from tuple of tuples
|
||
|
v = self.rows.ado_results[key][self.index]
|
||
|
if self.rows.converters is NotImplemented:
|
||
|
return v
|
||
|
return convert_to_python(v, self.rows.converters[key])
|
||
|
|
||
|
def __len__(self):
|
||
|
return self.rows.numberOfColumns
|
||
|
|
||
|
def __getitem__(self, key): # used for row[key] type of value access
|
||
|
if isinstance(key, int): # normal row[1] designation
|
||
|
try:
|
||
|
return self._getValue(key)
|
||
|
except IndexError:
|
||
|
raise
|
||
|
if isinstance(key, slice):
|
||
|
indices = key.indices(self.rows.numberOfColumns)
|
||
|
vl = [self._getValue(i) for i in range(*indices)]
|
||
|
return tuple(vl)
|
||
|
try:
|
||
|
return self._getValue(
|
||
|
self.rows.columnNames[key.lower()]
|
||
|
) # extension row[columnName] designation
|
||
|
except (KeyError, TypeError):
|
||
|
er, st, tr = sys.exc_info()
|
||
|
raise er(
|
||
|
'No such key as "%s" in %s' % (repr(key), self.__repr__())
|
||
|
).with_traceback(tr)
|
||
|
|
||
|
def __iter__(self):
|
||
|
return iter(self.__next__())
|
||
|
|
||
|
def __next__(self):
|
||
|
for n in range(self.rows.numberOfColumns):
|
||
|
yield self._getValue(n)
|
||
|
|
||
|
def __repr__(self): # create a human readable representation
|
||
|
taglist = sorted(list(self.rows.columnNames.items()), key=lambda x: x[1])
|
||
|
s = "<SQLrow={"
|
||
|
for name, i in taglist:
|
||
|
s += name + ":" + repr(self._getValue(i)) + ", "
|
||
|
return s[:-2] + "}>"
|
||
|
|
||
|
def __str__(self): # create a pretty human readable representation
|
||
|
return str(
|
||
|
tuple(str(self._getValue(i)) for i in range(self.rows.numberOfColumns))
|
||
|
)
|
||
|
|
||
|
# TO-DO implement pickling an SQLrow directly
|
||
|
# def __getstate__(self): return self.__dict__
|
||
|
# def __setstate__(self, d): self.__dict__.update(d)
|
||
|
# which basically tell pickle to treat your class just like a normal one,
|
||
|
# taking self.__dict__ as representing the whole of the instance state,
|
||
|
# despite the existence of the __getattr__.
|
||
|
# # # #
|
||
|
|
||
|
|
||
|
class SQLrows(object):
|
||
|
# class to emulate a sequence for multiple rows using a container object
|
||
|
def __init__(self, ado_results, numberOfRows, cursor):
|
||
|
self.ado_results = ado_results # raw result of SQL get
|
||
|
try:
|
||
|
self.recordset_format = cursor.recordset_format
|
||
|
self.numberOfColumns = cursor.numberOfColumns
|
||
|
self.converters = cursor.converters
|
||
|
self.columnNames = cursor.columnNames
|
||
|
except AttributeError:
|
||
|
self.recordset_format = RS_ARRAY
|
||
|
self.numberOfColumns = 0
|
||
|
self.converters = []
|
||
|
self.columnNames = {}
|
||
|
self.numberOfRows = numberOfRows
|
||
|
|
||
|
def __len__(self):
|
||
|
return self.numberOfRows
|
||
|
|
||
|
def __getitem__(self, item): # used for row or row,column access
|
||
|
if not self.ado_results:
|
||
|
return []
|
||
|
if isinstance(item, slice): # will return a list of row objects
|
||
|
indices = item.indices(self.numberOfRows)
|
||
|
return [SQLrow(self, k) for k in range(*indices)]
|
||
|
elif isinstance(item, tuple) and len(item) == 2:
|
||
|
# d = some_rowsObject[i,j] will return a datum from a two-dimension address
|
||
|
i, j = item
|
||
|
if not isinstance(j, int):
|
||
|
try:
|
||
|
j = self.columnNames[j.lower()] # convert named column to numeric
|
||
|
except KeyError:
|
||
|
raise KeyError('adodbapi: no such column name as "%s"' % repr(j))
|
||
|
if self.recordset_format == RS_ARRAY: # retrieve from two-dimensional array
|
||
|
v = self.ado_results[j, i]
|
||
|
elif self.recordset_format == RS_REMOTE:
|
||
|
v = self.ado_results[i][j]
|
||
|
else: # pywin32 - retrieve from tuple of tuples
|
||
|
v = self.ado_results[j][i]
|
||
|
if self.converters is NotImplemented:
|
||
|
return v
|
||
|
return convert_to_python(v, self.converters[j])
|
||
|
else:
|
||
|
row = SQLrow(self, item) # new row descriptor
|
||
|
return row
|
||
|
|
||
|
def __iter__(self):
|
||
|
return iter(self.__next__())
|
||
|
|
||
|
def __next__(self):
|
||
|
for n in range(self.numberOfRows):
|
||
|
row = SQLrow(self, n)
|
||
|
yield row
|
||
|
# # # # #
|
||
|
|
||
|
# # # # # functions to re-format SQL requests to other paramstyle requirements # # # # # # # # # #
|
||
|
|
||
|
|
||
|
def changeNamedToQmark(
|
||
|
op,
|
||
|
): # convert from 'named' paramstyle to ADO required '?'mark parameters
|
||
|
outOp = ""
|
||
|
outparms = []
|
||
|
chunks = op.split(
|
||
|
"'"
|
||
|
) # quote all literals -- odd numbered list results are literals.
|
||
|
inQuotes = False
|
||
|
for chunk in chunks:
|
||
|
if inQuotes: # this is inside a quote
|
||
|
if chunk == "": # double apostrophe to quote one apostrophe
|
||
|
outOp = outOp[:-1] # so take one away
|
||
|
else:
|
||
|
outOp += "'" + chunk + "'" # else pass the quoted string as is.
|
||
|
else: # is SQL code -- look for a :namedParameter
|
||
|
while chunk: # some SQL string remains
|
||
|
sp = chunk.split(":", 1)
|
||
|
outOp += sp[0] # concat the part up to the :
|
||
|
s = ""
|
||
|
try:
|
||
|
chunk = sp[1]
|
||
|
except IndexError:
|
||
|
chunk = None
|
||
|
if chunk: # there was a parameter - parse it out
|
||
|
i = 0
|
||
|
c = chunk[0]
|
||
|
while c.isalnum() or c == "_":
|
||
|
i += 1
|
||
|
try:
|
||
|
c = chunk[i]
|
||
|
except IndexError:
|
||
|
break
|
||
|
s = chunk[:i]
|
||
|
chunk = chunk[i:]
|
||
|
if s:
|
||
|
outparms.append(s) # list the parameters in order
|
||
|
outOp += "?" # put in the Qmark
|
||
|
inQuotes = not inQuotes
|
||
|
return outOp, outparms
|
||
|
|
||
|
|
||
|
def changeFormatToQmark(
|
||
|
op,
|
||
|
): # convert from 'format' paramstyle to ADO required '?'mark parameters
|
||
|
outOp = ""
|
||
|
outparams = []
|
||
|
chunks = op.split(
|
||
|
"'"
|
||
|
) # quote all literals -- odd numbered list results are literals.
|
||
|
inQuotes = False
|
||
|
for chunk in chunks:
|
||
|
if inQuotes:
|
||
|
if (
|
||
|
outOp != "" and chunk == ""
|
||
|
): # he used a double apostrophe to quote one apostrophe
|
||
|
outOp = outOp[:-1] # so take one away
|
||
|
else:
|
||
|
outOp += "'" + chunk + "'" # else pass the quoted string as is.
|
||
|
else: # is SQL code -- look for a %s parameter
|
||
|
if "%(" in chunk: # ugh! pyformat!
|
||
|
while chunk: # some SQL string remains
|
||
|
sp = chunk.split("%(", 1)
|
||
|
outOp += sp[0] # concat the part up to the %
|
||
|
if len(sp) > 1:
|
||
|
try:
|
||
|
s, chunk = sp[1].split(")s", 1) # find the ')s'
|
||
|
except ValueError:
|
||
|
raise ProgrammingError(
|
||
|
'Pyformat SQL has incorrect format near "%s"' % chunk
|
||
|
)
|
||
|
outparams.append(s)
|
||
|
outOp += "?" # put in the Qmark
|
||
|
else:
|
||
|
chunk = None
|
||
|
else: # proper '%s' format
|
||
|
sp = chunk.split("%s") # make each %s
|
||
|
outOp += "?".join(sp) # into ?
|
||
|
inQuotes = not inQuotes # every other chunk is a quoted string
|
||
|
return outOp, outparams
|