altcodepro-polydb-python 2.5.4__py3-none-any.whl → 2.5.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.5.4
3
+ Version: 2.5.6
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
@@ -27,19 +27,6 @@ Requires-Dist: psycopg2-binary>=2.9.11
27
27
  Requires-Dist: tenacity>=9.1.4
28
28
  Requires-Dist: redis>=6.4.0
29
29
  Requires-Dist: python-dotenv>=1.1.1
30
- Requires-Dist: azure-storage-queue>=12.15.0
31
- Requires-Dist: azure-storage-blob>=12.29.0
32
- Requires-Dist: boto3>=1.43.18
33
- Requires-Dist: google-cloud-storage>=3.10.1
34
- Requires-Dist: azure-storage-file-share>=12.25.0
35
- Requires-Dist: azure-data-tables>=12.7.0
36
- Requires-Dist: ipfshttpclient>=0.7.0
37
- Requires-Dist: web3>=7.16.0
38
- Requires-Dist: google-cloud-firestore>=2.27.0
39
- Requires-Dist: google-cloud-pubsub>=2.38.0
40
- Requires-Dist: pymongo>=4.17.0
41
- Requires-Dist: build>=1.5.0
42
- Requires-Dist: twine>=6.2.0
43
30
  Provides-Extra: aws
44
31
  Requires-Dist: boto3>=1.42.47; extra == "aws"
45
32
  Requires-Dist: botocore>=1.42.47; extra == "aws"
@@ -49,6 +36,8 @@ Requires-Dist: azure-data-tables>=12.7.0; extra == "azure"
49
36
  Requires-Dist: azure-storage-blob>=12.28.0; extra == "azure"
50
37
  Requires-Dist: azure-storage-file-share>=12.24.0; extra == "azure"
51
38
  Requires-Dist: azure-storage-queue>=12.15.0; extra == "azure"
39
+ Requires-Dist: azure-identity>=1.16.0; extra == "azure"
40
+ Requires-Dist: azure-keyvault-secrets>=4.8.0; extra == "azure"
52
41
  Provides-Extra: gcp
53
42
  Requires-Dist: google-api-core>=2.29.0; extra == "gcp"
54
43
  Requires-Dist: google-auth>=2.48.0; extra == "gcp"
@@ -56,18 +45,27 @@ Requires-Dist: google-cloud-core>=2.5.0; extra == "gcp"
56
45
  Requires-Dist: google-cloud-firestore>=2.23.0; extra == "gcp"
57
46
  Requires-Dist: google-cloud-pubsub>=2.35.0; extra == "gcp"
58
47
  Requires-Dist: google-cloud-storage>=3.9.0; extra == "gcp"
48
+ Requires-Dist: google-cloud-secret-manager>=2.20.0; extra == "gcp"
59
49
  Provides-Extra: mongodb
60
50
  Requires-Dist: pymongo>=4.16.0; extra == "mongodb"
61
51
  Provides-Extra: rabbitmq
62
52
  Requires-Dist: pika>=1.3.2; extra == "rabbitmq"
53
+ Provides-Extra: kafka
54
+ Requires-Dist: kafka-python>=2.0.2; extra == "kafka"
63
55
  Provides-Extra: vercel
64
56
  Requires-Dist: requests>=2.32.5; extra == "vercel"
57
+ Provides-Extra: blockchain
58
+ Requires-Dist: ipfshttpclient>=0.7.0; extra == "blockchain"
59
+ Requires-Dist: web3>=7.14.1; extra == "blockchain"
65
60
  Provides-Extra: bolckchain
66
61
  Requires-Dist: ipfshttpclient>=0.7.0; extra == "bolckchain"
67
62
  Requires-Dist: web3>=7.14.1; extra == "bolckchain"
63
+ Provides-Extra: secrets
64
+ Requires-Dist: hvac>=2.1.0; extra == "secrets"
68
65
  Provides-Extra: generic
69
66
  Requires-Dist: pymongo>=4.16.0; extra == "generic"
70
67
  Requires-Dist: pika>=1.3.2; extra == "generic"
68
+ Requires-Dist: kafka-python>=2.0.2; extra == "generic"
71
69
  Requires-Dist: boto3>=1.42.47; extra == "generic"
72
70
  Provides-Extra: security
73
71
  Requires-Dist: cryptography>=42.0.0; extra == "security"
@@ -79,13 +77,18 @@ Requires-Dist: azure-data-tables>=12.7.0; extra == "all"
79
77
  Requires-Dist: azure-storage-blob>=12.28.0; extra == "all"
80
78
  Requires-Dist: azure-storage-file-share>=12.24.0; extra == "all"
