altcodepro-polydb-python 2.3.27__tar.gz → 2.3.28__tar.gz

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.
Files changed (93) hide show
  1. {altcodepro_polydb_python-2.3.27/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.28}/PKG-INFO +1 -1
  2. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/pyproject.toml +1 -1
  3. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
  4. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +1 -0
  5. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/AzureQueueAdapter.py +59 -1
  6. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/MongoDBAdapter.py +3 -1
  7. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/databaseFactory.py +20 -17
  8. altcodepro_polydb_python-2.3.28/tests/test_azure_queue_receipt_handle.py +141 -0
  9. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/LICENSE +0 -0
  10. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/MANIFEST.in +0 -0
  11. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/README.md +0 -0
  12. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/requirements-aws.txt +0 -0
  13. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/requirements-azure.txt +0 -0
  14. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/requirements-dev.txt +0 -0
  15. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/requirements-gcp.txt +0 -0
  16. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/requirements-generic.txt +0 -0
  17. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/requirements.txt +0 -0
  18. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/setup.cfg +0 -0
  19. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/setup.py +0 -0
  20. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  21. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
  22. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  23. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/PolyDB.py +0 -0
  24. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/__init__.py +0 -0
  25. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  26. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  27. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/AzureTableStorageAdapter.py +0 -0
  28. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
  29. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
  30. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
  31. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
  32. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
  33. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/EFSAdapter.py +0 -0
  34. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/FirestoreAdapter.py +0 -0
  35. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
  36. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/PostgreSQLAdapter.py +0 -0
  39. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/S3Adapter.py +0 -0
  40. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  41. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/SQSAdapter.py +0 -0
  42. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/VercelFileAdapter.py +0 -0
  44. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  45. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  46. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/__init__.py +0 -0
  47. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/advanced_query.py +0 -0
  48. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/AuditStorage.py +0 -0
  49. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/__init__.py +0 -0
  50. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/context.py +0 -0
  51. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/manager.py +0 -0
  52. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/models.py +0 -0
  53. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/base/NoSQLKVAdapter.py +0 -0
  54. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  55. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/base/QueueAdapter.py +0 -0
  56. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/base/SharedFilesAdapter.py +0 -0
  57. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/base/__init__.py +0 -0
  58. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/batch.py +0 -0
  59. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/cache.py +0 -0
  60. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/cloudDatabaseFactory.py +0 -0
  61. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/decorators.py +0 -0
  62. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/errors.py +0 -0
  63. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/json_safe.py +0 -0
  64. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/models.py +0 -0
  65. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/monitoring.py +0 -0
  66. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/multitenancy.py +0 -0
  67. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/observability/__init__.py +0 -0
  68. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/observability/logging.py +0 -0
  69. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/py.typed +0 -0
  70. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/query.py +0 -0
  71. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/registry.py +0 -0
  72. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/retry.py +0 -0
  73. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/schema.py +0 -0
  74. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/security.py +0 -0
  75. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/services/__init__.py +0 -0
  76. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/services/compliance_service.py +0 -0
  77. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/services/security_service.py +0 -0
  78. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/types.py +0 -0
  79. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/utils.py +0 -0
  80. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/src/polydb/validation.py +0 -0
  81. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_atomic_decrement.py +0 -0
  82. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_aws.py +0 -0
  83. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_azure.py +0 -0
  84. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_azure_table_empty_filter.py +0 -0
  85. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_azure_table_unpack_entity.py +0 -0
  86. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_blockchain.py +0 -0
  87. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_cloud_factory.py +0 -0
  88. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_gcp.py +0 -0
  89. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_mongodb.py +0 -0
  90. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_multi_engine.py +0 -0
  91. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_observability.py +0 -0
  92. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_postgresql.py +0 -0
  93. {altcodepro_polydb_python-2.3.27 → altcodepro_polydb_python-2.3.28}/tests/test_vercel.py +0 -0
@@ -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.28
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "altcodepro-polydb-python"
7
- version = "2.3.27"
7
+ version = "2.3.28"
8
8
  description = "Production-ready multi-cloud database abstraction layer with connection pooling, retry logic, and thread safety"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.11"
@@ -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.28
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
@@ -78,6 +78,7 @@ src/polydb/services/security_service.py
78
78
  tests/test_atomic_decrement.py
79
79
  tests/test_aws.py
80
80
  tests/test_azure.py
81
+ tests/test_azure_queue_receipt_handle.py
81
82
  tests/test_azure_table_empty_filter.py
82
83
  tests/test_azure_table_unpack_entity.py
83
84
  tests/test_blockchain.py
