altcodepro-polydb-python 2.3.27__py3-none-any.whl → 2.3.29__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.3.27
3
+ Version: 2.3.29
4
4
  Summary: Production-ready multi-cloud database abstraction layer with connection pooling, retry logic, and thread safety
5
5
  Author: AltCodePro
6
6
  Project-URL: Homepage, https://github.com/altcodepro/polydb-python
@@ -1,11 +1,11 @@
1
- altcodepro_polydb_python-2.3.27.dist-info/licenses/LICENSE,sha256=9X8GLocsBwy-5aR5JGOt2SAMDDPs9Qv-YnqmHBHOXrw,1067
1
+ altcodepro_polydb_python-2.3.29.dist-info/licenses/LICENSE,sha256=9X8GLocsBwy-5aR5JGOt2SAMDDPs9Qv-YnqmHBHOXrw,1067
2
2
  polydb/PolyDB.py,sha256=MG7-nV59zDvrUQwXNEgV4eetHCeW9TYY9DQqtxu_r7k,23543
3
3
  polydb/__init__.py,sha256=d9KSz888A336904TPEjWPYBtyJLPB0lTdcaOnYp8byY,1535
4
4
  polydb/advanced_query.py,sha256=cxMB-EB-qT3bWXJlhmjnMCUtrzogORWyoEfS50Dy7go,4280
5
5
  polydb/batch.py,sha256=_DjWZa1ZXYSk6MLKqFe0eT7SYVRZtYNqZb9bI8Y2sao,4566
6
6
  polydb/cache.py,sha256=JBXF1XEK-fY80ar8SDE893Z1Z116YtXAEG0PaJ0Nkcw,7658
7
7
  polydb/cloudDatabaseFactory.py,sha256=Gp6L__YtgrkGahD8B7ItzXMHCoj2ZUGDjXLS9w0TujY,17780
8
- polydb/databaseFactory.py,sha256=Or3usoXizzrvuZ8X8x8AFdJA4Pul96lqsrGgtVkjTQ4,43861
8
+ polydb/databaseFactory.py,sha256=AdEMRtWcwf0elJ9JhrWS5krre-3RVFfIgt0mA93eJJQ,44068
9
9
  polydb/decorators.py,sha256=L_WP2uXP_k8Ac49SUm-mthbM4jWI-XYfHXEyKzumOww,43062
10
10
  polydb/errors.py,sha256=jF_55fbfg6b6mBc116dzU_uBG9qiIKbr5jvmDuJx-Jw,1573
11
11
  polydb/json_safe.py,sha256=R5PrqAGirqjYKPyy-8KH-lSXjLH0FPr2TSGozy4eheU,149
@@ -23,7 +23,7 @@ polydb/utils.py,sha256=tS_KI1vt-VZ6JftbFxbnVxC0xpie1-zYikTGf1nSoFw,3183
23
23
  polydb/validation.py,sha256=a1o1d02k3c6PWQwkBbw_0nEmIgrdB5RR8OcpNQMn4cA,4810
24
24
  polydb/adapters/AzureBlobStorageAdapter.py,sha256=4vD55Z8DBTzBK66jIJbo5bNMY-AQ61MlP0-P2Fv_JgQ,7083
25
25
  polydb/adapters/AzureFileStorageAdapter.py,sha256=VZNprqlBXCuWUgtqClNT-NrQmRf-XFYEiRA2BLbf-Sc,7046
26
- polydb/adapters/AzureQueueAdapter.py,sha256=5PrKAX4OQxUD5nReZKrInF_mjQVdFcj2aYd0Xp-HjjQ,5254
26
+ polydb/adapters/AzureQueueAdapter.py,sha256=LsNvrNHUgFF2UwXXTwlOiqK8VdP4htLaFnTu4oxZDlo,9468
27
27
  polydb/adapters/AzureTableStorageAdapter.py,sha256=FCWojIRyOQdDyga5sxbHXBfZMdb3T1xn3Q2VX5klebo,27824
28
28
  polydb/adapters/BlockchainBlobAdapter.py,sha256=D01Yua9mkKfaQrxKYApblIyI6DSP0dtNAh4Tav51HJ4,3299
29
29
  polydb/adapters/BlockchainFileAdapter.py,sha256=G749xOVpG20HuKS8zCgi6PMjoJNu-YXK7zitygjLdzM,8335
