wherobots-python-dbapi 0.28.2__tar.gz → 0.30.0__tar.gz

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.
Files changed (21) hide show
  1. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/PKG-INFO +62 -1
  2. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/README.md +61 -0
  3. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/pyproject.toml +1 -1
  4. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/wherobots/db/__init__.py +6 -0
  5. wherobots_python_dbapi-0.30.0/wherobots/db/_transport.py +57 -0
  6. wherobots_python_dbapi-0.30.0/wherobots/db/connection.py +1369 -0
  7. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/wherobots/db/constants.py +10 -0
  8. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/wherobots/db/cursor.py +10 -0
  9. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/wherobots/db/driver.py +104 -17
  10. wherobots_python_dbapi-0.30.0/wherobots/db/errors.py +103 -0
  11. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/wherobots/db/types.py +3 -0
  12. wherobots_python_dbapi-0.28.2/wherobots/db/_transport.py +0 -24
  13. wherobots_python_dbapi-0.28.2/wherobots/db/connection.py +0 -593
  14. wherobots_python_dbapi-0.28.2/wherobots/db/errors.py +0 -26
  15. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/.gitignore +0 -0
  16. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/LICENSE +0 -0
  17. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/wherobots/db/models.py +0 -0
  18. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/wherobots/db/redaction.py +0 -0
  19. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/wherobots/db/region.py +0 -0
  20. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/wherobots/db/runtime.py +0 -0
  21. {wherobots_python_dbapi-0.28.2 → wherobots_python_dbapi-0.30.0}/wherobots/db/session_type.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: wherobots-python-dbapi
3
- Version: 0.28.2
3
+ Version: 0.30.0
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
@@ -225,6 +225,49 @@ Progress events are best-effort and may not be available for all query
225
225
  types or server versions. The handler is simply not invoked when no
226
226
  progress information is available.
227
227
 
228
+ ### Recovering from a lost connection
229
+
230
+ If the WebSocket to the SQL session drops while queries are running (a
231
+ network path failure, a keepalive timeout), the connection reconnects to
232
+ the same SQL session by itself and re-attaches every pending query, so
233
+ their results still arrive: there's nothing for your code to do. It keeps
234
+ trying, with backoff, for up to `reconnect_timeout_seconds` (60 seconds by
235
+ default; see below). It never starts a new session, and gives up at once
236
+ if the session refuses the connection, for example because it's gone.
237
+ While it reconnects, `execute()` and cancelling (closing a cursor, or
238
+ executing on it) wait for it to finish.
239
+
240
+ A re-attached query can still fail:
241
+
242
+ * If the SQL session doesn't know the execution (the statement didn't
243
+ reach it before the connection dropped, or the session restarted),
244
+ fetching raises `ExecutionNotFoundError`, a subclass of
245
+ `OperationalError`.
246
+ * If the SQL session cancelled the execution while the connection was
247
+ down (for example, its grace period for a lost connection ran out, or
248
+ another client cancelled it), fetching raises
249
+ `ExecutionCancelledError`, also an `OperationalError` subclass: its
250
+ outcome, including whether a write committed, is unknown. An execution
251
+ that you cancel yourself, by closing the cursor or executing on it,
252
+ completes with an empty result, as usual.
253
+ * An older SQL session that doesn't support attaching still answers with
254
+ the execution's state, and the driver polls it for the outcome (see
255
+ `stale_query_probe_seconds` below). If such a session reports no
256
+ location for a stored result, because it predates reporting it or
257
+ because the stored result is empty, the driver can't tell which, and
258
+ fails the query with an `OperationalError`. With probes disabled, a
259
+ running execution on such a session fails right away.
260
+
261
+ If the connection can't reconnect in time, or reconnecting is turned off
262
+ (`reconnect_timeout_seconds=None`), fetching a pending query's outcome
263
+ raises `ConnectionLostError`, another `OperationalError` subclass. The
264
+ query may still have run, or be running, on the SQL session: its outcome,
265
+ including whether a write committed, is unknown. The error's
266
+ `execution_id` and `session_id` attributes identify it, and a cursor's
267
+ `execution_id` property holds the ID of its current or last execution.
268
+ (When `execute()` itself raises `ConnectionLostError`, the connection was
269
+ already lost and the statement was never sent.)
270
+
228
271
  ### Runtime and region selection
229
272
 
230
273
  You can chose the Wherobots runtime you want to use using the `runtime`
@@ -302,3 +345,21 @@ users may find useful:
302
345
  caller needs to interrupt it (e.g. on client disconnect or timeout).
