Vehicle-Anti-Theft-Face-Rec.../venv/Lib/site-packages/adodbapi/apibase.py

654 lines
28 KiB
Python
Raw Permalink Normal View History

2020-11-12 16:05:57 +00:00
"""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 sys
import time
import datetime
import decimal
import numbers
# 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 DBNull, DateTime
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 #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
_BaseException = Exception
else: #python 2.x
# noinspection PyUnresolvedReferences
from exceptions import Exception as _BaseException
memoryViewType = type(buffer(''))
makeByteBuffer = buffer
StringTypes = (str,str) # will be messed up by 2to3 but never used
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, # this line will be altered by 2to3 to 'str:'
bool :adc.adBoolean, #v2.1 Cole
decimal.Decimal : adc.adDecimal }
if longType != int: #not Python 3
typeMap[longType] = adc.adBigInt #works in python 2.x
typeMap[int] = adc.adInteger
typeMap[bytes] = adc.adBSTR # 2.x string type
else: #python 3.0 integrated integers
## Should this differentiate between an int that fits in a long and one that requires 64 bit datatype?
typeMap[int] = adc.adBigInt
typeMap[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) # will be altered by 2to3 to '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 dateconverter # this will only be called when adodbapi is in use, and very rarely
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