@@ -35,7 +35,7 @@ polydb/adapters/FirestoreAdapter.py,sha256=e3DEODOZ-UAWULLtGTIED-Ym1JrxOKaOLPQVK
35
35
  polydb/adapters/GCPFilestoreAdapter.py,sha256=yjFQQwsWYWc8mo8XwMViVTWb5_D--xAyTMvE-4AOpNM,3006
36
36
  polydb/adapters/GCPPubSubAdapter.py,sha256=7XNots2VA0ReEDku-rjg-OTYmftIpx5UgnXYDdXNkOo,8692
37
37
  polydb/adapters/GCPStorageAdapter.py,sha256=9yS1Jhcn5_rCRdZ5uOqcRW6Ba-UNb6VOYpwENP-C6Qk,7133
38
- polydb/adapters/MongoDBAdapter.py,sha256=vX3SAHDLbTnHABGesES9N-gYSQqPqdqFLJgd7pYWZzw,7471
38
+ polydb/adapters/MongoDBAdapter.py,sha256=Ak8q_qeAaUTe0Yc2oEefmAbzoODjTWVZQOirW-seBQU,7574
39
39
  polydb/adapters/PostgreSQLAdapter.py,sha256=lUe65kpJkTu6R1Tc1zgl63Fa1PI44UVbWRTKRUSSGak,36579
40
40
  polydb/adapters/S3Adapter.py,sha256=5R0zHAL2SkGFjp1L3bp-IU468bXYdSf6nKx974MN104,7586
41
41
  polydb/adapters/S3CompatibleAdapter.py,sha256=jpafqbAjA8-irdXBrfXa1QJySIzrcUQ6UrFt5h5FAEc,7006
@@ -60,7 +60,7 @@ polydb/observability/logging.py,sha256=gv1SshNjaSkmR98Uv7Qbwq3gkeQUzVCA4ichqHKL6
60
60
  polydb/services/__init__.py,sha256=nMolMXzmhejRSm7f42kU9fCHF0GIpqpejN2kywuBsUQ,241
61
61
  polydb/services/compliance_service.py,sha256=-0ncX7PrXy4-NuyWXIv65cLUsqz0jXzb3LtsD8rlrXE,4983
62
62
  polydb/services/security_service.py,sha256=q7GzK6sbMnBBsc4fuL4oXTd-ui2b-MYQyrWTBvdVNSk,5062
63
- altcodepro_polydb_python-2.3.27.dist-info/METADATA,sha256=U0hT_pcZTOTdbyTH2CzkFvasDDyRqnNRxbyB3zeSby0,12359
64
- altcodepro_polydb_python-2.3.27.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
65
- altcodepro_polydb_python-2.3.27.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
66
- altcodepro_polydb_python-2.3.27.dist-info/RECORD,,
63
+ altcodepro_polydb_python-2.3.29.dist-info/METADATA,sha256=7bWio-A4c46Q0twrvo3OX7m5s3wYBpfR0NajgheWDgQ,12359
64
+ altcodepro_polydb_python-2.3.29.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
65
+ altcodepro_polydb_python-2.3.29.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
66
+ altcodepro_polydb_python-2.3.29.dist-info/RECORD,,
@@ -46,6 +46,20 @@ class AzureQueueAdapter(QueueAdapter):
46
46
  name = re.sub(r"-+", "-", name) # collapse multiple dashes
47
47
  return name.strip("-")
48
48
 
49
+ @staticmethod
50
+ def _encode_receipt(message_id: str, pop_receipt: str) -> str:
51
+ # Azure needs BOTH message_id and pop_receipt to delete/update a
52
+ # message, but the generic queue contract (WorkerPool, SQSAdapter)
53
+ # only ever carries a single opaque `receipt_handle` string end to
54
+ # end. Pack both into one string here so ack()/nack() can recover
55
+ # them without any caller needing to know Azure's two-part scheme.
56
+ return f"{message_id}|{pop_receipt}"
57
+
58
+ @staticmethod
59
+ def _decode_receipt(receipt_handle: str) -> "tuple[str, str]":
60
+ message_id, _, pop_receipt = (receipt_handle or "").partition("|")
61
+ return message_id, pop_receipt
62
+
49
63
  def _initialize_client(self) -> None:
50
64
  """Initialize Azure Queue client"""
51
65
  from azure.storage.queue import QueueServiceClient
@@ -96,15 +110,38 @@ class AzureQueueAdapter(QueueAdapter):
96
110
  except Exception as e:
97
111
  raise QueueError(f"Azure Queue send failed: {e}")
98
112
 
