Skip to content

Commit 3ffd484

Browse files
authored
Dbapi2 (#160)
* added dbapi2 errors and extended Cursor and Connection functionality * disable quoting for boolean query params * updated connection and response * workaround for update and insert * workaround for lastrowid * implemented fetchone logic * minor changes * minor changes and added dbapi2 descriptions * set use_list to False for Response object
1 parent bff6a5d commit 3ffd484

File tree

5 files changed

+303
-49
lines changed

5 files changed

+303
-49
lines changed

tarantool/connection.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import ctypes
1313
import ctypes.util
14+
1415
try:
1516
from ctypes import c_ssize_t
1617
except ImportError:
@@ -55,6 +56,12 @@
5556
InterfaceError,
5657
SchemaError,
5758
NetworkWarning,
59+
OperationalError,
60+
DataError,
61+
IntegrityError,
62+
InternalError,
63+
ProgrammingError,
64+
NotSupportedError,
5865
SchemaReloadException,
5966
warn
6067
)
@@ -77,11 +84,20 @@ class Connection(object):
7784
Also this class provides low-level interface to data manipulation
7885
(insert/delete/update/select).
7986
'''
87+
# DBAPI Extension: supply exceptions as attributes on the connection
8088
Error = tarantool.error
8189
DatabaseError = DatabaseError
8290
InterfaceError = InterfaceError
8391
SchemaError = SchemaError
8492
NetworkError = NetworkError
93+
Warning = Warning
94+
DataError = DataError
95+
OperationalError = OperationalError
96+
IntegrityError = IntegrityError
97+
InternalError = InternalError
98+
ProgrammingError = ProgrammingError
99+
NotSupportedError = NotSupportedError
100+
ImproperlyConfigured = Exception
85101

86102
def __init__(self, host, port,
87103
user=None,
@@ -293,13 +309,12 @@ def check(): # Check that connection is alive
293309
retbytes = self._sys_recv(sock_fd, buf, 1, flag)
294310

295311
err = 0
296-
if os.name!= 'nt':
312+
if os.name != 'nt':
297313
err = ctypes.get_errno()
298314
else:
299315
err = ctypes.get_last_error()
300316
self._socket.setblocking(True)
301317

302-
303318
WWSAEWOULDBLOCK = 10035
304319
if (retbytes < 0) and (err == errno.EAGAIN or
305320
err == errno.EWOULDBLOCK or
@@ -791,10 +806,10 @@ def execute(self, query, params=None):
791806
'''
792807
Execute SQL request.
793808
Execute SQL query in database.
794-
795-
:param query: SQL syntax query
809+
810+
:param query: SQL syntax query
796811
:type query: str
797-
812+
798813
:param params: Bind values to use in query
799814
:type params: list, dict
800815

tarantool/dbapi.py

+250-37
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,268 @@
1-
# -*- coding: utf-8 -*-
1+
"""
2+
https://www.python.org/dev/peps/pep-0249/
3+
"""
4+
import re
25

6+
from tarantool.error import InterfaceError
7+
from .connection import Connection as BaseConnection
38

4-
class Cursor(object):
9+
update_insert_pattern = re.compile(r'^UPDATE|INSERT')
510

11+
12+
class Cursor:
13+
_lastrowid = 0
14+
_rowcount = 0
615
description = None
7-
rowcount = None
8-
arraysize = None
16+
position = 0
17+
arraysize = 200
18+
autocommit = True
919

