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
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
|
|
@@ -564,6 +584,7 @@ class DatabaseFactory:
|
|
|
564
584
|
before.get("PartitionKey")
|
|
565
585
|
or before.get("partition_key")
|
|
566
586
|
or before.get("pk")
|
|
587
|
+
or before.get("_pk")
|
|
567
588
|
or (before.get(meta.pk_field) if meta.pk_field else None)
|
|
568
589
|
)
|
|
569
590
|
rkey = None
|
|
@@ -623,9 +644,9 @@ class DatabaseFactory:
|
|
|
623
644
|
except Exception:
|
|
624
645
|
raise
|
|
625
646
|
|
|
626
|
-
#
|
|
647
|
+
# ──────────────────────────────────────────────────────────
|
|
627
648
|
# UPSERT
|
|
628
|
-
#
|
|
649
|
+
# ──────────────────────────────────────────────────────────
|
|
629
650
|
|
|
630
651
|
def upsert(
|
|
631
652
|
self,
|
|
@@ -634,6 +655,7 @@ class DatabaseFactory:
|
|
|
634
655
|
*,
|
|
635
656
|
replace: bool = False,
|
|
636
657
|
engine_override: Optional[EngineOverride] = None,
|
|
658
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
637
659
|
) -> JsonDict:
|
|
638
660
|
name = _model_name(model)
|
|
639
661
|
meta = _extract_meta(model)
|
|
@@ -651,7 +673,7 @@ class DatabaseFactory:
|
|
|
651
673
|
def _op() -> JsonDict:
|
|
652
674
|
nonlocal after_plain, success
|
|
653
675
|
if self._is_sql(meta, engine_override):
|
|
654
|
-
result = adapters.sql.upsert(meta.table, data)
|
|
676
|
+
result = adapters.sql.upsert(meta.table, data, session_vars=session_vars)
|
|
655
677
|
else:
|
|
656
678
|
cls = (
|
|
657
679
|
model
|
|
@@ -683,9 +705,9 @@ class DatabaseFactory:
|
|
|
683
705
|
except Exception:
|
|
684
706
|
raise
|
|
685
707
|
|
|
686
|
-
#
|
|
708
|
+
# ──────────────────────────────────────────────────────────
|
|
687
709
|
# DELETE
|
|
688
|
-
#
|
|
710
|
+
# ──────────────────────────────────────────────────────────
|
|
689
711
|
|
|
690
712
|
def delete(
|
|
691
713
|
self,
|
|
@@ -695,6 +717,7 @@ class DatabaseFactory:
|
|
|
695
717
|
etag: Optional[str] = None,
|
|
696
718
|
hard: bool = False,
|
|
697
719
|
engine_override: Optional[EngineOverride] = None,
|
|
720
|
+
session_vars: Optional[Dict[str, str]] = None,
|
|
698
721
|
) -> JsonDict:
|
|
699
722
|
meta = _extract_meta(model)
|
|
700
723
|
name = _model_name(model)
|
|
@@ -708,6 +731,7 @@ class DatabaseFactory:
|
|
|
708
731
|
"deleted_by": AuditContext.actor_id.get(),
|
|
709
732
|
},
|
|
710
733
|
engine_override=engine_override,
|
|
734
|
+
session_vars=session_vars,
|
|
711
735
|
)
|
|
712
736
|
|
|
713
737
|
adapters = self._adapters_for(model, meta, engine_override)
|
|
@@ -717,6 +741,7 @@ class DatabaseFactory:
|
|
|
717
741
|
no_cache=True,
|
|
718
742
|
include_deleted=True,
|
|
719
743
|
engine_override=engine_override,
|
|
744
|
+
session_vars=session_vars,
|
|
720
745
|
)
|
|
721
746
|
success = False
|
|
722
747
|
error: Optional[str] = None
|
|
@@ -724,7 +749,7 @@ class DatabaseFactory:
|
|
|
724
749
|
def _op() -> JsonDict:
|
|
725
750
|
nonlocal success
|
|
726
751
|
if self._is_sql(meta, engine_override):
|
|
727
|
-
result = adapters.sql.delete(meta.table, entity_id)
|
|
752
|
+
result = adapters.sql.delete(meta.table, entity_id, session_vars=session_vars)
|
|
728
753
|
else:
|
|
729
754
|
cls = (
|
|
730
755
|
model
|
|
@@ -735,18 +760,36 @@ class DatabaseFactory:
|
|
|
735
760
|
# scalar entity_id alone isn't enough once a model's pk_field
|
|
736
761
|
# /rk_field differ from "id").
|
|
737
762
|
en_id = entity_id
|
|
738
|
-
if
|
|
763
|
+
if before:
|
|
739
764
|
pkey = (
|
|
740
765
|
before.get("PartitionKey")
|
|
741
766
|
or before.get("partition_key")
|
|
742
767
|
or before.get("pk")
|
|
768
|
+
or before.get("_pk")
|
|
743
769
|
or (before.get(meta.pk_field) if meta.pk_field else None)
|
|
744
770
|
)
|
|
745
771
|
if pkey:
|
|
746
772
|
rkey = None
|
|
747
773
|
if meta.rk_field and meta.rk_field != "id":
|
|
748
774
|
rkey = before.get(meta.rk_field)
|
|
749
|
-
en_id
|
|
775
|
+
if isinstance(en_id, dict):
|
|
776
|
+
en_pk = (
|
|
777
|
+
en_id.get("PartitionKey")
|
|
778
|
+
or en_id.get("partition_key")
|
|
779
|
+
or en_id.get("pk")
|
|
780
|
+
)
|
|
781
|
+
if not en_pk:
|
|
782
|
+
en_id["partition_key"] = pkey
|
|
783
|
+
en_rk = (
|
|
784
|
+
en_id.get("RowKey")
|
|
785
|
+
or en_id.get("row_key")
|
|
786
|
+
or en_id.get("rk")
|
|
787
|
+
or en_id.get("id")
|
|
788
|
+
)
|
|
789
|
+
if not en_rk and rkey:
|
|
790
|
+
en_id["row_key"] = rkey
|
|
791
|
+
else:
|
|
792
|
+
en_id = {"partition_key": pkey, "row_key": rkey or entity_id}
|
|
750
793
|
result = adapters.nosql.delete(cls, en_id, etag=etag)
|
|
751
794
|
success = True
|
|
752
795
|
if self._enable_cache and self._cache:
|
|
@@ -769,9 +812,9 @@ class DatabaseFactory:
|
|
|
769
812
|
except Exception:
|
|
770
813
|
raise
|
|
771
814
|
|
|
772
|
-
#
|
|
815
|
+
# ──────────────────────────────────────────────────────────
|
|
773
816
|
# QUERY (LINQ-style)
|
|
774
|
-
#
|
|
817
|
+
# ──────────────────────────────────────────────────────────
|
|
775
818
|
|
|
776
819
|
def query_linq(
|
|
777
820
|
self,
|
|
@@ -803,9 +846,9 @@ class DatabaseFactory:
|
|
|
803
846
|
return result
|
|
804
847
|
return self._run(_op)
|
|
805
848
|
|
|
806
|
-
#
|
|
849
|
+
# ──────────────────────────────────────────────────────────
|
|
807
850
|
# PAGINATION (legacy simple)
|
|
808
|
-
#
|
|
851
|
+
# ──────────────────────────────────────────────────────────
|
|
809
852
|
|
|
810
853
|
def read_page(
|
|
811
854
|
self,
|
|
@@ -852,9 +895,9 @@ class DatabaseFactory:
|
|
|
852
895
|
return result
|
|
853
896
|
return self._run(_op)
|
|
854
897
|
|
|
855
|
-
#
|
|
898
|
+
# ──────────────────────────────────────────────────────────
|
|
856
899
|
# PAGINATION (generic — order_by, cursor, field projection)
|
|
857
|
-
#
|
|
900
|
+
# ──────────────────────────────────────────────────────────
|
|
858
901
|
|
|
859
902
|
def query_paged(
|
|
860
903
|
self,
|
|
@@ -877,15 +920,12 @@ class DatabaseFactory:
|
|
|
877
920
|
if self._is_sql(meta, engine_override):
|
|
878
921
|
return adapters.sql.query_paged(meta.table, request)
|
|
879
922
|
|
|
880
|
-
cls = (
|
|
881
|
-
model if isinstance(model, type)
|
|
882
|
-
else type(name, (), {"__polydb__": meta.__dict__})
|
|
883
|
-
)
|
|
923
|
+
cls = model if isinstance(model, type) else type(name, (), {"__polydb__": meta.__dict__})
|
|
884
924
|
return adapters.nosql.query_paged(cls, request)
|
|
885
925
|
|
|
886
|
-
#
|
|
926
|
+
# ════════════════════════════════════════════════════════════════════════════════════
|
|
887
927
|
# BLOB STORAGE
|
|
888
|
-
#
|
|
928
|
+
# ═══════════════════════════════════════════════════════════════════════════════════
|
|
889
929
|
|
|
890
930
|
def upload_blob(
|
|
891
931
|
self,
|
|
@@ -934,9 +974,9 @@ class DatabaseFactory:
|
|
|
934
974
|
)
|
|
935
975
|
return storage.list(prefix)
|
|
936
976
|
|
|
937
|
-
#
|
|
977
|
+
# ════════════════════════════════════════════════════════════════════════════════════
|
|
938
978
|
# QUEUE
|
|
939
|
-
#
|
|
979
|
+
# ═══════════════════════════════════════════════════════════════════════════════════
|
|
940
980
|
|
|
941
981
|
def send_queue(
|
|
942
982
|
self,
|
|
@@ -956,6 +996,7 @@ class DatabaseFactory:
|
|
|
956
996
|
adapter_name: str = "azure_queue",
|
|
957
997
|
) -> List[Dict[str, Any]]:
|
|
958
998
|
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
999
|
+
max_messages = min(max_messages, QUEUE_RECEIVE_MAX_MESSAGES_CAP)
|
|
959
1000
|
return queue.receive(queue_name=queue_name, max_messages=max_messages)
|
|
960
1001
|
|
|
961
1002
|
def ack_queue(
|
|
@@ -983,9 +1024,80 @@ class DatabaseFactory:
|
|
|
983
1024
|
else queue.delete(message_id, queue_name)
|
|
984
1025
|
)
|
|
985
1026
|
|
|
986
|
-
|
|
1027
|
+
def nack_queue(
|
|
1028
|
+
self, ack_id: str, *, queue_name: str = "default", adapter_name: str = "azure_queue"
|
|
1029
|
+
) -> bool:
|
|
1030
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1031
|
+
return queue.nack(ack_id, queue_name)
|
|
1032
|
+
|
|
1033
|
+
def purge_queue(self, *, queue_name: str = "default", adapter_name: str = "azure_queue") -> int:
|
|
1034
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1035
|
+
return queue.purge(queue_name)
|
|
1036
|
+
|
|
1037
|
+
def declare_queue(
|
|
1038
|
+
self,
|
|
1039
|
+
*,
|
|
1040
|
+
queue_name: str = "default",
|
|
1041
|
+
durable: bool = True,
|
|
1042
|
+
dead_letter_queue: Optional[str] = None,
|
|
1043
|
+
adapter_name: str = "azure_queue",
|
|
1044
|
+
) -> bool:
|
|
1045
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1046
|
+
return queue.declare(queue_name, durable=durable, dead_letter_queue=dead_letter_queue)
|
|
1047
|
+
|
|
1048
|
+
def queue_status(
|
|
1049
|
+
self, *, queue_name: str = "default", adapter_name: str = "azure_queue"
|
|
1050
|
+
) -> Dict[str, Any]:
|
|
1051
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1052
|
+
return queue.status(queue_name)
|
|
1053
|
+
|
|
1054
|
+
# extend/delay/cancel -- real only where the underlying adapter
|
|
1055
|
+
# genuinely implements one (see QueueAdapter's own module comment
|
|
1056
|
+
# for the full per-backend feasibility matrix). An adapter that
|
|
1057
|
+
# doesn't implement a given method raises NotImplementedError by
|
|
1058
|
+
# name, propagated here unchanged rather than caught and faked.
|
|
1059
|
+
|
|
1060
|
+
def extend_queue(
|
|
1061
|
+
self,
|
|
1062
|
+
ack_id: str,
|
|
1063
|
+
*,
|
|
1064
|
+
queue_name: str = "default",
|
|
1065
|
+
visibility_timeout: int = 30,
|
|
1066
|
+
adapter_name: str = "azure_queue",
|
|
1067
|
+
) -> bool:
|
|
1068
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1069
|
+
return queue.extend(ack_id, queue_name, visibility_timeout=visibility_timeout)
|
|
1070
|
+
|
|
1071
|
+
def delay_queue(
|
|
1072
|
+
self,
|
|
1073
|
+
message: Dict[str, Any],
|
|
1074
|
+
*,
|
|
1075
|
+
queue_name: str = "default",
|
|
1076
|
+
delay_seconds: int = 0,
|
|
1077
|
+
adapter_name: str = "azure_queue",
|
|
1078
|
+
) -> str:
|
|
1079
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1080
|
+
return queue.delay(message, queue_name, delay_seconds=delay_seconds)
|
|
1081
|
+
|
|
1082
|
+
def cancel_queue(
|
|
1083
|
+
self,
|
|
1084
|
+
message_id: str,
|
|
1085
|
+
*,
|
|
1086
|
+
queue_name: str = "default",
|
|
1087
|
+
adapter_name: str = "azure_queue",
|
|
1088
|
+
**kwargs: Any,
|
|
1089
|
+
) -> bool:
|
|
1090
|
+
queue = self._engines[0].cloud_factory.get_queue(adapter_name)
|
|
1091
|
+
# RabbitMQAdapter.cancel needs a `delay_seconds` kwarg (which
|
|
1092
|
+
# per-duration delay queue to scan) that no other adapter's own
|
|
1093
|
+
# cancel() takes -- passed through only when the caller supplies
|
|
1094
|
+
# it, so every other adapter's plain cancel(message_id,
|
|
1095
|
+
# queue_name) signature is untouched.
|
|
1096
|
+
return queue.cancel(message_id, queue_name, **kwargs)
|
|
1097
|
+
|
|
1098
|
+
# ════════════════════════════════════════════════════════════════════════════════════
|
|
987
1099
|
# FILE STORAGE
|
|
988
|
-
#
|
|
1100
|
+
# ═══════════════════════════════════════════════════════════════════════════════════
|
|
989
1101
|
|
|
990
1102
|
def write_file(
|
|
991
1103
|
self, path: str, data: Union[bytes, str], *, adapter_name: str = "files"
|
|
@@ -1005,9 +1117,9 @@ class DatabaseFactory:
|
|
|
1005
1117
|
files = self._engines[0].cloud_factory.get_files(adapter_name)
|
|
1006
1118
|
return files.list(directory)
|
|
1007
1119
|
|
|
1008
|
-
#
|
|
1120
|
+
# ════════════════════════════════════════════════════════════════════════════════════
|
|
1009
1121
|
# CACHE
|
|
1010
|
-
#
|
|
1122
|
+
# ═══════════════════════════════════════════════════════════════════════════════════
|
|
1011
1123
|
|
|
1012
1124
|
def set_cache(self, model: str, key: Any, value: Any, ttl: int = 300) -> None:
|
|
1013
1125
|
if self._cache:
|
|
@@ -1023,3 +1135,67 @@ class DatabaseFactory:
|
|
|
1023
1135
|
self._cache.invalidate(model, key)
|
|
1024
1136
|
else:
|
|
1025
1137
|
self._cache.clear()
|
|
1138
|
+
|
|
1139
|
+
# ------------------------------------------------------------------
|
|
1140
|
+
# Generic Redis KV -- distinct from set_cache/get_cache/invalidate_cache
|
|
1141
|
+
# above (which back the automatic, best-effort query-result cache and
|
|
1142
|
+
# degrade silently with no Redis configured). These back a caller's
|
|
1143
|
+
# own explicit business logic reached from outside this class (e.g. a
|
|
1144
|
+
# manifest workflow step's "redis" integration doing rate limiting or
|
|
1145
|
+
# a distributed lock), so a missing/misconfigured cache backend raises
|
|
1146
|
+
# CacheError instead of returning a silently-wrong 0/None/False.
|
|
1147
|
+
# ------------------------------------------------------------------
|
|
1148
|
+
|
|
1149
|
+
def redis_get(self, key: str, *, namespace: str = "generic") -> Optional[Any]:
|
|
1150
|
+
from .errors import CacheError
|
|
1151
|
+
|
|
1152
|
+
if not self._cache:
|
|
1153
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1154
|
+
return self._cache.get_raw(namespace, key)
|
|
1155
|
+
|
|
1156
|
+
def redis_set(
|
|
1157
|
+
self, key: str, value: Any, *, namespace: str = "generic", ttl: Optional[int] = None
|
|
1158
|
+
) -> bool:
|
|
1159
|
+
from .errors import CacheError
|
|
1160
|
+
|
|
1161
|
+
if not self._cache:
|
|
1162
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1163
|
+
return self._cache.set_raw(namespace, key, value, ttl)
|
|
1164
|
+
|
|
1165
|
+
def redis_delete(self, key: str, *, namespace: str = "generic") -> None:
|
|
1166
|
+
from .errors import CacheError
|
|
1167
|
+
|
|
1168
|
+
if not self._cache:
|
|
1169
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1170
|
+
self._cache.delete_key(namespace, key)
|
|
1171
|
+
|
|
1172
|
+
def redis_incr(self, key: str, *, namespace: str = "generic", amount: int = 1) -> int:
|
|
1173
|
+
from .errors import CacheError
|
|
1174
|
+
|
|
1175
|
+
if not self._cache:
|
|
1176
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1177
|
+
return self._cache.incrby(namespace, key, amount)
|
|
1178
|
+
|
|
1179
|
+
def redis_decr(self, key: str, *, namespace: str = "generic", amount: int = 1) -> int:
|
|
1180
|
+
return self.redis_incr(key, namespace=namespace, amount=-amount)
|
|
1181
|
+
|
|
1182
|
+
def redis_exists(self, key: str, *, namespace: str = "generic") -> bool:
|
|
1183
|
+
from .errors import CacheError
|
|
1184
|
+
|
|
1185
|
+
if not self._cache:
|
|
1186
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1187
|
+
return self._cache.exists_raw(namespace, key)
|
|
1188
|
+
|
|
1189
|
+
def redis_ttl(self, key: str, *, namespace: str = "generic") -> int:
|
|
1190
|
+
from .errors import CacheError
|
|
1191
|
+
|
|
1192
|
+
if not self._cache:
|
|
1193
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1194
|
+
return self._cache.ttl_raw(namespace, key)
|
|
1195
|
+
|
|
1196
|
+
def redis_expire(self, key: str, ttl: int, *, namespace: str = "generic") -> None:
|
|
1197
|
+
from .errors import CacheError
|
|
1198
|
+
|
|
1199
|
+
if not self._cache:
|
|
1200
|
+
raise CacheError("No Redis cache backend configured (set REDIS_CACHE_URL/REDIS_URL)")
|
|
1201
|
+
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
|
|