crate 2.0.0.dev0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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