altcodepro-polydb-python 2.3.9__py3-none-any.whl → 2.3.11__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.
Files changed (30) hide show
  1. {altcodepro_polydb_python-2.3.9.dist-info → altcodepro_polydb_python-2.3.11.dist-info}/METADATA +11 -1
  2. altcodepro_polydb_python-2.3.11.dist-info/RECORD +61 -0
  3. polydb/PolyDB.py +2 -1
  4. polydb/adapters/AzureBlobStorageAdapter.py +92 -90
  5. polydb/adapters/AzureFileStorageAdapter.py +74 -74
  6. polydb/adapters/AzureQueueAdapter.py +9 -5
  7. polydb/adapters/AzureTableStorageAdapter.py +5 -5
  8. polydb/adapters/BlockchainBlobAdapter.py +1 -1
  9. polydb/adapters/BlockchainFileAdapter.py +217 -0
  10. polydb/adapters/BlockchainKVAdapter.py +4 -3
  11. polydb/adapters/BlockchainQueueAdapter.py +3 -2
  12. polydb/adapters/DynamoDBAdapter.py +12 -3
  13. polydb/adapters/EFSAdapter.py +45 -19
  14. polydb/adapters/FirestoreAdapter.py +15 -13
  15. polydb/adapters/GCPFilestoreAdapter.py +77 -0
  16. polydb/adapters/GCPPubSubAdapter.py +4 -4
  17. polydb/adapters/GCPStorageAdapter.py +78 -117
  18. polydb/adapters/PostgreSQLAdapter.py +2 -1
  19. polydb/adapters/S3Adapter.py +5 -2
  20. polydb/adapters/S3CompatibleAdapter.py +87 -57
  21. polydb/adapters/SQSAdapter.py +5 -2
  22. polydb/adapters/VercelFileAdapter.py +29 -0
  23. polydb/audit/__init__.py +1 -1
  24. polydb/base/SharedFilesAdapter.py +5 -5
  25. polydb/cloudDatabaseFactory.py +37 -66
  26. polydb/databaseFactory.py +23 -7
  27. altcodepro_polydb_python-2.3.9.dist-info/RECORD +0 -58
  28. {altcodepro_polydb_python-2.3.9.dist-info → altcodepro_polydb_python-2.3.11.dist-info}/WHEEL +0 -0
  29. {altcodepro_polydb_python-2.3.9.dist-info → altcodepro_polydb_python-2.3.11.dist-info}/licenses/LICENSE +0 -0
  30. {altcodepro_polydb_python-2.3.9.dist-info → altcodepro_polydb_python-2.3.11.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.3.9
3
+ Version: 2.3.11
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"
@@ -0,0 +1,61 @@
1
+ altcodepro_polydb_python-2.3.11.dist-info/licenses/LICENSE,sha256=9X8GLocsBwy-5aR5JGOt2SAMDDPs9Qv-YnqmHBHOXrw,1067
2
+ polydb/PolyDB.py,sha256=DJjS1a-gjkqqo32avhRM-4CT-9ZZO3LZJ_sUOfZ99L0,23485
3
+ polydb/__init__.py,sha256=UhUzfSvmMgKbV2tSME1ooIyfshIBi7_WyU4xl1tWWiA,1454
4
+ polydb/advanced_query.py,sha256=cxMB-EB-qT3bWXJlhmjnMCUtrzogORWyoEfS50Dy7go,4280
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=xbcQTJ4kSsayV7rmWfQ3akvbvbCzOnpxjO-KNXykNeE,40862
9
+ polydb/decorators.py,sha256=Rzk8Bj8wHi8YFtc06HEYT5r_Vqqn7TGaCtR5qvHdY-E,420
10
+ polydb/errors.py,sha256=rcFeBH0cenjJ86v0cmDc2Yjj4R019pLCBcTeSC4qps4,1428
11
+ polydb/json_safe.py,sha256=R5PrqAGirqjYKPyy-8KH-lSXjLH0FPr2TSGozy4eheU,149
12
+ polydb/models.py,sha256=9uu_BaJ95194n-vnd0Rx9KLc6aPS-mxn10P4W5grUcI,8155
13
+ polydb/monitoring.py,sha256=UMm3ybyRJjAQi-prXXMLl9zuHhnhMnYBzMD3XWK66y8,9571
14
+ polydb/multitenancy.py,sha256=9kyY98RpKg8xDy9ejB_MyV_YzF7eZd4uxashw5S8vlg,6408
15
+ polydb/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ polydb/query.py,sha256=L13FL8A06HxT0F_LGlY9IBh-62j1VWZrv2Tu_wS-Ed0,6348
17
+ polydb/registry.py,sha256=RD_elvFXcmhTdCyZDm2f3ej0elxqhArnSJ2aO9k5VCU,2352
18
+ polydb/retry.py,sha256=etsj8MGo1WMvlcZMzWmFELAsWCRs-XPEuJe6K76QgbM,2548
19
+ polydb/schema.py,sha256=VrOayX6V6AD2Qh3-lm4ZVPTpI24e4V52IYheZf2rNQ4,5812
20
+ polydb/security.py,sha256=-bXdRjFmvq4X6ie6FrZMcO9ZbgjWFkNySSbRwFt1X1Q,16281
21
+ polydb/types.py,sha256=XB_85Un8_aWt4dSfpjIGotHbK3KBY2WurQGXr9EOxWY,2992
22
+ polydb/utils.py,sha256=G_ki5zKr5rGPgpFQM1CTq6twQd5OytaHKfet267MftM,1662
23
+ polydb/validation.py,sha256=a1o1d02k3c6PWQwkBbw_0nEmIgrdB5RR8OcpNQMn4cA,4810
24
+ polydb/adapters/AzureBlobStorageAdapter.py,sha256=4vD55Z8DBTzBK66jIJbo5bNMY-AQ61MlP0-P2Fv_JgQ,7083
25
+ polydb/adapters/AzureFileStorageAdapter.py,sha256=VZNprqlBXCuWUgtqClNT-NrQmRf-XFYEiRA2BLbf-Sc,7046
26
+ polydb/adapters/AzureQueueAdapter.py,sha256=5PrKAX4OQxUD5nReZKrInF_mjQVdFcj2aYd0Xp-HjjQ,5254
27
+ polydb/adapters/AzureTableStorageAdapter.py,sha256=EA7v5YUJwe0S3ql0EPg-ObNtX2iNJ38fJiwvRU-1Blo,23295
28
+ polydb/adapters/BlockchainBlobAdapter.py,sha256=D01Yua9mkKfaQrxKYApblIyI6DSP0dtNAh4Tav51HJ4,3299
29
+ polydb/adapters/BlockchainFileAdapter.py,sha256=G749xOVpG20HuKS8zCgi6PMjoJNu-YXK7zitygjLdzM,8335
30
+ polydb/adapters/BlockchainKVAdapter.py,sha256=UFYHyTgvdW-sZUBqyHEG3Cdx6wSTiF2QowEVWL3XPTg,4564
31
+ polydb/adapters/BlockchainQueueAdapter.py,sha256=XsN5ag9Wulaelfkb3RCuBuSBw_4MUx9Wi3Gd3DBn7ME,4196
32
+ polydb/adapters/DynamoDBAdapter.py,sha256=bS4t-XgVMIqMEVfr4K2XMvs-YA1ORTI0Plg5jJ0tW_Q,18429
33
+ polydb/adapters/EFSAdapter.py,sha256=Ca7-Esm3KNVm90Qo6tWmmftwk8Nq54DIzU3fAHX53io,2937
34
+ polydb/adapters/FirestoreAdapter.py,sha256=e3DEODOZ-UAWULLtGTIED-Ym1JrxOKaOLPQVKhhehvk,13939
35
+ polydb/adapters/GCPFilestoreAdapter.py,sha256=yjFQQwsWYWc8mo8XwMViVTWb5_D--xAyTMvE-4AOpNM,3006
36
+ polydb/adapters/GCPPubSubAdapter.py,sha256=7XNots2VA0ReEDku-rjg-OTYmftIpx5UgnXYDdXNkOo,8692
37
+ polydb/adapters/GCPStorageAdapter.py,sha256=9yS1Jhcn5_rCRdZ5uOqcRW6Ba-UNb6VOYpwENP-C6Qk,7133
38
+ polydb/adapters/MongoDBAdapter.py,sha256=vX3SAHDLbTnHABGesES9N-gYSQqPqdqFLJgd7pYWZzw,7471
39
+ polydb/adapters/PostgreSQLAdapter.py,sha256=atK8MEm2ui5kb-xWaDW0-6kn4vwTmaBl5G4BPhK4Gh0,24867
40
+ polydb/adapters/S3Adapter.py,sha256=5R0zHAL2SkGFjp1L3bp-IU468bXYdSf6nKx974MN104,7586
41
+ polydb/adapters/S3CompatibleAdapter.py,sha256=jpafqbAjA8-irdXBrfXa1QJySIzrcUQ6UrFt5h5FAEc,7006
42
+ polydb/adapters/SQSAdapter.py,sha256=1vfbNoqIDy-b8t2xcxy91SoxSYBPFDfUh7yCQWxdS84,5778
43
+ polydb/adapters/VercelBlobAdapter.py,sha256=Hu1u8vE-3rvJMBDotnPKeRoVaH1Vl9PdM1hBKphgj2w,6983
44
+ 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
47
+ polydb/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
+ polydb/audit/AuditStorage.py,sha256=A5HLhkWG8kef_caUuWakJf4fSK_r03NN4JLbbK7N55c,4949
49
+ polydb/audit/__init__.py,sha256=Z7-y5djq3glQ2Yun6nj-13Efpj3oGz9Qc0veS2g06Y4,245
50
+ polydb/audit/context.py,sha256=-A1FMtmr-2snVAHpTrVT80u-D_MCaqX6AoV4Ku2bz_o,1955
51
+ polydb/audit/manager.py,sha256=KzaaOf5bDfr4M-CkCAZBG_U_4xIBCKDLRAf3hsm-DAk,1236
52
+ polydb/audit/models.py,sha256=BgkSEQRbjbourxyGcEeJYIYzozwTM-pqTiSOM_BhWHs,2256
53
+ polydb/base/NoSQLKVAdapter.py,sha256=oa3MT7Z6E5zsF6mDeqjaMfefScKXJgne79LkB7dN8ZA,11557
54
+ polydb/base/ObjectStorageAdapter.py,sha256=mNdJnhoB3VqSCQvmcoel5PohrVQw7Nrajdd5suGBOvQ,2242
55
+ polydb/base/QueueAdapter.py,sha256=jFgyG-SUK4nhRNxm2NbzUbwnA9b_5iAC-ikLSUpXRwk,799
56
+ polydb/base/SharedFilesAdapter.py,sha256=kXbJmtn_cwEyAZ-1AvFrmesCLSwu43ycTV3S4BmwrO4,853
57
+ polydb/base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
+ altcodepro_polydb_python-2.3.11.dist-info/METADATA,sha256=CAYda7inNH2AkpAJMK0f12Td5t8TA9W9x8SD6ffYpck,12303
59
+ altcodepro_polydb_python-2.3.11.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
60
+ altcodepro_polydb_python-2.3.11.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
61
+ altcodepro_polydb_python-2.3.11.dist-info/RECORD,,
polydb/PolyDB.py CHANGED
@@ -23,7 +23,6 @@ from .types import JsonDict, Lookup
23
23
  from .utils import setup_logger
24
24
  from .validation import ModelValidator, SchemaValidator
25
25
 
26
-
27
26
  ModelRef = Union[Type, str]
28
27
 
29
28
 
@@ -336,6 +335,7 @@ class PolyDB:
336
335
  metadata: Optional[Dict[str, Any]] = None,
337
336
  storage_name: str = "default",
338
337
  optimize: bool = True,
338
+ container_name: Optional[str] = None,
339
339
  ) -> str:
340
340
  storage = self.get_object_storage(storage_name)
341
341
  return storage.put(
@@ -345,6 +345,7 @@ class PolyDB:
345
345
  optimize=optimize,
346
346
  media_type=media_type,
347
347
  metadata=metadata or {},
348
+ container_name=container_name,
348
349
  )
349
350
 
350
351
  def get_blob(
@@ -2,11 +2,8 @@
2
2
 
3
3
  import os
4
4
  import threading
5
- from typing import Any, Dict, List, Optional
6
5
  import mimetypes
7
- from azure.storage.blob import BlobServiceClient, ContainerClient, ContentSettings
8
- from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError
9
-
6
+ from typing import Any, Dict, List, Optional
10
7
  from ..base.ObjectStorageAdapter import ObjectStorageAdapter
11
8
  from ..errors import ConnectionError, StorageError
12
9
  from ..retry import retry
@@ -16,12 +13,10 @@ class AzureBlobStorageAdapter(ObjectStorageAdapter):
16
13
  """
17
14
  Production-grade Azure Blob Storage adapter.
18
15
 
19
- Features
20
- - Thread-safe client initialization
16
+ - One BlobServiceClient, many containers (resolved + cached per call)
21
17
  - Container auto-creation
22
- - Retry support
23
- - Structured logging
24
- - Connection reuse
18
+ - Thread-safe, retryable, structured logging
19
+ - put/get/delete are symmetric: a blob is stored at `key` and fetched at `key`
25
20
  """
26
21
 
27
22
  def __init__(self, connection_string: str = "", container_name: str = ""):
@@ -33,42 +28,78 @@ class AzureBlobStorageAdapter(ObjectStorageAdapter):
33
28
  if not self.connection_string:
34
29
  raise ConnectionError("AZURE_STORAGE_CONNECTION_STRING is not configured")
35
30
 
36
- self._client: Optional[BlobServiceClient] = None
37
- self._container: Optional[ContainerClient] = None
31
+ self._client = None
32
+ self._containers = {}
38
33
  self._lock = threading.Lock()
39
34
 
40
35
  self._initialize_client()
41
36
 
37
+ # ------------------------------------------------------------------
38
+ # CLIENT / CONTAINER RESOLUTION
39
+ # ------------------------------------------------------------------
42
40
  def _initialize_client(self) -> None:
43
- """Initialize Azure Blob client and container"""
41
+ """Initialize the shared Azure Blob service client."""
42
+ from azure.storage.blob import BlobServiceClient
43
+
44
44
  try:
45
45
  with self._lock:
46
- if self._client is not None:
47
- return
48
- if not self.connection_string:
49
- raise ConnectionError("AZURE_STORAGE_CONNECTION_STRING is not configured")
50
- self._client = BlobServiceClient.from_connection_string(self.connection_string)
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
51
 
52
- self._container = self._client.get_container_client(self.container_name)
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
53
55
 
54
- try:
55
- self._container.create_container()
56
- self.logger.info(f"Created container: {self.container_name}")
57
- except ResourceExistsError:
58
- pass
56
+ if self._client is None:
57
+ raise ConnectionError("Azure Blob Storage client is not initialized")
59
58
 
60
- self.logger.info(
61
- f"Azure Blob Storage initialized (container={self.container_name})"
62
- )
59
+ name = container_name or self.container_name
63
60
 
64
- except Exception as e:
65
- raise ConnectionError(f"Failed to initialize Azure Blob Storage: {e}")
61
+ cached = self._containers.get(name)
62
+ if cached is not None:
63
+ return cached
66
64
 
67
- def _require_container(self) -> ContainerClient:
68
- """Ensure container exists"""
69
- if self._container is None:
70
- raise ConnectionError("Azure Blob Storage client is not initialized")
71
- return self._container
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
+ )
72
103
 