81
79
  Requires-Dist: azure-storage-queue>=12.15.0; extra == "all"
80
+ Requires-Dist: azure-identity>=1.16.0; extra == "all"
81
+ Requires-Dist: azure-keyvault-secrets>=4.8.0; extra == "all"
82
82
  Requires-Dist: google-cloud-firestore>=2.23.0; extra == "all"
83
83
  Requires-Dist: google-cloud-pubsub>=2.35.0; extra == "all"
84
84
  Requires-Dist: google-cloud-storage>=3.9.0; extra == "all"
85
+ Requires-Dist: google-cloud-secret-manager>=2.20.0; extra == "all"
85
86
  Requires-Dist: pymongo>=4.16.0; extra == "all"
86
87
  Requires-Dist: pika>=1.3.2; extra == "all"
88
+ Requires-Dist: kafka-python>=2.0.2; extra == "all"
87
89
  Requires-Dist: requests>=2.32.5; extra == "all"
88
90
  Requires-Dist: cryptography>=42.0.0; extra == "all"
91
+ Requires-Dist: hvac>=2.1.0; extra == "all"
89
92
  Provides-Extra: dev
90
93
  Requires-Dist: black>=26.1.0; extra == "dev"
91
94
  Requires-Dist: flake8>=7.3.0; extra == "dev"
@@ -97,6 +100,7 @@ Requires-Dist: pytest-asyncio>=1.2.0; extra == "test"
97
100
  Requires-Dist: pytest-cov>=7.0.0; extra == "test"
98
101
  Requires-Dist: pytest-mock>=3.15.1; extra == "test"
99
102
  Requires-Dist: moto>=5.1.21; extra == "test"
103
+ Requires-Dist: hvac>=2.1.0; extra == "test"
100
104
  Dynamic: license-file
101
105
 
102
106
 
@@ -291,19 +295,64 @@ Supported systems:
291
295
  * AWS SQS
292
296
  * Azure Queue
293
297
  * Google Pub/Sub
294
- * Vercel Queue
298
+ * Vercel Queue (cloud REST API, or a plain local `redis://` URL for
299
+ dev/test)
300
+ * Kafka
301
+ * RabbitMQ
295
302
  * Blockchain event queues
296
303
 
297
- Example:
304
+ Example (method names below match `QueueAdapter`'s actual signatures —
305
+ `send`/`receive`/`ack`/`delete`, not `send_message`/`receive_message`):
298
306
 
299
307
  ```python
300
- queue = factory.get_queue()
308
+ queue = factory.get_queue("rabbitmq")
301
309
 
302
- queue.send_message("jobs", {"task": "process"})
303
- msg = queue.receive_message("jobs")
304
- queue.delete_message("jobs", msg)
310
+ message_id = queue.send({"task": "process"}, queue_name="jobs")
311
+ messages = queue.receive(queue_name="jobs", max_messages=1)
312
+ queue.ack(messages[0]["receipt_handle"], queue_name="jobs")
305
313
  ```
306
314
 
315
+ ## Extended queue management (`nack`/`purge`/`declare`/`status`)
316
+
317
+ `QueueAdapter`'s base class also defines `nack`, `purge`, `declare`
318
+ (explicit provisioning ahead of first use, with optional dead-letter
319
+ queue wiring), and `status` — regular (non-abstract) base methods that
320
+ raise `NotImplementedError` by default, overridden only where a
321
+ subclass genuinely implements them. Honest status, verified against a
322
+ real local broker/client, not assumed:
323
+
324
+ * **RabbitMQAdapter** — all four implemented for real against a live
325
+ broker: `nack` is `basic_nack(requeue=True)` (immediate redelivery,
326
+ not dead-lettering); `purge` reads the real purged count off
327
+ `queue_purge`'s `Queue.PurgeOk` response; `declare` wires real AMQP
328
+ dead-lettering (`x-dead-letter-exchange`/`x-dead-letter-routing-key`
329
+ queue arguments — not a hand-rolled shadow re-router) when
330
+ `dead_letter_queue` is given, and `status` reads `message_count`/
331
+ `consumer_count` off a passive `queue_declare`. A message only
332
+ actually gets dead-lettered by a real reject-without-requeue
333
+ (`basic_nack`/`basic_reject` with `requeue=False`) or a TTL/
334
+ max-length policy — `nack()`'s own contract is "redeliver now", so it
335
+ deliberately does **not** trigger dead-lettering itself. `dlq_list`/
336
+ `dlq_replay` aren't separate adapter methods — they're just
337
+ `receive`/`send`+`ack` aimed at the DLQ's own queue name.
338
+ * **KafkaQueueAdapter** — only `nack` is implemented, and it's a
339
+ documented no-op: `receive()` already never commits an offset until
340
+ `ack()`/`delete()` does, so a received-but-unacked message is already
341
+ effectively "nacked". `purge`/`declare`/`status` are **not**
342
+ implemented — not practical against `kafka-python`'s client-level API
343
+ without broker admin tooling; they raise the base class's
344
+ `NotImplementedError`.
345
+ * **SQS / Azure Queue / GCP Pub/Sub / Blockchain** — none of the four
346
+ new methods are implemented; all raise the base class's
347
+ `NotImplementedError`. No credentials/emulator for any of these three
348
+ cloud providers, and no real blockchain node, were available where
349
+ this was built and tested.
350
+ * **Vercel Queue (local-redis mode)** — also not implemented for these
351
+ four, though flagged as a real, likely-tractable gap for later: Redis
352
+ Streams' `XPENDING`/`XCLAIM`/`XTRIM` could plausibly back `nack`/
353
+ `status`/`purge` for the local-redis mode specifically. Deliberately
354
+ left out of this round rather than half-implemented.
355
+
307
356
  ---
