altcodepro-polydb-python 2.5.4__py3-none-any.whl → 2.5.6__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.
- {altcodepro_polydb_python-2.5.4.dist-info → altcodepro_polydb_python-2.5.6.dist-info}/METADATA +69 -20
- {altcodepro_polydb_python-2.5.4.dist-info → altcodepro_polydb_python-2.5.6.dist-info}/RECORD +28 -21
- polydb/__init__.py +1 -1
- polydb/adapters/AWSSecretsManagerAdapter.py +72 -0
- polydb/adapters/AzureKeyVaultAdapter.py +65 -0
- polydb/adapters/AzureQueueAdapter.py +82 -4
- polydb/adapters/DynamoDBAdapter.py +31 -16
- polydb/adapters/FirestoreAdapter.py +30 -13
- polydb/adapters/GCPPubSubAdapter.py +46 -0
- polydb/adapters/GCPSecretManagerAdapter.py +79 -0
- polydb/adapters/KafkaQueueAdapter.py +332 -0
- polydb/adapters/PostgreSQLAdapter.py +114 -6
- polydb/adapters/RabbitMQAdapter.py +465 -0
- polydb/adapters/SQSAdapter.py +60 -0
- polydb/adapters/VaultAdapter.py +75 -0
- polydb/adapters/VercelKVAdapter.py +38 -24
- polydb/adapters/VercelQueueAdapter.py +30 -2
- polydb/base/QueueAdapter.py +123 -1
- polydb/base/SecretsAdapter.py +30 -0
- polydb/cache.py +90 -0
- polydb/cloudDatabaseFactory.py +129 -15
- polydb/databaseFactory.py +225 -49
- polydb/errors.py +6 -0
- polydb/models.py +77 -0
- polydb/retry.py +7 -0
- {altcodepro_polydb_python-2.5.4.dist-info → altcodepro_polydb_python-2.5.6.dist-info}/WHEEL +0 -0
- {altcodepro_polydb_python-2.5.4.dist-info → altcodepro_polydb_python-2.5.6.dist-info}/licenses/LICENSE +0 -0
- {altcodepro_polydb_python-2.5.4.dist-info → altcodepro_polydb_python-2.5.6.dist-info}/top_level.txt +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import os
|
|
3
3
|
import threading
|
|
4
4
|
import time
|
|
5
|
-
from typing import Any, Iterator, List, Optional, Tuple, Union
|
|
5
|
+
from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
|
|
6
6
|
import hashlib
|
|
7
7
|
from contextlib import contextmanager
|
|
8
8
|
import json
|
|
@@ -20,6 +20,20 @@ from ..utils import validate_table_name, validate_column_name
|
|
|
20
20
|
from ..query import QueryBuilder, Operator
|
|
21
21
|
from ..types import JsonDict, Lookup
|
|
22
22
|
|
|
23
|
+
# Postgres session variables this adapter is willing to SET (scoped to one
|
|
24
|
+
# transaction, via set_config(..., is_local=True)) on a caller's behalf.
|
|
25
|
+
# `session_vars` is a keyword-only parameter no request body/query string
|
|
26
|
+
# ever reaches directly -- every caller is trusted engine code, never
|
|
27
|
+
# attacker input -- but this allow-list is a second, independent guard
|
|
28
|
+
# against a future caller (or a bug in one) asking this adapter to SET an
|
|
29
|
+
# arbitrary Postgres GUC (search_path, role, statement_timeout, ...) it was
|
|
30
|
+
# never built to accept. polydb stays tenant-unaware on purpose (see
|
|
31
|
+
# databaseFactory.py's own module docstring) -- this mechanism is generic
|
|
32
|
+
# ("set these session vars for one transaction"), not tenant-specific; the
|
|
33
|
+
# one entry below is simply the one real caller (universal_engine's RLS
|
|
34
|
+
# policies) needs today. Add a name here only when a real caller needs it.
|
|
35
|
+
ALLOWED_SESSION_VARS = frozenset({"app.tenant_id"})
|
|
36
|
+
|
|
23
37
|
|
|
24
38
|
class PostgreSQLAdapter:
|
|
25
39
|
"""PostgreSQL with full LINQ support, connection pooling, JSON/JSONB support"""
|
|
@@ -208,6 +222,18 @@ class PostgreSQLAdapter:
|
|
|
208
222
|
# ---------------------------------------------------------------------
|
|
209
223
|
|
|
210
224
|
def reset_pool(self):
|
|
225
|
+
# `self._lock` is a plain threading.Lock (not reentrant) --
|
|
226
|
+
# _initialize_pool() below acquires it itself, so it must NOT
|
|
227
|
+
# still be held when we call it, or this self-deadlocks (found by
|
|
228
|
+
# actually calling reset_pool() for the first time anywhere in
|
|
229
|
+
# this codebase, in tests/test_session_vars.py's pooled-connection
|
|
230
|
+
# leak test -- no prior caller ever exercised this method). The
|
|
231
|
+
# tiny window here where self._pool is briefly None before
|
|
232
|
+
# _initialize_pool() re-acquires the lock and sets it back is
|
|
233
|
+
# safe: that method's own `if not self._pool` check under its own
|
|
234
|
+
# lock is exactly what already protects concurrent callers of it
|
|
235
|
+
# (e.g. _get_connection()'s own lazy re-init), so it's not a new
|
|
236
|
+
# exposure.
|
|
211
237
|
with self._lock:
|
|
212
238
|
if self._pool:
|
|
213
239
|
try:
|
|
@@ -215,7 +241,7 @@ class PostgreSQLAdapter:
|
|
|
215
241
|
except Exception:
|
|
216
242
|
pass
|
|
217
243
|
self._pool = None
|
|
218
|
-
|
|
244
|
+
self._initialize_pool()
|
|
219
245
|
|
|
220
246
|
def begin_transaction(self) -> Any:
|
|
221
247
|
conn = self._get_connection()
|
|
@@ -223,6 +249,40 @@ class PostgreSQLAdapter:
|
|
|
223
249
|
conn.autocommit = False
|
|
224
250
|
return conn
|
|
225
251
|
|
|
252
|
+
def _apply_session_vars(self, conn: Any, session_vars: Optional[Dict[str, str]]) -> None:
|
|
253
|
+
"""SET LOCAL-equivalent for a fixed, allow-listed set of Postgres
|
|
254
|
+
session variables, scoped to whatever transaction `conn` is
|
|
255
|
+
currently in -- reverts automatically at COMMIT/ROLLBACK, never
|
|
256
|
+
outlives the transaction it was set for. This is the fix for the
|
|
257
|
+
gap README documents: `SET LOCAL <var> = <value>` only means
|
|
258
|
+
anything on the exact connection/transaction the real query after
|
|
259
|
+
it runs on, so it has to be issued here, immediately before the
|
|
260
|
+
query, on the same `conn` -- never as a separate call the caller
|
|
261
|
+
could race against a pool checkin/checkout.
|
|
262
|
+
|
|
263
|
+
Uses `set_config(name, value, is_local)` rather than a literal
|
|
264
|
+
`SET LOCAL {key} = %s` string: `SET` itself has no bind-parameter
|
|
265
|
+
syntax in Postgres (the value has no placeholder form), but
|
|
266
|
+
`set_config` is an ordinary SQL function, so both the name and the
|
|
267
|
+
value are real query parameters -- nothing about either is ever
|
|
268
|
+
f-string'd into the SQL text. `is_local=True` is what gives this
|
|
269
|
+
`SET LOCAL`'s exact semantics (reverts at the end of the current
|
|
270
|
+
transaction) rather than `SET`'s session-lifetime one -- the
|
|
271
|
+
session-lifetime form is exactly the pooled-connection leak this
|
|
272
|
+
whole mechanism exists to avoid (see tests/test_session_vars.py's
|
|
273
|
+
leak-across-checkout proof).
|
|
274
|
+
"""
|
|
275
|
+
if not session_vars:
|
|
276
|
+
return
|
|
277
|
+
with conn.cursor() as cur:
|
|
278
|
+
for key, value in session_vars.items():
|
|
279
|
+
if key not in ALLOWED_SESSION_VARS:
|
|
280
|
+
raise DatabaseError(
|
|
281
|
+
f"session_vars: {key!r} is not in the allow-list of Postgres "
|
|
282
|
+
f"session variables this adapter will set"
|
|
283
|
+
)
|
|
284
|
+
cur.execute("SELECT set_config(%s, %s, true)", [key, str(value)])
|
|
285
|
+
|
|
226
286
|
def commit(self, tx: Any):
|
|
227
287
|
if tx:
|
|
228
288
|
tx.commit()
|
|
@@ -298,7 +358,14 @@ class PostgreSQLAdapter:
|
|
|
298
358
|
# ---------------------------------------------------------------------
|
|
299
359
|
|
|
300
360
|
@retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
|
|
301
|
-
def insert(
|
|
361
|
+
def insert(
|
|
362
|
+
self,
|
|
363
|
+
table: str,
|
|
364
|
+
data: JsonDict,
|
|
365
|
+
tx: Optional[Any] = None,
|
|
366
|
+
*,
|
|
367
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
368
|
+
) -> JsonDict:
|
|
302
369
|
table = validate_table_name(table)
|
|
303
370
|
for k in data.keys():
|
|
304
371
|
validate_column_name(k)
|
|
@@ -310,6 +377,13 @@ class PostgreSQLAdapter:
|
|
|
310
377
|
own_conn = True
|
|
311
378
|
|
|
312
379
|
try:
|
|
380
|
+
# Applied regardless of who owns `conn` -- see this method's
|
|
381
|
+
# own `session_vars` parameter doc on why a caller-supplied tx
|
|
382
|
+
# still honors it (databaseFactory.py's create() is own_conn;
|
|
383
|
+
# universal_engine's idempotency.py/batches.py call insert-
|
|
384
|
+
# shaped methods on their own already-open tx and need this
|
|
385
|
+
# exact same call to take effect on it).
|
|
386
|
+
self._apply_session_vars(conn, session_vars)
|
|
313
387
|
cursor = conn.cursor()
|
|
314
388
|
columns = ", ".join(data.keys())
|
|
315
389
|
placeholders = ", ".join(["%s"] * len(data))
|
|
@@ -343,6 +417,8 @@ class PostgreSQLAdapter:
|
|
|
343
417
|
limit: Optional[int] = None,
|
|
344
418
|
offset: Optional[int] = None,
|
|
345
419
|
tx: Optional[Any] = None,
|
|
420
|
+
*,
|
|
421
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
346
422
|
) -> List[JsonDict]:
|
|
347
423
|
table = validate_table_name(table)
|
|
348
424
|
conn = tx
|
|
@@ -352,6 +428,7 @@ class PostgreSQLAdapter:
|
|
|
352
428
|
own_conn = True
|
|
353
429
|
|
|
354
430
|
try:
|
|
431
|
+
self._apply_session_vars(conn, session_vars)
|
|
355
432
|
cursor = conn.cursor()
|
|
356
433
|
sql = f"SELECT * FROM {table}"
|
|
357
434
|
params: List[Any] = []
|
|
@@ -411,9 +488,13 @@ class PostgreSQLAdapter:
|
|
|
411
488
|
page_size: int,
|
|
412
489
|
continuation_token: Optional[str] = None,
|
|
413
490
|
tx: Optional[Any] = None,
|
|
491
|
+
*,
|
|
492
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
414
493
|
) -> Tuple[List[JsonDict], Optional[str]]:
|
|
415
494
|
offset = int(continuation_token) if continuation_token else 0
|
|
416
|
-
results = self.select(
|
|
495
|
+
results = self.select(
|
|
496
|
+
table, query, limit=page_size + 1, offset=offset, tx=tx, session_vars=session_vars
|
|
497
|
+
)
|
|
417
498
|
has_more = len(results) > page_size
|
|
418
499
|
if has_more:
|
|
419
500
|
results = results[:page_size]
|
|
@@ -431,6 +512,8 @@ class PostgreSQLAdapter:
|
|
|
431
512
|
entity_id: Union[Any, Lookup],
|
|
432
513
|
data: JsonDict,
|
|
433
514
|
tx: Optional[Any] = None,
|
|
515
|
+
*,
|
|
516
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
434
517
|
) -> JsonDict:
|
|
435
518
|
table = validate_table_name(table)
|
|
436
519
|
for k in data.keys():
|
|
@@ -443,6 +526,7 @@ class PostgreSQLAdapter:
|
|
|
443
526
|
own_conn = True
|
|
444
527
|
|
|
445
528
|
try:
|
|
529
|
+
self._apply_session_vars(conn, session_vars)
|
|
446
530
|
cursor = conn.cursor()
|
|
447
531
|
set_clause = ", ".join([f"{k} = %s" for k in data.keys()])
|
|
448
532
|
params: List[Any] = [self._serialize_value(v) for v in data.values()]
|
|
@@ -485,7 +569,14 @@ class PostgreSQLAdapter:
|
|
|
485
569
|
# ---------------------------------------------------------------------
|
|
486
570
|
|
|
487
571
|
@retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
|
|
488
|
-
def upsert(
|
|
572
|
+
def upsert(
|
|
573
|
+
self,
|
|
574
|
+
table: str,
|
|
575
|
+
data: JsonDict,
|
|
576
|
+
tx: Optional[Any] = None,
|
|
577
|
+
*,
|
|
578
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
579
|
+
) -> JsonDict:
|
|
489
580
|
table = validate_table_name(table)
|
|
490
581
|
for k in data.keys():
|
|
491
582
|
validate_column_name(k)
|
|
@@ -497,6 +588,7 @@ class PostgreSQLAdapter:
|
|
|
497
588
|
own_conn = True
|
|
498
589
|
|
|
499
590
|
try:
|
|
591
|
+
self._apply_session_vars(conn, session_vars)
|
|
500
592
|
cursor = conn.cursor()
|
|
501
593
|
columns = ", ".join(data.keys())
|
|
502
594
|
placeholders = ", ".join(["%s"] * len(data))
|
|
@@ -548,7 +640,12 @@ class PostgreSQLAdapter:
|
|
|
548
640
|
|
|
549
641
|
@retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
|
|
550
642
|
def delete(
|
|
551
|
-
self,
|
|
643
|
+
self,
|
|
644
|
+
table: str,
|
|
645
|
+
entity_id: Union[Any, Lookup],
|
|
646
|
+
tx: Optional[Any] = None,
|
|
647
|
+
*,
|
|
648
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
552
649
|
) -> JsonDict:
|
|
553
650
|
table = validate_table_name(table)
|
|
554
651
|
conn = tx
|
|
@@ -558,6 +655,7 @@ class PostgreSQLAdapter:
|
|
|
558
655
|
own_conn = True
|
|
559
656
|
|
|
560
657
|
try:
|
|
658
|
+
self._apply_session_vars(conn, session_vars)
|
|
561
659
|
cursor = conn.cursor()
|
|
562
660
|
params: List[Any] = []
|
|
563
661
|
if isinstance(entity_id, dict):
|
|
@@ -742,6 +840,7 @@ class PostgreSQLAdapter:
|
|
|
742
840
|
*,
|
|
743
841
|
fetch: bool = False,
|
|
744
842
|
fetch_one: bool = False,
|
|
843
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
745
844
|
) -> Union[None, JsonDict, List[JsonDict]]:
|
|
746
845
|
conn = tx
|
|
747
846
|
own_conn = False
|
|
@@ -751,6 +850,15 @@ class PostgreSQLAdapter:
|
|
|
751
850
|
|
|
752
851
|
cursor = None
|
|
753
852
|
try:
|
|
853
|
+
# execute() is the raw-SQL escape hatch a few callers already
|
|
854
|
+
# use for a query with no generic-CRUD equivalent (an atomic
|
|
855
|
+
# claim, a bulk UPDATE, an ORDER BY) -- those are exactly the
|
|
856
|
+
# callers RLS-protected engine-infrastructure tables need this
|
|
857
|
+
# for (see universal_engine's idempotency.py/batches.py),
|
|
858
|
+
# since they run several statements against the same `tx` and
|
|
859
|
+
# only need the session var set once per transaction, same as
|
|
860
|
+
# every other method above.
|
|
861
|
+
self._apply_session_vars(conn, session_vars)
|
|
754
862
|
cursor = conn.cursor()
|
|
755
863
|
self.logger.debug("Executing raw SQL (%d params)", len(params or []))
|
|
756
864
|
exec_params = self._serialize_params(params or [])
|