Skip to content

Commit 282419d

Browse files
committed
See [1] for details. The main motivation for the module creation was
the integration Django with Tarantool database through django-tarantool database backend [2] which requires dbapi connector to the database. All the optional extensions and methods were ignored because Django does not require them. Anyway, feel free to suggest its implementation as needed. Interactive transactions are not currently supported by Tarantool and theirs implementation will be added in the connector when the feature is stable in Tarantool itself. [1] https://www.python.org/dev/peps/pep-0249/ [2] https://github.com/artembo/django-tarantool
1 parent a2039b1 commit 282419d

File tree

4 files changed

+349
-2
lines changed

4 files changed

+349
-2
lines changed

tarantool/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,4 @@ def connectmesh(addrs=({'host': 'localhost', 'port': 3301},), user=None,
7575

7676
__all__ = ['connect', 'Connection', 'connectmesh', 'MeshConnection', 'Schema',
7777
'Error', 'DatabaseError', 'NetworkError', 'NetworkWarning',
78-
'SchemaError']
78+
'SchemaError', 'dbapi']

tarantool/connection.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,20 @@
5050
ITERATOR_ALL
5151
)
5252
from tarantool.error import (
53+
Error,
5354
NetworkError,
5455
DatabaseError,
5556
InterfaceError,
5657
SchemaError,
5758
NetworkWarning,
59+
OperationalError,
60+
DataError,
61+
IntegrityError,
62+
InternalError,
63+
ProgrammingError,
64+
NotSupportedError,
5865
SchemaReloadException,
66+
Warning,
5967
warn
6068
)
6169
from tarantool.schema import Schema
@@ -77,11 +85,19 @@ class Connection(object):
7785
Also this class provides low-level interface to data manipulation
7886
(insert/delete/update/select).
7987
'''
80-
Error = tarantool.error
88+
# DBAPI Extension: supply exceptions as attributes on the connection
89+
Error = Error
8190
DatabaseError = DatabaseError
8291
InterfaceError = InterfaceError
8392
SchemaError = SchemaError
8493
NetworkError = NetworkError
94+
Warning = Warning
95+
DataError = DataError
96+
OperationalError = OperationalError
97+
IntegrityError = IntegrityError
98+
InternalError = InternalError
99+
ProgrammingError = ProgrammingError
100+
NotSupportedError = NotSupportedError
85101

86102
def __init__(self, host, port,
87103
user=None,
@@ -137,6 +153,13 @@ def close(self):
137153
self._socket.close()
138154
self._socket = None
139155

156+
def is_closed(self):
157+
self._check_not_closed()
158+
159+
def _check_not_closed(self, error=None):
160+
if self._socket is None:
161+
raise DatabaseError(error or "The connector is closed")
162+
140163
def connect_basic(self):
141164
if self.host == None:
142165
self.connect_unix()
@@ -801,6 +824,7 @@ def execute(self, query, params=None):
801824
:return: query result data
802825
:rtype: list
803826
'''
827+
self._check_not_closed()
804828
if not params:
805829
params = []
806830
request = RequestExecute(self, query, params)