10-
def __init__(self):
11-
pass
20+
def __init__(self, connection):
21+
self._c = connection
22+
self.rows = []
23+
24+
def callproc(self, procname, *params): # TODO
25+
"""
26+
Call a stored database procedure with the given name. The sequence of
27+
parameters must contain one entry for each argument that the
28+
procedure expects. The result of the call is returned as modified
29+
copy of the input sequence. Input parameters are left untouched,
30+
output and input/output parameters replaced with possibly new values.
31+
32+
The procedure may also provide a result set as output. This must then
33+
be made available through the standard .fetch*() methods.
34+
"""
35+
36+
def close(self): # TODO
37+
"""
38+
Close the cursor now (rather than whenever __del__ is called).
39+
40+
The cursor will be unusable from this point forward; an Error (or
41+
subclass) exception will be raised if any operation is attempted with
42+
the cursor.
43+
"""
44+
45+
@staticmethod
46+
def _convert_param(p):
47+
if p is None:
48+
return "NULL"
49+
if isinstance(p, bool):
50+
return str(p)
51+
return "'%s'" % p
52+
53+
@staticmethod
54+
def _extract_last_row_id(body): # TODO: Need to be checked
55+
try:
56+
val = tuple(tuple(body.items())[0][-1].items())[-1][-1][0]
57+
except TypeError:
58+
val = -1
59+
return val
60+
61+
def execute(self, query, params=None):
62+
"""
63+
Prepare and execute a database operation (query or command).
64+
65+
Parameters may be provided as sequence or mapping and will be bound
66+
to variables in the operation. Variables are specified in a
67+
database-specific notation (see the module's paramstyle attribute for
68+
details).
69+
70+
A reference to the operation will be retained by the cursor. If the
71+
same operation object is passed in again, then the cursor can
72+
optimize its behavior. This is most effective for algorithms where
73+
the same operation is used, but different parameters are bound to it
74+
(many times).
75+
76+
For maximum efficiency when reusing an operation, it is best to use
77+
the .setinputsizes() method to specify the parameter types and sizes
78+
ahead of time. It is legal for a parameter to not match the
79+
predefined information; the implementation should compensate,
80+
possibly with a loss of efficiency.
81+
82+
The parameters may also be specified as list of tuples to e.g. insert
83+
multiple rows in a single operation, but this kind of usage is
84+
deprecated: .executemany() should be used instead.
85+
86+
Return values are not defined.
87+
"""
88+
if params:
89+
query = query % tuple(
90+
self._convert_param(param) for param in params)
91+
92+
response = self._c.execute(query)
93+
94+
self.rows = tuple(response.body.values())[1] if len(
95+
response.body) > 1 else []
96+
97+
if update_insert_pattern.match(query.upper()):
98+
try:
99+
self._rowcount = response.rowcount
100+
except InterfaceError:
101+
self._rowcount = 1
102+
else:
103+
self._rowcount = 1
104+
105+
if query.upper().startswith('INSERT'):
106+
self._lastrowid = self._extract_last_row_id(response.body)
107+
return response
12108

13-
def callproc(self, procname, *params):
14-
pass
15-
16-
def close(self):
17-
pass
18-
19-
def execute(self, query, params):
20-
pass
21-
22109
def executemany(self, query, params):
23-
pass
24-
110+
return self.execute(query, params)
111+
112+
@property
113+
def lastrowid(self):
114+
"""
115+
This read-only attribute provides the rowid of the last modified row
116+
(most databases return a rowid only when a single INSERT operation is
117+
performed). If the operation does not set a rowid or if the database
118+
does not support rowids, this attribute should be set to None.
119+
120+
The semantics of .lastrowid are undefined in case the last executed
121+
statement modified more than one row, e.g. when using INSERT with
122+
.executemany().
123+
124+
Warning Message: "DB-API extension cursor.lastrowid used"
125+
"""
126+
return self._lastrowid
127+
128+
@property
129+
def rowcount(self):
130+
"""
131+
This read-only attribute specifies the number of rows that the last
132+
.execute*() produced (for DQL statements like SELECT) or affected (
133+
for DML statements like UPDATE or INSERT).
134+
135+
The attribute is -1 in case no .execute*() has been performed on the
136+
cursor or the rowcount of the last operation is cannot be determined
137+
by the interface.
138+
139+
Note:
140+
Future versions of the DB API specification could redefine the latter
141+
case to have the object return None instead of -1.
142+
"""
143+
return self._rowcount
144+
25145
def fetchone(self):
26-
pass
146+
"""
147+
Fetch the next row of a query result set, returning a single
148+
sequence, or None when no more data is available.
149+
150+
An Error (or subclass) exception is raised if the previous call to
151+
.execute*() did not produce any result set or no call was issued yet.
152+
"""
153+
154+
return self.fetchmany(1)[0] if len(self.rows) else None
155+
156+
def fetchmany(self, size):
157+
"""
158+
Fetch the next set of rows of a query result, returning a sequence of
159+
sequences (e.g. a list of tuples). An empty sequence is returned when
160+
no more rows are available.
161+
162+
The number of rows to fetch per call is specified by the parameter.
163+
If it is not given, the cursor's arraysize determines the number of
164+
rows to be fetched. The method should try to fetch as many rows as
165+
indicated by the size parameter. If this is not possible due to the
166+
specified number of rows not being available, fewer rows may be
167+
returned.
168+
169+
An Error (or subclass) exception is raised if the previous call to
170+
.execute*() did not produce any result set or no call was issued yet.
171+
172+
Note there are performance considerations involved with the size
173+
parameter. For optimal performance, it is usually best to use the
174+
.arraysize attribute. If the size parameter is used, then it is best
175+
for it to retain the same value from one .fetchmany() call to the next.
176+
"""
177+
if len(self.rows) < size:
178+
items = self.rows
179+
self.rows = []
180+
else:
181+
items, self.rows = self.rows[:size], self.rows[size:]
182+
183+
return items if len(items) else []
27184