73
104
  @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
74
105
  def _put_raw(
@@ -78,105 +109,76 @@ class AzureBlobStorageAdapter(ObjectStorageAdapter):
78
109
  fileName: str = "",
79
110
  media_type: Optional[str] = None,
80
111
  metadata: Dict[str, Any] | None = None,
112
+ container_name: Optional[str] = None,
81
113
  ) -> str:
82
- """Upload blob with filename, media type, and metadata"""
83
- try:
84
- container = self._require_container()
114
+ """Upload blob. Stored at `key` so get/delete can find it by the same key."""
115
+ from azure.storage.blob import ContentSettings
85
116
 
86
- # --------------------------------------------------
87
- # Resolve filename
88
- # --------------------------------------------------
89
- filename = fileName or os.path.basename(key)
117
+ try:
118
+ container = self._get_container(container_name)
90
119
 
91
- # --------------------------------------------------
92
- # Ensure extension from media_type if missing
93
- # --------------------------------------------------
120
+ # filename is metadata only — it must NOT alter the blob key
121
+ filename = fileName or os.path.basename(key) or key
94
122
  if media_type:
95
123
  ext = mimetypes.guess_extension(media_type) or ""
96
124
  if ext and not filename.lower().endswith(ext):
97
125
  filename += ext
98
126
 