308
357
 
309
358
  # Blockchain Storage
@@ -1,49 +1,55 @@
1
- altcodepro_polydb_python-2.5.4.dist-info/licenses/LICENSE,sha256=9X8GLocsBwy-5aR5JGOt2SAMDDPs9Qv-YnqmHBHOXrw,1067
1
+ altcodepro_polydb_python-2.5.6.dist-info/licenses/LICENSE,sha256=9X8GLocsBwy-5aR5JGOt2SAMDDPs9Qv-YnqmHBHOXrw,1067
2
2
  polydb/PolyDB.py,sha256=MG7-nV59zDvrUQwXNEgV4eetHCeW9TYY9DQqtxu_r7k,23543
3
- polydb/__init__.py,sha256=d9KSz888A336904TPEjWPYBtyJLPB0lTdcaOnYp8byY,1535
3
+ polydb/__init__.py,sha256=4-R-S5OD0aXGL2w2AJx5is4xqccdtWWOBrEHn18R51M,1535
4
4
  polydb/advanced_query.py,sha256=JZUQr4wyC9ydrcO9VbZiH2ueiQiC4dxOX9hzG_JaFhs,6931
5
5
  polydb/batch.py,sha256=_DjWZa1ZXYSk6MLKqFe0eT7SYVRZtYNqZb9bI8Y2sao,4566
6
- polydb/cache.py,sha256=JBXF1XEK-fY80ar8SDE893Z1Z116YtXAEG0PaJ0Nkcw,7658
7
- polydb/cloudDatabaseFactory.py,sha256=Gp6L__YtgrkGahD8B7ItzXMHCoj2ZUGDjXLS9w0TujY,17780
8
- polydb/databaseFactory.py,sha256=AdEMRtWcwf0elJ9JhrWS5krre-3RVFfIgt0mA93eJJQ,44068
6
+ polydb/cache.py,sha256=9UUHhiCVSWHQ-x-Ac_kIHYAadj0AQeqE4x8dwtyYFII,11781
7
+ polydb/cloudDatabaseFactory.py,sha256=M2qtHJmZUr6ZSZY6RCOgC3MLmq5o9WJeKgVubU77chk,22943
8
+ polydb/databaseFactory.py,sha256=tznE9-CldL9sxo9ccCX2qBQYg56CEC6zDgx1DJXtQY0,51667
9
9
  polydb/decorators.py,sha256=L_WP2uXP_k8Ac49SUm-mthbM4jWI-XYfHXEyKzumOww,43062
10
- polydb/errors.py,sha256=ZM3XEefhDMo82v3MMRQ7CRLdKI4N_lYLZN6O0ajBM7M,1712
10
+ polydb/errors.py,sha256=ynQ732utRGA3nFan0tv1CVNxZgV07r7lidpFf-wVhSU,1830
11
11
  polydb/json_safe.py,sha256=R5PrqAGirqjYKPyy-8KH-lSXjLH0FPr2TSGozy4eheU,149
12
- polydb/models.py,sha256=pkGpYnEEJmXdoWVpWS2K3CnuO9JZ_91DKp1bpVNQCns,10259
12
+ polydb/models.py,sha256=HPOdcg0F1rPONJsnbwja75FDQ-38o94U1KsLktngwoI,12600
13
13
  polydb/monitoring.py,sha256=UMm3ybyRJjAQi-prXXMLl9zuHhnhMnYBzMD3XWK66y8,9571
14
14
  polydb/multitenancy.py,sha256=BfnSJRRyRl4ObM4noXDJhsGJHLESXzvYRsdKwic7TQc,8135