28-
def fetchmany(self):
29-
pass
30-
31185
def fetchall(self):
32-
pass
186+
"""Fetch all (remaining) rows of a query result, returning them as a
187+
sequence of sequences (e.g. a list of tuples). Note that the cursor's
188+
arraysize attribute can affect the performance of this operation.
189+
190+
An Error (or subclass) exception is raised if the previous call to
191+
.execute*() did not produce any result set or no call was issued yet.
192+
"""
193+
items = self.rows[:]
194+
self.rows = []
195+
return items
33196

34197
def setinputsizes(self, sizes):
35-
pass
36-
198+
"""This can be used before a call to .execute*() to predefine memory
199+
areas for the operation's parameters.
200+
201+
sizes is specified as a sequence — one item for each input parameter.
202+
The item should be a Type Object that corresponds to the input that
203+
will be used, or it should be an integer specifying the maximum
204+
length of a string parameter. If the item is None, then no predefined
205+
memory area will be reserved for that column (this is useful to avoid
206+
predefined areas for large inputs).
207+
208+
This method would be used before the .execute*() method is invoked.
209+
210+
Implementations are free to have this method do nothing and users are
211+
free to not use it."""
212+
37213
def setoutputsize(self, size, column=None):
38214
pass
39215

40-
class Connection(object):
41216

42-
def __init__(self):
43-
pass
44-
45-
def close(self):
46-
pass
47-
48-
def commit(self):
49-
pass
217+
class Connection(BaseConnection):
218+
_cursor = None
219+
220+
server_version = 2
221+
222+
def commit(self): # TODO
223+
"""
224+
Commit any pending transaction to the database.
225+
226+
Note that if the database supports an auto-commit feature, this must
227+
be initially off. An interface method may be provided to turn it back
228+
on.
229+
230+
Database modules that do not support transactions should implement
231+
this method with void functionality.
232+
"""
50233

51234
def rollback(self):
52-
pass
53-
54-
def cursor(self):
55-
pass
235+
"""
236+
In case a database does provide transactions this method causes the
237+
database to roll back to the start of any pending transaction.
238+
Closing a connection without committing the changes first will cause
239+
an implicit rollback to be performed.
240+
"""
241+
242+
def close(self):
243+
"""
244+
Close the connection now (rather than whenever .__del__() is called).
245+
246+
The connection will be unusable from this point forward; an Error (or
247+
subclass) exception will be raised if any operation is attempted with
248+
the connection. The same applies to all cursor objects trying to use
249+
the connection. Note that closing a connection without committing the
250+
changes first will cause an implicit rollback to be performed.
251+
"""
252+
if self._socket:
253+
self._socket.close()
254+
self._socket = None
255+
256+
def _set_cursor(self):
257+
self._cursor = Cursor(self)
258+
return self._cursor
259+
260+
def cursor(self, params=None):
261+
"""
262+
Return a new Cursor Object using the connection.
263+
264+
If the database does not provide a direct cursor concept, the module
265+
will have to emulate cursors using other means to the extent needed
266+
by this specification.
267+
"""
268+
return self._cursor or self._set_cursor()

0 commit comments

Comments
 (0)