altcodepro-polydb-python 2.5.3__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.
- {altcodepro_polydb_python-2.5.3.dist-info → altcodepro_polydb_python-2.5.6.dist-info}/METADATA +69 -20
- {altcodepro_polydb_python-2.5.3.dist-info → altcodepro_polydb_python-2.5.6.dist-info}/RECORD +29 -22
- polydb/__init__.py +1 -1
- polydb/adapters/AWSSecretsManagerAdapter.py +72 -0
- polydb/adapters/AzureKeyVaultAdapter.py +65 -0
- polydb/adapters/AzureQueueAdapter.py +82 -4
- polydb/adapters/AzureTableStorageAdapter.py +32 -5
- polydb/adapters/DynamoDBAdapter.py +31 -16
- polydb/adapters/FirestoreAdapter.py +30 -13
- polydb/adapters/GCPPubSubAdapter.py +46 -0
- polydb/adapters/GCPSecretManagerAdapter.py +79 -0
- polydb/adapters/KafkaQueueAdapter.py +332 -0
- polydb/adapters/PostgreSQLAdapter.py +114 -6
- polydb/adapters/RabbitMQAdapter.py +465 -0
- polydb/adapters/SQSAdapter.py +60 -0
- polydb/adapters/VaultAdapter.py +75 -0
- polydb/adapters/VercelKVAdapter.py +38 -24
- polydb/adapters/VercelQueueAdapter.py +30 -2
- polydb/base/QueueAdapter.py +123 -1
- polydb/base/SecretsAdapter.py +30 -0
- polydb/cache.py +90 -0
- polydb/cloudDatabaseFactory.py +129 -15
- polydb/databaseFactory.py +225 -49
- polydb/errors.py +6 -0
- polydb/models.py +77 -0
- polydb/retry.py +7 -0
- {altcodepro_polydb_python-2.5.3.dist-info → altcodepro_polydb_python-2.5.6.dist-info}/WHEEL +0 -0
- {altcodepro_polydb_python-2.5.3.dist-info → altcodepro_polydb_python-2.5.6.dist-info}/licenses/LICENSE +0 -0
- {altcodepro_polydb_python-2.5.3.dist-info → altcodepro_polydb_python-2.5.6.dist-info}/top_level.txt +0 -0
|
@@ -23,10 +23,12 @@ class DynamoDBAdapter(NoSQLKVAdapter):
|
|
|
23
23
|
Production-grade DynamoDB adapter with optional S3 overflow.
|
|
24
24
|
|
|
25
25
|
Goals (matches your adapter test style)
|
|
26
|
-
- stored row keeps "id" ==
|
|
27
|
-
|
|
26
|
+
- stored row keeps "id" == rk (the row key -- see NoSQLKVAdapter._get_pk_rk,
|
|
27
|
+
which derives rk from data.get(rk_field, ...) with rk_field defaulting
|
|
28
|
+
to "id"); put() returns the full stored record, not just {"id": ...}
|
|
29
|
+
- query({"id": ...}) works (matches the record's own "id"/rk, not PK)
|
|
28
30
|
- patch() merges (handled by NoSQLKVAdapter.patch)
|
|
29
|
-
- delete() returns {"id": <
|
|
31
|
+
- delete() returns {"id": <rk>} and raises sqlite3.DatabaseError on missing
|
|
30
32
|
- query_page() uses DynamoDB LastEvaluatedKey token (stable)
|
|
31
33
|
- LocalStack support (endpoint_url) + auto create table/bucket in test/dev
|
|
32
34
|
"""
|
|
@@ -273,16 +275,24 @@ class DynamoDBAdapter(NoSQLKVAdapter):
|
|
|
273
275
|
payload["PK"] = pk
|
|
274
276
|
payload["SK"] = rk
|
|
275
277
|
|
|
276
|
-
#
|
|
277
|
-
|
|
278
|
+
# "id" is the row key (NoSQLKVAdapter._get_pk_rk derives `rk` from
|
|
279
|
+
# data.get(rk_field, ...) with rk_field defaulting to "id"), so it
|
|
280
|
+
# is already present in `payload` via dict(data) above whenever the
|
|
281
|
+
# caller supplied one. Only fall back to `rk` -- never `pk`, which
|
|
282
|
+
# would silently collide every row in the same partition onto the
|
|
283
|
+
# same "id" and break id-addressed lookups (query/get/patch/delete
|
|
284
|
+
# by {"id": ...}), the same corruption previously present here and
|
|
285
|
+
# in VercelKVAdapter.
|
|
286
|
+
payload.setdefault("id", rk)
|
|
278
287
|
payload.setdefault("_pk", pk)
|
|
279
288
|
payload.setdefault("_rk", rk)
|
|
280
289
|
|
|
281
290
|
ref = self._maybe_overflow_to_s3(model, pk, rk, payload)
|
|
282
291
|
table.put_item(Item=ref if ref is not None else payload)
|
|
283
292
|
|
|
284
|
-
#
|
|
285
|
-
|
|
293
|
+
# Return the full stored record, not just {"id": pk} -- callers
|
|
294
|
+
# (e.g. DatabaseFactory.create()) use this as the created row.
|
|
295
|
+
return payload
|
|
286
296
|
|
|
287
297
|
except Exception as e:
|
|
288
298
|
raise NoSQLError(f"DynamoDB put failed: {e}")
|
|
@@ -297,7 +307,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
|
|
|
297
307
|
if not item:
|
|
298
308
|
return None
|
|
299
309
|
|
|
300
|
-
item.setdefault("id",
|
|
310
|
+
item.setdefault("id", rk)
|
|
301
311
|
return self._resolve_overflow(item)
|
|
302
312
|
|
|
303
313
|
except Exception as e:
|
|
@@ -318,8 +328,11 @@ class DynamoDBAdapter(NoSQLKVAdapter):
|
|
|
318
328
|
table = self._get_table(model)
|
|
319
329
|
filters = filters or {}
|
|
320
330
|
|
|
321
|
-
#
|
|
322
|
-
|
|
331
|
+
# "id" is the row key by convention (see _put_raw), not the
|
|
332
|
+
# partition key -- only route to Query when the caller names the
|
|
333
|
+
# actual PK explicitly. A bare {"id": ...} filter falls through to
|
|
334
|
+
# Scan below with "id" applied as a normal attribute filter.
|
|
335
|
+
pk_value = filters.get("PK") or filters.get("partition_key")
|
|
323
336
|
|
|
324
337
|
if pk_value is not None:
|
|
325
338
|
key_cond = Key("PK").eq(str(pk_value))
|
|
@@ -330,7 +343,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
|
|
|
330
343
|
|
|
331
344
|
# Other filters as FilterExpression
|
|
332
345
|
other = {
|
|
333
|
-
k: v for k, v in filters.items() if k not in ("PK", "SK", "partition_key"
|
|
346
|
+
k: v for k, v in filters.items() if k not in ("PK", "SK", "partition_key")
|
|
334
347
|
}
|
|
335
348
|
if other:
|
|
336
349
|
expr = None
|
|
@@ -361,7 +374,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
|
|
|
361
374
|
|
|
362
375
|
out: List[JsonDict] = []
|
|
363
376
|
for it in items:
|
|
364
|
-
it.setdefault("id", it.get("
|
|
377
|
+
it.setdefault("id", it.get("_rk") or it.get("SK"))
|
|
365
378
|
out.append(self._resolve_overflow(it))
|
|
366
379
|
return out
|
|
367
380
|
|
|
@@ -392,7 +405,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
|
|
|
392
405
|
pass
|
|
393
406
|
|
|
394
407
|
table.delete_item(Key={"PK": pk, "SK": rk})
|
|
395
|
-
return {"id":
|
|
408
|
+
return {"id": rk}
|
|
396
409
|
|
|
397
410
|
except DatabaseError:
|
|
398
411
|
raise
|
|
@@ -427,7 +440,9 @@ class DynamoDBAdapter(NoSQLKVAdapter):
|
|
|
427
440
|
|
|
428
441
|
start_key = self._decode_token(continuation_token) if continuation_token else None
|
|
429
442
|
|
|
430
|
-
|
|
443
|
+
# "id" is the row key by convention, not the partition key --
|
|
444
|
+
# see _query_raw.
|
|
445
|
+
pk_value = query.get("PK") or query.get("partition_key")
|
|
431
446
|
|
|
432
447
|
items: List[JsonDict] = []
|
|
433
448
|
last_key = start_key
|
|
@@ -448,7 +463,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
|
|
|
448
463
|
kwargs["ExclusiveStartKey"] = last_key
|
|
449
464
|
|
|
450
465
|
other_filters = {
|
|
451
|
-
k: v for k, v in query.items() if k not in ("PK", "SK", "partition_key"
|
|
466
|
+
k: v for k, v in query.items() if k not in ("PK", "SK", "partition_key")
|
|
452
467
|
}
|
|
453
468
|
|
|
454
469
|
if other_filters:
|
|
@@ -501,7 +516,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
|
|
|
501
516
|
out: List[JsonDict] = []
|
|
502
517
|
|
|
503
518
|
for it in items:
|
|
504
|
-
it.setdefault("id", it.get("
|
|
519
|
+
it.setdefault("id", it.get("_rk") or it.get("SK"))
|
|
505
520
|
out.append(self._resolve_overflow(it))
|
|
506
521
|
|
|
507
522
|
next_tok = self._encode_token(last_key) if last_key else None
|
|
@@ -25,12 +25,21 @@ class FirestoreAdapter(NoSQLKVAdapter):
|
|
|
25
25
|
Production-grade Firestore adapter with optional GCS overflow.
|
|
26
26
|
|
|
27
27
|
Goals (matches your tests)
|
|
28
|
-
-
|
|
28
|
+
- stored row keeps "id" == rk (the row key -- see NoSQLKVAdapter._get_pk_rk,
|
|
29
|
+
which derives rk from data.get(rk_field, ...) with rk_field defaulting
|
|
30
|
+
to "id"), so querying {"id": ...} matches the record's own id
|
|
29
31
|
- patch() merges (preserves existing fields)
|
|
30
|
-
- delete() returns {"id": <
|
|
32
|
+
- delete() returns {"id": <rk>} and raises DatabaseError on missing
|
|
31
33
|
- query_page() returns (rows, token) with stable pagination
|
|
32
34
|
- Emulator support via FIRESTORE_EMULATOR_HOST
|
|
33
35
|
|
|
36
|
+
NOTE: the Firestore *document id* is still `pk` alone (see _doc_id) --
|
|
37
|
+
this adapter does not compose pk+rk into the physical document key the
|
|
38
|
+
way VercelKVAdapter/DynamoDBAdapter do. That is a pre-existing, separate
|
|
39
|
+
design constraint (two rows sharing the same pk collide onto one
|
|
40
|
+
document) left as-is here since fixing it is a bigger structural change
|
|
41
|
+
this pass didn't verify against a live Firestore/emulator.
|
|
42
|
+
|
|
34
43
|
NOTE: google-cloud-firestore / google-cloud-storage are imported lazily
|
|
35
44
|
inside the methods that use them, so installing them is only required when
|
|
36
45
|
this adapter is actually used.
|
|
@@ -118,7 +127,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
|
|
|
118
127
|
return self._client.collection(self._collection_name(model))
|
|
119
128
|
|
|
120
129
|
def _doc_id(self, pk: str) -> str:
|
|
121
|
-
#
|
|
130
|
+
# Document id is the partition key alone (not composed with rk).
|
|
122
131
|
return str(pk)
|
|
123
132
|
|
|
124
133
|
def _blob_key(self, model: type, pk: str, rk: str, checksum: str) -> str:
|
|
@@ -148,7 +157,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
|
|
|
148
157
|
blob.upload_from_string(data_bytes)
|
|
149
158
|
|
|
150
159
|
ref: JsonDict = {
|
|
151
|
-
"id":
|
|
160
|
+
"id": rk,
|
|
152
161
|
"_pk": pk,
|
|
153
162
|
"_rk": rk,
|
|
154
163
|
"_overflow": True,
|
|
@@ -203,9 +212,15 @@ class FirestoreAdapter(NoSQLKVAdapter):
|
|
|
203
212
|
collection = self._get_collection(model)
|
|
204
213
|
doc_id = self._doc_id(pk)
|
|
205
214
|
|
|
206
|
-
#
|
|
215
|
+
# "id" is the row key (see NoSQLKVAdapter._get_pk_rk, which derives
|
|
216
|
+
# rk from data.get(rk_field, ...) with rk_field defaulting to "id").
|
|
217
|
+
# It is already present in `payload` via dict(data) above whenever
|
|
218
|
+
# the caller supplied one -- only fall back to `rk`, never `pk`,
|
|
219
|
+
# which would silently collapse every row's "id" to its partition
|
|
220
|
+
# key and break id-addressed lookups (the same corruption
|
|
221
|
+
# previously present here and in VercelKVAdapter/DynamoDBAdapter).
|
|
207
222
|
payload: JsonDict = dict(data or {})
|
|
208
|
-
payload
|
|
223
|
+
payload.setdefault("id", rk)
|
|
209
224
|
payload["_pk"] = pk
|
|
210
225
|
payload["_rk"] = rk
|
|
211
226
|
|
|
@@ -215,7 +230,8 @@ class FirestoreAdapter(NoSQLKVAdapter):
|
|
|
215
230
|
else:
|
|
216
231
|
collection.document(doc_id).set(payload)
|
|
217
232
|
|
|
218
|
-
|
|
233
|
+
# Return the full stored record, not just {"id": pk}.
|
|
234
|
+
return overflow_ref if overflow_ref is not None else payload
|
|
219
235
|
|
|
220
236
|
except Exception as e:
|
|
221
237
|
raise NoSQLError(f"Firestore put failed: {e}")
|
|
@@ -231,7 +247,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
|
|
|
231
247
|
return None
|
|
232
248
|
|
|
233
249
|
doc_data = snap.to_dict() or {}
|
|
234
|
-
doc_data.setdefault("id",
|
|
250
|
+
doc_data.setdefault("id", rk)
|
|
235
251
|
|
|
236
252
|
return self._resolve_overflow(doc_data)
|
|
237
253
|
|
|
@@ -275,8 +291,9 @@ class FirestoreAdapter(NoSQLKVAdapter):
|
|
|
275
291
|
out: List[JsonDict] = []
|
|
276
292
|
for d in docs:
|
|
277
293
|
row = d.to_dict() or {}
|
|
278
|
-
#
|
|
279
|
-
|
|
294
|
+
# Fall back to the row key, not the partition key, when "id"
|
|
295
|
+
# is absent from the stored document (legacy/incomplete data).
|
|
296
|
+
row.setdefault("id", row.get("_rk") or d.id)
|
|
280
297
|
out.append(self._resolve_overflow(row))
|
|
281
298
|
return out
|
|
282
299
|
|
|
@@ -288,7 +305,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
|
|
|
288
305
|
"""
|
|
289
306
|
Test expectations:
|
|
290
307
|
- deleting nonexistent raises sqlite3.DatabaseError
|
|
291
|
-
- delete returns {"id":
|
|
308
|
+
- delete returns {"id": rk}
|
|
292
309
|
- deletes overflow blob if present
|
|
293
310
|
"""
|
|
294
311
|
try:
|
|
@@ -311,7 +328,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
|
|
|
311
328
|
pass
|
|
312
329
|
|
|
313
330
|
collection.document(doc_id).delete()
|
|
314
|
-
return {"id":
|
|
331
|
+
return {"id": rk}
|
|
315
332
|
|
|
316
333
|
except DatabaseError:
|
|
317
334
|
raise
|
|
@@ -355,7 +372,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
|
|
|
355
372
|
rows: List[JsonDict] = []
|
|
356
373
|
for d in docs:
|
|
357
374
|
row = d.to_dict() or {}
|
|
358
|
-
row.setdefault("id", row.get("
|
|
375
|
+
row.setdefault("id", row.get("_rk") or d.id)
|
|
359
376
|
rows.append(self._resolve_overflow(row))
|
|
360
377
|
|
|
361
378
|
next_token = None
|
|
@@ -237,3 +237,49 @@ class GCPPubSubAdapter(QueueAdapter):
|
|
|
237
237
|
|
|
238
238
|
except Exception as e:
|
|
239
239
|
raise QueueError(f"Pub/Sub ack failed: {e}")
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------
|
|
242
|
+
# extend -- real GCP Pub/Sub ModifyAckDeadline. delay()/cancel() are
|
|
243
|
+
# NOT overridden: a published Pub/Sub message is deliverable
|
|
244
|
+
# immediately (no "don't make this visible until N seconds from
|
|
245
|
+
# now" primitive exists), so the base class's NotImplementedError
|
|
246
|
+
# is the honest answer for both.
|
|
247
|
+
# ---------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
# Pub/Sub's own real, documented maximum ack deadline. Google's API
|
|
250
|
+
# rejects anything above this with an invalid-argument error, so
|
|
251
|
+
# enforce it here rather than leaving callers to hit an opaque
|
|
252
|
+
# remote error.
|
|
253
|
+
MAX_ACK_DEADLINE_SECONDS = 600
|
|
254
|
+
|
|
255
|
+
@retry(max_attempts=3, delay=1.0, exceptions=(QueueError,))
|
|
256
|
+
def extend(
|
|
257
|
+
self, ack_id: str, queue_name: str = "default", *, visibility_timeout: int = 30
|
|
258
|
+
) -> bool:
|
|
259
|
+
"""Real Pub/Sub ModifyAckDeadline -- ack_id is the same ack_id
|
|
260
|
+
receive() returned for this message."""
|
|
261
|
+
if not ack_id:
|
|
262
|
+
raise QueueError("ack_id is required for Pub/Sub extend")
|
|
263
|
+
if visibility_timeout > self.MAX_ACK_DEADLINE_SECONDS:
|
|
264
|
+
raise QueueError(
|
|
265
|
+
f"Pub/Sub ack deadline cannot exceed {self.MAX_ACK_DEADLINE_SECONDS}s "
|
|
266
|
+
f"(GCP's own real maximum), got {visibility_timeout}"
|
|
267
|
+
)
|
|
268
|
+
try:
|
|
269
|
+
if not self._subscriber:
|
|
270
|
+
raise ConnectionError("Pub/Sub subscriber not initialized")
|
|
271
|
+
|
|
272
|
+
_, subscription = self._resolve_names(queue_name)
|
|
273
|
+
sub_path = self._subscription_path(subscription)
|
|
274
|
+
|
|
275
|
+
self._subscriber.modify_ack_deadline(
|
|
276
|
+
request={
|
|
277
|
+
"subscription": sub_path,
|
|
278
|
+
"ack_ids": [ack_id],
|
|
279
|
+
"ack_deadline_seconds": visibility_timeout,
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
except Exception as e:
|
|
285
|
+
raise QueueError(f"Pub/Sub extend failed: {e}")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# src/polydb/adapters/GCPSecretManagerAdapter.py
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from ..base.SecretsAdapter import SecretsAdapter
|
|
6
|
+
from ..errors import ConnectionError
|
|
7
|
+
from ..retry import retry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GCPSecretManagerAdapter(SecretsAdapter):
|
|
11
|
+
def __init__(self, project_id: str = ""):
|
|
12
|
+
super().__init__()
|
|
13
|
+
|
|
14
|
+
self.project_id = project_id or os.getenv("GOOGLE_CLOUD_PROJECT")
|
|
15
|
+
if not self.project_id:
|
|
16
|
+
raise ConnectionError("GOOGLE_CLOUD_PROJECT is not configured")
|
|
17
|
+
|
|
18
|
+
self._client = None
|
|
19
|
+
self._initialize_client()
|
|
20
|
+
|
|
21
|
+
def _initialize_client(self) -> None:
|
|
22
|
+
from google.cloud import secretmanager
|
|
23
|
+
|
|
24
|
+
self._client = secretmanager.SecretManagerServiceClient()
|
|
25
|
+
|
|
26
|
+
def _secret_path(self, key: str) -> str:
|
|
27
|
+
return f"projects/{self.project_id}/secrets/{key}"
|
|
28
|
+
|
|
29
|
+
@retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
|
|
30
|
+
def get_secret(self, key: str) -> Optional[str]:
|
|
31
|
+
from google.api_core.exceptions import NotFound
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
resp = self._client.access_secret_version(
|
|
35
|
+
name=f"{self._secret_path(key)}/versions/latest"
|
|
36
|
+
)
|
|
37
|
+
except NotFound:
|
|
38
|
+
return None
|
|
39
|
+
return resp.payload.data.decode("utf-8")
|
|
40
|
+
|
|
41
|
+
@retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
|
|
42
|
+
def set_secret(self, key: str, value: str) -> None:
|
|
43
|
+
from google.api_core.exceptions import AlreadyExists
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
self._client.create_secret(
|
|
47
|
+
request={
|
|
48
|
+
"parent": f"projects/{self.project_id}",
|
|
49
|
+
"secret_id": key,
|
|
50
|
+
"secret": {"replication": {"automatic": {}}},
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
except AlreadyExists:
|
|
54
|
+
pass
|
|
55
|
+
self._client.add_secret_version(
|
|
56
|
+
request={
|
|
57
|
+
"parent": self._secret_path(key),
|
|
58
|
+
"payload": {"data": value.encode("utf-8")},
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
|
|
63
|
+
def delete_secret(self, key: str) -> bool:
|
|
64
|
+
from google.api_core.exceptions import NotFound
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
self._client.delete_secret(request={"name": self._secret_path(key)})
|
|
68
|
+
return True
|
|
69
|
+
except NotFound:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
@retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
|
|
73
|
+
def list_secrets(self, prefix: str = "") -> list[str]:
|
|
74
|
+
secrets = self._client.list_secrets(request={"parent": f"projects/{self.project_id}"})
|
|
75
|
+
return [
|
|
76
|
+
s.name.rsplit("/", 1)[-1]
|
|
77
|
+
for s in secrets
|
|
78
|
+
if s.name.rsplit("/", 1)[-1].startswith(prefix)
|
|
79
|
+
]
|