15
15
  polydb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  polydb/query.py,sha256=72QbnQaljNGvIWktSD29BII0476MMjrJ-7dQmOcWuZc,8509
17
17
  polydb/registry.py,sha256=RD_elvFXcmhTdCyZDm2f3ej0elxqhArnSJ2aO9k5VCU,2352
18
- polydb/retry.py,sha256=QduWwDyiU-dt1fHFQmm6ltbnaJY2460cDfQ7ZE8wjXs,4466
18
+ polydb/retry.py,sha256=tX8vniILKe1A_2MLXLACY5iGEBT_zSLYGggkzvxt618,4926
19
19
  polydb/schema.py,sha256=hI7uJM4WvH31A-h0kGZhCD5mCYcnrNvEega--_kvafE,6889
20
20
  polydb/security.py,sha256=KqV5cD19hLyIvykW2DkKCXuj1hBDLt3xQ1tFtz_sCQE,20202
21
21
  polydb/types.py,sha256=XB_85Un8_aWt4dSfpjIGotHbK3KBY2WurQGXr9EOxWY,2992
22
22
  polydb/utils.py,sha256=tS_KI1vt-VZ6JftbFxbnVxC0xpie1-zYikTGf1nSoFw,3183
23
23
  polydb/validation.py,sha256=a1o1d02k3c6PWQwkBbw_0nEmIgrdB5RR8OcpNQMn4cA,4810
24
+ polydb/adapters/AWSSecretsManagerAdapter.py,sha256=W4B3xhQFcBAlNlwQ4uTgcDmRrOmZr6v-suC4qihTpJw,2747
24
25
  polydb/adapters/AzureBlobStorageAdapter.py,sha256=4vD55Z8DBTzBK66jIJbo5bNMY-AQ61MlP0-P2Fv_JgQ,7083
25
26
  polydb/adapters/AzureFileStorageAdapter.py,sha256=VZNprqlBXCuWUgtqClNT-NrQmRf-XFYEiRA2BLbf-Sc,7046
26
- polydb/adapters/AzureQueueAdapter.py,sha256=LsNvrNHUgFF2UwXXTwlOiqK8VdP4htLaFnTu4oxZDlo,9468
27
+ polydb/adapters/AzureKeyVaultAdapter.py,sha256=7Nizs9qGh6cytbZNgV78f-odOO4Z6sUlwm_HQYqt-pc,2339
28
+ polydb/adapters/AzureQueueAdapter.py,sha256=15w0QQMBM53NIg10MZqdoweJyYtYdgR-SbZVwFqx3BI,13660
27
29
  polydb/adapters/AzureTableStorageAdapter.py,sha256=jMgGjiaqI6bUeKRa--f8xHz1pb8j8AMGaP-VA5CxlYw,30126
28
30
  polydb/adapters/BlockchainBlobAdapter.py,sha256=D01Yua9mkKfaQrxKYApblIyI6DSP0dtNAh4Tav51HJ4,3299
29
31
  polydb/adapters/BlockchainFileAdapter.py,sha256=G749xOVpG20HuKS8zCgi6PMjoJNu-YXK7zitygjLdzM,8335
30
32
  polydb/adapters/BlockchainKVAdapter.py,sha256=UFYHyTgvdW-sZUBqyHEG3Cdx6wSTiF2QowEVWL3XPTg,4564
31
33
  polydb/adapters/BlockchainQueueAdapter.py,sha256=XsN5ag9Wulaelfkb3RCuBuSBw_4MUx9Wi3Gd3DBn7ME,4196
32
- polydb/adapters/DynamoDBAdapter.py,sha256=bS4t-XgVMIqMEVfr4K2XMvs-YA1ORTI0Plg5jJ0tW_Q,18429
34
+ polydb/adapters/DynamoDBAdapter.py,sha256=kULYZMa9hFxV2VHVO_VsJcTh9OqKR8vQg2ceO1OvYbk,19545
33
35
  polydb/adapters/EFSAdapter.py,sha256=Ca7-Esm3KNVm90Qo6tWmmftwk8Nq54DIzU3fAHX53io,2937
34
- polydb/adapters/FirestoreAdapter.py,sha256=e3DEODOZ-UAWULLtGTIED-Ym1JrxOKaOLPQVKhhehvk,13939
36
+ polydb/adapters/FirestoreAdapter.py,sha256=WCYZ6EThZgDGsdJcKXBFXHckdLnIY9jvtVYHSb3z7ao,15260
35
37
  polydb/adapters/GCPFilestoreAdapter.py,sha256=yjFQQwsWYWc8mo8XwMViVTWb5_D--xAyTMvE-4AOpNM,3006
