altcodepro-polydb-python 2.3.8__tar.gz → 2.3.10__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.8/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.10}/PKG-INFO +11 -1
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/pyproject.toml +11 -1
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10/src/altcodepro_polydb_python.egg-info}/PKG-INFO +11 -1
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +3 -1
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/altcodepro_polydb_python.egg-info/requires.txt +10 -0
- altcodepro_polydb_python-2.3.10/src/polydb/adapters/AzureBlobStorageAdapter.py +184 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/AzureFileStorageAdapter.py +74 -74
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/AzureQueueAdapter.py +9 -5
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/AzureTableStorageAdapter.py +5 -5
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/BlockchainBlobAdapter.py +1 -1
- altcodepro_polydb_python-2.3.10/src/polydb/adapters/BlockchainFileAdapter.py +217 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/BlockchainKVAdapter.py +4 -3
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/BlockchainQueueAdapter.py +3 -2
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/DynamoDBAdapter.py +12 -3
- altcodepro_polydb_python-2.3.10/src/polydb/adapters/EFSAdapter.py +77 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/FirestoreAdapter.py +15 -13
- altcodepro_polydb_python-2.3.10/src/polydb/adapters/GCPFilestoreAdapter.py +77 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/GCPPubSubAdapter.py +4 -4
- altcodepro_polydb_python-2.3.10/src/polydb/adapters/GCPStorageAdapter.py +186 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/PostgreSQLAdapter.py +3 -2
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/S3Adapter.py +5 -2
- altcodepro_polydb_python-2.3.10/src/polydb/adapters/S3CompatibleAdapter.py +174 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/SQSAdapter.py +5 -2
- altcodepro_polydb_python-2.3.10/src/polydb/adapters/VercelFileAdapter.py +29 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/audit/__init__.py +1 -1
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/base/SharedFilesAdapter.py +5 -5
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/cloudDatabaseFactory.py +37 -66
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/databaseFactory.py +23 -7
- altcodepro_polydb_python-2.3.8/example_usage.py +0 -191
- altcodepro_polydb_python-2.3.8/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -182
- altcodepro_polydb_python-2.3.8/src/polydb/adapters/EFSAdapter.py +0 -51
- altcodepro_polydb_python-2.3.8/src/polydb/adapters/GCPStorageAdapter.py +0 -225
- altcodepro_polydb_python-2.3.8/src/polydb/adapters/S3CompatibleAdapter.py +0 -144
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/LICENSE +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/MANIFEST.in +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/README.md +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements-aws.txt +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements-azure.txt +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements-dev.txt +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements-gcp.txt +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements-generic.txt +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements.txt +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/setup.cfg +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/setup.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/PolyDB.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/MongoDBAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/VercelKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/advanced_query.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/audit/AuditStorage.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/audit/context.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/audit/manager.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/audit/models.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/base/NoSQLKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/base/ObjectStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/base/QueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/base/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/batch.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/cache.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/decorators.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/errors.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/json_safe.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/models.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/monitoring.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/multitenancy.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/py.typed +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/query.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/registry.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/retry.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/schema.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/security.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/types.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/utils.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/validation.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_aws.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_azure.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_blockchain.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_cloud_factory.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_gcp.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_mongodb.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_multi_engine.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_postgresql.py +0 -0
- {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/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.10
|
|
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
|
|
@@ -28,6 +28,16 @@ Requires-Dist: tenacity>=9.1.4
|
|
|
28
28
|
Requires-Dist: redis>=6.4.0
|
|
29
29
|
Requires-Dist: python-dotenv>=1.1.1
|
|
30
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
|
|
31
41
|
Provides-Extra: aws
|
|
32
42
|
Requires-Dist: boto3>=1.42.47; extra == "aws"
|
|
33
43
|
Requires-Dist: botocore>=1.42.47; extra == "aws"
|
|
@@ -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.10"
|
|
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"
|
|
@@ -47,6 +47,16 @@ dependencies = [
|
|
|
47
47
|
"redis>=6.4.0",
|
|
48
48
|
"python-dotenv>=1.1.1",
|
|
49
49
|
"azure-storage-queue>=12.15.0",
|
|
50
|
+
"azure-storage-blob>=12.29.0",
|
|
51
|
+
"boto3>=1.43.18",
|
|
52
|
+
"google-cloud-storage>=3.10.1",
|
|
53
|
+
"azure-storage-file-share>=12.25.0",
|
|
54
|
+
"azure-data-tables>=12.7.0",
|
|
55
|
+
"ipfshttpclient>=0.7.0",
|
|
56
|
+
"web3>=7.16.0",
|
|
57
|
+
"google-cloud-firestore>=2.27.0",
|
|
58
|
+
"google-cloud-pubsub>=2.38.0",
|
|
59
|
+
"pymongo>=4.17.0",
|
|
50
60
|
]
|
|
51
61
|
|
|
52
62
|
# Generic/Open-source stack (cheapest option)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: altcodepro-polydb-python
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.10
|
|
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
|
|
@@ -28,6 +28,16 @@ Requires-Dist: tenacity>=9.1.4
|
|
|
28
28
|
Requires-Dist: redis>=6.4.0
|
|
29
29
|
Requires-Dist: python-dotenv>=1.1.1
|
|
30
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
|
|
31
41
|
Provides-Extra: aws
|
|
32
42
|
Requires-Dist: boto3>=1.42.47; extra == "aws"
|
|
33
43
|
Requires-Dist: botocore>=1.42.47; extra == "aws"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
MANIFEST.in
|
|
3
3
|
README.md
|
|
4
|
-
example_usage.py
|
|
5
4
|
pyproject.toml
|
|
6
5
|
requirements-aws.txt
|
|
7
6
|
requirements-azure.txt
|
|
@@ -42,11 +41,13 @@ src/polydb/adapters/AzureFileStorageAdapter.py
|
|
|
42
41
|
src/polydb/adapters/AzureQueueAdapter.py
|
|
43
42
|
src/polydb/adapters/AzureTableStorageAdapter.py
|
|
44
43
|
src/polydb/adapters/BlockchainBlobAdapter.py
|
|
44
|
+
src/polydb/adapters/BlockchainFileAdapter.py
|
|
45
45
|
src/polydb/adapters/BlockchainKVAdapter.py
|
|
46
46
|
src/polydb/adapters/BlockchainQueueAdapter.py
|
|
47
47
|
src/polydb/adapters/DynamoDBAdapter.py
|
|
48
48
|
src/polydb/adapters/EFSAdapter.py
|
|
49
49
|
src/polydb/adapters/FirestoreAdapter.py
|
|
50
|
+
src/polydb/adapters/GCPFilestoreAdapter.py
|
|
50
51
|
src/polydb/adapters/GCPPubSubAdapter.py
|
|
51
52
|
src/polydb/adapters/GCPStorageAdapter.py
|
|
52
53
|
src/polydb/adapters/MongoDBAdapter.py
|
|
@@ -55,6 +56,7 @@ src/polydb/adapters/S3Adapter.py
|
|
|
55
56
|
src/polydb/adapters/S3CompatibleAdapter.py
|
|
56
57
|
src/polydb/adapters/SQSAdapter.py
|
|
57
58
|
src/polydb/adapters/VercelBlobAdapter.py
|
|
59
|
+
src/polydb/adapters/VercelFileAdapter.py
|
|
58
60
|
src/polydb/adapters/VercelKVAdapter.py
|
|
59
61
|
src/polydb/adapters/VercelQueueAdapter.py
|
|
60
62
|
src/polydb/adapters/__init__.py
|
|
@@ -3,6 +3,16 @@ tenacity>=9.1.4
|
|
|
3
3
|
redis>=6.4.0
|
|
4
4
|
python-dotenv>=1.1.1
|
|
5
5
|
azure-storage-queue>=12.15.0
|
|
6
|
+
azure-storage-blob>=12.29.0
|
|
7
|
+
boto3>=1.43.18
|
|
8
|
+
google-cloud-storage>=3.10.1
|
|
9
|
+
azure-storage-file-share>=12.25.0
|
|
10
|
+
azure-data-tables>=12.7.0
|
|
11
|
+
ipfshttpclient>=0.7.0
|
|
12
|
+
web3>=7.16.0
|
|
13
|
+
google-cloud-firestore>=2.27.0
|
|
14
|
+
google-cloud-pubsub>=2.38.0
|
|
15
|
+
pymongo>=4.17.0
|
|
6
16
|
|
|
7
17
|
[all]
|
|
8
18
|
boto3>=1.42.47
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# src/polydb/adapters/AzureBlobStorageAdapter.py
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
import mimetypes
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
from ..base.ObjectStorageAdapter import ObjectStorageAdapter
|
|
8
|
+
from ..errors import ConnectionError, StorageError
|
|
9
|
+
from ..retry import retry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AzureBlobStorageAdapter(ObjectStorageAdapter):
|
|
13
|
+
"""
|
|
14
|
+
Production-grade Azure Blob Storage adapter.
|
|
15
|
+
|
|
16
|
+
- One BlobServiceClient, many containers (resolved + cached per call)
|
|
17
|
+
- Container auto-creation
|
|
18
|
+
- Thread-safe, retryable, structured logging
|
|
19
|
+
- put/get/delete are symmetric: a blob is stored at `key` and fetched at `key`
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, connection_string: str = "", container_name: str = ""):
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
self.connection_string = connection_string or os.getenv("AZURE_STORAGE_CONNECTION_STRING")
|
|
26
|
+
self.container_name = container_name or os.getenv("AZURE_CONTAINER_NAME", "polydb")
|
|
27
|
+
|
|
28
|
+
if not self.connection_string:
|
|
29
|
+
raise ConnectionError("AZURE_STORAGE_CONNECTION_STRING is not configured")
|
|
30
|
+
|
|
31
|
+
self._client = None
|
|
32
|
+
self._containers = {}
|
|
33
|
+
self._lock = threading.Lock()
|
|
34
|
+
|
|
35
|
+
self._initialize_client()
|
|
36
|
+
|
|
37
|
+
# ------------------------------------------------------------------
|
|
38
|
+
# CLIENT / CONTAINER RESOLUTION
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
def _initialize_client(self) -> None:
|
|
41
|
+
"""Initialize the shared Azure Blob service client."""
|
|
42
|
+
from azure.storage.blob import BlobServiceClient
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
with self._lock:
|
|
46
|
+
if self._client is None and self.connection_string is not None:
|
|
47
|
+
self._client = BlobServiceClient.from_connection_string(self.connection_string)
|
|
48
|
+
self.logger.info("Azure Blob Storage client initialized")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
raise ConnectionError(f"Failed to initialize Azure Blob Storage: {e}")
|
|
51
|
+
|
|
52
|
+
def _get_container(self, container_name: Optional[str] = None):
|
|
53
|
+
"""Resolve (and cache) a ContainerClient, auto-creating the container."""
|
|
54
|
+
from azure.core.exceptions import ResourceExistsError
|
|
55
|
+
|
|
56
|
+
if self._client is None:
|
|
57
|
+
raise ConnectionError("Azure Blob Storage client is not initialized")
|
|
58
|
+
|
|
59
|
+
name = container_name or self.container_name
|
|
60
|
+
|
|
61
|
+
cached = self._containers.get(name)
|
|
62
|
+
if cached is not None:
|
|
63
|
+
return cached
|
|
64
|
+
|
|
65
|
+
with self._lock:
|
|
66
|
+
cached = self._containers.get(name) # re-check under lock
|
|
67
|
+
if cached is not None:
|
|
68
|
+
return cached
|
|
69
|
+
|
|
70
|
+
container = self._client.get_container_client(name)
|
|
71
|
+
try:
|
|
72
|
+
container.create_container()
|
|
73
|
+
self.logger.info(f"Created container: {name}")
|
|
74
|
+
except ResourceExistsError:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
self._containers[name] = container
|
|
78
|
+
return container
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
# PUT
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
def put(
|
|
84
|
+
self,
|
|
85
|
+
key: str,
|
|
86
|
+
data: bytes,
|
|
87
|
+
fileName: str = "",
|
|
88
|
+
optimize: bool = True,
|
|
89
|
+
media_type: Optional[str] = None,
|
|
90
|
+
metadata: Dict[str, Any] | None = None,
|
|
91
|
+
container_name: Optional[str] = None,
|
|
92
|
+
) -> str:
|
|
93
|
+
if optimize and media_type:
|
|
94
|
+
data = self._optimize_media(data, media_type)
|
|
95
|
+
return self._put_raw(
|
|
96
|
+
key=key,
|
|
97
|
+
data=data,
|
|
98
|
+
fileName=fileName,
|
|
99
|
+
media_type=media_type,
|
|
100
|
+
metadata=metadata,
|
|
101
|
+
container_name=container_name,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
105
|
+
def _put_raw(
|
|
106
|
+
self,
|
|
107
|
+
key: str,
|
|
108
|
+
data: bytes,
|
|
109
|
+
fileName: str = "",
|
|
110
|
+
media_type: Optional[str] = None,
|
|
111
|
+
metadata: Dict[str, Any] | None = None,
|
|
112
|
+
container_name: Optional[str] = None,
|
|
113
|
+
) -> str:
|
|
114
|
+
"""Upload blob. Stored at `key` so get/delete can find it by the same key."""
|
|
115
|
+
from azure.storage.blob import ContentSettings
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
container = self._get_container(container_name)
|
|
119
|
+
|
|
120
|
+
# filename is metadata only — it must NOT alter the blob key
|
|
121
|
+
filename = fileName or os.path.basename(key) or key
|
|
122
|
+
if media_type:
|
|
123
|
+
ext = mimetypes.guess_extension(media_type) or ""
|
|
124
|
+
if ext and not filename.lower().endswith(ext):
|
|
125
|
+
filename += ext
|
|
126
|
+
|
|
127
|
+
blob_client = container.get_blob_client(key)
|
|
128
|
+
blob_client.upload_blob(
|
|
129
|
+
data,
|
|
130
|
+
overwrite=True,
|
|
131
|
+
content_settings=ContentSettings(
|
|
132
|
+
content_type=media_type or "application/octet-stream"
|
|
133
|
+
),
|
|
134
|
+
metadata={**(metadata or {}), "filename": filename},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
self.logger.debug(
|
|
138
|
+
f"Uploaded blob key={key} container={container.container_name} type={media_type}"
|
|
139
|
+
)
|
|
140
|
+
return blob_client.url
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
raise StorageError(f"Azure Blob put failed: {e}")
|
|
144
|
+
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
# GET / DELETE / LIST
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
149
|
+
def get(self, key: str, container_name: Optional[str] = None) -> bytes | None:
|
|
150
|
+
from azure.core.exceptions import ResourceNotFoundError
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
container = self._get_container(container_name)
|
|
154
|
+
data = container.get_blob_client(key).download_blob().readall()
|
|
155
|
+
self.logger.debug(f"Downloaded blob key={key}")
|
|
156
|
+
return data
|
|
157
|
+
except ResourceNotFoundError:
|
|
158
|
+
return None
|
|
159
|
+
except Exception as e:
|
|
160
|
+
raise StorageError(f"Azure Blob get failed: {e}")
|
|
161
|
+
|
|
162
|
+
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
163
|
+
def delete(self, key: str, container_name: Optional[str] = None) -> bool:
|
|
164
|
+
from azure.core.exceptions import ResourceNotFoundError
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
container = self._get_container(container_name)
|
|
168
|
+
container.get_blob_client(key).delete_blob(delete_snapshots="include")
|
|
169
|
+
self.logger.debug(f"Deleted blob key={key}")
|
|
170
|
+
return True
|
|
171
|
+
except ResourceNotFoundError:
|
|
172
|
+
return False
|
|
173
|
+
except Exception as e:
|
|
174
|
+
raise StorageError(f"Azure Blob delete failed: {e}")
|
|
175
|
+
|
|
176
|
+
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
177
|
+
def list(self, prefix: str = "", container_name: Optional[str] = None) -> List[str]:
|
|
178
|
+
try:
|
|
179
|
+
container = self._get_container(container_name)
|
|
180
|
+
results = [b.name for b in container.list_blobs(name_starts_with=prefix)]
|
|
181
|
+
self.logger.debug(f"Listed {len(results)} blobs prefix={prefix}")
|
|
182
|
+
return results
|
|
183
|
+
except Exception as e:
|
|
184
|
+
raise StorageError(f"Azure Blob list failed: {e}")
|
|
@@ -4,10 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import threading
|
|
7
|
-
from typing import List, Optional
|
|
8
|
-
|
|
9
|
-
from azure.storage.fileshare import ShareServiceClient
|
|
10
|
-
from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
11
8
|
|
|
12
9
|
from ..base.SharedFilesAdapter import SharedFilesAdapter
|
|
13
10
|
from ..errors import ConnectionError, StorageError
|
|
@@ -18,12 +15,9 @@ class AzureFileStorageAdapter(SharedFilesAdapter):
|
|
|
18
15
|
"""
|
|
19
16
|
Production-grade Azure File Storage adapter.
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
- Ensures directory structure exists
|
|
25
|
-
- Correct Azure file creation before upload
|
|
26
|
-
- Keeps write/read compatibility
|
|
18
|
+
- One service client, many shares (resolved + cached per call)
|
|
19
|
+
- Auto-creates share + nested directory structure
|
|
20
|
+
- per-call `share_name` overrides the configured share on every method
|
|
27
21
|
"""
|
|
28
22
|
|
|
29
23
|
def __init__(self, connection_string: str = "", share_name: str = ""):
|
|
@@ -32,153 +26,159 @@ class AzureFileStorageAdapter(SharedFilesAdapter):
|
|
|
32
26
|
self.connection_string = (
|
|
33
27
|
connection_string or os.getenv("AZURE_STORAGE_CONNECTION_STRING") or ""
|
|
34
28
|
)
|
|
35
|
-
|
|
36
29
|
self.share_name = share_name or os.getenv("AZURE_SHARE_NAME", "polydb")
|
|
37
30
|
|
|
38
31
|
if not self.connection_string:
|
|
39
32
|
raise ConnectionError("AZURE_STORAGE_CONNECTION_STRING not configured")
|
|
40
33
|
|
|
41
|
-
self._client
|
|
42
|
-
self.
|
|
34
|
+
self._client = None
|
|
35
|
+
self._shares: Dict[str, Any] = {}
|
|
43
36
|
self._lock = threading.Lock()
|
|
44
37
|
|
|
45
38
|
self._initialize_client()
|
|
46
39
|
|
|
47
40
|
# --------------------------------------------------
|
|
48
|
-
# Client
|
|
41
|
+
# Client / share resolution
|
|
49
42
|
# --------------------------------------------------
|
|
50
|
-
|
|
51
43
|
def _initialize_client(self):
|
|
44
|
+
from azure.storage.fileshare import ShareServiceClient
|
|
45
|
+
|
|
52
46
|
try:
|
|
53
47
|
with self._lock:
|
|
54
48
|
if self._client:
|
|
55
49
|
return
|
|
56
|
-
|
|
57
50
|
self._client = ShareServiceClient.from_connection_string(self.connection_string)
|
|
51
|
+
self.logger.info("Initialized Azure File Storage client")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise ConnectionError(f"Failed to initialize Azure File Storage: {str(e)}")
|
|
58
54
|
|
|
59
|
-
|
|
55
|
+
def _get_share(self, share_name: Optional[str] = None):
|
|
56
|
+
"""Resolve (and cache) a ShareClient, auto-creating the share."""
|
|
57
|
+
if self._client is None:
|
|
58
|
+
raise ConnectionError("Azure File Storage client is not initialized")
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
self._share.create_share()
|
|
63
|
-
except ResourceExistsError:
|
|
64
|
-
pass
|
|
60
|
+
name = share_name or self.share_name
|
|
65
61
|
|
|
66
|
-
|
|
62
|
+
cached = self._shares.get(name)
|
|
63
|
+
if cached is not None:
|
|
64
|
+
return cached
|
|
67
65
|
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
from azure.core.exceptions import ResourceExistsError
|
|
67
|
+
|
|
68
|
+
with self._lock:
|
|
69
|
+
cached = self._shares.get(name) # re-check under lock
|
|
70
|
+
if cached is not None:
|
|
71
|
+
return cached
|
|
72
|
+
|
|
73
|
+
share = self._client.get_share_client(name)
|
|
74
|
+
try:
|
|
75
|
+
share.create_share()
|
|
76
|
+
self.logger.info(f"Created share: {name}")
|
|
77
|
+
except ResourceExistsError:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
self._shares[name] = share
|
|
81
|
+
return share
|
|
70
82
|
|
|
71
83
|
# --------------------------------------------------
|
|
72
84
|
# Helpers
|
|
73
85
|
# --------------------------------------------------
|
|
74
|
-
|
|
75
86
|
def _split_path(self, path: str):
|
|
76
87
|
if "/" not in path:
|
|
77
88
|
return "", path
|
|
78
|
-
|
|
79
89
|
directory, filename = path.rsplit("/", 1)
|
|
80
90
|
return directory, filename
|
|
81
91
|
|
|
82
|
-
def _ensure_directory(self, directory: str):
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
def _ensure_directory(self, share, directory: str):
|
|
93
|
+
"""Create each directory level (Azure requires parents to exist)."""
|
|
94
|
+
if not directory:
|
|
95
|
+
return share.get_directory_client("")
|
|
96
|
+
|
|
97
|
+
from azure.core.exceptions import ResourceExistsError
|
|
98
|
+
|
|
99
|
+
current = ""
|
|
100
|
+
dir_client = share.get_directory_client("")
|
|
101
|
+
for part in (p for p in directory.split("/") if p):
|
|
102
|
+
current = f"{current}/{part}" if current else part
|
|
103
|
+
dir_client = share.get_directory_client(current)
|
|
104
|
+
try:
|
|
105
|
+
dir_client.create_directory()
|
|
106
|
+
except ResourceExistsError:
|
|
107
|
+
pass
|
|
94
108
|
return dir_client
|
|
95
109
|
|
|
96
110
|
# --------------------------------------------------
|
|
97
111
|
# Core operations
|
|
98
112
|
# --------------------------------------------------
|
|
99
|
-
|
|
100
113
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
101
|
-
def upload(self, path: str, data: bytes) -> str:
|
|
114
|
+
def upload(self, path: str, data: bytes, share_name: Optional[str] = None) -> str:
|
|
102
115
|
"""Upload file"""
|
|
103
116
|
try:
|
|
117
|
+
share = self._get_share(share_name)
|
|
104
118
|
directory, filename = self._split_path(path)
|
|
105
|
-
|
|
106
|
-
dir_client = self._ensure_directory(directory)
|
|
119
|
+
dir_client = self._ensure_directory(share, directory)
|
|
107
120
|
file_client = dir_client.get_file_client(filename)
|
|
108
121
|
|
|
109
122
|
file_client.create_file(len(data))
|
|
110
123
|
file_client.upload_file(data)
|
|
111
|
-
|
|
112
124
|
return path
|
|
113
|
-
|
|
114
125
|
except Exception as e:
|
|
115
126
|
raise StorageError(f"Azure File upload failed: {str(e)}")
|
|
116
127
|
|
|
117
128
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
118
|
-
def download(self, path: str) -> bytes:
|
|
129
|
+
def download(self, path: str, share_name: Optional[str] = None) -> bytes:
|
|
119
130
|
"""Download file"""
|
|
131
|
+
from azure.core.exceptions import ResourceNotFoundError
|
|
132
|
+
|
|
120
133
|
try:
|
|
134
|
+
share = self._get_share(share_name)
|
|
121
135
|
directory, filename = self._split_path(path)
|
|
122
|
-
|
|
123
|
-
raise ConnectionError("Azure File Storage share not initialized")
|
|
124
|
-
dir_client = self._share.get_directory_client(directory or "")
|
|
136
|
+
dir_client = share.get_directory_client(directory or "")
|
|
125
137
|
file_client = dir_client.get_file_client(filename)
|
|
126
|
-
|
|
127
138
|
return file_client.download_file().readall()
|
|
128
|
-
|
|
129
139
|
except ResourceNotFoundError:
|
|
130
140
|
raise StorageError(f"File not found: {path}")
|
|
131
141
|
except Exception as e:
|
|
132
142
|
raise StorageError(f"Azure File download failed: {str(e)}")
|
|
133
143
|
|
|
134
144
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
135
|
-
def delete(self, path: str) -> bool:
|
|
145
|
+
def delete(self, path: str, share_name: Optional[str] = None) -> bool:
|
|
136
146
|
"""Delete file"""
|
|
147
|
+
from azure.core.exceptions import ResourceNotFoundError
|
|
148
|
+
|
|
137
149
|
try:
|
|
150
|
+
share = self._get_share(share_name)
|
|
138
151
|
directory, filename = self._split_path(path)
|
|
139
|
-
|
|
140
|
-
raise ConnectionError("Azure File Storage share not initialized")
|
|
141
|
-
dir_client = self._share.get_directory_client(directory or "")
|
|
152
|
+
dir_client = share.get_directory_client(directory or "")
|
|
142
153
|
file_client = dir_client.get_file_client(filename)
|
|
143
|
-
|
|
144
154
|
file_client.delete_file()
|
|
145
155
|
return True
|
|
146
|
-
|
|
147
156
|
except ResourceNotFoundError:
|
|
148
157
|
return False
|
|
149
158
|
except Exception as e:
|
|
150
159
|
raise StorageError(f"Azure File delete failed: {str(e)}")
|
|
151
160
|
|
|
152
161
|
@retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
|
|
153
|
-
def list(self, directory: str = "") -> List[str]:
|
|
162
|
+
def list(self, directory: str = "", share_name: Optional[str] = None) -> List[str]:
|
|
154
163
|
"""List files"""
|
|
155
164
|
try:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
results: List[str] = []
|
|
161
|
-
|
|
162
|
-
for item in dir_client.list_directories_and_files():
|
|
163
|
-
results.append(item.name)
|
|
164
|
-
|
|
165
|
-
return results
|
|
166
|
-
|
|
165
|
+
share = self._get_share(share_name)
|
|
166
|
+
dir_client = share.get_directory_client(directory or "")
|
|
167
|
+
return [item.name for item in dir_client.list_directories_and_files()]
|
|
167
168
|
except Exception as e:
|
|
168
169
|
raise StorageError(f"Azure File list failed: {str(e)}")
|
|
169
170
|
|
|
170
171
|
# --------------------------------------------------
|
|
171
|
-
# Backward compatibility
|
|
172
|
+
# Backward compatibility (base interface)
|
|
172
173
|
# --------------------------------------------------
|
|
173
|
-
|
|
174
|
-
def write(self, path: str, data: bytes) -> bool:
|
|
174
|
+
def write(self, path: str, data: bytes, share_name: Optional[str] = None) -> bool:
|
|
175
175
|
"""Alias for upload"""
|
|
176
|
-
self.upload(path, data)
|
|
176
|
+
self.upload(path, data, share_name=share_name)
|
|
177
177
|
return True
|
|
178
178
|
|
|
179
|
-
def read(self, path: str) -> bytes | None:
|
|
179
|
+
def read(self, path: str, share_name: Optional[str] = None) -> bytes | None:
|
|
180
180
|
"""Alias for download"""
|
|
181
181
|
try:
|
|
182
|
-
return self.download(path)
|
|
182
|
+
return self.download(path, share_name=share_name)
|
|
183
183
|
except StorageError:
|
|
184
184
|
return None
|
|
@@ -7,8 +7,6 @@ import re
|
|
|
7
7
|
|
|
8
8
|
from typing import Any, Dict, List, Optional
|
|
9
9
|
|
|
10
|
-
from azure.storage.queue import QueueServiceClient, QueueClient
|
|
11
|
-
from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError
|
|
12
10
|
|
|
13
11
|
from ..base.QueueAdapter import QueueAdapter
|
|
14
12
|
from ..errors import ConnectionError, QueueError
|
|
@@ -35,8 +33,8 @@ class AzureQueueAdapter(QueueAdapter):
|
|
|
35
33
|
if not self.connection_string:
|
|
36
34
|
raise ConnectionError("AZURE_STORAGE_CONNECTION_STRING is not configured")
|
|
37
35
|
|
|
38
|
-
self._client
|
|
39
|
-
self._queues
|
|
36
|
+
self._client = None
|
|
37
|
+
self._queues = {}
|
|
40
38
|
|
|
41
39
|
self._lock = threading.Lock()
|
|
42
40
|
|
|
@@ -50,6 +48,8 @@ class AzureQueueAdapter(QueueAdapter):
|
|
|
50
48
|
|
|
51
49
|
def _initialize_client(self) -> None:
|
|
52
50
|
"""Initialize Azure Queue client"""
|
|
51
|
+
from azure.storage.queue import QueueServiceClient
|
|
52
|
+
|
|
53
53
|
try:
|
|
54
54
|
with self._lock:
|
|
55
55
|
if self._client is not None:
|
|
@@ -63,8 +63,10 @@ class AzureQueueAdapter(QueueAdapter):
|
|
|
63
63
|
except Exception as e:
|
|
64
64
|
raise ConnectionError(f"Failed to initialize Azure Queue Storage: {e}")
|
|
65
65
|
|
|
66
|
-
def _get_queue(self, queue_name: str)
|
|
66
|
+
def _get_queue(self, queue_name: str):
|
|
67
67
|
"""Get or create queue client"""
|
|
68
|
+
from azure.core.exceptions import ResourceExistsError
|
|
69
|
+
|
|
68
70
|
if self._client is None:
|
|
69
71
|
raise ConnectionError("Azure Queue client not initialized")
|
|
70
72
|
queue_name = self._normalize_queue_name(queue_name)
|
|
@@ -123,6 +125,8 @@ class AzureQueueAdapter(QueueAdapter):
|
|
|
123
125
|
|
|
124
126
|
def delete(self, message_id: str, queue_name: str = "default", pop_receipt: str = "") -> bool:
|
|
125
127
|
"""Delete message from queue"""
|
|
128
|
+
from azure.core.exceptions import ResourceNotFoundError
|
|
129
|
+
|
|
126
130
|
try:
|
|
127
131
|
queue_name = self._normalize_queue_name(queue_name)
|
|
128
132
|
queue_client = self._get_queue(queue_name)
|
|
@@ -8,6 +8,8 @@ import json
|
|
|
8
8
|
import base64
|
|
9
9
|
import hashlib
|
|
10
10
|
import threading
|
|
11
|
+
import logging
|
|
12
|
+
|
|
11
13
|
from datetime import datetime, date
|
|
12
14
|
from decimal import Decimal
|
|
13
15
|
from typing import Any, Dict, List, Optional
|
|
@@ -19,7 +21,6 @@ from ..errors import NoSQLError, ConnectionError
|
|
|
19
21
|
from ..retry import retry
|
|
20
22
|
from ..types import JsonDict
|
|
21
23
|
from ..models import PartitionConfig
|
|
22
|
-
import logging
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
25
26
|
|
|
@@ -540,7 +541,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
540
541
|
if "ResourceNotFound" in str(e):
|
|
541
542
|
return None
|
|
542
543
|
raise NoSQLError(f"Azure Table get failed: {str(e)}")
|
|
543
|
-
|
|
544
|
+
|
|
544
545
|
@retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
|
|
545
546
|
def _query_raw(
|
|
546
547
|
self, model: type, filters: Dict[str, Any], limit: Optional[int]
|
|
@@ -561,9 +562,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
561
562
|
sk = "RowKey"
|
|
562
563
|
else:
|
|
563
564
|
sk = (
|
|
564
|
-
self._sanitize_prop_name(orig_k)
|
|
565
|
-
if orig_k != _MODEL_FIELD
|
|
566
|
-
else _MODEL_FIELD
|
|
565
|
+
self._sanitize_prop_name(orig_k) if orig_k != _MODEL_FIELD else _MODEL_FIELD
|
|
567
566
|
)
|
|
568
567
|
|
|
569
568
|
ev = self._encode_value(orig_v)
|
|
@@ -611,6 +610,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
611
610
|
|
|
612
611
|
except Exception as e:
|
|
613
612
|
raise NoSQLError(f"Azure Table query failed: {str(e)}")
|
|
613
|
+
|
|
614
614
|
@retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
|
|
615
615
|
def _delete_raw(self, model: type, pk: str, rk: str, etag: Optional[str]) -> JsonDict:
|
|
616
616
|
try:
|