@@ -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
@@ -114,6 +128,13 @@ class AzureQueueAdapter(QueueAdapter):
114
128
  {
115
129
  "id": msg.id,
116
130
  "pop_receipt": msg.pop_receipt,
131
+ # WorkerPool (and the generic UDL queue contract)
132
+ # reads this key to ack/nack. Without it, ack() is
133
+ # always skipped (falsy receipt_handle), the message
134
+ # is never deleted, and it silently reappears once
135
+ # Azure's visibility timeout elapses — replaying the
136
+ # same task forever even after it already ran.
137
+ "receipt_handle": self._encode_receipt(msg.id, msg.pop_receipt),
117
138
  "body": payload,
118
139
  }
119
140
  )
@@ -156,8 +177,14 @@ class AzureQueueAdapter(QueueAdapter):
156
177
  Preferred usage:
157
178
  ack(pop_receipt=..., message_id=...)
158
179
 
159
- If message_id is not provided, this will fail safely.
180
+ Generic callers (WorkerPool, storage_router.queue_ack) only pass a
181
+ single `receipt_handle` positionally and never supply message_id —
182
+ in that case `pop_receipt` here is actually the combined value
183
+ produced by `receive()` (`"<message_id>|<pop_receipt>"`), so decode
184
+ it rather than failing.
160
185
  """
186
+ if not message_id:
187
+ message_id, pop_receipt = self._decode_receipt(pop_receipt)
161
188
  if not message_id:
162
189
  raise QueueError("AzureQueueAdapter.ack requires message_id")
163
190
  queue_name = self._normalize_queue_name(queue_name)
@@ -166,3 +193,34 @@ class AzureQueueAdapter(QueueAdapter):
166
193
  queue_name=queue_name,
167
194
  pop_receipt=pop_receipt,
168
195
  )
196
+
197
+ def nack(
198
+ self,
199
+ queue_name: str,
200
+ ack_id: str,
201
+ *,
202
+ delay: Optional[int] = None,
203
+ ) -> bool:
204
+ """
205
+ Make a message visible again (optionally after `delay` seconds)
206
+ without deleting it, instead of leaving it to reappear only once
207
+ Azure's default visibility timeout elapses.
208
+
209
+ `ack_id` is the combined `"<message_id>|<pop_receipt>"` receipt
210
+ handle produced by `receive()`. Matches the positional order used
211
+ by storage_router.queue_nack: nack(queue_name, ack_id, delay=...).
212
+ """
213
+ message_id, pop_receipt = self._decode_receipt(ack_id)
214
+ if not message_id:
215
+ raise QueueError("AzureQueueAdapter.nack requires a receipt_handle from receive()")
216
+ try:
217
+ queue_name = self._normalize_queue_name(queue_name)
218
+ queue_client = self._get_queue(queue_name)
219
+ queue_client.update_message(
220
+ message_id,
221
+ pop_receipt=pop_receipt,
222
+ visibility_timeout=delay or 0,
223
+ )
224
+ return True
225
+ except Exception as e:
226
+ 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()
@@ -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
 
@@ -0,0 +1,141 @@
1
+ """
2
+ tests/test_azure_queue_receipt_handle.py
3
+ =========================================
4
+ Regression coverage for the receipt_handle contract on AzureQueueAdapter.
5
+
6
+ Bug this guards against: `receive()` used to return only `id` and
7
+ `pop_receipt`, never the `receipt_handle` key that WorkerPool (and every
8
+ other adapter's ack contract) reads. That made
9
+ `WorkerPool._process_message`'s `msg.get("receipt_handle")` always `None`,
10
+ so the ack branch was silently skipped, the message was never deleted, and
11
+ Azure redelivered it once its visibility timeout elapsed — replaying the
12
+ same task forever even after it had already run to completion.
13
+
14
+ These tests mock the Azure SDK entirely (no live Azurite needed) and only
15
+ exercise the adapter's own encode/decode + ack/nack wiring.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import sys
22
+ from unittest.mock import MagicMock, patch
23
+
24
+ import pytest
25
+
26
+ # test_atomic_decrement.py stubs sys.modules["azure"] et al. with bare,
27
+ # path-less ModuleType objects (guarded by `if _mod not in sys.modules`) so
28
+ # it can run without cloud SDKs installed. If it collects before this file
29
+ # (alphabetically it does), that stub wins the module cache for the rest of
30
+ # the process, and the adapter's lazy `from azure.core.exceptions import
31
+ # ...` imports then fail with "'azure' is not a package" -- even though the
32
+ # real azure SDK is installed here. Drop any such stubs so this file always
33
+ # imports the genuine package fresh.
34
+ for _name in list(sys.modules):
35
+ if _name == "azure" or _name.startswith("azure."):
36
+ del sys.modules[_name]
37
+
38
+ from polydb.adapters.AzureQueueAdapter import AzureQueueAdapter
39
+
40
+
41
+ def _make_adapter() -> AzureQueueAdapter:
42
+ # Bypass the real Azure SDK entirely -- when the full suite runs
43
+ # together, other providers' tests leave azure.storage.queue in a state
44
+ # where importing QueueServiceClient from it fails (a pre-existing
45
+ # cross-test import-cache issue, unrelated to this adapter). Skipping
46
+ # `_initialize_client` sidesteps that import altogether; these tests
47
+ # only care about the adapter's own encode/decode + ack/nack wiring.
48
+ with patch.object(AzureQueueAdapter, "_initialize_client", lambda self: None):
49
+ adapter = AzureQueueAdapter(connection_string="UseDevelopmentStorage=true")
50
+ adapter._client = MagicMock()
51
+ return adapter
52
+
53
+
54
+ def _mock_queue_client(adapter: AzureQueueAdapter, queue_name: str = "default") -> MagicMock:
55
+ client = MagicMock()
56
+ adapter._queues[adapter._normalize_queue_name(queue_name)] = client
57
+ return client
58
+
59
+
60
+ class TestReceiveReturnsReceiptHandle:
61
+ def test_receive_includes_receipt_handle_key(self):
62
+ adapter = _make_adapter()
63
+ client = _mock_queue_client(adapter)
64
+
65
+ raw_msg = MagicMock()
66
+ raw_msg.id = "msg-123"
67
+ raw_msg.pop_receipt = "pop-abc"
68
+ raw_msg.content = json.dumps({"hello": "world"})
69
+ client.receive_messages.return_value = [raw_msg]
70
+
71
+ [received] = adapter.receive(queue_name="default", max_messages=1)
72
+
73
+ assert received["id"] == "msg-123"
74
+ assert received["pop_receipt"] == "pop-abc"
75
+ assert "receipt_handle" in received
76
+ assert received["receipt_handle"] # truthy -- WorkerPool checks `if receipt_handle`
77
+
78
+ def test_receipt_handle_round_trips_through_decode(self):
79
+ message_id, pop_receipt = AzureQueueAdapter._decode_receipt(
80
+ AzureQueueAdapter._encode_receipt("msg-123", "pop-abc")
81
+ )
82
+ assert message_id == "msg-123"
83
+ assert pop_receipt == "pop-abc"
84
+
85
+
86
+ class TestAckAcceptsSingleReceiptHandle:
87
+ def test_ack_with_generic_single_arg_deletes_using_decoded_parts(self):
88
+ """Mirrors how WorkerPool/storage_router actually call ack():
89
+ queue.ack(receipt_handle, queue_name) -- a single positional value,
90
+ never a separate message_id.
91
+ """
92
+ adapter = _make_adapter()
93
+ client = _mock_queue_client(adapter)
94
+
95
+ receipt_handle = AzureQueueAdapter._encode_receipt("msg-123", "pop-abc")
96
+ result = adapter.ack(receipt_handle, "default")
97
+
98
+ assert result is True
99
+ client.delete_message.assert_called_once_with("msg-123", "pop-abc")
100
+
101
+ def test_ack_still_supports_explicit_message_id(self):
102
+ adapter = _make_adapter()
103
+ client = _mock_queue_client(adapter)
104
+
105
+ result = adapter.ack("pop-abc", "default", message_id="msg-123")
106
+
107
+ assert result is True
108
+ client.delete_message.assert_called_once_with("msg-123", "pop-abc")
109
+
110
+ def test_ack_raises_if_receipt_handle_has_no_message_id(self):
111
+ adapter = _make_adapter()
112
+ _mock_queue_client(adapter)
113
+
114
+ with pytest.raises(Exception):
115
+ adapter.ack("", "default")
116
+
117
+
118
+ class TestNack:
119
+ def test_nack_updates_visibility_without_deleting(self):
120
+ adapter = _make_adapter()
121
+ client = _mock_queue_client(adapter)
122
+
123
+ receipt_handle = AzureQueueAdapter._encode_receipt("msg-123", "pop-abc")
124
+ result = adapter.nack("default", receipt_handle, delay=30)
125
+
126
+ assert result is True
127
+ client.update_message.assert_called_once_with(
128
+ "msg-123", pop_receipt="pop-abc", visibility_timeout=30
129
+ )
130
+ client.delete_message.assert_not_called()
131
+
132
+ def test_nack_defaults_delay_to_zero(self):
133
+ adapter = _make_adapter()
134
+ client = _mock_queue_client(adapter)
135
+
136
+ receipt_handle = AzureQueueAdapter._encode_receipt("msg-123", "pop-abc")
137
+ adapter.nack("default", receipt_handle)
138
+
139
+ client.update_message.assert_called_once_with(
140
+ "msg-123", pop_receipt="pop-abc", visibility_timeout=0
141
+ )