113
+ # How long a delivered message stays invisible to other consumers before
114
+ # Azure hands it out again. The SDK's own default is 30s — far shorter
115
+ # than a real task (an LLM generation call alone can run 30-120s+), so
116
+ # a still-in-flight message was becoming visible again and getting
117
+ # picked up by a second worker mid-processing: two concurrent
118
+ # executions of the same task, one of which stomps/duplicates the
119
+ # other's result. 300s covers realistic worst-case processing; ack()
120
+ # deletes the message immediately on completion regardless; a genuinely
121
+ # crashed worker still recovers the message after this timeout.
122
+ DEFAULT_VISIBILITY_TIMEOUT = 300
123
+
99
124
  @retry(max_attempts=3, delay=1.0, exceptions=(QueueError,))
100
- def receive(self, queue_name: str = "default", max_messages: int = 1) -> List[Dict[str, Any]]:
125
+ def receive(
126
+ self,
127
+ queue_name: str = "default",
128
+ max_messages: int = 1,
129
+ visibility_timeout: Optional[int] = None,
130
+ ) -> List[Dict[str, Any]]:
101
131
  """Receive messages"""
102
132
  try:
103
133
 
104
134
  queue_name = self._normalize_queue_name(queue_name)
105
135
  queue_client = self._get_queue(queue_name)
106
136
 
107
- messages = queue_client.receive_messages(max_messages=max_messages)
137
+ messages = queue_client.receive_messages(
138
+ max_messages=max_messages,
139
+ visibility_timeout=(
140
+ visibility_timeout
141
+ if visibility_timeout is not None
142
+ else self.DEFAULT_VISIBILITY_TIMEOUT
143
+ ),
144
+ )
108
145
 
109
146
  results = []
110
147
 
@@ -114,6 +151,19 @@ class AzureQueueAdapter(QueueAdapter):
114
151
  {
115
152
  "id": msg.id,
116
153
  "pop_receipt": msg.pop_receipt,
154
+ # WorkerPool (and the generic UDL queue contract)
155
+ # reads this key to ack/nack. Without it, ack() is
156
+ # always skipped (falsy receipt_handle), the message
157
+ # is never deleted, and it silently reappears once
158
+ # Azure's visibility timeout elapses — replaying the
159
+ # same task forever even after it already ran.
160
+ "receipt_handle": self._encode_receipt(msg.id, msg.pop_receipt),
161
+ # How many times Azure has handed this same message
162
+ # out (1 on first delivery). Lets a consumer (e.g.
163
+ # WorkerPool) detect a poison message that keeps
164
+ # failing to ack/process and dead-letter it instead
165
+ # of retrying forever.
166
+ "dequeue_count": msg.dequeue_count,
117
167
  "body": payload,
118
168
  }
119
169
  )
@@ -156,8 +206,14 @@ class AzureQueueAdapter(QueueAdapter):
156
206
  Preferred usage:
157
207
  ack(pop_receipt=..., message_id=...)
158
208
 
