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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.5.3
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.3.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/AzureTableStorageAdapter.py,sha256=RDziVAOrEc_bFhHzlHupVJK-DvMYrD_gl9UySOMWoYk,28482
27
+ polydb/adapters/AzureKeyVaultAdapter.py,sha256=7Nizs9qGh6cytbZNgV78f-odOO4Z6sUlwm_HQYqt-pc,2339
28
+ polydb/adapters/AzureQueueAdapter.py,sha256=15w0QQMBM53NIg10MZqdoweJyYtYdgR-SbZVwFqx3BI,13660
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.3.dist-info/METADATA,sha256=73EuFiFXbrs0OZ-AiAkGGQLL0HIK7ayB-sRvbreXNDw,12492
64
- altcodepro_polydb_python-2.5.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
65
- altcodepro_polydb_python-2.5.3.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
66
- altcodepro_polydb_python-2.5.3.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)
@@ -24,6 +24,16 @@ from ..models import PageResult, PartitionConfig
24
24
 
25
25
  logger = logging.getLogger(__name__)
26
26
 
27
+ # Imported at module scope (unlike the clients, which stay lazy) because these
28
+ # are pure value types with no connection cost, and both the encode and decode
29
+ # paths need them per field. Guarded so importing polydb without the azure
30
+ # extra still works -- in that state nothing in this adapter functions anyway.
31
+ try:
32
+ from azure.data.tables import EdmType, EntityProperty
33
+ except ImportError: # pragma: no cover - azure extra not installed
34
+ EdmType = None # type: ignore[assignment]
35
+ EntityProperty = None # type: ignore[assignment]
36
+
27
37
  _BYTES_PREFIX = "@@polydb_bytes@@:"
28
38
  _JSON_PREFIX = "@@polydb_json@@:"
29
39
  _BASE64_RE = re.compile(r"^[A-Za-z0-9+/]*={0,2}$")
@@ -155,15 +165,25 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
155
165
 
156
166
  if isinstance(v, datetime):
157
167
  return v
158
- if isinstance(v, int):
168
+ if isinstance(v, int) and not isinstance(v, bool):
159
169
  # Azure Table Int32 range: -2_147_483_648 … 2_147_483_647
160
170
  if -2_147_483_648 <= v <= 2_147_483_647:
161
171
  return v
162
- # Outside Int32 → force Int64-compatible representation
163
- # The Azure SDK accepts Python ints larger than Int32 and stores them as Edm.Int64
164
- # when they fit in 64 bits. For values that exceed even Int64 we fall back to string.
172
+ # Outside Int32 → must be tagged Edm.Int64 EXPLICITLY.
173
+ #
174
+ # Returning the bare int here does NOT work, despite reading like
175
+ # it should: the SDK's _add_entity_properties dispatches on
176
+ # type(value), and every Python int maps to _to_entity_int32,
177
+ # which raises "<n> is too large to be cast to type EdmType.INT32"
178
+ # for anything >= 2**31. It never infers Int64. The one documented
179
+ # way to reach Edm.Int64 is an EntityProperty tuple -- the
180
+ # serializer branches on isinstance(value, tuple) and takes the
181
+ # edm_type from element 1 -- so that is what we hand it.
182
+ # _decode_value unwraps the same shape on the way back out.
165
183
  if -9_223_372_036_854_775_808 <= v <= 9_223_372_036_854_775_807:
166
- return v # will be stored as Edm.Int64
184
+ if EntityProperty is not None and EdmType is not None:
185
+ return EntityProperty(v, EdmType.INT64)
186
+ return str(v)
167
187
  return str(v) # safety net for arbitrarily large ints
168
188
  if isinstance(v, (str, bool, int, float)):
169
189
  return v
@@ -171,6 +191,13 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
171
191
  return _JSON_PREFIX + json.dumps(v, default=json_safe)
172
192
 
173
193
  def _decode_value(self, v: Any) -> Any:
194
+ # Edm.Int64 comes back as EntityProperty(value, EdmType.INT64), not as
195
+ # a plain int -- the SDK's _from_entity_int64 wraps it. Unwrap here so
196
+ # a large int round-trips as the int that was written; left alone it
197
+ # would surface to callers as a 2-tuple and, on the next write, be
198
+ # JSON-encoded by the list/tuple branch of _encode_value.
199
+ if EntityProperty is not None and isinstance(v, EntityProperty):
200
+ return v.value
174
201
  if isinstance(v, str):
175
202
  if v.startswith(_JSON_PREFIX):
176
203
  payload = v[len(_JSON_PREFIX) :]