99
- # --------------------------------------------------
100
- # Final blob key (include filename if needed)
101
- # --------------------------------------------------
102
- blob_key = key
103
- if fileName:
104
- blob_key = f"{key.rstrip('/')}/{filename}"
105
-
106
- blob_client = container.get_blob_client(blob_key)
107
-
108
- # --------------------------------------------------
109
- # Upload with content type + metadata
110
- # --------------------------------------------------
127
+ blob_client = container.get_blob_client(key)
111
128
  blob_client.upload_blob(
112
129
  data,
113
130
  overwrite=True,
114
131
  content_settings=ContentSettings(
115
132
  content_type=media_type or "application/octet-stream"
116
133
  ),
117
- metadata={
118
- **(metadata or {}),
119
- "filename": filename,
120
- },
134
+ metadata={**(metadata or {}), "filename": filename},
121
135
  )
122
136
 
123
- self.logger.debug(f"Uploaded blob key={blob_key}, type={media_type}")
124
-
137
+ self.logger.debug(
138
+ f"Uploaded blob key={key} container={container.container_name} type={media_type}"
139
+ )
125
140
  return blob_client.url
126
141
 
127
142
  except Exception as e:
128
143
  raise StorageError(f"Azure Blob put failed: {e}")
129
144
 
145
+ # ------------------------------------------------------------------
146
+ # GET / DELETE / LIST
147
+ # ------------------------------------------------------------------
130
148
  @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
