wherobots-python-dbapi 0.28.1__py3-none-any.whl → 0.28.2__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.
- wherobots/db/_transport.py +24 -0
- wherobots/db/connection.py +249 -27
- wherobots/db/constants.py +2 -0
- wherobots/db/cursor.py +3 -0
- wherobots/db/driver.py +7 -0
- {wherobots_python_dbapi-0.28.1.dist-info → wherobots_python_dbapi-0.28.2.dist-info}/METADATA +2 -2
- wherobots_python_dbapi-0.28.2.dist-info/RECORD +17 -0
- {wherobots_python_dbapi-0.28.1.dist-info → wherobots_python_dbapi-0.28.2.dist-info}/WHEEL +1 -1
- wherobots_python_dbapi-0.28.1.dist-info/RECORD +0 -16
- {wherobots_python_dbapi-0.28.1.dist-info → wherobots_python_dbapi-0.28.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Transport termination independent of the WebSocket protocol's send lock."""
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
import socket
|
|
5
|
+
|
|
6
|
+
from websockets.sync.client import ClientConnection
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def abort_connection(ws: ClientConnection) -> None:
|
|
10
|
+
"""Disable further writes and wake blocked I/O before publishing failures.
|
|
11
|
+
|
|
12
|
+
ClientConnection.close() takes the library's protocol mutex, which a
|
|
13
|
+
stalled sendall() may hold. Shutdown the owned socket instead; the library's
|
|
14
|
+
receive thread will observe EOF/error and finish protocol cleanup itself.
|
|
15
|
+
Already transmitted bytes may still execute remotely.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
ws.socket.shutdown(socket.SHUT_RDWR)
|
|
19
|
+
except OSError as exc:
|
|
20
|
+
if exc.errno not in (errno.ENOTCONN, errno.EBADF, errno.ECONNRESET):
|
|
21
|
+
raise
|
|
22
|
+
finally:
|
|
23
|
+
# Even if shutdown fails, prevent any later send on this socket object.
|
|
24
|
+
ws.socket.close()
|
wherobots/db/connection.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
import math
|
|
3
4
|
import textwrap
|
|
4
5
|
import threading
|
|
5
6
|
import uuid
|
|
@@ -12,10 +13,10 @@ import pandas
|
|
|
12
13
|
import pyarrow
|
|
13
14
|
import cbor2
|
|
14
15
|
import websockets.exceptions
|
|
15
|
-
import websockets.protocol
|
|
16
16
|
import websockets.sync.client
|
|
17
17
|
|
|
18
18
|
from .constants import DEFAULT_READ_TIMEOUT_SECONDS
|
|
19
|
+
from ._transport import abort_connection
|
|
19
20
|
from .cursor import Cursor
|
|
20
21
|
from .errors import NotSupportedError, OperationalError
|
|
21
22
|
from .models import ExecutionResult, ProgressInfo, Store, StoreResult
|
|
@@ -33,6 +34,41 @@ ProgressHandler = Callable[[ProgressInfo], None]
|
|
|
33
34
|
"""A callable invoked with a :class:`ProgressInfo` on every progress event."""
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
class _TransportError(Exception):
|
|
38
|
+
"""An I/O failure raised while receiving from the WebSocket."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_READER_JOIN_SECONDS = 1.0
|
|
42
|
+
"""How long close() waits for the reader thread once teardown has finished."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _handshake_bound(ws: Any) -> float | None:
|
|
46
|
+
"""The transport's ``close_timeout`` if it bounds the close handshake.
|
|
47
|
+
|
|
48
|
+
websockets documents ``close_timeout=None`` as disabling the timeout, which
|
|
49
|
+
would make a graceful close unbounded against an unresponsive peer. Return
|
|
50
|
+
``None`` for that, and for a missing, non-numeric (including ``bool`` and
|
|
51
|
+
mock), non-finite, non-positive, or too-large value; callers then abort
|
|
52
|
+
instead. Too large means the ``bound + 1s`` wait in ``close()`` would
|
|
53
|
+
exceed ``threading.TIMEOUT_MAX``.
|
|
54
|
+
Never raises ``Exception``: a failure here would strand pending queries.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
value = getattr(ws, "close_timeout", None)
|
|
58
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
59
|
+
return None
|
|
60
|
+
# Range-check a plain float, so a subclass can't override the
|
|
61
|
+
# comparisons. float() raises OverflowError for a huge int.
|
|
62
|
+
value = float(value)
|
|
63
|
+
if not math.isfinite(value) or value <= 0:
|
|
64
|
+
return None
|
|
65
|
+
if value > threading.TIMEOUT_MAX - 1.0:
|
|
66
|
+
return None
|
|
67
|
+
return value
|
|
68
|
+
except Exception:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
36
72
|
@dataclass
|
|
37
73
|
class Query:
|
|
38
74
|
sql: str
|
|
@@ -64,6 +100,7 @@ class Connection:
|
|
|
64
100
|
results_format: ResultsFormat | None = None,
|
|
65
101
|
data_compression: DataCompression | None = None,
|
|
66
102
|
geometry_representation: GeometryRepresentation | None = None,
|
|
103
|
+
session_id: str | None = None,
|
|
67
104
|
):
|
|
68
105
|
self.__ws = ws
|
|
69
106
|
self.__read_timeout = read_timeout
|
|
@@ -72,6 +109,15 @@ class Connection:
|
|
|
72
109
|
self.__geometry_representation = geometry_representation
|
|
73
110
|
self.__progress_handler: ProgressHandler | None = None
|
|
74
111
|
|
|
112
|
+
self.__session_id = session_id
|
|
113
|
+
self.__lock = threading.Lock()
|
|
114
|
+
self.__send_lock = threading.Lock()
|
|
115
|
+
self.__shutdown_done = threading.Event()
|
|
116
|
+
self.__shutdown_owner: int | None = None
|
|
117
|
+
# Resolved when shutdown starts (websockets reads close_timeout live),
|
|
118
|
+
# so the graceful/abort decision and a second closer's wait agree.
|
|
119
|
+
self.__close_bound: float | None = None
|
|
120
|
+
self.__closed = False
|
|
75
121
|
self.__queries: dict[str, Query] = {}
|
|
76
122
|
self.__thread = threading.Thread(
|
|
77
123
|
target=self.__main_loop, daemon=True, name="wherobots-connection"
|
|
@@ -85,7 +131,37 @@ class Connection:
|
|
|
85
131
|
self.close()
|
|
86
132
|
|
|
87
133
|
def close(self) -> None:
|
|
88
|
-
|
|
134
|
+
"""Close the transport, fail pending work, then wait for the reader.
|
|
135
|
+
|
|
136
|
+
The WebSocket close handshake is attempted when no send is in flight,
|
|
137
|
+
so the server can distinguish a client exit from a crash; otherwise the
|
|
138
|
+
socket is aborted. The handshake is bounded by the ``close_timeout`` of
|
|
139
|
+
the underlying ``ClientConnection`` (1s via ``connect``/
|
|
140
|
+
``connect_direct``; the library default of 10s for a caller-built
|
|
141
|
+
socket). If ``close_timeout`` doesn't bound it (``None``, missing, or
|
|
142
|
+
not a finite positive number), the socket is aborted instead.
|
|
143
|
+
|
|
144
|
+
Pending work has been failed by the time ``close()`` returns, also when
|
|
145
|
+
another thread is closing concurrently. After teardown, ``close()``
|
|
146
|
+
waits up to 1s for the reader thread, so a single call takes at most
|
|
147
|
+
about ``close_timeout`` + 1s. A second, concurrent ``close()`` can take
|
|
148
|
+
up to ``close_timeout`` + 2s (about 12s on a caller-built socket using
|
|
149
|
+
the library's 10s default).
|
|
150
|
+
|
|
151
|
+
Closing doesn't imply that server-side writes were rolled back. A
|
|
152
|
+
decoder or callback can outlive the bounded reader join.
|
|
153
|
+
"""
|
|
154
|
+
self.__fail_pending(graceful=True)
|
|
155
|
+
# A handler may close its own connection during terminal delivery.
|
|
156
|
+
if self.__shutdown_owner == threading.get_ident():
|
|
157
|
+
return
|
|
158
|
+
# Another thread may own shutdown and be mid-handshake, which is bounded
|
|
159
|
+
# by close_timeout; outlast it so close() never returns before pending
|
|
160
|
+
# work has been failed.
|
|
161
|
+
self.__shutdown_done.wait((self.__close_bound or 0.0) + 1.0)
|
|
162
|
+
# Timed from here, not from entry, so a slow handshake can't consume it.
|
|
163
|
+
if self.__thread is not threading.current_thread():
|
|
164
|
+
self.__thread.join(timeout=_READER_JOIN_SECONDS)
|
|
89
165
|
|
|
90
166
|
def commit(self) -> None:
|
|
91
167
|
raise NotSupportedError
|
|
@@ -114,18 +190,103 @@ class Connection:
|
|
|
114
190
|
def __main_loop(self) -> None:
|
|
115
191
|
"""Main background loop listening for messages from the SQL session."""
|
|
116
192
|
logging.info("Starting background connection handling loop...")
|
|
117
|
-
|
|
193
|
+
try:
|
|
194
|
+
self.__receive_loop()
|
|
195
|
+
finally:
|
|
196
|
+
self.__fail_pending()
|
|
197
|
+
|
|
198
|
+
def __receive_loop(self) -> None:
|
|
199
|
+
# recv drains buffered results before raising ConnectionClosed.
|
|
200
|
+
while True:
|
|
201
|
+
if self.__shutdown_done.is_set():
|
|
202
|
+
return
|
|
118
203
|
try:
|
|
119
204
|
self.__listen()
|
|
120
205
|
except TimeoutError:
|
|
121
206
|
# Expected, retry next time
|
|
122
207
|
continue
|
|
123
|
-
except websockets.exceptions.
|
|
208
|
+
except websockets.exceptions.ConnectionClosed:
|
|
124
209
|
logging.info("Connection closed; stopping main loop.")
|
|
125
210
|
return
|
|
211
|
+
except _TransportError:
|
|
212
|
+
logging.exception("SQL session transport failed; stopping main loop")
|
|
213
|
+
return
|
|
126
214
|
except Exception as e:
|
|
127
215
|
logging.exception("Error handling message from SQL session", exc_info=e)
|
|
128
216
|
|
|
217
|
+
def __connection_error(self, execution_id: str) -> OperationalError:
|
|
218
|
+
message = (
|
|
219
|
+
f"SQL connection lost (session={self.__session_id or 'unknown'}, "
|
|
220
|
+
f"execution={execution_id}). Commit outcome is unknown; "
|
|
221
|
+
"verify the operation before retrying writes."
|
|
222
|
+
)
|
|
223
|
+
return OperationalError(message)
|
|
224
|
+
|
|
225
|
+
def __fail_pending(self, graceful: bool = False) -> None:
|
|
226
|
+
# Stop admission first. Do not wait for __send_lock: its owner may be
|
|
227
|
+
# blocked in network I/O. __closed means closing until shutdown_done.
|
|
228
|
+
# Resolve the bound before taking __lock: it reads a transport
|
|
229
|
+
# attribute, which may be a property, and nothing foreign may run under
|
|
230
|
+
# __lock or between the latch and the delivery try/finally.
|
|
231
|
+
bound = _handshake_bound(self.__ws)
|
|
232
|
+
with self.__lock:
|
|
233
|
+
if self.__closed:
|
|
234
|
+
return
|
|
235
|
+
self.__closed = True
|
|
236
|
+
self.__shutdown_owner = threading.get_ident()
|
|
237
|
+
self.__close_bound = bound
|
|
238
|
+
# __closed is a one-way latch: nothing below may be skipped, or pending
|
|
239
|
+
# queries are stranded with no path to recovery.
|
|
240
|
+
try:
|
|
241
|
+
self.__terminate_transport(graceful)
|
|
242
|
+
with self.__lock:
|
|
243
|
+
pending = list(self.__queries.values())
|
|
244
|
+
self.__queries.clear()
|
|
245
|
+
for query in pending:
|
|
246
|
+
try:
|
|
247
|
+
query.handler(
|
|
248
|
+
ExecutionResult(
|
|
249
|
+
error=self.__connection_error(query.execution_id)
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
except Exception:
|
|
253
|
+
logging.exception(
|
|
254
|
+
"Could not deliver connection failure to query handler"
|
|
255
|
+
)
|
|
256
|
+
finally:
|
|
257
|
+
self.__shutdown_owner = None
|
|
258
|
+
self.__shutdown_done.set()
|
|
259
|
+
|
|
260
|
+
def __terminate_transport(self, graceful: bool) -> None:
|
|
261
|
+
"""Disable the transport. Never raises: delivery must not be skipped."""
|
|
262
|
+
# A non-blocking acquire tells us whether a sender is inside ws.send()
|
|
263
|
+
# right now, possibly stalled holding the library's protocol mutex,
|
|
264
|
+
# which close() would deadlock on. Releasing immediately is safe: the
|
|
265
|
+
# __closed latch is already set, and __send re-checks it under __lock
|
|
266
|
+
# after taking __send_lock, so no sender can reach ws.send() from here
|
|
267
|
+
# on. Holding the lock across the handshake would only park unrelated
|
|
268
|
+
# senders (including the reader's own __request_results) for up to
|
|
269
|
+
# close_timeout instead of letting them fail fast. On a reader-side
|
|
270
|
+
# failure the transport is already broken and a close frame is
|
|
271
|
+
# pointless, so callers abort directly. Without a handshake bound the
|
|
272
|
+
# close could block forever, so abort then too.
|
|
273
|
+
if (
|
|
274
|
+
graceful
|
|
275
|
+
and self.__close_bound is not None
|
|
276
|
+
and self.__send_lock.acquire(blocking=False)
|
|
277
|
+
):
|
|
278
|
+
self.__send_lock.release()
|
|
279
|
+
try:
|
|
280
|
+
self.__ws.close()
|
|
281
|
+
return
|
|
282
|
+
except Exception:
|
|
283
|
+
logging.debug("Graceful close failed; aborting", exc_info=True)
|
|
284
|
+
try:
|
|
285
|
+
abort_connection(self.__ws)
|
|
286
|
+
except Exception:
|
|
287
|
+
# The adapter still closes the socket object in its finally block.
|
|
288
|
+
logging.exception("Could not abort SQL session transport")
|
|
289
|
+
|
|
129
290
|
def __listen(self) -> None:
|
|
130
291
|
"""Waits for the next message from the SQL session and processes it.
|
|
131
292
|
|
|
@@ -168,16 +329,32 @@ class Connection:
|
|
|
168
329
|
# Terminal delivery: stop tracking the query first. Keeping it in
|
|
169
330
|
# __queries would retain its handler — and the results the handler
|
|
170
331
|
# references — for the connection's lifetime (WBC-922).
|
|
171
|
-
self.
|
|
172
|
-
|
|
332
|
+
with self.__lock:
|
|
333
|
+
claimed = self.__queries.pop(execution_id, None)
|
|
334
|
+
if claimed is not None:
|
|
335
|
+
claimed.handler(result)
|
|
336
|
+
|
|
337
|
+
def fail_query(action: str, error: Exception) -> None:
|
|
338
|
+
# This is a query-local failure, not evidence of transport loss.
|
|
339
|
+
# Exception text may contain result data; report only its type.
|
|
340
|
+
query.state = ExecutionState.FAILED
|
|
341
|
+
message = (
|
|
342
|
+
f"Could not {action} SQL results "
|
|
343
|
+
f"(session={self.__session_id or 'unknown'}, "
|
|
344
|
+
f"execution={execution_id}; {type(error).__name__}). "
|
|
345
|
+
"The statement may have completed; verify the operation before "
|
|
346
|
+
"retrying writes."
|
|
347
|
+
)
|
|
348
|
+
logging.error("%s", message)
|
|
349
|
+
complete_query(ExecutionResult(error=OperationalError(message)))
|
|
173
350
|
|
|
174
351
|
# Incoming state transitions are handled here.
|
|
175
352
|
if kind == EventKind.STATE_UPDATED or kind == EventKind.EXECUTION_RESULT:
|
|
176
353
|
try:
|
|
177
354
|
query.state = ExecutionState[message["state"].upper()]
|
|
178
355
|
logging.info("Query %s is now %s.", execution_id, query.state)
|
|
179
|
-
except KeyError:
|
|
180
|
-
|
|
356
|
+
except (KeyError, AttributeError, TypeError) as error:
|
|
357
|
+
fail_query("interpret", error)
|
|
181
358
|
return
|
|
182
359
|
|
|
183
360
|
if query.state == ExecutionState.SUCCEEDED:
|
|
@@ -212,21 +389,34 @@ class Connection:
|
|
|
212
389
|
return
|
|
213
390
|
|
|
214
391
|
# No store configured, request results normally
|
|
215
|
-
|
|
392
|
+
try:
|
|
393
|
+
self.__request_results(execution_id)
|
|
394
|
+
except Exception as error:
|
|
395
|
+
# Transport failures are handled by __send; a local
|
|
396
|
+
# retrieval failure must not orphan this execution.
|
|
397
|
+
fail_query("request", error)
|
|
216
398
|
return
|
|
217
399
|
|
|
218
400
|
# Otherwise, process the results from the execution_result event.
|
|
219
401
|
results = message.get("results")
|
|
220
|
-
if
|
|
402
|
+
if results is None or results == {}:
|
|
221
403
|
logging.warning("Got no results back from %s.", execution_id)
|
|
222
404
|
query.state = ExecutionState.COMPLETED
|
|
223
405
|
complete_query(ExecutionResult())
|
|
224
406
|
return
|
|
225
407
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
408
|
+
try:
|
|
409
|
+
if not isinstance(results, dict):
|
|
410
|
+
raise TypeError("Expected a result object")
|
|
411
|
+
decoded = self._handle_results(execution_id, results)
|
|
412
|
+
except Exception as error:
|
|
413
|
+
# Even OSError/TimeoutError here belong to decoding, not
|
|
414
|
+
# recv. Claim and deliver exactly once, including if close
|
|
415
|
+
# concurrently claims this execution.
|
|
416
|
+
fail_query("decode", error)
|
|
417
|
+
else:
|
|
418
|
+
query.state = ExecutionState.COMPLETED
|
|
419
|
+
complete_query(ExecutionResult(results=decoded))
|
|
230
420
|
elif query.state == ExecutionState.CANCELLED:
|
|
231
421
|
logging.info(
|
|
232
422
|
"Query %s has been cancelled; returning empty results.",
|
|
@@ -264,16 +454,40 @@ class Connection:
|
|
|
264
454
|
with pyarrow.ipc.open_stream(stream) as reader:
|
|
265
455
|
return reader.read_pandas()
|
|
266
456
|
else:
|
|
267
|
-
|
|
457
|
+
raise NotSupportedError("Unsupported results format")
|
|
268
458
|
|
|
269
|
-
def __send(self, message: Dict[str, Any]) -> None:
|
|
459
|
+
def __send(self, message: Dict[str, Any], query: Query | None = None) -> None:
|
|
460
|
+
# Serialization and redaction are local work. Fail before registration,
|
|
461
|
+
# without poisoning unrelated cursors or misreporting a transport loss.
|
|
270
462
|
request = json.dumps(message)
|
|
271
463
|
# Only compute the redacted request (json.dumps + sqlparse parse) when
|
|
272
464
|
# DEBUG is actually enabled; the log argument is evaluated eagerly, so an
|
|
273
465
|
# unguarded call would redact on every request even with DEBUG off.
|
|
274
466
|
if logging.getLogger().isEnabledFor(logging.DEBUG):
|
|
275
467
|
logging.debug("Request: %s", self.__redacted_request(message))
|
|
276
|
-
self.
|
|
468
|
+
with self.__send_lock:
|
|
469
|
+
with self.__lock:
|
|
470
|
+
if self.__closed:
|
|
471
|
+
if query is not None:
|
|
472
|
+
raise self.__connection_error(query.execution_id)
|
|
473
|
+
return
|
|
474
|
+
if query is not None:
|
|
475
|
+
self.__queries[query.execution_id] = query
|
|
476
|
+
elif message.get("execution_id") not in self.__queries:
|
|
477
|
+
return
|
|
478
|
+
try:
|
|
479
|
+
self.__ws.send(request)
|
|
480
|
+
except (websockets.exceptions.ConnectionClosed, OSError):
|
|
481
|
+
pass # Terminate outside the send gate, before delivering errors.
|
|
482
|
+
except Exception:
|
|
483
|
+
# API/programming errors aren't evidence of connection loss.
|
|
484
|
+
if query is not None:
|
|
485
|
+
with self.__lock:
|
|
486
|
+
self.__queries.pop(query.execution_id, None)
|
|
487
|
+
raise
|
|
488
|
+
else:
|
|
489
|
+
return
|
|
490
|
+
self.__fail_pending()
|
|
277
491
|
|
|
278
492
|
@staticmethod
|
|
279
493
|
def __redacted_request(message: Dict[str, Any]) -> str:
|
|
@@ -289,7 +503,14 @@ class Connection:
|
|
|
289
503
|
return json.dumps(message)
|
|
290
504
|
|
|
291
505
|
def __recv(self) -> Dict[str, Any]:
|
|
292
|
-
|
|
506
|
+
try:
|
|
507
|
+
frame = self.__ws.recv(timeout=self.__read_timeout)
|
|
508
|
+
except TimeoutError:
|
|
509
|
+
raise # Idle polls are expected, not terminal I/O failures.
|
|
510
|
+
except OSError as e:
|
|
511
|
+
# Distinguish transport I/O failures from OSErrors raised later by
|
|
512
|
+
# protocol parsing or result decoding; only the former are terminal.
|
|
513
|
+
raise _TransportError from e
|
|
293
514
|
if isinstance(frame, str):
|
|
294
515
|
message = json.loads(frame)
|
|
295
516
|
elif isinstance(frame, bytes):
|
|
@@ -318,14 +539,6 @@ class Connection:
|
|
|
318
539
|
if store:
|
|
319
540
|
request["store"] = store.to_dict()
|
|
320
541
|
|
|
321
|
-
self.__queries[execution_id] = Query(
|
|
322
|
-
sql=sql,
|
|
323
|
-
execution_id=execution_id,
|
|
324
|
-
state=ExecutionState.EXECUTION_REQUESTED,
|
|
325
|
-
handler=handler,
|
|
326
|
-
store=store,
|
|
327
|
-
)
|
|
328
|
-
|
|
329
542
|
# Redact literal values before logging: this driver is embedded by other
|
|
330
543
|
# services, so raw SQL here would leak into their log streams (WBC-139).
|
|
331
544
|
logging.info(
|
|
@@ -334,7 +547,16 @@ class Connection:
|
|
|
334
547
|
get_statement_type(sql),
|
|
335
548
|
textwrap.shorten(redact_sql(sql), width=200),
|
|
336
549
|
)
|
|
337
|
-
self.__send(
|
|
550
|
+
self.__send(
|
|
551
|
+
request,
|
|
552
|
+
Query(
|
|
553
|
+
sql=sql,
|
|
554
|
+
execution_id=execution_id,
|
|
555
|
+
state=ExecutionState.EXECUTION_REQUESTED,
|
|
556
|
+
handler=handler,
|
|
557
|
+
store=store,
|
|
558
|
+
),
|
|
559
|
+
)
|
|
338
560
|
return execution_id
|
|
339
561
|
|
|
340
562
|
def __request_results(self, execution_id: str) -> None:
|
wherobots/db/constants.py
CHANGED
|
@@ -14,6 +14,8 @@ DEFAULT_REGION: Region = Region.AWS_US_WEST_2
|
|
|
14
14
|
DEFAULT_SESSION_TYPE: SessionType = SessionType.MULTI
|
|
15
15
|
DEFAULT_STORAGE_FORMAT: StorageFormat = StorageFormat.PARQUET
|
|
16
16
|
DEFAULT_READ_TIMEOUT_SECONDS: float = 0.25
|
|
17
|
+
# Bound on the WebSocket close handshake performed by a graceful close().
|
|
18
|
+
DEFAULT_CLOSE_TIMEOUT_SECONDS: float = 1.0
|
|
17
19
|
DEFAULT_SESSION_WAIT_TIMEOUT_SECONDS: float = 900
|
|
18
20
|
|
|
19
21
|
MAX_MESSAGE_SIZE: int = 100 * 2**20 # 100MiB
|
wherobots/db/cursor.py
CHANGED
|
@@ -186,6 +186,9 @@ class Cursor:
|
|
|
186
186
|
self.__rowcount = -1
|
|
187
187
|
self.__description = None
|
|
188
188
|
|
|
189
|
+
# A rejected submission must not leave the previous execution ID paired
|
|
190
|
+
# with this new empty queue (which would make a later fetch wait forever).
|
|
191
|
+
self.__current_execution_id = None
|
|
189
192
|
self.__current_execution_id = self.__exec_fn(
|
|
190
193
|
_substitute_parameters(operation, parameters),
|
|
191
194
|
self.__queue.put,
|
wherobots/db/driver.py
CHANGED
|
@@ -20,6 +20,7 @@ import certifi
|
|
|
20
20
|
|
|
21
21
|
from .connection import Connection
|
|
22
22
|
from .constants import (
|
|
23
|
+
DEFAULT_CLOSE_TIMEOUT_SECONDS,
|
|
23
24
|
DEFAULT_ENDPOINT,
|
|
24
25
|
DEFAULT_READ_TIMEOUT_SECONDS,
|
|
25
26
|
DEFAULT_SESSION_TYPE,
|
|
@@ -267,6 +268,9 @@ def connect(
|
|
|
267
268
|
data_compression=data_compression,
|
|
268
269
|
geometry_representation=geometry_representation,
|
|
269
270
|
cancel_event=cancel_event,
|
|
271
|
+
session_id=urllib.parse.urlparse(session_id_url)
|
|
272
|
+
.path.rstrip("/")
|
|
273
|
+
.rsplit("/", 1)[-1],
|
|
270
274
|
)
|
|
271
275
|
|
|
272
276
|
|
|
@@ -294,6 +298,7 @@ def connect_direct(
|
|
|
294
298
|
data_compression: Union[DataCompression, None] = None,
|
|
295
299
|
geometry_representation: Union[GeometryRepresentation, None] = None,
|
|
296
300
|
cancel_event: Union[threading.Event, None] = None,
|
|
301
|
+
session_id: str | None = None,
|
|
297
302
|
) -> Connection:
|
|
298
303
|
uri_with_protocol = f"{uri}/{protocol}"
|
|
299
304
|
ssl_context = ssl.create_default_context()
|
|
@@ -321,6 +326,7 @@ def connect_direct(
|
|
|
321
326
|
additional_headers=headers,
|
|
322
327
|
max_size=MAX_MESSAGE_SIZE,
|
|
323
328
|
open_timeout=DEFAULT_HTTP_TIMEOUT,
|
|
329
|
+
close_timeout=DEFAULT_CLOSE_TIMEOUT_SECONDS,
|
|
324
330
|
ssl=ssl_context,
|
|
325
331
|
)
|
|
326
332
|
|
|
@@ -337,4 +343,5 @@ def connect_direct(
|
|
|
337
343
|
results_format=results_format,
|
|
338
344
|
data_compression=data_compression,
|
|
339
345
|
geometry_representation=geometry_representation,
|
|
346
|
+
session_id=session_id,
|
|
340
347
|
)
|
{wherobots_python_dbapi-0.28.1.dist-info → wherobots_python_dbapi-0.28.2.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: wherobots-python-dbapi
|
|
3
|
-
Version: 0.28.
|
|
3
|
+
Version: 0.28.2
|
|
4
4
|
Summary: Python DB-API driver for Wherobots DB
|
|
5
5
|
Project-URL: Homepage, https://github.com/wherobots/wherobots-python-dbapi-driver
|
|
6
6
|
Project-URL: Tracker, https://github.com/wherobots/wherobots-python-dbapi-driver/issues
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
wherobots/db/__init__.py,sha256=HN_uGOUMor80GB3rZIqnPcYWDAia7QAC-y9HfGKnegs,744
|
|
2
|
+
wherobots/db/_transport.py,sha256=b26gp20zreMAjvXxfBlP_Nl6xrxjmG3k1POiDhfVphw,887
|
|
3
|
+
wherobots/db/connection.py,sha256=o3UsjmFcntMavPUg6aQQTDMV1uPBMGmTrKXvSnm7Xt4,24955
|
|
4
|
+
wherobots/db/constants.py,sha256=SbJVxGSuN-2kBGu4WxNVxUlPi-8hhoG6eeuOewaaq_I,861
|
|
5
|
+
wherobots/db/cursor.py,sha256=jF9v4UhfBqZ0iZEAmIYRxWYU4E8UbMFIrwfY_PlNI60,8701
|
|
6
|
+
wherobots/db/driver.py,sha256=oxztjXrdsjqglOYEHn5JDMgmKh8_VCWj-YDGEb4OZsg,13245
|
|
7
|
+
wherobots/db/errors.py,sha256=G8a-DB6pgB0pUld59Wyq8PuywL-JWxGJfvrr-g8BPuU,310
|
|
8
|
+
wherobots/db/models.py,sha256=uk0EqZMH_RFkQusSw2AvyFivGvGJBu_WYP8FHa6JJGM,4178
|
|
9
|
+
wherobots/db/redaction.py,sha256=issl5e5QvZQbs4d5UGUJIW7SIZIYfQ04MIyuT0Eo5_g,3744
|
|
10
|
+
wherobots/db/region.py,sha256=GoFzazvgOjE1kVVT1QKAsRK88c_1XSAebR3ME4foSlQ,400
|
|
11
|
+
wherobots/db/runtime.py,sha256=eAR1oJEtssgkp-IRPTLOrlPI-_5SM_ASw6T6hQ_qF4I,985
|
|
12
|
+
wherobots/db/session_type.py,sha256=jZNwCl0nxvY-0reIn8RR_-fIYSSvIcaGx_4pRc71Lww,137
|
|
13
|
+
wherobots/db/types.py,sha256=WXKT_gbdQCH2_yXD_FECk0BpB7yfNWOLewCfax0U_Lk,2516
|
|
14
|
+
wherobots_python_dbapi-0.28.2.dist-info/METADATA,sha256=NrBYfI2FBGEDhuejl5zhV0f-HMUjpFEU2yBomo8u4Ho,11173
|
|
15
|
+
wherobots_python_dbapi-0.28.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
16
|
+
wherobots_python_dbapi-0.28.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
17
|
+
wherobots_python_dbapi-0.28.2.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
wherobots/db/__init__.py,sha256=HN_uGOUMor80GB3rZIqnPcYWDAia7QAC-y9HfGKnegs,744
|
|
2
|
-
wherobots/db/connection.py,sha256=VqXqFoKeyy9DMXP8-t0GhijWBMYB154HevbDRvyMzF4,14352
|
|
3
|
-
wherobots/db/constants.py,sha256=qV-6CmOya7-f-DoRMzcDzlhcmV5PkLCcqG5TFIsxQ7o,744
|
|
4
|
-
wherobots/db/cursor.py,sha256=vaQY1WcTSehx-XmCkn-vRsv5YcTddxj0HR1ZaGAtEis,8495
|
|
5
|
-
wherobots/db/driver.py,sha256=5iZXEsR2TM0xVPdcPLwlOifIkJHsYHLM0zKkwMk4UTo,12975
|
|
6
|
-
wherobots/db/errors.py,sha256=G8a-DB6pgB0pUld59Wyq8PuywL-JWxGJfvrr-g8BPuU,310
|
|
7
|
-
wherobots/db/models.py,sha256=uk0EqZMH_RFkQusSw2AvyFivGvGJBu_WYP8FHa6JJGM,4178
|
|
8
|
-
wherobots/db/redaction.py,sha256=issl5e5QvZQbs4d5UGUJIW7SIZIYfQ04MIyuT0Eo5_g,3744
|
|
9
|
-
wherobots/db/region.py,sha256=GoFzazvgOjE1kVVT1QKAsRK88c_1XSAebR3ME4foSlQ,400
|
|
10
|
-
wherobots/db/runtime.py,sha256=eAR1oJEtssgkp-IRPTLOrlPI-_5SM_ASw6T6hQ_qF4I,985
|
|
11
|
-
wherobots/db/session_type.py,sha256=jZNwCl0nxvY-0reIn8RR_-fIYSSvIcaGx_4pRc71Lww,137
|
|
12
|
-
wherobots/db/types.py,sha256=WXKT_gbdQCH2_yXD_FECk0BpB7yfNWOLewCfax0U_Lk,2516
|
|
13
|
-
wherobots_python_dbapi-0.28.1.dist-info/METADATA,sha256=EuIBcH4k9NCG9eIQOkyJvowab-iD0LVDXbWFQ1jg28c,11173
|
|
14
|
-
wherobots_python_dbapi-0.28.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
15
|
-
wherobots_python_dbapi-0.28.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
16
|
-
wherobots_python_dbapi-0.28.1.dist-info/RECORD,,
|
{wherobots_python_dbapi-0.28.1.dist-info → wherobots_python_dbapi-0.28.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|