36
- polydb/adapters/GCPPubSubAdapter.py,sha256=7XNots2VA0ReEDku-rjg-OTYmftIpx5UgnXYDdXNkOo,8692
38
+ polydb/adapters/GCPPubSubAdapter.py,sha256=NcxzvCqjWWNFuGUU5LS5IYSV32cg_AGwGmSBn_gXE6Y,10685
39
+ polydb/adapters/GCPSecretManagerAdapter.py,sha256=ZFL2FGh7uTaBfpUo-HKXZAxS9s-w0w4yI_5I39KsG3A,2659
37
40
  polydb/adapters/GCPStorageAdapter.py,sha256=9yS1Jhcn5_rCRdZ5uOqcRW6Ba-UNb6VOYpwENP-C6Qk,7133
41
+ polydb/adapters/KafkaQueueAdapter.py,sha256=JqzYGjeJtSLlL1jBqwWmBlnFKwwGIQUQzhsuM9wujm4,14411
38
42
  polydb/adapters/MongoDBAdapter.py,sha256=qB7RwQb3tvSLLPBiKV_BoAHCvl8Me3C02ieunj01Iew,11788
39
- polydb/adapters/PostgreSQLAdapter.py,sha256=9VI4-JOdDPeCDba8lRar4RkVMuF0bAymkvSTE3HILjs,38239
43
+ polydb/adapters/PostgreSQLAdapter.py,sha256=tEzwFB-ENSOfOqDcGK5MUCN7cU7k4c1vFgm1fJUYf3A,43877
44
+ polydb/adapters/RabbitMQAdapter.py,sha256=c6usZrKgvUYwkfhcGjGFPjZkCKscU2xH9Fh9Ov9frYE,19903
40
45
  polydb/adapters/S3Adapter.py,sha256=5R0zHAL2SkGFjp1L3bp-IU468bXYdSf6nKx974MN104,7586
41
46
  polydb/adapters/S3CompatibleAdapter.py,sha256=jpafqbAjA8-irdXBrfXa1QJySIzrcUQ6UrFt5h5FAEc,7006
42
- polydb/adapters/SQSAdapter.py,sha256=1vfbNoqIDy-b8t2xcxy91SoxSYBPFDfUh7yCQWxdS84,5778
47
+ polydb/adapters/SQSAdapter.py,sha256=GhbbQKGA8P5teVdkeMZCl6aeH_8Dqi5qQJiJecwgV0A,8133
48
+ polydb/adapters/VaultAdapter.py,sha256=6R1hpn-BlPEm4fwoKc6304GmLrQeJohFmlqijlolq68,2716
43
49
  polydb/adapters/VercelBlobAdapter.py,sha256=Hu1u8vE-3rvJMBDotnPKeRoVaH1Vl9PdM1hBKphgj2w,6983
44
50
  polydb/adapters/VercelFileAdapter.py,sha256=-fLRCi0AUbyXAR4nkHCV-wXkociHF2hzzEDqWtixIpE,1033
45
- polydb/adapters/VercelKVAdapter.py,sha256=QZxRkuYzVNWFCEFaJPSph8YEAut-YtlXPqbCt0JlROI,8647
46
- polydb/adapters/VercelQueueAdapter.py,sha256=cWtPaMIWCako0HHr_rzAE6vMLugSR6zBXqp3VP9MXwY,2375
51
+ polydb/adapters/VercelKVAdapter.py,sha256=jcuhXUcvhd3-QJUu1C_P4lN8DMu6qmjNQa3HwkzYcAE,10005
52
+ polydb/adapters/VercelQueueAdapter.py,sha256=lpfKdl-BPiQZJlwlyichTIWwj2iX9iEykAgoUd2wxmI,3894
47
53
  polydb/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
54
  polydb/audit/AuditStorage.py,sha256=6luCS7hTNL5MH2kkn6QlBlBsH-3FLuEFwP-cmkugRss,10469
49
55
  polydb/audit/__init__.py,sha256=Z7-y5djq3glQ2Yun6nj-13Efpj3oGz9Qc0veS2g06Y4,245
@@ -52,7 +58,8 @@ polydb/audit/manager.py,sha256=KzaaOf5bDfr4M-CkCAZBG_U_4xIBCKDLRAf3hsm-DAk,1236
52
58
  polydb/audit/models.py,sha256=cSDkzW22FnsMvjQFvLQACQUm9lYFcnJ2Ew5mngBF9yQ,7043
53
59
  polydb/base/NoSQLKVAdapter.py,sha256=U64nXaeHd40GCZV4DQnogZRUxvI8HHLEhNBMv4oddq4,14491
54
60
  polydb/base/ObjectStorageAdapter.py,sha256=VeJ3qXET6H0xd3lJpE8-WSsKs8EyK9S0-9VNR0EJzME,2288
