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.
Files changed (88) hide show
  1. {altcodepro_polydb_python-2.3.8/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.10}/PKG-INFO +11 -1
  2. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/pyproject.toml +11 -1
  3. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10/src/altcodepro_polydb_python.egg-info}/PKG-INFO +11 -1
  4. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +3 -1
  5. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/altcodepro_polydb_python.egg-info/requires.txt +10 -0
  6. altcodepro_polydb_python-2.3.10/src/polydb/adapters/AzureBlobStorageAdapter.py +184 -0
  7. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/AzureFileStorageAdapter.py +74 -74
  8. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/AzureQueueAdapter.py +9 -5
  9. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/AzureTableStorageAdapter.py +5 -5
  10. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/BlockchainBlobAdapter.py +1 -1
  11. altcodepro_polydb_python-2.3.10/src/polydb/adapters/BlockchainFileAdapter.py +217 -0
  12. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/BlockchainKVAdapter.py +4 -3
  13. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/BlockchainQueueAdapter.py +3 -2
  14. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/DynamoDBAdapter.py +12 -3
  15. altcodepro_polydb_python-2.3.10/src/polydb/adapters/EFSAdapter.py +77 -0
  16. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/FirestoreAdapter.py +15 -13
  17. altcodepro_polydb_python-2.3.10/src/polydb/adapters/GCPFilestoreAdapter.py +77 -0
  18. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/GCPPubSubAdapter.py +4 -4
  19. altcodepro_polydb_python-2.3.10/src/polydb/adapters/GCPStorageAdapter.py +186 -0
  20. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/PostgreSQLAdapter.py +3 -2
  21. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/S3Adapter.py +5 -2
  22. altcodepro_polydb_python-2.3.10/src/polydb/adapters/S3CompatibleAdapter.py +174 -0
  23. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/SQSAdapter.py +5 -2
  24. altcodepro_polydb_python-2.3.10/src/polydb/adapters/VercelFileAdapter.py +29 -0
  25. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/audit/__init__.py +1 -1
  26. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/base/SharedFilesAdapter.py +5 -5
  27. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/cloudDatabaseFactory.py +37 -66
  28. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/databaseFactory.py +23 -7
  29. altcodepro_polydb_python-2.3.8/example_usage.py +0 -191
  30. altcodepro_polydb_python-2.3.8/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -182
  31. altcodepro_polydb_python-2.3.8/src/polydb/adapters/EFSAdapter.py +0 -51
  32. altcodepro_polydb_python-2.3.8/src/polydb/adapters/GCPStorageAdapter.py +0 -225
  33. altcodepro_polydb_python-2.3.8/src/polydb/adapters/S3CompatibleAdapter.py +0 -144
  34. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/LICENSE +0 -0
  35. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/MANIFEST.in +0 -0
  36. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/README.md +0 -0
  37. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements-aws.txt +0 -0
  38. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements-azure.txt +0 -0
  39. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements-dev.txt +0 -0
  40. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements-gcp.txt +0 -0
  41. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements-generic.txt +0 -0
  42. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/requirements.txt +0 -0
  43. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/setup.cfg +0 -0
  44. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/setup.py +0 -0
  45. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  46. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  47. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/PolyDB.py +0 -0
  48. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/__init__.py +0 -0
  49. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  50. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  51. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  52. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  53. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/adapters/__init__.py +0 -0
  54. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/advanced_query.py +0 -0
  55. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/audit/AuditStorage.py +0 -0
  56. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/audit/context.py +0 -0
  57. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/audit/manager.py +0 -0
  58. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/audit/models.py +0 -0
  59. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/base/NoSQLKVAdapter.py +0 -0
  60. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  61. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/base/QueueAdapter.py +0 -0
  62. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/base/__init__.py +0 -0
  63. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/batch.py +0 -0
  64. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/cache.py +0 -0
  65. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/decorators.py +0 -0
  66. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/errors.py +0 -0
  67. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/json_safe.py +0 -0
  68. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/models.py +0 -0
  69. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/monitoring.py +0 -0
  70. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/multitenancy.py +0 -0
  71. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/py.typed +0 -0
  72. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/query.py +0 -0
  73. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/registry.py +0 -0
  74. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/retry.py +0 -0
  75. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/schema.py +0 -0
  76. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/security.py +0 -0
  77. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/types.py +0 -0
  78. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/utils.py +0 -0
  79. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/src/polydb/validation.py +0 -0
  80. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_aws.py +0 -0
  81. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_azure.py +0 -0
  82. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_blockchain.py +0 -0
  83. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_cloud_factory.py +0 -0
  84. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_gcp.py +0 -0
  85. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_mongodb.py +0 -0
  86. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_multi_engine.py +0 -0
  87. {altcodepro_polydb_python-2.3.8 → altcodepro_polydb_python-2.3.10}/tests/test_postgresql.py +0 -0
  88. {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.8
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.8"
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.8
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
- Fixes:
22
- - Adds upload/download methods expected by tests
23
- - Ensures share exists
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: Optional[ShareServiceClient] = None
42
- self._share = None
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 initialization
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
- self._share = self._client.get_share_client(self.share_name)
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
- try:
62
- self._share.create_share()
63
- except ResourceExistsError:
64
- pass
60
+ name = share_name or self.share_name
65
61
 
66
- self.logger.info("Initialized Azure File Storage client")
62
+ cached = self._shares.get(name)
63
+ if cached is not None:
64
+ return cached
67
65
 
68
- except Exception as e:
69
- raise ConnectionError(f"Failed to initialize Azure File Storage: {str(e)}")
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
- if not directory and self._share:
84
- return self._share.get_directory_client("")
85
- if not self._share:
86
- raise ConnectionError("Azure File Storage share not initialized")
87
- dir_client = self._share.get_directory_client(directory)
88
-
89
- try:
90
- dir_client.create_directory()
91
- except ResourceExistsError:
92
- pass
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
- if not self._share:
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
- if not self._share:
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
- if not self._share:
157
- raise ConnectionError("Azure File Storage share not initialized")
158
- dir_client = self._share.get_directory_client(directory or "")
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: Optional[QueueServiceClient] = None
39
- self._queues: Dict[str, QueueClient] = {}
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) -> QueueClient:
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:
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import os
4
4
  from typing import Any, Dict, List, Optional
5
5
 
6
- import ipfshttpclient
6
+
7
7
  from dotenv import load_dotenv
8
8
 
9
9
  from ..base.ObjectStorageAdapter import ObjectStorageAdapter