Skip to content

Commit 3a97b15

Browse files
Add pep-249 dbapi module
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 for the database. The most of 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 Co-authored-by: Denis Ignatenko <[email protected]>
1 parent f03df36 commit 3a97b15

File tree

4 files changed

+336
-5
lines changed

4 files changed

+336
-5
lines changed

Diff for: tarantool/__init__.py

+1-1
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']

Diff for: tarantool/connection.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,21 @@
5050
ITERATOR_ALL
5151
)
5252
from tarantool.error import (
53+
Error,
5354
NetworkError,
5455
DatabaseError,
5556
InterfaceError,
5657
ConfigurationError,
5758
SchemaError,
5859
NetworkWarning,
60+
OperationalError,
61+
DataError,
62+
IntegrityError,
63+
InternalError,
64+
ProgrammingError,
65+
NotSupportedError,
5966
SchemaReloadException,
67+
Warning,
6068
warn
6169
)
6270
from tarantool.schema import Schema
@@ -78,12 +86,20 @@ class Connection(object):
7886
Also this class provides low-level interface to data manipulation
7987
(insert/delete/update/select).
8088
'''
81-
Error = tarantool.error
89+
# DBAPI Extension: supply exceptions as attributes on the connection
90+
Error = Error
8291
DatabaseError = DatabaseError
8392
InterfaceError = InterfaceError
8493
ConfigurationError = ConfigurationError
8594
SchemaError = SchemaError
8695
NetworkError = NetworkError
96+
Warning = Warning
97+
DataError = DataError
98+
OperationalError = OperationalError
99+
IntegrityError = IntegrityError
100+
InternalError = InternalError
101+
ProgrammingError = ProgrammingError
102+
NotSupportedError = NotSupportedError
87103

88104
def __init__(self, host, port,
89105
user=None,
@@ -146,6 +162,13 @@ def close(self):
146162
self._socket.close()
147163
self._socket = None
148164

165+
def is_closed(self):
166+
'''
167+
Returns the state of the Connection instance
168+
:rtype: Boolean
169+
'''
170+
return self._socket is None
171+
149172
def connect_basic(self):
150173
if self.host == None:
151174
self.connect_unix()

Diff for: tarantool/dbapi.py

+248
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
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._rows = None
19+
20+
def callproc(self, procname, *params):
21+
"""
22+
Call a stored database procedure with the given name. The sequence of
23+
parameters must contain one entry for each argument that the
24+
procedure expects. The result of the call is returned as modified
25+
copy of the input sequence. Input parameters are left untouched,
26+
output and input/output parameters replaced with possibly new values.
27+
"""
28+
raise NotSupportedError("callproc() method is not supported")
29+
30+
@property
31+
def rows(self):
32+
return self._rows
33+
34+
@property
35+
def description(self):
36+
# FIXME Implement this method please
37+
raise NotImplementedError("description() property is not implemented")
38+
39+
def close(self):
40+
"""
41+
Close the cursor now (rather than whenever __del__ is called).
42+
The cursor will be unusable from this point forward; DatabaseError
43+
exception will be raised if any operation is attempted with
44+
the cursor.
45+
"""
46+
self._c = None
47+
self._rows = None
48+
self._lastrowid = None
49+
self._rowcount = None
50+
51+
def _check_not_closed(self, error=None):
52+
if self._c is None:
53+
raise InterfaceError(error or "Can not operate on a closed cursor")
54+
if self._c.is_closed():
55+
raise InterfaceError("The cursor can not be used "
56+
"with a closed connection")
57+
58+
def execute(self, query, params=None):
59+
"""
60+
Prepare and execute a database operation (query or command).
61+
"""
62+
self._check_not_closed("Can not execute on closed cursor.")
63+
64+
response = self._c.execute(query, params)
65+
66+
self._rows = response.data
67+
self._rowcount = response.affected_row_count or -1
68+
if response.autoincrement_ids:
69+
self._lastrowid = response.autoincrement_ids[-1]
70+
else:
71+
self._lastrowid = None
72+
73+
def executemany(self, query, param_sets):
74+
self._check_not_closed("Can not execute on closed cursor.")
75+
rowcount = 0
76+
for params in param_sets:
77+
self.execute(query, params)
78+
if self.rowcount == -1:
79+
rowcount = -1
80+
if rowcount != -1:
81+
rowcount += self.rowcount
82+
self._rowcount = rowcount
83+
84+
@property
85+
def lastrowid(self):
86+
"""
87+
This read-only attribute provides the rowid of the last modified row
88+
(most databases return a rowid only when a single INSERT operation is
89+
performed).
90+
"""
91+
return self._lastrowid
92+
93+
@property
94+
def rowcount(self):
95+
"""
96+
This read-only attribute specifies the number of rows that the last
97+
.execute*() produced (for DQL statements like SELECT) or affected (
98+
for DML statements like UPDATE or INSERT).
99+
"""
100+
return self._rowcount
101+
102+
def _check_result_set(self, error=None):
103+
"""
104+
Non-public method for raising an error when Cursor object does not have
105+
any row to fetch. Useful for checking access after DQL requests.
106+
"""
107+
if self._rows is None:
108+
raise InterfaceError(error or "No result set to fetch from")
109+
110+
def fetchone(self):
111+
"""
112+
Fetch the next row of a query result set, returning a single
113+
sequence, or None when no more data is available.
114+
"""
115+
self._check_result_set()
116+
return self.fetchmany(1)[0] if self._rows else None
117+
118+
def fetchmany(self, size=None):
119+
"""
120+
Fetch the next set of rows of a query result, returning a sequence of
121+
sequences (e.g. a list of tuples). An empty sequence is returned when
122+
no more rows are available.
123+
"""
124+
self._check_result_set()
125+
126+
size = size or self.arraysize
127+
128+
if len(self._rows) < size:
129+
items = self._rows
130+
self._rows = []
131+
else:
132+
items, self._rows = self._rows[:size], self._rows[size:]
133+
134+
return items
135+
136+
def fetchall(self):
137+
"""Fetch all (remaining) rows of a query result, returning them as a
138+
sequence of sequences (e.g. a list of tuples). Note that the cursor's
139+
arraysize attribute can affect the performance of this operation.
140+
"""
141+
self._check_result_set()
142+
143+
items = self._rows
144+
self._rows = []
145+
return items
146+
147+
def setinputsizes(self, sizes):
148+
"""PEP-249 allows to not implement this method and do nothing."""
149+
150+
def setoutputsize(self, size, column=None):
151+
"""PEP-249 allows to not implement this method and do nothing."""
152+
153+
154+
class Connection(BaseConnection):
155+
156+
def __init__(self, *args, **kwargs):
157+
super(Connection, self).__init__(*args, **kwargs)
158+
self._set_autocommit(kwargs.get('autocommit', True))
159+
160+
def _set_autocommit(self, autocommit):
161+
"""Autocommit is True by default and the default will be changed
162+
to False. Set the autocommit property explicitly to True or verify
163+
it when lean on autocommit behaviour."""
164+
if not isinstance(autocommit, bool):
165+
raise InterfaceError("autocommit parameter must be boolean, "
166+
"not %s" % autocommit.__class__.__name__)
167+
if autocommit is False:
168+
raise NotSupportedError("The connector supports "
169+
"only autocommit mode")
170+
self._autocommit = autocommit
171+
172+
@property
173+
def autocommit(self):
174+
"""Autocommit state"""
175+
return self._autocommit
176+
177+
@autocommit.setter
178+
def autocommit(self, autocommit):
179+
"""Set autocommit state"""
180+
self._set_autocommit(autocommit)
181+
182+
def _check_not_closed(self, error=None):
183+
"""
184+
Checks if the connection is not closed and rises an error if it is.
185+
"""
186+
if self.is_closed():
187+
raise InterfaceError(error or "The connector is closed")
188+
189+
def close(self):
190+
"""
191+
Closes the connection
192+
"""
193+
self._check_not_closed("The closed connector can not be closed again.")
194+
super(Connection, self).close()
195+
196+
def commit(self):
197+
"""
198+
Commit any pending transaction to the database.
199+
"""
200+
self._check_not_closed("Can not commit on the closed connection")
201+
202+
def rollback(self):
203+
"""
204+
Roll back pending transaction
205+
"""
206+
self._check_not_closed("Can not roll back on a closed connection")
207+
raise NotSupportedError("Transactions are not supported in this"
208+
"version of connector")
209+
210+
def cursor(self):
211+
"""
212+
Return a new Cursor Object using the connection.
213+
"""
214+
self._check_not_closed("Cursor creation is not allowed on a closed "
215+
"connection")
216+
return Cursor(self)
217+
218+
219+
def connect(dsn=None, host=None, port=None,
220+
user=None, password=None, **kwargs):
221+
"""
222+
Constructor for creating a connection to the database.
223+
224+
:param str dsn: Data source name (Tarantool URI)
225+
([[[username[:password]@]host:]port)
226+
:param str host: Server hostname or IP-address
227+
:param int port: Server port
228+
:param str user: Tarantool user
229+
:param str password: User password
230+
:rtype: Connection
231+
"""
232+
233+
if dsn:
234+
raise NotImplementedError("dsn param is not implemented in"
235+
"this version of dbapi module")
236+
params = {}
237+
if host:
238+
params["host"] = host
239+
if port:
240+
params["port"] = port
241+
if user:
242+
params["user"] = user
243+
if password:
244+
params["password"] = password
245+
246+
kwargs.update(params)
247+
248+
return Connection(**kwargs)

0 commit comments

Comments
 (0)