55
- polydb/base/QueueAdapter.py,sha256=jFgyG-SUK4nhRNxm2NbzUbwnA9b_5iAC-ikLSUpXRwk,799
61
+ polydb/base/QueueAdapter.py,sha256=rphkdq9LEErwAKbmapUiHDshiOvgzvS5ZgFoOS8m9xc,8207
62
+ polydb/base/SecretsAdapter.py,sha256=HVk0H9637BkOJTykZNZbF2wKtSkFFj_-BZVpING6Hs4,989
56
63
  polydb/base/SharedFilesAdapter.py,sha256=kXbJmtn_cwEyAZ-1AvFrmesCLSwu43ycTV3S4BmwrO4,853
57
64
  polydb/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
65
  polydb/observability/__init__.py,sha256=ZIMx5ODCBZI_FnROJFb11Q9E5kRq0ZcCp6_RmlwG53k,122
@@ -60,7 +67,7 @@ polydb/observability/logging.py,sha256=gv1SshNjaSkmR98Uv7Qbwq3gkeQUzVCA4ichqHKL6
60
67
  polydb/services/__init__.py,sha256=nMolMXzmhejRSm7f42kU9fCHF0GIpqpejN2kywuBsUQ,241
61
68
  polydb/services/compliance_service.py,sha256=-0ncX7PrXy4-NuyWXIv65cLUsqz0jXzb3LtsD8rlrXE,4983
62
69
  polydb/services/security_service.py,sha256=q7GzK6sbMnBBsc4fuL4oXTd-ui2b-MYQyrWTBvdVNSk,5062
63
- altcodepro_polydb_python-2.5.4.dist-info/METADATA,sha256=MttihuAbzlmJ71naPgSMf8EhjvbYwhiZz47nQTlvJHc,12492
64
- altcodepro_polydb_python-2.5.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
65
- altcodepro_polydb_python-2.5.4.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
66
- altcodepro_polydb_python-2.5.4.dist-info/RECORD,,
70
+ altcodepro_polydb_python-2.5.6.dist-info/METADATA,sha256=HY5mBLl5CIIUAJuIwhFNhs1wwhyD9PLIxvUtIfvuwlE,15598
71
+ altcodepro_polydb_python-2.5.6.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
72
+ altcodepro_polydb_python-2.5.6.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
73
+ altcodepro_polydb_python-2.5.6.dist-info/RECORD,,
polydb/__init__.py CHANGED
@@ -4,7 +4,7 @@ PolyDB - Enterprise Cloud-Independent Database Abstraction
4
4
  Full LINQ support, field-level audit, cache, soft delete, overflow storage
