crate 2.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crate/client/__init__.py +37 -0
- crate/client/_pep440.py +1 -0
- crate/client/blob.py +105 -0
- crate/client/connection.py +221 -0
- crate/client/converter.py +143 -0
- crate/client/cursor.py +321 -0
- crate/client/exceptions.py +101 -0
- crate/client/http.py +694 -0
- crate/testing/__init__.py +0 -0
- crate/testing/layer.py +428 -0
- crate/testing/util.py +95 -0
- crate-2.0.0.dev0.dist-info/LICENSE +178 -0
- crate-2.0.0.dev0.dist-info/METADATA +168 -0
- crate-2.0.0.dev0.dist-info/NOTICE +24 -0
- crate-2.0.0.dev0.dist-info/RECORD +17 -0
- crate-2.0.0.dev0.dist-info/WHEEL +5 -0
- crate-2.0.0.dev0.dist-info/top_level.txt +1 -0
crate/client/cursor.py
ADDED
@@ -0,0 +1,321 @@
|
|
1
|
+
# -*- coding: utf-8; -*-
|
2
|
+
#
|
3
|
+
# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
|
4
|
+
# license agreements. See the NOTICE file distributed with this work for
|
5
|
+
# additional information regarding copyright ownership. Crate licenses
|
6
|
+
# this file to you under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License. You may
|
8
|
+
# obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
14
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
15
|
+
# License for the specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
#
|
18
|
+
# However, if you have executed another commercial license agreement
|
19
|
+
# with Crate these terms will supersede the license and you may use the
|
20
|
+
# software solely pursuant to the terms of the relevant commercial agreement.
|
21
|
+
import typing as t
|
22
|
+
import warnings
|
23
|
+
from datetime import datetime, timedelta, timezone
|
24
|
+
|
25
|
+
from .converter import Converter, DataType
|
26
|
+
from .exceptions import ProgrammingError
|
27
|
+
|
28
|
+
|
29
|
+
class Cursor:
|
30
|
+
"""
|
31
|
+
not thread-safe by intention
|
32
|
+
should not be shared between different threads
|
33
|
+
"""
|
34
|
+
|
35
|
+
lastrowid = None # currently not supported
|
36
|
+
|
37
|
+
def __init__(self, connection, converter: Converter, **kwargs):
|
38
|
+
self.arraysize = 1
|
39
|
+
self.connection = connection
|
40
|
+
self._converter = converter
|
41
|
+
self._closed = False
|
42
|
+
self._result: t.Dict[str, t.Any] = {}
|
43
|
+
self.rows = None
|
44
|
+
self._time_zone = None
|
45
|
+
self.time_zone = kwargs.get("time_zone")
|
46
|
+
|
47
|
+
def execute(self, sql, parameters=None, bulk_parameters=None):
|
48
|
+
"""
|
49
|
+
Prepare and execute a database operation (query or command).
|
50
|
+
"""
|
51
|
+
if self.connection._closed:
|
52
|
+
raise ProgrammingError("Connection closed")
|
53
|
+
|
54
|
+
if self._closed:
|
55
|
+
raise ProgrammingError("Cursor closed")
|
56
|
+
|
57
|
+
self._result = self.connection.client.sql(
|
58
|
+
sql, parameters, bulk_parameters
|
59
|
+
)
|
60
|
+
if "rows" in self._result:
|
61
|
+
if self._converter is None:
|
62
|
+
self.rows = iter(self._result["rows"])
|
63
|
+
else:
|
64
|
+
self.rows = iter(self._convert_rows())
|
65
|
+
|
66
|
+
def executemany(self, sql, seq_of_parameters):
|
67
|
+
"""
|
68
|
+
Prepare a database operation (query or command) and then execute it
|
69
|
+
against all parameter sequences or mappings found in the sequence
|
70
|
+
``seq_of_parameters``.
|
71
|
+
"""
|
72
|
+
row_counts = []
|
73
|
+
durations = []
|
74
|
+
self.execute(sql, bulk_parameters=seq_of_parameters)
|
75
|
+
|
76
|
+
for result in self._result.get("results", []):
|
77
|
+
if result.get("rowcount") > -1:
|
78
|
+
row_counts.append(result.get("rowcount"))
|
79
|
+
if self.duration > -1:
|
80
|
+
durations.append(self.duration)
|
81
|
+
|
82
|
+
self._result = {
|
83
|
+
"rowcount": sum(row_counts) if row_counts else -1,
|
84
|
+
"duration": sum(durations) if durations else -1,
|
85
|
+
"rows": [],
|
86
|
+
"cols": self._result.get("cols", []),
|
87
|
+
"col_types": self._result.get("col_types", []),
|
88
|
+
"results": self._result.get("results"),
|
89
|
+
}
|
90
|
+
if self._converter is None:
|
91
|
+
self.rows = iter(self._result["rows"])
|
92
|
+
else:
|
93
|
+
self.rows = iter(self._convert_rows())
|
94
|
+
return self._result["results"]
|
95
|
+
|
96
|
+
def fetchone(self):
|
97
|
+
"""
|
98
|
+
Fetch the next row of a query result set, returning a single sequence,
|
99
|
+
or None when no more data is available.
|
100
|
+
Alias for ``next()``.
|
101
|
+
"""
|
102
|
+
try:
|
103
|
+
return self.next()
|
104
|
+
except StopIteration:
|
105
|
+
return None
|
106
|
+
|
107
|
+
def __iter__(self):
|
108
|
+
"""
|
109
|
+
support iterator interface:
|
110
|
+
http://legacy.python.org/dev/peps/pep-0249/#iter
|
111
|
+
|
112
|
+
This iterator is shared. Advancing this iterator will advance other
|
113
|
+
iterators created from this cursor.
|
114
|
+
"""
|
115
|
+
warnings.warn("DB-API extension cursor.__iter__() used", stacklevel=2)
|
116
|
+
return self
|
117
|
+
|
118
|
+
def fetchmany(self, count=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
|
+
if count is None:
|
125
|
+
count = self.arraysize
|
126
|
+
if count == 0:
|
127
|
+
return self.fetchall()
|
128
|
+
result = []
|
129
|
+
for _ in range(count):
|
130
|
+
try:
|
131
|
+
result.append(self.next())
|
132
|
+
except StopIteration:
|
133
|
+
pass
|
134
|
+
return result
|
135
|
+
|
136
|
+
def fetchall(self):
|
137
|
+
"""
|
138
|
+
Fetch all (remaining) rows of a query result, returning them as a
|
139
|
+
sequence of sequences (e.g. a list of tuples). Note that the cursor's
|
140
|
+
arraysize attribute can affect the performance of this operation.
|
141
|
+
"""
|
142
|
+
result = []
|
143
|
+
iterate = True
|
144
|
+
while iterate:
|
145
|
+
try:
|
146
|
+
result.append(self.next())
|
147
|
+
except StopIteration:
|
148
|
+
iterate = False
|
149
|
+
return result
|
150
|
+
|
151
|
+
def close(self):
|
152
|
+
"""
|
153
|
+
Close the cursor now
|
154
|
+
"""
|
155
|
+
self._closed = True
|
156
|
+
self._result = {}
|
157
|
+
|
158
|
+
def setinputsizes(self, sizes):
|
159
|
+
"""
|
160
|
+
Not supported method.
|
161
|
+
"""
|
162
|
+
pass
|
163
|
+
|
164
|
+
def setoutputsize(self, size, column=None):
|
165
|
+
"""
|
166
|
+
Not supported method.
|
167
|
+
"""
|
168
|
+
pass
|
169
|
+
|
170
|
+
@property
|
171
|
+
def rowcount(self):
|
172
|
+
"""
|
173
|
+
This read-only attribute specifies the number of rows that the last
|
174
|
+
.execute*() produced (for DQL statements like ``SELECT``) or affected
|
175
|
+
(for DML statements like ``UPDATE`` or ``INSERT``).
|
176
|
+
"""
|
177
|
+
if self._closed or not self._result or "rows" not in self._result:
|
178
|
+
return -1
|
179
|
+
return self._result.get("rowcount", -1)
|
180
|
+
|
181
|
+
def next(self):
|
182
|
+
"""
|
183
|
+
Return the next row of a query result set, respecting if cursor was
|
184
|
+
closed.
|
185
|
+
"""
|
186
|
+
if self.rows is None:
|
187
|
+
raise ProgrammingError(
|
188
|
+
"No result available. "
|
189
|
+
+ "execute() or executemany() must be called first."
|
190
|
+
)
|
191
|
+
if not self._closed:
|
192
|
+
return next(self.rows)
|
193
|
+
else:
|
194
|
+
raise ProgrammingError("Cursor closed")
|
195
|
+
|
196
|
+
__next__ = next
|
197
|
+
|
198
|
+
@property
|
199
|
+
def description(self):
|
200
|
+
"""
|
201
|
+
This read-only attribute is a sequence of 7-item sequences.
|
202
|
+
"""
|
203
|
+
if self._closed:
|
204
|
+
return None
|
205
|
+
|
206
|
+
description = []
|
207
|
+
for col in self._result["cols"]:
|
208
|
+
description.append((col, None, None, None, None, None, None))
|
209
|
+
return tuple(description)
|
210
|
+
|
211
|
+
@property
|
212
|
+
def duration(self):
|
213
|
+
"""
|
214
|
+
This read-only attribute specifies the server-side duration of a query
|
215
|
+
in milliseconds.
|
216
|
+
"""
|
217
|
+
if self._closed or not self._result or "duration" not in self._result:
|
218
|
+
return -1
|
219
|
+
return self._result.get("duration", 0)
|
220
|
+
|
221
|
+
def _convert_rows(self):
|
222
|
+
"""
|
223
|
+
Iterate rows, apply type converters, and generate converted rows.
|
224
|
+
"""
|
225
|
+
if not ("col_types" in self._result and self._result["col_types"]):
|
226
|
+
raise ValueError(
|
227
|
+
"Unable to apply type conversion "
|
228
|
+
"without `col_types` information"
|
229
|
+
)
|
230
|
+
|
231
|
+
# Resolve `col_types` definition to converter functions. Running
|
232
|
+
# the lookup redundantly on each row loop iteration would be a
|
233
|
+
# huge performance hog.
|
234
|
+
types = self._result["col_types"]
|
235
|
+
converters = [self._converter.get(type_) for type_ in types]
|
236
|
+
|
237
|
+
# Process result rows with conversion.
|
238
|
+
for row in self._result["rows"]:
|
239
|
+
yield [convert(value) for convert, value in zip(converters, row)]
|
240
|
+
|
241
|
+
@property
|
242
|
+
def time_zone(self):
|
243
|
+
"""
|
244
|
+
Get the current time zone.
|
245
|
+
"""
|
246
|
+
return self._time_zone
|
247
|
+
|
248
|
+
@time_zone.setter
|
249
|
+
def time_zone(self, tz):
|
250
|
+
"""
|
251
|
+
Set the time zone.
|
252
|
+
|
253
|
+
Different data types are supported. Available options are:
|
254
|
+
|
255
|
+
- ``datetime.timezone.utc``
|
256
|
+
- ``datetime.timezone(datetime.timedelta(hours=7), name="MST")``
|
257
|
+
- ``pytz.timezone("Australia/Sydney")``
|
258
|
+
- ``zoneinfo.ZoneInfo("Australia/Sydney")``
|
259
|
+
- ``+0530`` (UTC offset in string format)
|
260
|
+
|
261
|
+
The driver always returns timezone-"aware" `datetime` objects,
|
262
|
+
with their `tzinfo` attribute set.
|
263
|
+
|
264
|
+
When `time_zone` is `None`, the returned `datetime` objects are
|
265
|
+
using Coordinated Universal Time (UTC), because CrateDB is storing
|
266
|
+
timestamp values in this format.
|
267
|
+
|
268
|
+
When `time_zone` is given, the timestamp values will be transparently
|
269
|
+
converted from UTC to use the given time zone.
|
270
|
+
"""
|
271
|
+
|
272
|
+
# Do nothing when time zone is reset.
|
273
|
+
if tz is None:
|
274
|
+
self._time_zone = None
|
275
|
+
return
|
276
|
+
|
277
|
+
# Requesting datetime-aware `datetime` objects
|
278
|
+
# needs the data type converter.
|
279
|
+
# Implicitly create one, when needed.
|
280
|
+
if self._converter is None:
|
281
|
+
self._converter = Converter()
|
282
|
+
|
283
|
+
# When the time zone is given as a string,
|
284
|
+
# assume UTC offset format, e.g. `+0530`.
|
285
|
+
if isinstance(tz, str):
|
286
|
+
tz = self._timezone_from_utc_offset(tz)
|
287
|
+
|
288
|
+
self._time_zone = tz
|
289
|
+
|
290
|
+
def _to_datetime_with_tz(
|
291
|
+
value: t.Optional[float],
|
292
|
+
) -> t.Optional[datetime]:
|
293
|
+
"""
|
294
|
+
Convert CrateDB's `TIMESTAMP` value to a native Python `datetime`
|
295
|
+
object, with timezone-awareness.
|
296
|
+
"""
|
297
|
+
if value is None:
|
298
|
+
return None
|
299
|
+
return datetime.fromtimestamp(value / 1e3, tz=self._time_zone)
|
300
|
+
|
301
|
+
# Register converter function for `TIMESTAMP` type.
|
302
|
+
self._converter.set(DataType.TIMESTAMP_WITH_TZ, _to_datetime_with_tz)
|
303
|
+
self._converter.set(DataType.TIMESTAMP_WITHOUT_TZ, _to_datetime_with_tz)
|
304
|
+
|
305
|
+
@staticmethod
|
306
|
+
def _timezone_from_utc_offset(tz) -> timezone:
|
307
|
+
"""
|
308
|
+
UTC offset in string format (e.g. `+0530`) to `datetime.timezone`.
|
309
|
+
"""
|
310
|
+
if len(tz) != 5:
|
311
|
+
raise ValueError(
|
312
|
+
f"Time zone '{tz}' is given in invalid UTC offset format"
|
313
|
+
)
|
314
|
+
try:
|
315
|
+
hours = int(tz[:3])
|
316
|
+
minutes = int(tz[0] + tz[3:])
|
317
|
+
return timezone(timedelta(hours=hours, minutes=minutes), name=tz)
|
318
|
+
except Exception as ex:
|
319
|
+
raise ValueError(
|
320
|
+
f"Time zone '{tz}' is given in invalid UTC offset format: {ex}"
|
321
|
+
) from ex
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# -*- coding: utf-8; -*-
|
2
|
+
#
|
3
|
+
# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
|
4
|
+
# license agreements. See the NOTICE file distributed with this work for
|
5
|
+
# additional information regarding copyright ownership. Crate licenses
|
6
|
+
# this file to you under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License. You may
|
8
|
+
# obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
14
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
15
|
+
# License for the specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
#
|
18
|
+
# However, if you have executed another commercial license agreement
|
19
|
+
# with Crate these terms will supersede the license and you may use the
|
20
|
+
# software solely pursuant to the terms of the relevant commercial agreement.
|
21
|
+
|
22
|
+
|
23
|
+
class Error(Exception):
|
24
|
+
def __init__(self, msg=None, error_trace=None):
|
25
|
+
# for compatibility reasons we want to keep the exception message
|
26
|
+
# attribute because clients may depend on it
|
27
|
+
if msg:
|
28
|
+
self.message = msg
|
29
|
+
super(Error, self).__init__(msg)
|
30
|
+
self.error_trace = error_trace
|
31
|
+
|
32
|
+
def __str__(self):
|
33
|
+
if self.error_trace is None:
|
34
|
+
return super().__str__()
|
35
|
+
return "\n".join([super().__str__(), str(self.error_trace)])
|
36
|
+
|
37
|
+
|
38
|
+
# A001 Variable `Warning` is shadowing a Python builtin
|
39
|
+
class Warning(Exception): # noqa: A001
|
40
|
+
pass
|
41
|
+
|
42
|
+
|
43
|
+
class InterfaceError(Error):
|
44
|
+
pass
|
45
|
+
|
46
|
+
|
47
|
+
class DatabaseError(Error):
|
48
|
+
pass
|
49
|
+
|
50
|
+
|
51
|
+
class InternalError(DatabaseError):
|
52
|
+
pass
|
53
|
+
|
54
|
+
|
55
|
+
class OperationalError(DatabaseError):
|
56
|
+
pass
|
57
|
+
|
58
|
+
|
59
|
+
class ProgrammingError(DatabaseError):
|
60
|
+
pass
|
61
|
+
|
62
|
+
|
63
|
+
class IntegrityError(DatabaseError):
|
64
|
+
pass
|
65
|
+
|
66
|
+
|
67
|
+
class DataError(DatabaseError):
|
68
|
+
pass
|
69
|
+
|
70
|
+
|
71
|
+
class NotSupportedError(DatabaseError):
|
72
|
+
pass
|
73
|
+
|
74
|
+
|
75
|
+
# exceptions not in db api
|
76
|
+
|
77
|
+
|
78
|
+
# A001 Variable `ConnectionError` is shadowing a Python builtin
|
79
|
+
class ConnectionError(OperationalError): # noqa: A001
|
80
|
+
pass
|
81
|
+
|
82
|
+
|
83
|
+
class BlobException(Exception):
|
84
|
+
def __init__(self, table, digest):
|
85
|
+
self.table = table
|
86
|
+
self.digest = digest
|
87
|
+
|
88
|
+
def __str__(self):
|
89
|
+
return "{table}/{digest}".format(table=self.table, digest=self.digest)
|
90
|
+
|
91
|
+
|
92
|
+
class DigestNotFoundException(BlobException):
|
93
|
+
pass
|
94
|
+
|
95
|
+
|
96
|
+
class BlobLocationNotFoundException(BlobException):
|
97
|
+
pass
|
98
|
+
|
99
|
+
|
100
|
+
class TimezoneUnawareException(Error):
|
101
|
+
pass
|