303
346
  The event is checked before each HTTP request, between retry
304
347
  attempts, and before the WebSocket handshake.
348
+ * `session_key`: keeps this connection on SQL sessions of its own. The
349
+ API reuses a session only for connections that ask with the same key,
350
+ and never one opened without a key, so a heavy query elsewhere never
351
+ holds the session your queries run on. When a new session would exceed
352
+ your organization's quota, the API shares another of your sessions
353
+ under another key instead, with or without `session_key`, and the
354
+ connection's `session_fallback` is `"quota"` (it is `None` otherwise).
355
+ `connection.session_id` names the session.
356
+ * `stale_query_probe_seconds`: how long a running query may go without
357
+ any message from the SQL session before the driver asks the session
358
+ for its state again, in case the query's completion event was lost.
359
+ Repeat probes back off to at most 8 times this interval. Defaults to
360
+ 30 seconds; pass `None` to disable probing.
361
+ * `reconnect_timeout_seconds`: how long the connection keeps trying to
362
+ reconnect to the same SQL session after losing its WebSocket (see
363
+ "Recovering from a lost connection" above). Defaults to 60 seconds,
364
+ which fits in the SQL session's 120-second grace period for the
365
+ queries of a lost connection; pass `None` to turn reconnecting off.
@@ -200,6 +200,49 @@ Progress events are best-effort and may not be available for all query
200
200
  types or server versions. The handler is simply not invoked when no
201
201
  progress information is available.
202
202
 
203
+ ### Recovering from a lost connection
204
+
205
+ If the WebSocket to the SQL session drops while queries are running (a
206
+ network path failure, a keepalive timeout), the connection reconnects to
207
+ the same SQL session by itself and re-attaches every pending query, so
208
+ their results still arrive: there's nothing for your code to do. It keeps
209
+ trying, with backoff, for up to `reconnect_timeout_seconds` (60 seconds by
210
+ default; see below). It never starts a new session, and gives up at once
211
+ if the session refuses the connection, for example because it's gone.
212
+ While it reconnects, `execute()` and cancelling (closing a cursor, or
213
+ executing on it) wait for it to finish.
214
+
215
+ A re-attached query can still fail:
216
+
217
+ * If the SQL session doesn't know the execution (the statement didn't
218
+ reach it before the connection dropped, or the session restarted),
219
+ fetching raises `ExecutionNotFoundError`, a subclass of
220
+ `OperationalError`.
221
+ * If the SQL session cancelled the execution while the connection was
222
+ down (for example, its grace period for a lost connection ran out, or
223
+ another client cancelled it), fetching raises
224
+ `ExecutionCancelledError`, also an `OperationalError` subclass: its
225
+ outcome, including whether a write committed, is unknown. An execution
226
+ that you cancel yourself, by closing the cursor or executing on it,
227
+ completes with an empty result, as usual.
228
+ * An older SQL session that doesn't support attaching still answers with
229
+ the execution's state, and the driver polls it for the outcome (see
230
+ `stale_query_probe_seconds` below). If such a session reports no
231
+ location for a stored result, because it predates reporting it or
232
+ because the stored result is empty, the driver can't tell which, and
233
+ fails the query with an `OperationalError`. With probes disabled, a
234
+ running execution on such a session fails right away.
235
+
236
+ If the connection can't reconnect in time, or reconnecting is turned off
237
+ (`reconnect_timeout_seconds=None`), fetching a pending query's outcome
238
+ raises `ConnectionLostError`, another `OperationalError` subclass. The
239
+ query may still have run, or be running, on the SQL session: its outcome,
240
+ including whether a write committed, is unknown. The error's
241
+ `execution_id` and `session_id` attributes identify it, and a cursor's
242
+ `execution_id` property holds the ID of its current or last execution.
243
+ (When `execute()` itself raises `ConnectionLostError`, the connection was
244
+ already lost and the statement was never sent.)
245
+
203
246
  ### Runtime and region selection
204
247
 
205
248
  You can chose the Wherobots runtime you want to use using the `runtime`
@@ -277,3 +320,21 @@ users may find useful:
277
320
  caller needs to interrupt it (e.g. on client disconnect or timeout).
278
321
  The event is checked before each HTTP request, between retry
279
322
  attempts, and before the WebSocket handshake.