159
- If message_id is not provided, this will fail safely.
209
+ Generic callers (WorkerPool, storage_router.queue_ack) only pass a
210
+ single `receipt_handle` positionally and never supply message_id —
211
+ in that case `pop_receipt` here is actually the combined value
212
+ produced by `receive()` (`"<message_id>|<pop_receipt>"`), so decode
213
+ it rather than failing.
160
214
  """
215
+ if not message_id:
216
+ message_id, pop_receipt = self._decode_receipt(pop_receipt)
161
217
  if not message_id:
162
218
  raise QueueError("AzureQueueAdapter.ack requires message_id")
163
219
  queue_name = self._normalize_queue_name(queue_name)
@@ -166,3 +222,34 @@ class AzureQueueAdapter(QueueAdapter):
166
222
  queue_name=queue_name,
167
223
  pop_receipt=pop_receipt,
168
224
  )
225
+
226
+ def nack(
227
+ self,
228
+ queue_name: str,
229
+ ack_id: str,
230
+ *,
231
+ delay: Optional[int] = None,
232
+ ) -> bool:
233
+ """
234
+ Make a message visible again (optionally after `delay` seconds)
235
+ without deleting it, instead of leaving it to reappear only once
236
+ Azure's default visibility timeout elapses.
237
+
238
+ `ack_id` is the combined `"<message_id>|<pop_receipt>"` receipt
239
+ handle produced by `receive()`. Matches the positional order used
240
+ by storage_router.queue_nack: nack(queue_name, ack_id, delay=...).
241
+ """
242
+ message_id, pop_receipt = self._decode_receipt(ack_id)
243
+ if not message_id:
244
+ raise QueueError("AzureQueueAdapter.nack requires a receipt_handle from receive()")
245
+ try:
246
+ queue_name = self._normalize_queue_name(queue_name)
247
+ queue_client = self._get_queue(queue_name)
248
+ queue_client.update_message(
249
+ message_id,
250
+ pop_receipt=pop_receipt,
251
+ visibility_timeout=delay or 0,
252
+ )
253
+ return True
254
+ except Exception as e:
255
+ raise QueueError(f"Azure Queue nack failed: {e}")
@@ -50,7 +50,9 @@ class MongoDBAdapter(NoSQLKVAdapter):
50
50
  self.mongo_uri,
51
51
  maxPoolSize=int(os.getenv("MONGODB_MAX_POOL_SIZE", "10")),
52
52
  minPoolSize=int(os.getenv("MONGODB_MIN_POOL_SIZE", "1")),
53
- serverSelectionTimeoutMS=5000,
53
+ serverSelectionTimeoutMS=int(
54
+ os.getenv("MONGODB_SERVER_SELECTION_TIMEOUT_MS", "5000")
55
+ ),
54
56
  )
55
57
 
56
58
  self._client.server_info()
polydb/databaseFactory.py CHANGED
@@ -109,8 +109,8 @@ class EngineOverride:
109
109
 
110
110
  @dataclass
111
111
  class _ResolvedAdapters:
112
- sql: PostgreSQLAdapter
113
- nosql: NoSQLKVAdapter
112
+ sql: Optional[PostgreSQLAdapter]
113
+ nosql: Optional[NoSQLKVAdapter]
114
114
  engine_name: str
115
115
 
116
116
 
@@ -233,35 +233,38 @@ class DatabaseFactory:
233
233
  def _resolve_adapters(
234
234
  self, model_name: str, storage: str, override: Optional[EngineOverride] = None
235
235
  ) -> _ResolvedAdapters:
236
+ # Only construct the adapter this call actually needs. Every caller
237
+ # (create/read/update/upsert/delete/query*) branches on `storage` and
238
+ # touches exactly one of .sql/.nosql — building both unconditionally
239
+ # meant a purely-SQL deployment with no reachable NoSQL backend (or
240
+ # vice versa) failed on every single call, not just ones that needed
241
+ # the missing engine.
242
+ def _build(engine: "EngineConfig") -> _ResolvedAdapters:
243
+ return _ResolvedAdapters(
244
+ sql=engine.sql() if storage == "sql" else None,
245
+ nosql=engine.nosql() if storage == "nosql" else None,
246
+ engine_name=engine.name,
247
+ )
248
+
236
249
  if override:
237
250
  engine = self._engine_by_name.get(override.engine_name)
238
251
  if engine is None:
239
252
  raise AdapterConfigurationError(
240
253
  f"Unknown engine '{override.engine_name}'. Available: {list(self._engine_by_name)}"
241
254
  )
242
- return _ResolvedAdapters(
243
- sql=engine.sql(), nosql=engine.nosql(), engine_name=engine.name
244
- )
255
+ return _build(engine)
245
256
 
246
257
  for engine in self._engines:
247
258
  if storage == "sql" and engine.sql_models and model_name in engine.sql_models:
248
- return _ResolvedAdapters(
249
- sql=engine.sql(), nosql=engine.nosql(), engine_name=engine.name
250
- )
259
+ return _build(engine)
251
260
  if storage == "nosql" and engine.nosql_models and model_name in engine.nosql_models:
252
- return _ResolvedAdapters(
253
- sql=engine.sql(), nosql=engine.nosql(), engine_name=engine.name
254
- )
261
+ return _build(engine)
255
262
 
256
263
  for engine in self._engines:
257
264
  if storage == "sql" and engine.is_default_sql:
258
- return _ResolvedAdapters(
259
- sql=engine.sql(), nosql=engine.nosql(), engine_name=engine.name
260
- )
265
+ return _build(engine)
261
266
  if storage == "nosql" and engine.is_default_nosql:
262
- return _ResolvedAdapters(
263
- sql=engine.sql(), nosql=engine.nosql(), engine_name=engine.name
264
- )
267
+ return _build(engine)
265
268
 
266
269
  raise AdapterConfigurationError(f"No engine for model='{model_name}' storage='{storage}'")
267
270