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.
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 — building both unconditionally
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(meta.table, where, update_data)
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(meta.table, query, limit=limit, offset=offset)
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 and meta.pk_field:
561
- pkey = data.get(meta.pk_field)
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 (before.get(meta.pk_field) if meta.pk_field else None)
606
+ or before.get("_pk")
607
+ or before.get(pk_field_name)
568
608
  )
569
609
  rkey = None
570
- if meta.rk_field and meta.rk_field != "id":
571
- rkey = data.get(meta.rk_field) or (
572
- before.get(meta.rk_field) if before else None
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
- if not isinstance(en_id, dict) and before:
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 (before.get(meta.pk_field) if meta.pk_field else None)
794
+ or before.get("_pk")
795
+ or before.get(pk_field_name)
744
796
  )
745
- if pkey:
746
- rkey = None
747
- if meta.rk_field and meta.rk_field != "id":
748
- rkey = before.get(meta.rk_field)
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