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.
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
@@ -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 not isinstance(en_id, dict) and before:
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 = {"partition_key": pkey, "row_key": rkey or entity_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