323
+ * `session_key`: keeps this connection on SQL sessions of its own. The
324
+ API reuses a session only for connections that ask with the same key,
325
+ and never one opened without a key, so a heavy query elsewhere never
326
+ holds the session your queries run on. When a new session would exceed
327
+ your organization's quota, the API shares another of your sessions
328
+ under another key instead, with or without `session_key`, and the
329
+ connection's `session_fallback` is `"quota"` (it is `None` otherwise).
330
+ `connection.session_id` names the session.
331
+ * `stale_query_probe_seconds`: how long a running query may go without
332
+ any message from the SQL session before the driver asks the session
333
+ for its state again, in case the query's completion event was lost.
334
+ Repeat probes back off to at most 8 times this interval. Defaults to
335
+ 30 seconds; pass `None` to disable probing.
336
+ * `reconnect_timeout_seconds`: how long the connection keeps trying to
337
+ reconnect to the same SQL session after losing its WebSocket (see
338
+ "Recovering from a lost connection" above). Defaults to 60 seconds,
339
+ which fits in the SQL session's 120-second grace period for the
340
+ queries of a lost connection; pass `None` to turn reconnecting off.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wherobots-python-dbapi"
3
- version = "0.28.2"
3
+ version = "0.30.0"
4
4
  description = "Python DB-API driver for Wherobots DB"
5
5
  authors = [{ name = "Maxime Petazzoni", email = "max@wherobots.com" }]
6
6
  requires-python = ">=3.10, <4"
@@ -3,7 +3,10 @@ from .cursor import Cursor
3
3
  from .driver import connect, connect_direct
4
4
  from .errors import (
5
5
  Error,
6
+ ConnectionLostError,
6
7
  DatabaseError,
8
+ ExecutionCancelledError,
9
+ ExecutionNotFoundError,
7
10
  InternalError,
8
11
  InterfaceError,
9
12
  OperationalError,
@@ -26,6 +29,9 @@ __all__ = [
26
29
  "InternalError",
27
30
  "InterfaceError",
28
31
  "OperationalError",
32
+ "ConnectionLostError",
33
+ "ExecutionCancelledError",
34
+ "ExecutionNotFoundError",
29
35
  "ProgrammingError",
30
36
  "NotSupportedError",
31
37
  "Region",
@@ -0,0 +1,57 @@
1
+ """Transport termination independent of the WebSocket protocol's send lock."""
2
+
3
+ import errno
4
+ import select
5
+ import selectors
6
+ import socket
7
+
8
+ from websockets.sync.client import ClientConnection
9
+
10
+
11
+ def abort_connection(ws: ClientConnection) -> None:
12
+ """Disable further writes and wake blocked I/O before publishing failures.
13
+
14
+ ClientConnection.close() takes the library's protocol mutex, which a
15
+ stalled sendall() may hold. Shutdown the owned socket instead; the library's
16
+ receive thread will observe EOF/error and finish protocol cleanup itself.
17
+ Already transmitted bytes may still execute remotely.
18
+ """
19
+ try:
20
+ ws.socket.shutdown(socket.SHUT_RDWR)
21
+ except OSError as exc:
22
+ if exc.errno not in (errno.ENOTCONN, errno.EBADF, errno.ECONNRESET):
23
+ raise
24
+ finally:
25
+ # Even if shutdown fails, prevent any later send on this socket object.
26
+ ws.socket.close()
27
+
28
+
29
+ def socket_writable(ws: ClientConnection) -> bool:
30
+ """Whether a small write to the transport's socket won't block right now.
31
+
32
+ For a send that must not stall the reader thread: a full send buffer
33
+ (a peer that keeps sending but stops reading) would park sendall(), which
34
+ also holds the library's protocol mutex, so even keepalive pings stall.
35
+
36
+ Never raises. Returns ``True`` (proceed, as without this check) on any
37
+ failure or for a transport without a real socket, such as a test fake:
38
+ ``False`` would silently disable the caller's sends. Uses ``poll`` (or a
39
+ selector), not ``select.select``, which fails for fds above FD_SETSIZE.
40
+ """
41
+ try:
42
+ sock = getattr(ws, "socket", None)
43
+ if not isinstance(sock, socket.socket):
44
+ return True
45
+ fd = sock.fileno()
46
+ if fd < 0:
47
+ return True # Closed: let the send itself report it.
48
+ if hasattr(select, "poll"):
49
+ poller = select.poll()
50
+ poller.register(fd, select.POLLOUT)
51
+ # POLLERR/POLLHUP count as writable: the send then fails fast.
52
+ return bool(poller.poll(0))
53
+ with selectors.DefaultSelector() as selector:
54
+ selector.register(fd, selectors.EVENT_WRITE)
55
+ return bool(selector.select(0))
56
+ except Exception:
57
+ return True