altcodepro-polydb-python 2.5.4__py3-none-any.whl → 2.5.7__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.7.dist-info}/METADATA +69 -20
- {altcodepro_polydb_python-2.5.4.dist-info → altcodepro_polydb_python-2.5.7.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 +262 -60
- 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.7.dist-info}/WHEEL +0 -0
- {altcodepro_polydb_python-2.5.4.dist-info → altcodepro_polydb_python-2.5.7.dist-info}/licenses/LICENSE +0 -0
- {altcodepro_polydb_python-2.5.4.dist-info → altcodepro_polydb_python-2.5.7.dist-info}/top_level.txt +0 -0
polydb/databaseFactory.py
CHANGED
|
@@ -39,6 +39,16 @@ import re as _re
|
|
|
39
39
|
|
|
40
40
|
logger = logging.getLogger(__name__)
|
|
41
41
|
|
|
42
|
+
# queue.receive/dlq_list's own max_messages had no artificial cap --
|
|
43
|
+
# bounded only by real queue depth, which lets a caller-supplied
|
|
44
|
+
# max_messages ask for an unbounded batch. Operator-configurable, the
|
|
45
|
+
# same POLYDB_QUEUE_VISIBILITY_TIMEOUT env-var precedent
|
|
46
|
+
# AzureQueueAdapter.DEFAULT_VISIBILITY_TIMEOUT already establishes --
|
|
47
|
+
# config-driven, never a manifest/caller-suppliable value. A caller
|
|
48
|
+
# asking for fewer than this still gets exactly what it asked for;
|
|
49
|
+
# only a request above the cap is silently clamped down to it.
|
|
50
|
+
QUEUE_RECEIVE_MAX_MESSAGES_CAP = int(os.environ.get("POLYDB_QUEUE_RECEIVE_MAX_MESSAGES") or 1000)
|
|
51
|
+
|
|
42
52
|
_DEFAULT_RETRY = retry(
|
|
43
53
|
wait=wait_exponential(multiplier=0.5, min=0.5, max=6),
|
|
44
54
|
stop=stop_after_attempt(3),
|
|
@@ -68,9 +78,9 @@ def _parse_unique_violation_columns(exc: BaseException) -> list:
|
|
|
68
78
|
return [c.strip() for c in m.group(1).split(",") if c.strip()]
|
|
69
79
|
|
|
70
80
|
|
|
71
|
-
#
|
|
81
|
+
# ═══════════════════════════════════════════════════════════════════════════════════
|
|
72
82
|
# ENGINE CONFIG
|
|
73
|
-
#
|
|
83
|
+
# ═══════════════════════════════════════════════════════════════════════════════════
|
|
74
84
|
|
|
75
85
|
|
|
76
86
|
@dataclass
|
|
@@ -114,9 +124,9 @@ class _ResolvedAdapters:
|
|
|
114
124
|
engine_name: str
|
|
115
125
|
|
|
116
126
|
|
|
117
|
-
#
|
|
127
|
+
# ════════════════════════════════════════════════════════════════════════════════════
|
|
118
128
|
# MODEL META RESOLUTION (lightweight — no registry enforcement)
|
|
119
|
-
#
|
|
129
|
+
# ════════════════════════════════════════════════════════════════════════════════════
|
|
120
130
|
|
|
121
131
|
|
|
122
132
|
def _extract_meta(model: Union[type, str]) -> ModelMeta:
|
|
@@ -140,9 +150,9 @@ def _model_name(model: Union[type, str]) -> str:
|
|
|
140
150
|
return model.__name__ if isinstance(model, type) else str(model)
|
|
141
151
|
|
|
142
152
|
|
|
143
|
-
#
|
|
153
|
+
# ════════════════════════════════════════════════════════════════════════════════════
|
|
144
154
|
# DATABASE FACTORY
|
|
145
|
-
#
|
|
155
|
+
# ═══════════════════════════════════════════════════════════════════════════════════
|
|
146
156
|
|
|
147
157
|
|
|
148
158
|
class DatabaseFactory:
|
|
@@ -226,16 +236,16 @@ class DatabaseFactory:
|
|
|
226
236
|
self._engine_by_name: Dict[str, EngineConfig] = {e.name: e for e in self._engines}
|
|
227
237
|
self._provider_name = self._engines[0].cloud_factory.provider.value
|
|
228
238
|
|
|
229
|
-
#
|
|
239
|
+
# ─────────────────────────────────────────────────────────────
|
|
230
240
|
# ENGINE ROUTING
|
|
231
|
-
#
|
|
241
|
+
# ─────────────────────────────────────────────────────────────
|
|
232
242
|
|
|
233
243
|
def _resolve_adapters(
|
|
234
244
|
self, model_name: str, storage: str, override: Optional[EngineOverride] = None
|
|
235
245
|
) -> _ResolvedAdapters:
|
|
236
246
|
# Only construct the adapter this call actually needs. Every caller
|
|
237
247
|
# (create/read/update/upsert/delete/query*) branches on `storage` and
|
|
238
|
-
# touches exactly one of .sql/.nosql
|
|
248
|
+
# touches exactly one of .sql/.nosql -- building both unconditionally
|
|
239
249
|
# meant a purely-SQL deployment with no reachable NoSQL backend (or
|
|
240
250
|
# vice versa) failed on every single call, not just ones that needed
|
|
241
251
|
# the missing engine.
|
|
@@ -280,9 +290,9 @@ class DatabaseFactory:
|
|
|
280
290
|
storage = "sql" if (meta.storage == "sql" and meta.table) else "nosql"
|
|
281
291
|
return self._resolve_adapters(name, storage, override)
|
|
282
292
|
|
|
283
|
-
#
|
|
293
|
+
# ──────────────────────────────────────────────────────────
|
|
284
294
|
# ENGINE MANAGEMENT
|
|
285
|
-
#
|
|
295
|
+
# ──────────────────────────────────────────────────────────
|
|
286
296
|
|
|
287
297
|
def register_engine(self, engine: EngineConfig) -> None:
|
|
288
298
|
if engine.name in self._engine_by_name:
|
|
@@ -315,9 +325,9 @@ class DatabaseFactory:
|
|
|
315
325
|
return e.nosql()
|
|
316
326
|
return self._engines[0].nosql()
|
|
317
327
|
|
|
318
|
-
#
|
|
328
|
+
# ──────────────────────────────────────────────────────────
|
|
319
329
|
# HELPERS
|
|
320
|
-
#
|
|
330
|
+
# ──────────────────────────────────────────────────────────
|
|
321
331
|
|
|
322
332
|
def _inject_audit_fields(self, data: JsonDict, is_create: bool = False) -> JsonDict:
|
|
323
333
|
data = dict(data)
|
|
@@ -349,9 +359,9 @@ class DatabaseFactory:
|
|
|
349
359
|
return False
|
|
350
360
|
return meta.storage == "sql" and bool(meta.table)
|
|
351
361
|
|
|
352
|
-
#
|
|
362
|
+
# ──────────────────────────────────────────────────────────
|
|
353
363
|
# CREATE
|
|
354
|
-
#
|
|
364
|
+
# ──────────────────────────────────────────────────────────
|
|
355
365
|
|
|
356
366
|
def create(
|
|
357
367
|
self,
|
|
@@ -359,6 +369,7 @@ class DatabaseFactory:
|
|
|
359
369
|
data: JsonDict,
|
|
360
370
|
*,
|
|
361
371
|
engine_override: Optional[EngineOverride] = None,
|
|
372
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
362
373
|
) -> JsonDict:
|
|
363
374
|
meta = _extract_meta(model)
|
|
364
375
|
name = _model_name(model)
|
|
@@ -378,7 +389,7 @@ class DatabaseFactory:
|
|
|
378
389
|
nonlocal after_plain, success, entity_id
|
|
379
390
|
if self._is_sql(meta, engine_override):
|
|
380
391
|
try:
|
|
381
|
-
result = adapters.sql.insert(meta.table, data)
|
|
392
|
+
result = adapters.sql.insert(meta.table, data, session_vars=session_vars)
|
|
382
393
|
except Exception as exc:
|
|
383
394
|
if not _is_unique_violation(exc):
|
|
384
395
|
raise
|
|
@@ -392,7 +403,9 @@ class DatabaseFactory:
|
|
|
392
403
|
conflict_cols,
|
|
393
404
|
)
|
|
394
405
|
update_data = {k: v for k, v in data.items() if k not in conflict_cols}
|
|
395
|
-
result = adapters.sql.update(
|
|
406
|
+
result = adapters.sql.update(
|
|
407
|
+
meta.table, where, update_data, session_vars=session_vars
|
|
408
|
+
)
|
|
396
409
|
else:
|
|
397
410
|
result = adapters.nosql.put(
|
|
398
411
|
(
|
|
@@ -427,9 +440,9 @@ class DatabaseFactory:
|
|
|
427
440
|
except Exception:
|
|
428
441
|
raise
|
|
429
442
|
|
|
430
|
-
#
|
|
443
|
+
# ──────────────────────────────────────────────────────────
|
|
431
444
|
# READ
|
|
432
|
-
#
|
|
445
|
+
# ──────────────────────────────────────────────────────────
|
|
433
446
|
|
|
434
447
|
def read(
|
|
435
448
|
self,
|
|
@@ -442,6 +455,7 @@ class DatabaseFactory:
|
|
|
442
455
|
cache_ttl: Optional[int] = None,
|
|
443
456
|
include_deleted: bool = False,
|
|
444
457
|
engine_override: Optional[EngineOverride] = None,
|
|
458
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
445
459
|
) -> List[JsonDict]:
|
|
446
460
|
name = _model_name(model)
|
|
447
461
|
meta = _extract_meta(model)
|
|
@@ -455,7 +469,9 @@ class DatabaseFactory:
|
|
|
455
469
|
|
|
456
470
|
def _op() -> List[JsonDict]:
|
|
457
471
|
if self._is_sql(meta, engine_override):
|
|
458
|
-
raw = adapters.sql.select(
|
|
472
|
+
raw = adapters.sql.select(
|
|
473
|
+
meta.table, query, limit=limit, offset=offset, session_vars=session_vars
|
|
474
|
+
)
|
|
459
475
|
else:
|
|
460
476
|
cls = (
|
|
461
477
|
model
|
|
@@ -496,6 +512,7 @@ class DatabaseFactory:
|
|
|
496
512
|
no_cache: bool = False,
|
|
497
513
|
include_deleted: bool = False,
|
|
498
514
|
engine_override: Optional[EngineOverride] = None,
|
|
515
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
499
516
|
) -> Optional[JsonDict]:
|
|
500
517
|
rows = self.read(
|
|
501
518
|
model,
|
|
@@ -504,12 +521,13 @@ class DatabaseFactory:
|
|
|
504
521
|
no_cache=no_cache,
|
|
505
522
|
include_deleted=include_deleted,
|
|
506
523
|
engine_override=engine_override,
|
|
524
|
+
session_vars=session_vars,
|
|
507
525
|
)
|
|
508
526
|
return rows[0] if rows else None
|
|
509
527
|
|
|
510
|
-
#
|
|
528
|
+
# ──────────────────────────────────────────────────────────
|
|
511
529
|
# UPDATE
|
|
512
|
-
#
|
|
530
|
+
# ──────────────────────────────────────────────────────────
|
|
513
531
|
|
|
514
532
|
def update(
|
|
515
533
|
self,
|
|
@@ -520,6 +538,7 @@ class DatabaseFactory:
|
|
|
520
538
|
etag: Optional[str] = None,
|
|
521
539
|
replace: bool = False,
|
|
522
540
|
engine_override: Optional[EngineOverride] = None,
|
|
541
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
523
542
|
) -> JsonDict:
|
|
524
543
|
name = _model_name(model)
|
|
525
544
|
meta = _extract_meta(model)
|
|
@@ -536,6 +555,7 @@ class DatabaseFactory:
|
|
|
536
555
|
no_cache=True,
|
|
537
556
|
include_deleted=True,
|
|
538
557
|
engine_override=engine_override,
|
|
558
|
+
session_vars=session_vars,
|
|
539
559
|
)
|
|
540
560
|
after_plain = None
|
|
541
561
|
success = False
|
|
@@ -544,7 +564,7 @@ class DatabaseFactory:
|
|
|
544
564
|
def _op() -> JsonDict:
|
|
545
565
|
nonlocal after_plain, success
|
|
546
566
|
if self._is_sql(meta, engine_override):
|
|
547
|
-
result = adapters.sql.update(meta.table, entity_id, data)
|
|
567
|
+
result = adapters.sql.update(meta.table, entity_id, data, session_vars=session_vars)
|
|
548
568
|
else:
|
|
549
569
|
# A scalar entity_id is usually the record's "id" property,
|
|
550
570
|
# not its physical PartitionKey/RowKey — those come from the
|
|
@@ -556,20 +576,40 @@ class DatabaseFactory:
|
|
|
556
576
|
# `before`, so a plain id-addressed update() still lands on
|
|
557
577
|
# the entity's real physical key instead of a mismatched
|
|
558
578
|
# default partition / the id used as a row key it never had.
|
|
579
|
+
#
|
|
580
|
+
# meta.pk_field is None for the overwhelming majority of
|
|
581
|
+
# models (nothing declares an explicit partition/sort key) —
|
|
582
|
+
# that does NOT mean there's no partition key to recover, it
|
|
583
|
+
# means the adapter's own real default applies instead
|
|
584
|
+
# (NoSQLKVAdapter._get_pk_rk/_pk_rk_field_names: "tenant_id"
|
|
585
|
+
# / "id"). Resolving against meta.pk_field alone and giving
|
|
586
|
+
# up when it's unset silently produced pkey=None here, which
|
|
587
|
+
# patch()'s own dict-shaped entity_id branch has no fallback
|
|
588
|
+
# for either — the combination wrote every default-tier
|
|
589
|
+
# update to a bogus PartitionKey="None" row instead of the
|
|
590
|
+
# real one. Also check entity_id itself first (the caller's
|
|
591
|
+
# own already-resolved tenant/partition value, e.g.
|
|
592
|
+
# core_db.py's db_update passing {"id":..., "tenant_id":...})
|
|
593
|
+
# — it's more direct and just as authoritative as `before`.
|
|
594
|
+
pk_field_name = meta.pk_field or "tenant_id"
|
|
595
|
+
rk_field_name = meta.rk_field or "id"
|
|
559
596
|
pkey = data.get("PartitionKey") or data.get("partition_key") or data.get("pk")
|
|
560
|
-
if not pkey
|
|
561
|
-
pkey = data.get(
|
|
597
|
+
if not pkey:
|
|
598
|
+
pkey = data.get(pk_field_name)
|
|
599
|
+
if not pkey and isinstance(entity_id, dict):
|
|
600
|
+
pkey = entity_id.get(pk_field_name)
|
|
562
601
|
if not pkey and before:
|
|
563
602
|
pkey = (
|
|
564
603
|
before.get("PartitionKey")
|
|
565
604
|
or before.get("partition_key")
|
|
566
605
|
or before.get("pk")
|
|
567
|
-
or
|
|
606
|
+
or before.get("_pk")
|
|
607
|
+
or before.get(pk_field_name)
|
|
568
608
|
)
|
|
569
609
|
rkey = None
|
|
570
|
-
if
|
|
571
|
-
rkey = data.get(
|
|
572
|
-
before.get(
|
|
610
|
+
if rk_field_name != "id":
|
|
611
|
+
rkey = data.get(rk_field_name) or (
|
|
612
|
+
before.get(rk_field_name) if before else None
|
|
573
613
|
)
|
|
574
614
|
en_id = entity_id
|
|
575
615
|
if pkey:
|
|
@@ -623,9 +663,9 @@ class DatabaseFactory:
|
|
|
623
663
|
except Exception:
|
|
624
664
|
raise
|
|
625
665
|
|
|
626
|
-
#
|
|
666
|
+
# ──────────────────────────────────────────────────────────
|
|
627
667
|
# UPSERT
|
|
628
|
-
#
|
|
668
|
+
# ──────────────────────────────────────────────────────────
|
|
629
669
|
|
|
630
670
|
def upsert(
|
|
631
671
|
self,
|
|
@@ -634,6 +674,7 @@ class DatabaseFactory:
|
|
|
634
674
|
*,
|
|
635
675
|
replace: bool = False,
|
|
636
676
|
engine_override: Optional[EngineOverride] = None,
|
|
677
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
637
678
|
) -> JsonDict:
|
|
638
679
|
name = _model_name(model)
|
|
639
680
|
meta = _extract_meta(model)
|
|
@@ -651,7 +692,7 @@ class DatabaseFactory:
|
|
|
651
692
|
def _op() -> JsonDict:
|
|
652
693
|
nonlocal after_plain, success
|
|
653
694
|
if self._is_sql(meta, engine_override):
|
|
654
|
-
result = adapters.sql.upsert(meta.table, data)
|
|
695
|
+
result = adapters.sql.upsert(meta.table, data, session_vars=session_vars)
|
|
655
696
|
else:
|
|
656
697
|
cls = (
|
|
657
698
|
model
|
|
@@ -683,9 +724,9 @@ class DatabaseFactory:
|
|
|
683
724
|
except Exception:
|
|
684
725
|
raise
|
|
685
726
|
|
|
686
|
-
#
|
|
727
|
+
# ──────────────────────────────────────────────────────────
|
|
687
728
|
# DELETE
|
|
688
|
-
#
|
|
729
|
+
# ──────────────────────────────────────────────────────────
|
|
689
730
|
|
|
690
731
|
def delete(
|
|
691
732
|
self,
|
|
@@ -695,6 +736,7 @@ class DatabaseFactory:
|
|
|
695
736
|
etag: Optional[str] = None,
|
|
696
737
|
hard: bool = False,
|
|
697
738
|
engine_override: Optional[EngineOverride] = None,
|
|
739
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
698
740
|
) -> JsonDict:
|
|
699
741
|
meta = _extract_meta(model)
|
|
700
742
|
name = _model_name(model)
|
|
@@ -708,6 +750,7 @@ class DatabaseFactory:
|
|
|
708
750
|
"deleted_by": AuditContext.actor_id.get(),
|
|
709
751
|
},
|
|
710
752
|
engine_override=engine_override,
|
|
753
|
+
session_vars=session_vars,
|
|
711
754
|
)
|
|
712
755
|
|
|
713
756
|
adapters = self._adapters_for(model, meta, engine_override)
|
|
@@ -717,6 +760,7 @@ class DatabaseFactory:
|
|
|
717
760
|
no_cache=True,
|
|
718
761
|
include_deleted=True,
|
|
719
762
|
engine_override=engine_override,
|
|
763
|
+
session_vars=session_vars,
|
|
720
764
|
)
|
|
721
765
|
success = False
|
|
722
766
|
error: Optional[str] = None
|
|
@@ -724,7 +768,7 @@ class DatabaseFactory:
|
|
|
724
768
|
def _op() -> JsonDict:
|
|
725
769
|
nonlocal success
|
|
726
770
|
if self._is_sql(meta, engine_override):
|
|
727
|
-
result = adapters.sql.delete(meta.table, entity_id)
|
|
771
|
+
result = adapters.sql.delete(meta.table, entity_id, session_vars=session_vars)
|
|
728
772
|
else:
|
|
729
773
|
cls = (
|
|
730
774
|
model
|
|
@@ -733,19 +777,44 @@ class DatabaseFactory:
|
|
|
733
777
|
)
|
|
734
778
|
# Same physical-key recovery as update() (see there for why a
|
|
735
779
|
# scalar entity_id alone isn't enough once a model's pk_field
|
|
736
|
-
# /rk_field differ from "id"
|
|
780
|
+
# /rk_field differ from "id", and why meta.pk_field being
|
|
781
|
+
# unset must fall back to the adapter's own real default
|
|
782
|
+
# ("tenant_id"/"id"), never a silent no-op).
|
|
783
|
+
pk_field_name = meta.pk_field or "tenant_id"
|
|
784
|
+
rk_field_name = meta.rk_field or "id"
|
|
737
785
|
en_id = entity_id
|
|
738
|
-
|
|
786
|
+
pkey = None
|
|
787
|
+
if isinstance(entity_id, dict):
|
|
788
|
+
pkey = entity_id.get(pk_field_name)
|
|
789
|
+
if not pkey and before:
|
|
739
790
|
pkey = (
|
|
740
791
|
before.get("PartitionKey")
|
|
741
792
|
or before.get("partition_key")
|
|
742
793
|
or before.get("pk")
|
|
743
|
-
or
|
|
794
|
+
or before.get("_pk")
|
|
795
|
+
or before.get(pk_field_name)
|
|
744
796
|
)
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
797
|
+
if pkey:
|
|
798
|
+
rkey = None
|
|
799
|
+
if rk_field_name != "id":
|
|
800
|
+
rkey = before.get(rk_field_name) if before else None
|
|
801
|
+
if isinstance(en_id, dict):
|
|
802
|
+
en_pk = (
|
|
803
|
+
en_id.get("PartitionKey")
|
|
804
|
+
or en_id.get("partition_key")
|
|
805
|
+
or en_id.get("pk")
|
|
806
|
+
)
|
|
807
|
+
if not en_pk:
|
|
808
|
+
en_id["partition_key"] = pkey
|
|
809
|
+
en_rk = (
|
|
810
|
+
en_id.get("RowKey")
|
|
811
|
+
or en_id.get("row_key")
|
|
812
|
+
or en_id.get("rk")
|
|
813
|
+
or en_id.get("id")
|
|
814
|
+
)
|
|
815
|
+
if not en_rk and rkey:
|
|
816
|
+
en_id["row_key"] = rkey
|
|
817
|
+
else:
|
|
749
818
|
en_id = {"partition_key": pkey, "row_key": rkey or entity_id}
|
|
750
819
|
result = adapters.nosql.delete(cls, en_id, etag=etag)
|
|
751
820
|
success = True
|
|
@@ -769,9 +838,9 @@ class DatabaseFactory:
|
|
|
769
838
|
except Exception:
|
|
770
839
|
raise
|
|
771
840
|
|
|
772
|
-
#
|
|
841
|
+
# ──────────────────────────────────────────────────────────
|
|
773
842
|
# QUERY (LINQ-style)
|
|
774
|
-
#
|
|
843
|
+
# ──────────────────────────────────────────────────────────
|
|
775
844
|
|
|
776
845
|
def query_linq(
|
|
777
846
|
self,
|
|
@@ -803,9 +872,9 @@ class DatabaseFactory:
|
|
|
803
872
|
return result
|
|
804
873
|
return self._run(_op)
|
|
805
874
|
|
|
806
|
-
#
|
|
875
|
+
# ──────────────────────────────────────────────────────────
|
|
807
876
|
# PAGINATION (legacy simple)
|
|
808
|
-
#
|
|
877
|
+
# ──────────────────────────────────────────────────────────
|
|
809
878
|
|
|
810
879
|
def read_page(
|
|
811
880
|
self,
|
|
@@ -852,9 +921,9 @@ class DatabaseFactory:
|
|
|
852
921
|
return result
|
|
853
922
|
return self._run(_op)
|
|
854
923
|
|
|
855
|
-
#
|
|
924
|
+
# ──────────────────────────────────────────────────────────
|
|
856
925
|
# PAGINATION (generic — order_by, cursor, field projection)
|
|
857
|
-
#
|
|
926
|
+
# ──────────────────────────────────────────────────────────
|
|
858
927
|
|
|
859
928
|
def query_paged(
|
|
860
929
|
self,
|
|
@@ -877,15 +946,12 @@ class DatabaseFactory:
|
|
|
877
946
|
if self._is_sql(meta, engine_override):
|
|
878
947
|
return adapters.sql.query_paged(meta.table, request)
|
|
879
948
|
|
|
880
|
-
cls = (
|
|
881
|
-
model if isinstance(model, type)
|
|
882
|
-
else type(name, (), {"__polydb__": meta.__dict__})
|
|
883
|
-
)
|
|
949
|
+
cls = model if isinstance(model, type) else type(name, (), {"__polydb__": meta.__dict__})
|
|
884
950
|
return adapters.nosql.query_paged(cls, request)
|
|
885
951
|
|
|
886
|
-
#
|
|
952
|
+
# ════════════════════════════════════════════════════════════════════════════════════
|
|
887
953
|
# BLOB STORAGE
|
|
888
|
-
#
|
|
954
|
+
# ═══════════════════════════════════════════════════════════════════════════════════
|
|
889
955
|
|
|
890
956
|
def upload_blob(
|
|
891
957
|
self,
|
|
@@ -934,9 +1000,9 @@ class DatabaseFactory:
|
|
|
934
1000
|
)
|
|
935
1001
|
return storage.list(prefix)
|
|
936
1002
|
|
|
937
|
-
#
|
|
1003
|
+
# ════════════════════════════════════════════════════════════════════════════════════
|
|
938
1004
|
# QUEUE
|
|
939
|
-
#
|
|
1005
|
+
# ═══════════════════════════════════════════════════════════════════════════════════
|
|
940
1006
|
|
|
941
1007
|
def send_queue(
|
|
942
1008
|
self,
|
|
@@ -956,6 +1022,7 @@ class DatabaseFactory:
|
|
|
956
1022
|
adapter_name: str = "azure_queue",
|
|
957
1023
|
) -> List[Dict[str, Any]]:
|
|
958
1024
|
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1025
|
+
max_messages = min(max_messages, QUEUE_RECEIVE_MAX_MESSAGES_CAP)
|
|
959
1026
|
return queue.receive(queue_name=queue_name, max_messages=max_messages)
|
|
960
1027
|
|
|
961
1028
|
def ack_queue(
|
|
@@ -983,9 +1050,80 @@ class DatabaseFactory:
|
|
|
983
1050
|
else queue.delete(message_id, queue_name)
|
|
984
1051
|
)
|
|
985
1052
|
|
|
986
|
-
|
|
1053
|
+
def nack_queue(
|
|
1054
|
+
self, ack_id: str, *, queue_name: str = "default", adapter_name: str = "azure_queue"
|
|
1055
|
+
) -> bool:
|
|
1056
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1057
|
+
return queue.nack(ack_id, queue_name)
|
|
1058
|
+
|
|
1059
|
+
def purge_queue(self, *, queue_name: str = "default", adapter_name: str = "azure_queue") -> int:
|
|
1060
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1061
|
+
return queue.purge(queue_name)
|
|
1062
|
+
|
|
1063
|
+
def declare_queue(
|
|
1064
|
+
self,
|
|
1065
|
+
*,
|
|
1066
|
+
queue_name: str = "default",
|
|
1067
|
+
durable: bool = True,
|
|
1068
|
+
dead_letter_queue: Optional[str] = None,
|
|
1069
|
+
adapter_name: str = "azure_queue",
|
|
1070
|
+
) -> bool:
|
|
1071
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1072
|
+
return queue.declare(queue_name, durable=durable, dead_letter_queue=dead_letter_queue)
|
|
1073
|
+
|
|
1074
|
+
def queue_status(
|
|
1075
|
+
self, *, queue_name: str = "default", adapter_name: str = "azure_queue"
|
|
1076
|
+
) -> Dict[str, Any]:
|
|
1077
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1078
|
+
return queue.status(queue_name)
|
|
1079
|
+
|
|
1080
|
+
# extend/delay/cancel -- real only where the underlying adapter
|
|
1081
|
+
# genuinely implements one (see QueueAdapter's own module comment
|
|
1082
|
+
# for the full per-backend feasibility matrix). An adapter that
|
|
1083
|
+
# doesn't implement a given method raises NotImplementedError by
|
|
1084
|
+
# name, propagated here unchanged rather than caught and faked.
|
|
1085
|
+
|
|
1086
|
+
def extend_queue(
|
|
1087
|
+
self,
|
|
1088
|
+
ack_id: str,
|
|
1089
|
+
*,
|
|
1090
|
+
queue_name: str = "default",
|
|
1091
|
+
visibility_timeout: int = 30,
|
|
1092
|
+
adapter_name: str = "azure_queue",
|
|
1093
|
+
) -> bool:
|
|
1094
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1095
|
+
return queue.extend(ack_id, queue_name, visibility_timeout=visibility_timeout)
|
|
1096
|
+
|
|
1097
|
+
def delay_queue(
|
|
1098
|
+
self,
|
|
1099
|
+
message: Dict[str, Any],
|
|
1100
|
+
*,
|
|
1101
|
+
queue_name: str = "default",
|
|
1102
|
+
delay_seconds: int = 0,
|
|
1103
|
+
adapter_name: str = "azure_queue",
|
|
1104
|
+
) -> str:
|
|
1105
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1106
|
+
return queue.delay(message, queue_name, delay_seconds=delay_seconds)
|
|
1107
|
+
|
|
1108
|
+
def cancel_queue(
|
|
1109
|
+
self,
|
|
1110
|
+
message_id: str,
|
|
1111
|
+
*,
|
|
1112
|
+
queue_name: str = "default",
|
|
1113
|
+
adapter_name: str = "azure_queue",
|
|
1114
|
+
**kwargs: Any,
|
|
1115
|
+
) -> bool:
|
|
1116
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1117
|
+
# RabbitMQAdapter.cancel needs a `delay_seconds` kwarg (which
|
|
1118
|
+
# per-duration delay queue to scan) that no other adapter's own
|
|
1119
|
+
# cancel() takes -- passed through only when the caller supplies
|
|
1120
|
+
# it, so every other adapter's plain cancel(message_id,
|
|
1121
|
+
# queue_name) signature is untouched.
|
|
1122
|
+
return queue.cancel(message_id, queue_name, **kwargs)
|
|
1123
|
+
|
|
1124
|
+
# ════════════════════════════════════════════════════════════════════════════════════
|
|
987
1125
|
# FILE STORAGE
|
|
988
|
-
#
|
|
1126
|
+
# ═══════════════════════════════════════════════════════════════════════════════════
|
|
989
1127
|
|
|
990
1128
|
def write_file(
|
|
991
1129
|
self, path: str, data: Union[bytes, str], *, adapter_name: str = "files"
|
|
@@ -1005,9 +1143,9 @@ class DatabaseFactory:
|
|
|
1005
1143
|
files = self._engines[0].cloud_factory.get_files(adapter_name)
|
|
1006
1144
|
return files.list(directory)
|
|
1007
1145
|
|
|
1008
|
-
#
|
|
1146
|
+
# ════════════════════════════════════════════════════════════════════════════════════
|
|
1009
1147
|
# CACHE
|
|
1010
|
-
#
|
|
1148
|
+
# ═══════════════════════════════════════════════════════════════════════════════════
|
|
1011
1149
|
|
|
1012
1150
|
def set_cache(self, model: str, key: Any, value: Any, ttl: int = 300) -> None:
|
|
1013
1151
|
if self._cache:
|
|
@@ -1023,3 +1161,67 @@ class DatabaseFactory:
|
|
|
1023
1161
|
self._cache.invalidate(model, key)
|
|
1024
1162
|
else:
|
|
1025
1163
|
self._cache.clear()
|
|
1164
|
+
|
|
1165
|
+
# ------------------------------------------------------------------
|
|
1166
|
+
# Generic Redis KV -- distinct from set_cache/get_cache/invalidate_cache
|
|
1167
|
+
# above (which back the automatic, best-effort query-result cache and
|
|
1168
|
+
# degrade silently with no Redis configured). These back a caller's
|
|
1169
|
+
# own explicit business logic reached from outside this class (e.g. a
|
|
1170
|
+
# manifest workflow step's "redis" integration doing rate limiting or
|
|
1171
|
+
# a distributed lock), so a missing/misconfigured cache backend raises
|
|
1172
|
+
# CacheError instead of returning a silently-wrong 0/None/False.
|
|
1173
|
+
# ------------------------------------------------------------------
|
|
1174
|
+
|
|
1175
|
+
def redis_get(self, key: str, *, namespace: str = "generic") -> Optional[Any]:
|
|
1176
|
+
from .errors import CacheError
|
|
1177
|
+
|
|
1178
|
+
if not self._cache:
|
|
1179
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1180
|
+
return self._cache.get_raw(namespace, key)
|
|
1181
|
+
|
|
1182
|
+
def redis_set(
|
|
1183
|
+
self, key: str, value: Any, *, namespace: str = "generic", ttl: Optional[int] = None
|
|
1184
|
+
) -> bool:
|
|
1185
|
+
from .errors import CacheError
|
|
1186
|
+
|
|
1187
|
+
if not self._cache:
|
|
1188
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1189
|
+
return self._cache.set_raw(namespace, key, value, ttl)
|
|
1190
|
+
|
|
1191
|
+
def redis_delete(self, key: str, *, namespace: str = "generic") -> None:
|
|
1192
|
+
from .errors import CacheError
|
|
1193
|
+
|
|
1194
|
+
if not self._cache:
|
|
1195
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1196
|
+
self._cache.delete_key(namespace, key)
|
|
1197
|
+
|
|
1198
|
+
def redis_incr(self, key: str, *, namespace: str = "generic", amount: int = 1) -> int:
|
|
1199
|
+
from .errors import CacheError
|
|
1200
|
+
|
|
1201
|
+
if not self._cache:
|
|
1202
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1203
|
+
return self._cache.incrby(namespace, key, amount)
|
|
1204
|
+
|
|
1205
|
+
def redis_decr(self, key: str, *, namespace: str = "generic", amount: int = 1) -> int:
|
|
1206
|
+
return self.redis_incr(key, namespace=namespace, amount=-amount)
|
|
1207
|
+
|
|
1208
|
+
def redis_exists(self, key: str, *, namespace: str = "generic") -> bool:
|
|
1209
|
+
from .errors import CacheError
|
|
1210
|
+
|
|
1211
|
+
if not self._cache:
|
|
1212
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1213
|
+
return self._cache.exists_raw(namespace, key)
|
|
1214
|
+
|
|
1215
|
+
def redis_ttl(self, key: str, *, namespace: str = "generic") -> int:
|
|
1216
|
+
from .errors import CacheError
|
|
1217
|
+
|
|
1218
|
+
if not self._cache:
|
|
1219
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1220
|
+
return self._cache.ttl_raw(namespace, key)
|
|
1221
|
+
|
|
1222
|
+
def redis_expire(self, key: str, ttl: int, *, namespace: str = "generic") -> None:
|
|
1223
|
+
from .errors import CacheError
|
|
1224
|
+
|
|
1225
|
+
if not self._cache:
|
|
1226
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1227
|
+
self._cache.expire_key(namespace, key, ttl)
|
polydb/errors.py
CHANGED
|
@@ -41,6 +41,12 @@ class ConnectionError(CloudDBError):
|
|
|
41
41
|
pass
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
class CacheError(CloudDBError):
|
|
45
|
+
"""Cache/Redis operation failed, or no cache backend is configured"""
|
|
46
|
+
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
44
50
|
class ValidationError(CloudDBError):
|
|
45
51
|
"""Input validation failed"""
|
|
46
52
|
|