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/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