5
5
  """
6
6
 
7
- __version__ = "2.2.3"
7
+ __version__ = "2.5.6"
8
8
 
9
9
  from .cloudDatabaseFactory import CloudDatabaseFactory
10
10
  from .databaseFactory import DatabaseFactory
@@ -0,0 +1,72 @@
1
+ # src/polydb/adapters/AWSSecretsManagerAdapter.py
2
+ import os
3
+ from typing import Optional
4
+
5
+ from ..base.SecretsAdapter import SecretsAdapter
6
+ from ..retry import retry
7
+
8
+
9
+ class AWSSecretsManagerAdapter(SecretsAdapter):
10
+ def __init__(self, region: str = ""):
11
+ super().__init__()
12
+
13
+ self.region = region or os.getenv("AWS_REGION", "us-east-1")
14
+ self._client = None
15
+ self._initialize_client()
16
+
17
+ def _initialize_client(self) -> None:
18
+ import boto3
19
+
20
+ self._client = boto3.client("secretsmanager", region_name=self.region)
21
+
22
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
23
+ def get_secret(self, key: str) -> Optional[str]:
24
+ from botocore.exceptions import ClientError
25
+
26
+ try:
27
+ resp = self._client.get_secret_value(SecretId=key)
28
+ except ClientError as exc:
29
+ if exc.response["Error"]["Code"] == "ResourceNotFoundException":
30
+ return None
31
+ raise
32
+ return resp.get("SecretString")
33
+
34
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
35
+ def set_secret(self, key: str, value: str) -> None:
36
+ from botocore.exceptions import ClientError
37
+
38
+ try:
39
+ self._client.put_secret_value(SecretId=key, SecretString=value)
40
+ except ClientError as exc:
41
+ if exc.response["Error"]["Code"] == "ResourceNotFoundException":
42
+ self._client.create_secret(Name=key, SecretString=value)
43
+ else:
44
+ raise
45
+
46
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
47
+ def delete_secret(self, key: str) -> bool:
48
+ from botocore.exceptions import ClientError
49
+
50
+ # Real AWS raises ResourceNotFoundException for deleting a missing
51
+ # secret, but at least one mock (moto) returns a synthetic success
52
+ # response instead of raising -- checking existence explicitly
53
+ # first makes this return value correct regardless of what a given
54
+ # backend/mock does on delete of something that was never there.
55
+ if self.get_secret(key) is None:
56
+ return False
57
+
58
+ try:
59
+ self._client.delete_secret(SecretId=key, ForceDeleteWithoutRecovery=True)
60
+ return True
61
+ except ClientError as exc:
62
+ if exc.response["Error"]["Code"] == "ResourceNotFoundException":
63
+ return False
64
+ raise
65
+
66
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
67
+ def list_secrets(self, prefix: str = "") -> list[str]:
68
+ names: list[str] = []
69
+ paginator = self._client.get_paginator("list_secrets")
70
+ for page in paginator.paginate():
71
+ names.extend(s["Name"] for s in page["SecretList"] if s["Name"].startswith(prefix))
72
+ return names
@@ -0,0 +1,65 @@
1
+ # src/polydb/adapters/AzureKeyVaultAdapter.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 AzureKeyVaultAdapter(SecretsAdapter):
11
+ def __init__(self, vault_url: str = ""):
12
+ super().__init__()
13
+
14
+ self.vault_url = vault_url or os.getenv("AZURE_KEY_VAULT_URL")
15
+ if not self.vault_url:
16
+ raise ConnectionError("AZURE_KEY_VAULT_URL is not configured")
17
+
18
+ self._client = None
19
+ self._initialize_client()
20
+
21
+ def _initialize_client(self) -> None:
22
+ from azure.identity import DefaultAzureCredential
23
+ from azure.keyvault.secrets import SecretClient
24
+
25
+ self._client = SecretClient(vault_url=self.vault_url, credential=DefaultAzureCredential())
26
+
27
+ # Key Vault secret names may only contain alphanumerics and dashes --
28
+ # our secret keys (e.g. "tenant-42/stripe/api_key") use slashes, so
29
+ # translate to a Key-Vault-safe name deterministically both ways.
30
+ @staticmethod
31
+ def _kv_name(key: str) -> str:
32
+ return key.replace("/", "--")
33
+
34
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
35
+ def get_secret(self, key: str) -> Optional[str]:
36
+ from azure.core.exceptions import ResourceNotFoundError
37
+
38
+ try:
39
+ return self._client.get_secret(self._kv_name(key)).value
40
+ except ResourceNotFoundError:
41
+ return None
42
+
43
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
44
+ def set_secret(self, key: str, value: str) -> None:
45
+ self._client.set_secret(self._kv_name(key), value)
46
+
47
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
48
+ def delete_secret(self, key: str) -> bool:
49
+ from azure.core.exceptions import ResourceNotFoundError
50
+
51
+ try:
52
+ poller = self._client.begin_delete_secret(self._kv_name(key))
53
+ poller.wait()
54
+ return True
55
+ except ResourceNotFoundError:
56
+ return False
57
+
58
+ @retry(max_attempts=3, delay=1.0, exceptions=(Exception,))
59
+ def list_secrets(self, prefix: str = "") -> list[str]:
60
+ kv_prefix = self._kv_name(prefix)
61
+ return [
62
+ p.name.replace("--", "/")
63
+ for p in self._client.list_properties_of_secrets()
64
+ if p.name.startswith(kv_prefix)
65
+ ]
@@ -116,10 +116,29 @@ class AzureQueueAdapter(QueueAdapter):
116
116
  # a still-in-flight message was becoming visible again and getting
117
117
  # picked up by a second worker mid-processing: two concurrent
118
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
119
+ # other's result. ack() deletes the message immediately on completion
120
+ # regardless; a genuinely crashed worker recovers it after this timeout.
121
+ #
122
+ # This MUST be >= the longest a consumer may legitimately hold a message.
123
+ # It was 300s while the platform's durable tasks are allowed 3600s
124
+ # (execution/durable_job.py: timeout_seconds=3600.0), so every task that
125
+ # ran longer than five minutes was redelivered WHILE STILL RUNNING -- and
126
+ # again at ten, at fifteen, each redelivery starting another concurrent
127
+ # execution of the same job. Observed on an artifact generation run: the
128
+ # second invocation found the first's per-artifact lock held, recorded the
129
+ # artifact as `already_running`, and carried on to the next one, so the
130
+ # first artifact sat at `running` forever while its dependents blocked on
131
+ # it. CPU pegged, RSS climbed with every duplicate, and the process was
132
+ # eventually OOM-killed. The de-duplication guard upstream could not save
133
+ # it: a redelivery carries the SAME run id as the original, so nothing in
134
+ # the payload distinguishes them.
135
+ #
136
+ # Matched to the durable task timeout deliberately: the queue must not
137
+ # reclaim a message the executor is still allowed to be working on. The
138
+ # cost is that a hard worker crash now leaves the message invisible for up
139
+ # to an hour before recovery, which is the right trade -- a delayed retry
140
+ # is recoverable, two concurrent runs mutating the same rows are not.
141
+ DEFAULT_VISIBILITY_TIMEOUT = int(os.environ.get("POLYDB_QUEUE_VISIBILITY_TIMEOUT") or 3600)
123
142
 
124
143
  @retry(max_attempts=3, delay=1.0, exceptions=(QueueError,))
125
144
  def receive(
@@ -253,3 +272,62 @@ class AzureQueueAdapter(QueueAdapter):
253
272
  return True
254
273
  except Exception as e:
255
274
  raise QueueError(f"Azure Queue nack failed: {e}")
275
+
276
+ # ---------------------------------------------------------
277
+ # extend / delay / cancel -- all three real on Azure Queue.
278
+ # ---------------------------------------------------------
279
+
280
+ def extend(
281
+ self, ack_id: str, queue_name: str = "default", *, visibility_timeout: int = 30
282
+ ) -> bool:
283
+ """Real Azure UpdateMessage call extending visibility without
284
+ touching the stored body -- ack_id is the combined
285
+ "<message_id>|<pop_receipt>" receipt_handle receive() returned."""
286
+ message_id, pop_receipt = self._decode_receipt(ack_id)
287
+ if not message_id:
288
+ raise QueueError("AzureQueueAdapter.extend requires a receipt_handle from receive()")
289
+ try:
290
+ queue_name = self._normalize_queue_name(queue_name)
291
+ queue_client = self._get_queue(queue_name)
292
+ # No `content=` argument: this is the SDK's own documented
293
+ # way to leave the stored message body unchanged while only
294
+ # updating visibility_timeout.
295
+ queue_client.update_message(
296
+ message_id,
297
+ pop_receipt=pop_receipt,
298
+ visibility_timeout=visibility_timeout,
299
+ )
300
+ return True
301
+ except Exception as e:
302
+ raise QueueError(f"Azure Queue extend failed: {e}")
303
+
304
+ @retry(max_attempts=3, delay=1.0, exceptions=(QueueError,))
305
+ def delay(
306
+ self, message: Dict[str, Any], queue_name: str = "default", *, delay_seconds: int = 0
307
+ ) -> str:
308
+ """Real Azure send_message with its own visibility_timeout param
309
+ -- the message is stored immediately but not deliverable until
310
+ delay_seconds elapses. Returns the combined receipt (packed via
311
+ _encode_receipt) so cancel() has enough to delete it before then."""
312
+ try:
313
+ queue_name = self._normalize_queue_name(queue_name)
314
+ queue_client = self._get_queue(queue_name)
315
+
316
+ response = queue_client.send_message(
317
+ json.dumps(message, default=json_safe),
318
+ visibility_timeout=delay_seconds,
319
+ )
320
+
321
+ return self._encode_receipt(response.id, response.pop_receipt)
322
+
323
+ except Exception as e:
324
+ raise QueueError(f"Azure Queue delay failed: {e}")
325
+
326
+ def cancel(self, message_id: str, queue_name: str = "default") -> bool:
327
+ """Cancel a still-delayed message before its visibility_timeout
328
+ elapses. message_id is the combined receipt delay() returned."""
329
+ decoded_id, pop_receipt = self._decode_receipt(message_id)
330
+ if not decoded_id:
331
+ raise QueueError("AzureQueueAdapter.cancel requires the receipt delay() returned")
332
+ queue_name = self._normalize_queue_name(queue_name)
333
+ return self.delete(message_id=decoded_id, queue_name=queue_name, pop_receipt=pop_receipt)
@@ -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" == pk
27
- - query({"id": ...}) works
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": <pk>} and raises sqlite3.DatabaseError on missing
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
- # Ensure "id" field exists for tests/querying
277
- payload.setdefault("id", pk)
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
- # match pattern: put returns {"id": pk}
285
- return {"id": pk}
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", pk)
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
- # Treat 'id' as PK by convention (since we store PK=pk=id in NoSQLKVAdapter by default)
322
- pk_value = filters.get("PK") or filters.get("partition_key") or filters.get("id")
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", "id")
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("_pk") or it.get("PK"))
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": pk}
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
- pk_value = query.get("PK") or query.get("partition_key") or query.get("id")
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", "id")
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("_pk") or it.get("PK"))
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