tarantool/dbapi.py

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
# -*- coding: utf-8 -*-
2+
from tarantool.connection import Connection as BaseConnection
3+
from tarantool.error import *
4+
5+
6+
paramstyle = 'named'
7+
apilevel = "2.0"
8+
threadsafety = 1
9+
10+
11+
class Cursor:
12+
13+
def __init__(self, conn):
14+
self._c = conn
15+
self._lastrowid = None
16+
self._rowcount = None
17+
self.arraysize = 1
18+
self.autocommit = False
19+
self.closed = False
20+
self._rows = None
21+
22+
def callproc(self, procname, *params): # TODO
23+
"""
24+
Call a stored database procedure with the given name. The sequence of
25+
parameters must contain one entry for each argument that the
26+
procedure expects. The result of the call is returned as modified
27+
copy of the input sequence. Input parameters are left untouched,
28+
output and input/output parameters replaced with possibly new values.
29+
30+
The procedure may also provide a result set as output. This must then
31+
be made available through the standard .fetch*() methods.
32+
"""
33+
34+
@property
35+
def rows(self):
36+
return self._rows
37+
38+
def close(self):
39+
"""
40+
Close the cursor now (rather than whenever __del__ is called).
41+
42+
The cursor will be unusable from this point forward; an Error (or
43+
subclass) exception will be raised if any operation is attempted with
44+
the cursor.
45+
"""
46+
self._c = None
47+
self._rows = None
48+
49+
def execute(self, query, params=None):
50+
"""
51+
Prepare and execute a database operation (query or command).
52+
53+
Parameters may be provided as sequence or mapping and will be bound
54+
to variables in the operation. Variables are specified in a
55+
database-specific notation (see the module's paramstyle attribute for
56+
details).
57+
58+
A reference to the operation will be retained by the cursor. If the
59+
same operation object is passed in again, then the cursor can
60+
optimize its behavior. This is most effective for algorithms where
61+
the same operation is used, but different parameters are bound to it
62+
(many times).
63+
64+
For maximum efficiency when reusing an operation, it is best to use
65+
the .setinputsizes() method to specify the parameter types and sizes
66+
ahead of time. It is legal for a parameter to not match the
67+
predefined information; the implementation should compensate,
68+
possibly with a loss of efficiency.
69+
70+
The parameters may also be specified as list of tuples to e.g. insert
71+
multiple rows in a single operation, but this kind of usage is
72+
deprecated: .executemany() should be used instead.
73+
74+
Return values are not defined.
75+
"""
76+
if self.closed:
77+
raise ProgrammingError()
78+
79+
response = self._c.execute(query, params)
80+
81+
self._rows = response.data if len(response.body) > 1 else None
82+
83+
self._rowcount = response.rowcount
84+
self._lastrowid = response.lastrowid
85+
86+
def executemany(self, query, param_sets):
87+
rowcount = 0
88+
for params in param_sets:
89+
self.execute(query, params)
90+
if self.rowcount == -1:
91+
rowcount = -1
92+
if rowcount != -1:
93+
rowcount += self.rowcount
94+
self._rowcount = rowcount
95+
96+
@property
97+
def lastrowid(self):
98+
"""
99+
This read-only attribute provides the rowid of the last modified row
100+
(most databases return a rowid only when a single INSERT operation is
101+
performed). If the operation does not set a rowid or if the database
102+
does not support rowids, this attribute should be set to None.
103+
104+
The semantics of .lastrowid are undefined in case the last executed
105+
statement modified more than one row, e.g. when using INSERT with
106+
.executemany().
107+
108+
Warning Message: "DB-API extension cursor.lastrowid used"
109+
"""
110+
return self._lastrowid
111+
112+
@property
113+
def rowcount(self):
114+
"""
115+
This read-only attribute specifies the number of rows that the last
116+
.execute*() produced (for DQL statements like SELECT) or affected (
117+
for DML statements like UPDATE or INSERT).
118+
119+
The attribute is -1 in case no .execute*() has been performed on the
120+
cursor or the rowcount of the last operation is cannot be determined
121+
by the interface.
122+
123+
Note:
124+
Future versions of the DB API specification could redefine the latter
125+
case to have the object return None instead of -1.
126+
"""
127+
return self._rowcount
128+
129+
def fetchone(self):
130+
"""
131+
Fetch the next row of a query result set, returning a single
132+
sequence, or None when no more data is available.
133+
134+
An Error (or subclass) exception is raised if the previous call to
135+
.execute*() did not produce any result set or no call was issued yet.
136+
"""
137+
if self._rows is None:
138+
raise ProgrammingError('Nothing to fetch')
139+
return self.fetchmany(1)[0] if self._rows else None
140+
141+
def fetchmany(self, size=None):
142+
"""
143+
Fetch the next set of rows of a query result, returning a sequence of
144+
sequences (e.g. a list of tuples). An empty sequence is returned when
145+
no more rows are available.
146+
147+
The number of rows to fetch per call is specified by the parameter.
148+
If it is not given, the cursor's arraysize determines the number of
149+
rows to be fetched. The method should try to fetch as many rows as
150+
indicated by the size parameter. If this is not possible due to the
151+
specified number of rows not being available, fewer rows may be
152+
returned.
153+
154+
An Error (or subclass) exception is raised if the previous call to
155+
.execute*() did not produce any result set or no call was issued yet.
156+
157+
Note there are performance considerations involved with the size
158+
parameter. For optimal performance, it is usually best to use the
159+
.arraysize attribute. If the size parameter is used, then it is best
160+
for it to retain the same value from one .fetchmany() call to the next.
161+
"""
162+
size = size or self.arraysize
163+
164+
if self._rows is None:
165+
raise ProgrammingError('Nothing to fetch')
166+
167+
if len(self._rows) < size:
168+
items = self._rows
169+
self._rows = []
170+
else:
171+
items, self._rows = self._rows[:size], self._rows[size:]
172+
173+
return items
174+
175+
def fetchall(self):
176+
"""Fetch all (remaining) rows of a query result, returning them as a
177+
sequence of sequences (e.g. a list of tuples). Note that the cursor's
178+
arraysize attribute can affect the performance of this operation.
179+
180+
An Error (or subclass) exception is raised if the previous call to
181+
.execute*() did not produce any result set or no call was issued yet.
182+
"""
183+
if self._rows is None:
184+
raise ProgrammingError('Nothing to fetch')
185+
186+
items = self._rows
187+
self._rows = []
188+
return items
189+
190+
def setinputsizes(self, sizes):
191+
"""This can be used before a call to .execute*() to predefine memory
192+
areas for the operation's parameters.
193+
sizes is specified as a sequence - one item for each input parameter.
194+
The item should be a Type Object that corresponds to the input that
195+
will be used, or it should be an integer specifying the maximum
196+
length of a string parameter. If the item is None, then no predefined
197+
memory area will be reserved for that column (this is useful to avoid
198+
predefined areas for large inputs).
199+
200+
This method would be used before the .execute*() method is invoked.
201+
202+
Implementations are free to have this method do nothing and users are
203+
free to not use it."""
204+
205+
def setoutputsize(self, size, column=None):
206+
"""Set a column buffer size for fetches of large columns (e.g. LONGs,
207+
BLOBs, etc.). The column is specified as an index into the result
208+
sequence. Not specifying the column will set the default size for all
209+
large columns in the cursor.
210+
This method would be used before the .execute*() method is invoked.
211+
Implementations are free to have this method do nothing and users are
212+
free to not use it."""
213+
214+
215+
class Connection(BaseConnection):
216+
217+
@property
218+
def server_version(self):
219+
return getattr(self, 'version_id', None)
220+
221+
def close(self):
222+
self._check_not_closed()
223+
super(Connection, self).close()
224+
225+
def commit(self):
226+
"""
227+
Commit any pending transaction to the database.
228+
229+
Note that if the database supports an auto-commit feature, this must
230+
be initially off. An interface method may be provided to turn it back
231+
on.
232+
233+
Database modules that do not support transactions should implement
234+
this method with void functionality.
235+
"""
236+
self._check_not_closed()
237+
238+
def rollback(self):
239+
"""
240+
In case a database does provide transactions this method causes the
241+
database to roll back to the start of any pending transaction.
242+
Closing a connection without committing the changes first will cause
243+
an implicit rollback to be performed.
244+
"""
245+
self._check_not_closed()
246+
247+
def cursor(self, params=None):
248+
"""
249+
Return a new Cursor Object using the connection.
250+
251+
If the database does not provide a direct cursor concept, the module
252+
will have to emulate cursors using other means to the extent needed
253+
by this specification.
254+
"""
255+
return Cursor(self)
256+
257+
258+
def connect(dsn=None, host=None, port=None,
259+
user=None, password=None, **kwargs):
260+
"""
261+
Constructor for creating a connection to the database.
262+
263+
:param str dsn: Data source name (Tarantool URI)
264+
([[[username[:password]@]host:]port)
265+
:param str host: Server hostname or IP-address
266+
:param int port: Server port
267+
:param str user: Tarantool user
268+
:param str password: User password
269+
:rtype: Connection
270+
"""
271+
272+
if dsn:
273+
raise NotImplementedError("dsn param is not implemented in"
274+
"this version of dbapi module")
275+
params = {}
276+
if host:
277+
params["host"] = host
278+
if port:
279+
params["port"] = port
280+
if user:
281+
params["user"] = user
282+
if password:
283+
params["password"] = password
284+
285+
if kwargs.get("use_list") is False:
286+
params["use_list"] = False
287+
288+
return Connection(**params)

0 commit comments

Comments
 (0)