altcodepro-polydb-python 2.3.26__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.
- {altcodepro_polydb_python-2.3.26/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.28}/PKG-INFO +1 -1
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/pyproject.toml +1 -1
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +2 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/AzureQueueAdapter.py +59 -1
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/AzureTableStorageAdapter.py +24 -17
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/MongoDBAdapter.py +3 -1
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/base/NoSQLKVAdapter.py +16 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/databaseFactory.py +65 -20
- altcodepro_polydb_python-2.3.28/tests/test_azure_queue_receipt_handle.py +141 -0
- altcodepro_polydb_python-2.3.28/tests/test_azure_table_unpack_entity.py +76 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/LICENSE +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/MANIFEST.in +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/README.md +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/requirements-aws.txt +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/requirements-azure.txt +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/requirements-dev.txt +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/requirements-gcp.txt +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/requirements-generic.txt +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/requirements.txt +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/setup.cfg +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/setup.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/PolyDB.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/EFSAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/FirestoreAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/PostgreSQLAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/S3Adapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/SQSAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/VercelFileAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/VercelKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/advanced_query.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/AuditStorage.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/context.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/manager.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/models.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/base/ObjectStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/base/QueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/base/SharedFilesAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/base/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/batch.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/cache.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/cloudDatabaseFactory.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/decorators.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/errors.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/json_safe.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/models.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/monitoring.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/multitenancy.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/observability/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/observability/logging.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/py.typed +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/query.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/registry.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/retry.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/schema.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/security.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/services/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/services/compliance_service.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/services/security_service.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/types.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/utils.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/validation.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_atomic_decrement.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_aws.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_azure.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_azure_table_empty_filter.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_blockchain.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_cloud_factory.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_gcp.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_mongodb.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_multi_engine.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_observability.py +0 -0
- {altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_postgresql.py +0 -0
- {altcodepro_polydb_python-2.3.26 → 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.
|
|
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.
|
|
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.
|
|
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,7 +78,9 @@ 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
|
|
83
|
+
tests/test_azure_table_unpack_entity.py
|
|
82
84
|
tests/test_blockchain.py
|
|
83
85
|
tests/test_cloud_factory.py
|
|
84
86
|
tests/test_gcp.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
|
-
|
|
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}")
|
|
@@ -255,7 +255,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
255
255
|
|
|
256
256
|
return entity
|
|
257
257
|
|
|
258
|
-
def _unpack_entity(self, entity: JsonDict) -> JsonDict:
|
|
258
|
+
def _unpack_entity(self, model: type, entity: JsonDict) -> JsonDict:
|
|
259
259
|
if not entity:
|
|
260
260
|
return {}
|
|
261
261
|
|
|
@@ -263,6 +263,9 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
263
263
|
raw.pop("etag", None)
|
|
264
264
|
raw.pop("ETag", None)
|
|
265
265
|
raw.pop("Timestamp", None)
|
|
266
|
+
# Internal: which model a row belongs to, used only to isolate models
|
|
267
|
+
# sharing a table. Never part of the public record shape.
|
|
268
|
+
raw.pop(_MODEL_FIELD, None)
|
|
266
269
|
|
|
267
270
|
keymap_str = raw.pop("__keymap__", None)
|
|
268
271
|
keymap: Dict[str, str] = {}
|
|
@@ -272,26 +275,30 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
272
275
|
except Exception:
|
|
273
276
|
keymap = {}
|
|
274
277
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
pk = raw.get("PartitionKey")
|
|
278
|
-
rk = raw.get("RowKey")
|
|
279
|
-
if pk is not None:
|
|
280
|
-
out["PartitionKey"] = pk
|
|
281
|
-
if rk is not None:
|
|
282
|
-
out["RowKey"] = rk
|
|
278
|
+
pk = raw.pop("PartitionKey", None)
|
|
279
|
+
rk = raw.pop("RowKey", None)
|
|
283
280
|
|
|
281
|
+
out: JsonDict = {}
|
|
284
282
|
for k, v in raw.items():
|
|
285
|
-
if k
|
|
286
|
-
continue
|
|
287
|
-
|
|
288
|
-
if k.startswith("_") or k in (_MODEL_FIELD,):
|
|
283
|
+
if k.startswith("_"):
|
|
289
284
|
out[k] = v
|
|
290
285
|
continue
|
|
291
286
|
|
|
292
287
|
orig_key = keymap.get(k, k)
|
|
293
288
|
out[orig_key] = self._decode_value(v)
|
|
294
289
|
|
|
290
|
+
# PartitionKey/RowKey are Azure Table Storage's own physical column
|
|
291
|
+
# names — never return them as-is. Map each back to the domain field
|
|
292
|
+
# it was derived from (model's declared partition_key/sort_key, same
|
|
293
|
+
# resolution _get_pk_rk used to write them), so the same field name
|
|
294
|
+
# round-trips on read as it did on write. Guarded so we never clobber
|
|
295
|
+
# a real property that already carries the value (the common case —
|
|
296
|
+
# pk/rk fields are usually also stored as regular properties).
|
|
297
|
+
pk_field, rk_field = self._pk_rk_field_names(model)
|
|
298
|
+
if pk is not None and pk_field not in out:
|
|
299
|
+
out[pk_field] = pk
|
|
300
|
+
if rk is not None and rk_field not in out:
|
|
301
|
+
out[rk_field] = rk
|
|
295
302
|
if "id" not in out and rk is not None:
|
|
296
303
|
out["id"] = rk
|
|
297
304
|
|
|
@@ -491,7 +498,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
491
498
|
|
|
492
499
|
table_client.upsert_entity(reference_entity)
|
|
493
500
|
|
|
494
|
-
restored = self._unpack_entity(entity)
|
|
501
|
+
restored = self._unpack_entity(model, entity)
|
|
495
502
|
restored["id"] = safe_rk
|
|
496
503
|
|
|
497
504
|
return restored
|
|
@@ -517,7 +524,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
517
524
|
|
|
518
525
|
restored_entity = self._restore_overflow_properties(entity_dict)
|
|
519
526
|
|
|
520
|
-
out = self._unpack_entity(restored_entity)
|
|
527
|
+
out = self._unpack_entity(model, restored_entity)
|
|
521
528
|
if "id" not in out:
|
|
522
529
|
out["id"] = safe_rk
|
|
523
530
|
|
|
@@ -589,7 +596,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
589
596
|
|
|
590
597
|
restored_entity = self._restore_overflow_properties(ent_dict)
|
|
591
598
|
|
|
592
|
-
out = self._unpack_entity(restored_entity)
|
|
599
|
+
out = self._unpack_entity(model, restored_entity)
|
|
593
600
|
|
|
594
601
|
if "id" not in out and "RowKey" in ent_dict:
|
|
595
602
|
out["id"] = ent_dict["RowKey"]
|
|
@@ -705,7 +712,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
705
712
|
for ent in page:
|
|
706
713
|
ent_dict = dict(ent)
|
|
707
714
|
restored = self._restore_overflow_properties(ent_dict)
|
|
708
|
-
out = self._unpack_entity(restored)
|
|
715
|
+
out = self._unpack_entity(model, restored)
|
|
709
716
|
if "id" not in out and "RowKey" in ent_dict:
|
|
710
717
|
out["id"] = ent_dict["RowKey"]
|
|
711
718
|
items.append(out)
|
|
@@ -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=
|
|
53
|
+
serverSelectionTimeoutMS=int(
|
|
54
|
+
os.getenv("MONGODB_SERVER_SELECTION_TIMEOUT_MS", "5000")
|
|
55
|
+
),
|
|
54
56
|
)
|
|
55
57
|
|
|
56
58
|
self._client.server_info()
|
|
@@ -34,6 +34,22 @@ class NoSQLKVAdapter:
|
|
|
34
34
|
self._lock = threading.Lock()
|
|
35
35
|
self.max_size = 1024 * 1024 # 1MB
|
|
36
36
|
|
|
37
|
+
def _pk_rk_field_names(self, model: type) -> Tuple[str, str]:
|
|
38
|
+
"""
|
|
39
|
+
The domain field names PK/RK were derived from — the mirror image of
|
|
40
|
+
_get_pk_rk's field resolution. Adapters use this to translate a
|
|
41
|
+
physical PartitionKey/RowKey (or pk/sk, whatever the backend calls
|
|
42
|
+
them) back into the model's own field name before returning a record,
|
|
43
|
+
instead of leaking storage-internal column names to callers. Falls
|
|
44
|
+
back to the same defaults _get_pk_rk uses (tenant_id / id) when a
|
|
45
|
+
model declares no explicit mapping, so unmapped models still round-
|
|
46
|
+
trip cleanly on upsert without erroring.
|
|
47
|
+
"""
|
|
48
|
+
meta = getattr(model, "__polydb__", {})
|
|
49
|
+
pk_field = meta.get("pk_field") or meta.get("partition_key", "tenant_id")
|
|
50
|
+
rk_field = meta.get("rk_field") or meta.get("sort_key", "id")
|
|
51
|
+
return pk_field, rk_field
|
|
52
|
+
|
|
37
53
|
def _get_pk_rk(self, model: type, data: JsonDict) -> Tuple[str, str]:
|
|
38
54
|
"""Extract PK/RK from model metadata"""
|
|
39
55
|
meta = getattr(model, "__polydb__", {})
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/databaseFactory.py
RENAMED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
@@ -543,14 +546,32 @@ class DatabaseFactory:
|
|
|
543
546
|
if self._is_sql(meta, engine_override):
|
|
544
547
|
result = adapters.sql.update(meta.table, entity_id, data)
|
|
545
548
|
else:
|
|
549
|
+
# A scalar entity_id is usually the record's "id" property,
|
|
550
|
+
# not its physical PartitionKey/RowKey — those come from the
|
|
551
|
+
# model's declared pk_field/rk_field (x-metadata partition_key
|
|
552
|
+
# /sort_key), which for most schema models is a different
|
|
553
|
+
# field entirely (e.g. Artifact: pk=project_slug, rk=
|
|
554
|
+
# template_key). Recover both from the patch payload, or —
|
|
555
|
+
# more reliably — from the record loaded just above as
|
|
556
|
+
# `before`, so a plain id-addressed update() still lands on
|
|
557
|
+
# the entity's real physical key instead of a mismatched
|
|
558
|
+
# default partition / the id used as a row key it never had.
|
|
546
559
|
pkey = data.get("PartitionKey") or data.get("partition_key") or data.get("pk")
|
|
547
|
-
|
|
560
|
+
if not pkey and meta.pk_field:
|
|
561
|
+
pkey = data.get(meta.pk_field)
|
|
548
562
|
if not pkey and before:
|
|
549
563
|
pkey = (
|
|
550
564
|
before.get("PartitionKey")
|
|
551
565
|
or before.get("partition_key")
|
|
552
566
|
or before.get("pk")
|
|
567
|
+
or (before.get(meta.pk_field) if meta.pk_field else None)
|
|
553
568
|
)
|
|
569
|
+
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
|
|
573
|
+
)
|
|
574
|
+
en_id = entity_id
|
|
554
575
|
if pkey:
|
|
555
576
|
if isinstance(en_id, dict):
|
|
556
577
|
en_pk = (
|
|
@@ -560,8 +581,16 @@ class DatabaseFactory:
|
|
|
560
581
|
)
|
|
561
582
|
if not en_pk:
|
|
562
583
|
en_id["partition_key"] = pkey
|
|
584
|
+
en_rk = (
|
|
585
|
+
en_id.get("RowKey")
|
|
586
|
+
or en_id.get("row_key")
|
|
587
|
+
or en_id.get("rk")
|
|
588
|
+
or en_id.get("id")
|
|
589
|
+
)
|
|
590
|
+
if not en_rk and rkey:
|
|
591
|
+
en_id["row_key"] = rkey
|
|
563
592
|
elif isinstance(en_id, str):
|
|
564
|
-
en_id = {"partition_key": pkey, "
|
|
593
|
+
en_id = {"partition_key": pkey, "row_key": rkey or entity_id}
|
|
565
594
|
|
|
566
595
|
cls = (
|
|
567
596
|
model
|
|
@@ -702,7 +731,23 @@ class DatabaseFactory:
|
|
|
702
731
|
if isinstance(model, type)
|
|
703
732
|
else type(name, (), {"__polydb__": meta.__dict__})
|
|
704
733
|
)
|
|
705
|
-
|
|
734
|
+
# Same physical-key recovery as update() (see there for why a
|
|
735
|
+
# scalar entity_id alone isn't enough once a model's pk_field
|
|
736
|
+
# /rk_field differ from "id").
|
|
737
|
+
en_id = entity_id
|
|
738
|
+
if not isinstance(en_id, dict) and before:
|
|
739
|
+
pkey = (
|
|
740
|
+
before.get("PartitionKey")
|
|
741
|
+
or before.get("partition_key")
|
|
742
|
+
or before.get("pk")
|
|
743
|
+
or (before.get(meta.pk_field) if meta.pk_field else None)
|
|
744
|
+
)
|
|
745
|
+
if pkey:
|
|
746
|
+
rkey = None
|
|
747
|
+
if meta.rk_field and meta.rk_field != "id":
|
|
748
|
+
rkey = before.get(meta.rk_field)
|
|
749
|
+
en_id = {"partition_key": pkey, "row_key": rkey or entity_id}
|
|
750
|
+
result = adapters.nosql.delete(cls, en_id, etag=etag)
|
|
706
751
|
success = True
|
|
707
752
|
if self._enable_cache and self._cache:
|
|
708
753
|
try:
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Unit tests for AzureTableStorageAdapter._unpack_entity (no Azurite needed).
|
|
2
|
+
|
|
3
|
+
Contract: PartitionKey/RowKey/__polydb_model__ are Azure Table Storage
|
|
4
|
+
internals and must never leak into a returned record. Each gets mapped back
|
|
5
|
+
to the domain field it was derived from (the model's declared
|
|
6
|
+
partition_key/sort_key — the same fields _get_pk_rk used to write them), and
|
|
7
|
+
only fills in that field if it isn't already present from a regular stored
|
|
8
|
+
property. Models with no explicit mapping still round-trip cleanly via the
|
|
9
|
+
same defaults _get_pk_rk uses (tenant_id / id) instead of erroring.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
pytest.importorskip("azure.data.tables") # adapter needs the SDK to import
|
|
15
|
+
|
|
16
|
+
from polydb.adapters.AzureTableStorageAdapter import AzureTableStorageAdapter # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _ArtifactComment:
|
|
20
|
+
__name__ = "ArtifactComment"
|
|
21
|
+
__polydb__ = {"partition_key": "artifact_id", "sort_key": "created_at"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _Unmapped:
|
|
25
|
+
__name__ = "Unmapped"
|
|
26
|
+
# no __polydb__ at all
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _adapter():
|
|
30
|
+
return object.__new__(AzureTableStorageAdapter) # bypass live-connection __init__
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_mapped_model_remaps_pk_rk_and_strips_internals():
|
|
34
|
+
entity = {
|
|
35
|
+
"PartitionKey": "artifact-123",
|
|
36
|
+
"RowKey": "2026-07-13T21_04_53",
|
|
37
|
+
"__polydb_model__": "ArtifactComment",
|
|
38
|
+
"artifact_id": "artifact-123",
|
|
39
|
+
"created_at": "2026-07-13T21:04:53",
|
|
40
|
+
"body": "hello",
|
|
41
|
+
}
|
|
42
|
+
out = _adapter()._unpack_entity(_ArtifactComment, entity)
|
|
43
|
+
assert "PartitionKey" not in out
|
|
44
|
+
assert "RowKey" not in out
|
|
45
|
+
assert "__polydb_model__" not in out
|
|
46
|
+
assert out["artifact_id"] == "artifact-123"
|
|
47
|
+
assert out["created_at"] == "2026-07-13T21:04:53" # real property wins over RowKey
|
|
48
|
+
assert out["body"] == "hello"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_unmapped_model_falls_back_without_erroring():
|
|
52
|
+
entity = {
|
|
53
|
+
"PartitionKey": "t1",
|
|
54
|
+
"RowKey": "row1",
|
|
55
|
+
"__polydb_model__": "Unmapped",
|
|
56
|
+
"email": "user@x.com",
|
|
57
|
+
}
|
|
58
|
+
out = _adapter()._unpack_entity(_Unmapped, entity)
|
|
59
|
+
assert "PartitionKey" not in out
|
|
60
|
+
assert "RowKey" not in out
|
|
61
|
+
assert "__polydb_model__" not in out
|
|
62
|
+
assert out["tenant_id"] == "t1"
|
|
63
|
+
assert out["id"] == "row1"
|
|
64
|
+
assert out["email"] == "user@x.com"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_pk_rk_fill_in_only_when_field_missing():
|
|
68
|
+
entity = {
|
|
69
|
+
"PartitionKey": "artifact-999",
|
|
70
|
+
"RowKey": "row-1",
|
|
71
|
+
"__polydb_model__": "ArtifactComment",
|
|
72
|
+
"body": "no explicit artifact_id property",
|
|
73
|
+
}
|
|
74
|
+
out = _adapter()._unpack_entity(_ArtifactComment, entity)
|
|
75
|
+
assert out["artifact_id"] == "artifact-999"
|
|
76
|
+
assert out["created_at"] == "row-1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/requirements-generic.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/S3Adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/adapters/__init__.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/advanced_query.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/AuditStorage.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/__init__.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/context.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/manager.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/audit/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/base/QueueAdapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/base/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/decorators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/monitoring.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/multitenancy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/services/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/src/polydb/validation.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_atomic_decrement.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_blockchain.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_cloud_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_multi_engine.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_observability.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.26 → altcodepro_polydb_python-2.3.28}/tests/test_postgresql.py
RENAMED
|
File without changes
|
|
File without changes
|