538 lines
20 KiB
Python
538 lines
20 KiB
Python
|
"""adodbapi.remote - 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
|
||
|
|
||
|
This library is free software; you can redistribute it and/or
|
||
|
modify it under the terms of the GNU Lesser General Public
|
||
|
License as published by the Free Software Foundation; either
|
||
|
version 2.1 of the License, or (at your option) any later version.
|
||
|
|
||
|
This library is distributed in the hope that it will be useful,
|
||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
|
Lesser General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU Lesser General Public
|
||
|
License along with this library; if not, write to the Free Software
|
||
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||
|
|
||
|
django adaptations and refactoring thanks to Adam Vandenberg
|
||
|
|
||
|
DB-API 2.0 specification: http://www.python.org/dev/peps/pep-0249/
|
||
|
|
||
|
This module source should run correctly in CPython versions 2.5 and later,
|
||
|
or IronPython version 2.7 and later,
|
||
|
or, after running through 2to3.py, CPython 3.0 or later.
|
||
|
"""
|
||
|
from __future__ import absolute_import
|
||
|
|
||
|
__version__ = '2.6.0.4'
|
||
|
version = 'adodbapi.remote v' + __version__
|
||
|
|
||
|
import os
|
||
|
import sys
|
||
|
import array
|
||
|
import time
|
||
|
import datetime
|
||
|
|
||
|
# Pyro4 is required for server and remote operation --> https://pypi.python.org/pypi/Pyro4/
|
||
|
try:
|
||
|
import Pyro4
|
||
|
except ImportError:
|
||
|
print('* * * Sorry, server operation requires Pyro4. Please "pip import" it.')
|
||
|
exit(11)
|
||
|
|
||
|
import adodbapi
|
||
|
import adodbapi.apibase as api
|
||
|
import adodbapi.process_connect_string
|
||
|
from adodbapi.apibase import ProgrammingError
|
||
|
|
||
|
_BaseException = api._BaseException
|
||
|
|
||
|
sys.excepthook = Pyro4.util.excepthook
|
||
|
Pyro4.config.PREFER_IP_VERSION = 0 # allow system to prefer IPv6
|
||
|
Pyro4.config.COMMTIMEOUT = 40.0 # a bit longer than the default SQL server Gtimeout
|
||
|
Pyro4.config.SERIALIZER = 'pickle'
|
||
|
|
||
|
try:
|
||
|
verbose = int(os.environ['ADODBAPI_VERBOSE'])
|
||
|
except:
|
||
|
verbose = False
|
||
|
if verbose:
|
||
|
print(version)
|
||
|
|
||
|
# --- define objects to smooth out Python3 <-> Python 2.x differences
|
||
|
unicodeType = str #this line will be altered by 2to3.py to '= str'
|
||
|
longType = int #this line will be altered by 2to3.py to '= int'
|
||
|
if sys.version[0] >= '3': #python 3.x
|
||
|
StringTypes = str
|
||
|
makeByteBuffer = bytes
|
||
|
memoryViewType = memoryview
|
||
|
else: #python 2.x
|
||
|
memoryViewType = type(buffer(''))
|
||
|
def makeByteBuffer(x): # special for remote to be pickle-able
|
||
|
return bytes(x)
|
||
|
try: #jdhardy -- handle bytes under IronPython
|
||
|
bytes
|
||
|
except NameError:
|
||
|
bytes = str
|
||
|
StringTypes = (str,str) # will be messed up by 2to3 but never used
|
||
|
# -----------------------------------------------------------
|
||
|
# conversion functions mandated by PEP 249
|
||
|
Binary = makeByteBuffer # override the function from apibase.py
|
||
|
def Date(year,month,day):
|
||
|
return datetime.date(year,month,day) #dateconverter.Date(year,month,day)
|
||
|
def Time(hour,minute,second):
|
||
|
return datetime.time(hour, minute, second) # dateconverter.Time(hour,minute,second)
|
||
|
def Timestamp(year,month,day,hour,minute,second):
|
||
|
return datetime.datetime(year,month,day,hour,minute,second)
|
||
|
def DateFromTicks(ticks):
|
||
|
return Date(*time.gmtime(ticks)[:3])
|
||
|
def TimeFromTicks(ticks):
|
||
|
return Time(*time.gmtime(ticks)[3:6])
|
||
|
def TimestampFromTicks(ticks):
|
||
|
return Timestamp(*time.gmtime(ticks)[:6])
|
||
|
|
||
|
def connect(*args, **kwargs): # --> a remote db-api connection object
|
||
|
"""Create and open a remote db-api database connection object"""
|
||
|
# process the argument list the programmer gave us
|
||
|
kwargs = adodbapi.process_connect_string.process(args, kwargs)
|
||
|
# the "proxy_xxx" keys tell us where to find the PyRO proxy server
|
||
|
kwargs.setdefault('pyro_connection', 'PYRO:ado.connection@%(proxy_host)s:%(proxy_port)s')
|
||
|
if not 'proxy_port' in kwargs:
|
||
|
try:
|
||
|
pport = os.environ['PROXY_PORT']
|
||
|
except KeyError:
|
||
|
pport = 9099
|
||
|
kwargs['proxy_port'] = pport
|
||
|
if not 'proxy_host' in kwargs or not kwargs['proxy_host']:
|
||
|
try:
|
||
|
phost = os.environ['PROXY_HOST']
|
||
|
except KeyError:
|
||
|
phost = '[::1]' # '127.0.0.1'
|
||
|
kwargs['proxy_host'] = phost
|
||
|
ado_uri = kwargs['pyro_connection'] % kwargs
|
||
|
# ask PyRO make us a remote connection object
|
||
|
auto_retry = 3
|
||
|
while auto_retry:
|
||
|
try:
|
||
|
dispatcher = Pyro4.Proxy(ado_uri)
|
||
|
if 'comm_timeout' in kwargs:
|
||
|
dispatcher._pyroTimeout = float(kwargs['comm_timeout'])
|
||
|
uri = dispatcher.make_connection()
|
||
|
break
|
||
|
except Pyro4.core.errors.PyroError:
|
||
|
auto_retry -= 1
|
||
|
if auto_retry:
|
||
|
time.sleep(1)
|
||
|
else:
|
||
|
raise api.DatabaseError ('Cannot create connection to=%s' % ado_uri)
|
||
|
|
||
|
conn_uri = fix_uri(uri, kwargs) # get a host connection from the proxy server
|
||
|
while auto_retry:
|
||
|
try:
|
||
|
host_conn = Pyro4.Proxy(conn_uri) # bring up an exclusive Pyro connection for my ADO connection
|
||
|
break
|
||
|
except Pyro4.core.errors.PyroError:
|
||
|
auto_retry -= 1
|
||
|
if auto_retry:
|
||
|
time.sleep(1)
|
||
|
else:
|
||
|
raise api.DatabaseError ('Cannot create ADO connection object using=%s' % conn_uri)
|
||
|
if 'comm_timeout' in kwargs:
|
||
|
host_conn._pyroTimeout = float(kwargs['comm_timeout'])
|
||
|
# make a local clone
|
||
|
myConn = Connection()
|
||
|
while auto_retry:
|
||
|
try:
|
||
|
myConn.connect(kwargs, host_conn) # call my connect method -- hand him the host connection
|
||
|
break
|
||
|
except Pyro4.core.errors.PyroError:
|
||
|
auto_retry -= 1
|
||
|
if auto_retry:
|
||
|
time.sleep(1)
|
||
|
else:
|
||
|
raise api.DatabaseError ('Pyro error creating connection to/thru=%s' % repr(kwargs))
|
||
|
except _BaseException as e:
|
||
|
raise api.DatabaseError('Error creating remote connection to=%s, e=%s, %s' % (repr(kwargs), repr(e),sys.exc_info()[2]))
|
||
|
return myConn
|
||
|
|
||
|
def fix_uri(uri, kwargs):
|
||
|
"""convert a generic pyro uri with '0.0.0.0' into the address we actually called"""
|
||
|
u = uri.asString()
|
||
|
s = u.split('[::0]') # IPv6 generic address
|
||
|
if len(s) == 1: # did not find one
|
||
|
s = u.split('0.0.0.0') # IPv4 generic address
|
||
|
if len(s) > 1: # found a generic
|
||
|
return kwargs['proxy_host'].join(s) # fill in our address for the host
|
||
|
return uri
|
||
|
|
||
|
# # # # # ----- the Class that defines a connection ----- # # # # #
|
||
|
class Connection(object):
|
||
|
# include connection attributes required by api definition.
|
||
|
Warning = api.Warning
|
||
|
Error = api.Error
|
||
|
InterfaceError = api.InterfaceError
|
||
|
DataError = api.DataError
|
||
|
DatabaseError = api.DatabaseError
|
||
|
OperationalError = api.OperationalError
|
||
|
IntegrityError = api.IntegrityError
|
||
|
InternalError = api.InternalError
|
||
|
NotSupportedError = api.NotSupportedError
|
||
|
ProgrammingError = api.ProgrammingError
|
||
|
# set up some class attributes
|
||
|
paramstyle = api.paramstyle
|
||
|
|
||
|
@property
|
||
|
def dbapi(self): # a proposed db-api version 3 extension.
|
||
|
"Return a reference to the DBAPI module for this Connection."
|
||
|
return api
|
||
|
|
||
|
def __init__(self):
|
||
|
self.proxy = None
|
||
|
self.kwargs = {}
|
||
|
self.errorhandler = None
|
||
|
self.supportsTransactions = False
|
||
|
self.paramstyle = api.paramstyle
|
||
|
self.timeout = 30
|
||
|
self.cursors = {}
|
||
|
|
||
|
def connect(self, kwargs, connection_maker):
|
||
|
self.kwargs = kwargs
|
||
|
if verbose:
|
||
|
print('%s attempting: "%s"' % (version, repr(kwargs)))
|
||
|
self.proxy = connection_maker
|
||
|
##try:
|
||
|
ret = self.proxy.connect(kwargs) # ask the server to hook us up
|
||
|
##except ImportError, e: # Pyro is trying to import pywinTypes.comerrer
|
||
|
## self._raiseConnectionError(api.DatabaseError, 'Proxy cannot connect using=%s' % repr(kwargs))
|
||
|
if ret is not True:
|
||
|
self._raiseConnectionError(api.OperationalError, 'Proxy returns error message=%s'%repr(ret))
|
||
|
|
||
|
self.supportsTransactions = self.getIndexedValue('supportsTransactions')
|
||
|
self.paramstyle = self.getIndexedValue('paramstyle')
|
||
|
self.timeout = self.getIndexedValue('timeout')
|
||
|
if verbose:
|
||
|
print('adodbapi.remote New connection at %X' % id(self))
|
||
|
|
||
|
def _raiseConnectionError(self, errorclass, errorvalue):
|
||
|
eh = self.errorhandler
|
||
|
if eh is None:
|
||
|
eh = api.standardErrorHandler
|
||
|
eh(self, None, errorclass, errorvalue)
|
||
|
|
||
|
|
||
|
def close(self):
|
||
|
"""Close the connection now (rather than whenever __del__ is called).
|
||
|
|
||
|
The connection will be unusable from this point forward;
|
||
|
an Error (or subclass) exception will be raised if any operation is attempted with the connection.
|
||
|
The same applies to all cursor objects trying to use the connection.
|
||
|
"""
|
||
|
for crsr in list(self.cursors.values())[:]: # copy the list, then close each one
|
||
|
crsr.close()
|
||
|
try:
|
||
|
"""close the underlying remote Connection object"""
|
||
|
self.proxy.close()
|
||
|
if verbose:
|
||
|
print('adodbapi.remote Closed connection at %X' % id(self))
|
||
|
object.__delattr__(self, 'proxy') # future attempts to use closed cursor will be caught by __getattr__
|
||
|
except Exception:
|
||
|
pass
|
||
|
|
||
|
def __del__(self):
|
||
|
try:
|
||
|
self.proxy.close()
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
def commit(self):
|
||
|
"""Commit any pending transaction to the database.
|
||
|
|
||
|
Note that if the database supports an auto-commit feature,
|
||
|
this must be initially off. An interface method may be provided to turn it back on.
|
||
|
Database modules that do not support transactions should implement this method with void functionality.
|
||
|
"""
|
||
|
if not self.supportsTransactions:
|
||
|
return
|
||
|
result = self.proxy.commit()
|
||
|
if result:
|
||
|
self._raiseConnectionError(api.OperationalError, 'Error during commit: %s' % result)
|
||
|
|
||
|
def _rollback(self):
|
||
|
"""In case a database does provide transactions this method causes the the database to roll back to
|
||
|
the start of any pending transaction. Closing a connection without committing the changes first will
|
||
|
cause an implicit rollback to be performed.
|
||
|
"""
|
||
|
result = self.proxy.rollback()
|
||
|
if result:
|
||
|
self._raiseConnectionError(api.OperationalError, 'Error during rollback: %s' % result)
|
||
|
|
||
|
def __setattr__(self, name, value):
|
||
|
if name in ('paramstyle', 'timeout', 'autocommit'):
|
||
|
if self.proxy:
|
||
|
self.proxy.send_attribute_to_host(name, value)
|
||
|
object.__setattr__(self, name, value) # store attribute locally (too)
|
||
|
|
||
|
def __getattr__(self, item):
|
||
|
if item == 'rollback': # the rollback method only appears if the database supports transactions
|
||
|
if self.supportsTransactions:
|
||
|
return self._rollback # return the rollback method so the caller can execute it.
|
||
|
else:
|
||
|
raise self.ProgrammingError ('this data provider does not support Rollback')
|
||
|
elif item in ('dbms_name', 'dbms_version', 'connection_string', 'autocommit'): # 'messages' ):
|
||
|
return self.getIndexedValue(item)
|
||
|
elif item == 'proxy':
|
||
|
raise self.ProgrammingError('Attempting to use closed connection')
|
||
|
else:
|
||
|
raise self.ProgrammingError('No remote access for attribute="%s"' % item)
|
||
|
|
||
|
def getIndexedValue(self, index):
|
||
|
r = self.proxy.get_attribute_for_remote(index)
|
||
|
return r
|
||
|
|
||
|
def cursor(self):
|
||
|
"Return a new Cursor Object using the connection."
|
||
|
myCursor = Cursor(self)
|
||
|
return myCursor
|
||
|
|
||
|
def _i_am_here(self, crsr):
|
||
|
"message from a new cursor proclaiming its existence"
|
||
|
self.cursors[crsr.id] = crsr
|
||
|
|
||
|
def _i_am_closing(self, crsr):
|
||
|
"message from a cursor giving connection a chance to clean up"
|
||
|
try:
|
||
|
del self.cursors[crsr.id]
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
def __enter__(self): # Connections are context managers
|
||
|
return(self)
|
||
|
|
||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
|
if exc_type:
|
||
|
self._rollback() #automatic rollback on errors
|
||
|
else:
|
||
|
self.commit()
|
||
|
|
||
|
def get_table_names(self):
|
||
|
return self.proxy.get_table_names()
|
||
|
|
||
|
def fixpickle(x):
|
||
|
"""pickle barfs on buffer(x) so we pass as array.array(x) then restore to original form for .execute()"""
|
||
|
if x is None:
|
||
|
return None
|
||
|
if isinstance(x, dict):
|
||
|
# for 'named' paramstyle user will pass a mapping
|
||
|
newargs = {}
|
||
|
for arg,val in list(x.items()):
|
||
|
if isinstance(val, memoryViewType):
|
||
|
newval = array.array('B')
|
||
|
newval.fromstring(val)
|
||
|
newargs[arg] = newval
|
||
|
else:
|
||
|
newargs[arg] = val
|
||
|
return newargs
|
||
|
# if not a mapping, then a sequence
|
||
|
newargs = []
|
||
|
for arg in x:
|
||
|
if isinstance(arg, memoryViewType):
|
||
|
newarg = array.array('B')
|
||
|
newarg.fromstring(arg)
|
||
|
newargs.append(newarg)
|
||
|
else:
|
||
|
newargs.append(arg)
|
||
|
return newargs
|
||
|
|
||
|
class Cursor(object):
|
||
|
def __init__(self, connection):
|
||
|
self.command = None
|
||
|
self.errorhandler = None ## was: connection.errorhandler
|
||
|
self.connection = connection
|
||
|
self.proxy = self.connection.proxy
|
||
|
self.rs = None # the fetchable data for this cursor
|
||
|
self.converters = NotImplemented
|
||
|
self.id = connection.proxy.build_cursor()
|
||
|
connection._i_am_here(self)
|
||
|
self.recordset_format = api.RS_REMOTE
|
||
|
if verbose:
|
||
|
print('%s New cursor at %X on conn %X' % (version, id(self), id(self.connection)))
|
||
|
|
||
|
def prepare(self, operation):
|
||
|
self.command = operation
|
||
|
try: del self.description
|
||
|
except AttributeError: pass
|
||
|
self.proxy.crsr_prepare(self.id, operation)
|
||
|
|
||
|
def __iter__(self): # [2.1 Zamarev]
|
||
|
return iter(self.fetchone, None) # [2.1 Zamarev]
|
||
|
|
||
|
def __next__(self):
|
||
|
r = self.fetchone()
|
||
|
if r:
|
||
|
return r
|
||
|
raise StopIteration
|
||
|
|
||
|
def __enter__(self):
|
||
|
"Allow database cursors to be used with context managers."
|
||
|
return self
|
||
|
|
||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
|
"Allow database cursors to be used with context managers."
|
||
|
self.close()
|
||
|
|
||
|
def __getattr__(self, key):
|
||
|
if key == 'numberOfColumns':
|
||
|
try:
|
||
|
return len(self.rs[0])
|
||
|
except:
|
||
|
return 0
|
||
|
if key == 'description':
|
||
|
try:
|
||
|
self.description = self.proxy.crsr_get_description(self.id)[:]
|
||
|
return self.description
|
||
|
except TypeError:
|
||
|
return None
|
||
|
if key == 'columnNames':
|
||
|
try:
|
||
|
r = dict(self.proxy.crsr_get_columnNames(self.id)) # copy the remote columns
|
||
|
|
||
|
except TypeError:
|
||
|
r = {}
|
||
|
self.columnNames = r
|
||
|
return r
|
||
|
|
||
|
if key == 'remote_cursor':
|
||
|
raise api.OperationalError
|
||
|
try:
|
||
|
return self.proxy.crsr_get_attribute_for_remote(self.id, key)
|
||
|
except AttributeError:
|
||
|
raise api.InternalError ('Failure getting attribute "%s" from proxy cursor.' % key)
|
||
|
|
||
|
def __setattr__(self, key, value):
|
||
|
if key == 'arraysize':
|
||
|
self.proxy.crsr_set_arraysize(self.id, value)
|
||
|
if key == 'paramstyle':
|
||
|
if value in api.accepted_paramstyles:
|
||
|
self.proxy.crsr_set_paramstyle(self.id, value)
|
||
|
else:
|
||
|
self._raiseCursorError(api.ProgrammingError, 'invalid paramstyle ="%s"' % value)
|
||
|
object.__setattr__(self, key, value)
|
||
|
|
||
|
def _raiseCursorError(self, errorclass, errorvalue):
|
||
|
eh = self.errorhandler
|
||
|
if eh is None:
|
||
|
eh = api.standardErrorHandler
|
||
|
eh(self.connection, self, errorclass, errorvalue)
|
||
|
|
||
|
def execute(self, operation, parameters=None):
|
||
|
if self.connection is None:
|
||
|
self._raiseCursorError(ProgrammingError, 'Attempted operation on closed cursor')
|
||
|
self.command = operation
|
||
|
try: del self.description
|
||
|
except AttributeError: pass
|
||
|
try: del self.columnNames
|
||
|
except AttributeError: pass
|
||
|
fp = fixpickle(parameters)
|
||
|
if verbose > 2:
|
||
|
print(('%s executing "%s" with params=%s' % (version, operation, repr(parameters))))
|
||
|
result = self.proxy.crsr_execute(self.id, operation, fp)
|
||
|
if result: # an exception was triggered
|
||
|
self._raiseCursorError(result[0], result[1])
|
||
|
|
||
|
def executemany(self, operation, seq_of_parameters):
|
||
|
if self.connection is None:
|
||
|
self._raiseCursorError(ProgrammingError, 'Attempted operation on closed cursor')
|
||
|
self.command = operation
|
||
|
try: del self.description
|
||
|
except AttributeError: pass
|
||
|
try: del self.columnNames
|
||
|
except AttributeError: pass
|
||
|
sq = [fixpickle(x) for x in seq_of_parameters]
|
||
|
if verbose > 2:
|
||
|
print(('%s executemany "%s" with params=%s' % (version, operation, repr(seq_of_parameters))))
|
||
|
self.proxy.crsr_executemany(self.id, operation, sq)
|
||
|
|
||
|
def nextset(self):
|
||
|
try: del self.description
|
||
|
except AttributeError: pass
|
||
|
try: del self.columnNames
|
||
|
except AttributeError: pass
|
||
|
if verbose > 2:
|
||
|
print(('%s nextset' % version))
|
||
|
return self.proxy.crsr_nextset(self.id)
|
||
|
|
||
|
def callproc(self, procname, parameters=None):
|
||
|
if self.connection is None:
|
||
|
self._raiseCursorError(ProgrammingError, 'Attempted operation on closed cursor')
|
||
|
self.command = procname
|
||
|
try: del self.description
|
||
|
except AttributeError: pass
|
||
|
try: del self.columnNames
|
||
|
except AttributeError: pass
|
||
|
fp = fixpickle(parameters)
|
||
|
if verbose > 2:
|
||
|
print(('%s callproc "%s" with params=%s' % (version, procname, repr(parameters))))
|
||
|
return self.proxy.crsr_callproc(self.id, procname, fp)
|
||
|
|
||
|
def fetchone(self):
|
||
|
try:
|
||
|
f1 = self.proxy.crsr_fetchone(self.id)
|
||
|
except _BaseException as e:
|
||
|
self._raiseCursorError(api.DatabaseError, e)
|
||
|
else:
|
||
|
if f1 is None:
|
||
|
return None
|
||
|
self.rs = [f1]
|
||
|
return api.SQLrows(self.rs, 1, self)[0] # new object to hold the results of the fetch
|
||
|
|
||
|
def fetchmany(self, size=None):
|
||
|
try:
|
||
|
self.rs = self.proxy.crsr_fetchmany(self.id, size)
|
||
|
if not self.rs:
|
||
|
return []
|
||
|
r = api.SQLrows(self.rs, len(self.rs), self)
|
||
|
return r
|
||
|
except Exception as e:
|
||
|
self._raiseCursorError(api.DatabaseError, e)
|
||
|
|
||
|
def fetchall(self):
|
||
|
try:
|
||
|
self.rs = self.proxy.crsr_fetchall(self.id)
|
||
|
if not self.rs:
|
||
|
return []
|
||
|
return api.SQLrows(self.rs, len(self.rs), self)
|
||
|
except Exception as e:
|
||
|
self._raiseCursorError(api.DatabaseError, e)
|
||
|
|
||
|
def close(self):
|
||
|
if self.connection is None:
|
||
|
return
|
||
|
self.connection._i_am_closing(self) # take me off the connection's cursors list
|
||
|
try:
|
||
|
self.proxy.crsr_close(self.id)
|
||
|
except: pass
|
||
|
try: del self.description
|
||
|
except: pass
|
||
|
try: del self.rs # let go of the recordset
|
||
|
except: pass
|
||
|
self.connection = None #this will make all future method calls on me throw an exception
|
||
|
self.proxy = None
|
||
|
if verbose:
|
||
|
print('adodbapi.remote Closed cursor at %X' % id(self))
|
||
|
|
||
|
def __del__(self):
|
||
|
try:
|
||
|
self.close()
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
def setinputsizes(self,sizes):
|
||
|
pass
|
||
|
|
||
|
def setoutputsize(self, size, column=None):
|
||
|
pass
|