131
- def get(self, key: str) -> bytes | None:
132
- """Download blob"""
133
- try:
134
- container = self._require_container()
135
-
136
- blob_client = container.get_blob_client(key)
137
-
138
- downloader = blob_client.download_blob()
139
- data = downloader.readall()
149
+ def get(self, key: str, container_name: Optional[str] = None) -> bytes | None:
150
+ from azure.core.exceptions import ResourceNotFoundError
140
151
 
152
+ try:
153
+ container = self._get_container(container_name)
154
+ data = container.get_blob_client(key).download_blob().readall()
141
155
  self.logger.debug(f"Downloaded blob key={key}")
142
-
143
156
  return data
144
-
145
157
  except ResourceNotFoundError:
146
158
  return None
147
159
  except Exception as e:
148
160
  raise StorageError(f"Azure Blob get failed: {e}")
149
161
 
150
162
  @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
151
- def delete(self, key: str) -> bool:
152
- """Delete blob"""
153
- try:
154
- container = self._require_container()
155
-
156
- blob_client = container.get_blob_client(key)
157
- blob_client.delete_blob(delete_snapshots="include")
163
+ def delete(self, key: str, container_name: Optional[str] = None) -> bool:
164
+ from azure.core.exceptions import ResourceNotFoundError
158
165
 
166
+ try:
167
+ container = self._get_container(container_name)
168
+ container.get_blob_client(key).delete_blob(delete_snapshots="include")
159
169
  self.logger.debug(f"Deleted blob key={key}")
160
-
161
170
  return True
162
-
163
171
  except ResourceNotFoundError:
164
172
  return False
165
173
  except Exception as e:
166
174
  raise StorageError(f"Azure Blob delete failed: {e}")
167
175
 
168
176
  @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
169
- def list(self, prefix: str = "") -> List[str]:
170
- """List blobs"""
177
+ def list(self, prefix: str = "", container_name: Optional[str] = None) -> List[str]:
171
178
  try:
172
- container = self._require_container()
173
-
174
- blobs = container.list_blobs(name_starts_with=prefix)
175
- results = [blob.name for blob in blobs]
176
-
179
+ container = self._get_container(container_name)
180
+ results = [b.name for b in container.list_blobs(name_starts_with=prefix)]
177
181
  self.logger.debug(f"Listed {len(results)} blobs prefix={prefix}")
178
-
179
182
  return results
180
-
181
183
  except Exception as e:
182
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