altcodepro-polydb-python 2.2.2__tar.gz → 2.2.3__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.2.2/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.2.3}/PKG-INFO +2 -1
  2. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/example_usage.py +42 -38
  3. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/pyproject.toml +2 -1
  4. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements.txt +3 -0
  5. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3/src/altcodepro_polydb_python.egg-info}/PKG-INFO +2 -1
  6. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +13 -3
  7. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/altcodepro_polydb_python.egg-info/requires.txt +1 -0
  8. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/__init__.py +2 -2
  9. altcodepro_polydb_python-2.2.3/src/polydb/adapters/AzureBlobStorageAdapter.py +141 -0
  10. altcodepro_polydb_python-2.2.3/src/polydb/adapters/AzureFileStorageAdapter.py +184 -0
  11. altcodepro_polydb_python-2.2.3/src/polydb/adapters/AzureQueueAdapter.py +125 -0
  12. altcodepro_polydb_python-2.2.3/src/polydb/adapters/AzureTableStorageAdapter.py +526 -0
  13. altcodepro_polydb_python-2.2.3/src/polydb/adapters/DynamoDBAdapter.py +503 -0
  14. altcodepro_polydb_python-2.2.3/src/polydb/adapters/FirestoreAdapter.py +366 -0
  15. altcodepro_polydb_python-2.2.3/src/polydb/adapters/GCPPubSubAdapter.py +217 -0
  16. altcodepro_polydb_python-2.2.3/src/polydb/adapters/GCPStorageAdapter.py +180 -0
  17. altcodepro_polydb_python-2.2.3/src/polydb/adapters/MongoDBAdapter.py +256 -0
  18. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/adapters/PostgreSQLAdapter.py +285 -83
  19. altcodepro_polydb_python-2.2.3/src/polydb/adapters/S3Adapter.py +175 -0
  20. altcodepro_polydb_python-2.2.3/src/polydb/adapters/SQSAdapter.py +163 -0
  21. altcodepro_polydb_python-2.2.3/src/polydb/adapters/VercelBlobAdapter.py +161 -0
  22. altcodepro_polydb_python-2.2.3/src/polydb/adapters/VercelKVAdapter.py +320 -0
  23. altcodepro_polydb_python-2.2.3/src/polydb/adapters/VercelQueueAdapter.py +61 -0
  24. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/audit/AuditStorage.py +1 -1
  25. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/base/NoSQLKVAdapter.py +113 -101
  26. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/base/ObjectStorageAdapter.py +23 -2
  27. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/base/QueueAdapter.py +2 -2
  28. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/base/SharedFilesAdapter.py +2 -2
  29. altcodepro_polydb_python-2.2.2/src/polydb/factory.py → altcodepro_polydb_python-2.2.3/src/polydb/cloudDatabaseFactory.py +49 -24
  30. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/databaseFactory.py +434 -101
  31. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/query.py +111 -42
  32. altcodepro_polydb_python-2.2.3/tests/test_aws.py +231 -0
  33. altcodepro_polydb_python-2.2.3/tests/test_azure.py +210 -0
  34. altcodepro_polydb_python-2.2.3/tests/test_cloud_factory.py +298 -0
  35. altcodepro_polydb_python-2.2.3/tests/test_gcp.py +247 -0
  36. altcodepro_polydb_python-2.2.3/tests/test_mongodb.py +283 -0
  37. altcodepro_polydb_python-2.2.3/tests/test_multi_engine.py +488 -0
  38. altcodepro_polydb_python-2.2.3/tests/test_postgresql.py +546 -0
  39. altcodepro_polydb_python-2.2.3/tests/test_vercel.py +99 -0
  40. altcodepro_polydb_python-2.2.2/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -77
  41. altcodepro_polydb_python-2.2.2/src/polydb/adapters/AzureFileStorageAdapter.py +0 -79
  42. altcodepro_polydb_python-2.2.2/src/polydb/adapters/AzureQueueAdapter.py +0 -63
  43. altcodepro_polydb_python-2.2.2/src/polydb/adapters/AzureTableStorageAdapter.py +0 -183
  44. altcodepro_polydb_python-2.2.2/src/polydb/adapters/DynamoDBAdapter.py +0 -216
  45. altcodepro_polydb_python-2.2.2/src/polydb/adapters/FirestoreAdapter.py +0 -194
  46. altcodepro_polydb_python-2.2.2/src/polydb/adapters/GCPStorageAdapter.py +0 -81
  47. altcodepro_polydb_python-2.2.2/src/polydb/adapters/MongoDBAdapter.py +0 -136
  48. altcodepro_polydb_python-2.2.2/src/polydb/adapters/PubSubAdapter.py +0 -85
  49. altcodepro_polydb_python-2.2.2/src/polydb/adapters/S3Adapter.py +0 -86
  50. altcodepro_polydb_python-2.2.2/src/polydb/adapters/SQSAdapter.py +0 -86
  51. altcodepro_polydb_python-2.2.2/src/polydb/adapters/VercelKVAdapter.py +0 -328
  52. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/LICENSE +0 -0
  53. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/MANIFEST.in +0 -0
  54. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/README.md +0 -0
  55. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements-aws.txt +0 -0
  56. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements-azure.txt +0 -0
  57. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements-dev.txt +0 -0
  58. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements-gcp.txt +0 -0
  59. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements-generic.txt +0 -0
  60. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/setup.cfg +0 -0
  61. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/setup.py +0 -0
  62. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  63. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  64. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/adapters/EFSAdapter.py +0 -0
  65. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  66. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/adapters/__init__.py +0 -0
  67. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/advanced_query.py +0 -0
  68. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/audit/__init__.py +0 -0
  69. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/audit/context.py +0 -0
  70. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/audit/manager.py +0 -0
  71. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/audit/models.py +0 -0
  72. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/base/__init__.py +0 -0
  73. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/batch.py +0 -0
  74. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/cache.py +0 -0
  75. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/decorators.py +0 -0
  76. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/errors.py +0 -0
  77. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/json_safe.py +0 -0
  78. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/models.py +0 -0
  79. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/monitoring.py +0 -0
  80. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/multitenancy.py +0 -0
  81. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/py.typed +0 -0
  82. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/registry.py +0 -0
  83. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/retry.py +0 -0
  84. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/schema.py +0 -0
  85. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/security.py +0 -0
  86. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/types.py +0 -0
  87. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/utils.py +0 -0
  88. {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.2.2
3
+ Version: 2.2.3
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
@@ -74,6 +74,7 @@ Requires-Dist: isort>=7.0.0; extra == "dev"
74
74
  Requires-Dist: mypy>=1.19.1; extra == "dev"
75
75
  Provides-Extra: test
76
76
  Requires-Dist: pytest>=9.0.2; extra == "test"
77
+ Requires-Dist: pytest-asyncio>=1.2.0; extra == "test"
77
78
  Requires-Dist: pytest-cov>=7.0.0; extra == "test"
78
79
  Requires-Dist: pytest-mock>=3.15.1; extra == "test"
79
80
  Requires-Dist: moto>=5.1.21; extra == "test"
@@ -54,11 +54,14 @@ AuditContext.set(
54
54
 
55
55
 
56
56
  # 4. CRUD operations with auto-audit and tenant injection
57
- user = db.create(User, {
58
- "name": "John Doe",
59
- "email": "john@example.com",
60
- "role": "admin",
61
- })
57
+ user = db.create(
58
+ User,
59
+ {
60
+ "name": "John Doe",
61
+ "email": "john@example.com",
62
+ "role": "admin",
63
+ },
64
+ )
62
65
  # Auto-injected: tenant_id, created_at, created_by, updated_at, updated_by
63
66
 
64
67
 
@@ -77,29 +80,24 @@ admins = db.query_linq(User, query)
77
80
 
78
81
 
79
82
  # 6. Count queries
80
- count_query = (
81
- QueryBuilder()
82
- .where("role", Operator.EQ, "admin")
83
- .count()
84
- )
83
+ count_query = QueryBuilder().where("role", Operator.EQ, "admin").count()
85
84
 
86
85
  admin_count = db.query_linq(User, count_query)
87
86
 
88
87
 
89
88
  # 7. Distinct queries
90
- distinct_query = (
91
- QueryBuilder()
92
- .select("role")
93
- .distinct_on()
94
- )
95
-
89
+ distinct_query = QueryBuilder().select("role").distinct()
96
90
  roles = db.query_linq(User, distinct_query)
97
91
 
98
92
 
99
93
  # 8. Update with field-level audit
100
- updated_user = db.update(User, user["id"], {
101
- "email": "newemail@example.com",
102
- })
94
+ updated_user = db.update(
95
+ User,
96
+ user["id"],
97
+ {
98
+ "email": "newemail@example.com",
99
+ },
100
+ )
103
101
  # Audit log shows: changed_fields = ["email", "updated_at", "updated_by"]
104
102
 
105
103
 
@@ -117,12 +115,15 @@ users = db.read(User, {"role": "admin"}, limit=10)
117
115
  # Cached for 600 seconds (from model cache_ttl)
118
116
 
119
117
  # 13. NoSQL with overflow storage (automatic)
120
- large_product = db.create(Product, {
121
- "category": "electronics",
122
- "product_id": "prod_123",
123
- "name": "Large Dataset Product",
124
- "data": {"key": "value"} * 100000, # >1MB # type: ignore
125
- })
118
+ large_product = db.create(
119
+ Product,
120
+ {
121
+ "category": "electronics",
122
+ "product_id": "prod_123",
123
+ "name": "Large Dataset Product",
124
+ "data": {"key": "value"} * 100000, # >1MB # type: ignore
125
+ },
126
+ )
126
127
  # Automatically stored in blob storage, transparent retrieval
127
128
 
128
129
 
@@ -141,12 +142,15 @@ products = db.query_linq(Product, complex_query)
141
142
 
142
143
 
143
144
  # 15. Upsert (insert or update)
144
- product = db.upsert(Product, {
145
- "category": "electronics",
146
- "product_id": "prod_123",
147
- "name": "Updated Product",
148
- "price": 299.99,
149
- })
145
+ product = db.upsert(
146
+ Product,
147
+ {
148
+ "category": "electronics",
149
+ "product_id": "prod_123",
150
+ "name": "Updated Product",
151
+ "price": 299.99,
152
+ },
153
+ )
150
154
 
151
155
 
152
156
  # 16. Include deleted records
@@ -163,25 +167,25 @@ print(f"Audit chain valid: {is_valid}")
163
167
 
164
168
  # 18. Cache invalidation
165
169
  from polydb.cache import CacheStrategy
170
+
171
+
166
172
  def bulk_insert(cursor):
167
- cursor.execute("INSERT INTO users (name, email) VALUES (%s, %s)", ("Alice", "alice@example.com"))
173
+ cursor.execute(
174
+ "INSERT INTO users (name, email) VALUES (%s, %s)", ("Alice", "alice@example.com")
175
+ )
168
176
  cursor.execute("INSERT INTO users (name, email) VALUES (%s, %s)", ("Bob", "bob@example.com"))
169
177
 
170
178
 
171
-
172
179
  # 20. Read one
173
180
  user = db.read_one(User, {"email": "john@example.com"})
174
181
 
175
182
 
176
183
  # 21. Complex LINQ with grouping (SQL)
177
184
  group_query = (
178
- QueryBuilder()
179
- .select("role")
180
- .where("created_at", Operator.GT, "2025-01-01")
181
- .group_by("role")
185
+ QueryBuilder().select("role").where("created_at", Operator.GT, "2025-01-01").group_by("role")
182
186
  )
183
187
 
184
188
  grouped = db.query_linq(User, group_query)
185
189
 
186
190
 
187
- print("✅ All operations completed with full audit trail")
191
+ print("✅ All operations completed with full audit trail")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "altcodepro-polydb-python"
7
- version = "2.2.2"
7
+ version = "2.2.3"
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.8"
@@ -102,6 +102,7 @@ dev = ["black>=26.1.0", "flake8>=7.3.0", "isort>=7.0.0", "mypy>=1.19.1"]
102
102
  # Testing with all providers
103
103
  test = [
104
104
  "pytest>=9.0.2",
105
+ "pytest-asyncio>=1.2.0",
105
106
  "pytest-cov>=7.0.0",
106
107
  "pytest-mock>=3.15.1",
107
108
  "moto>=5.1.21",
@@ -190,6 +190,8 @@ propcache==0.4.0
190
190
  proto-plus==1.26.1
191
191
  protobuf==6.32.1
192
192
  psutil==7.1.0
193
+ psycopg2-binary==2.9.11
194
+ psycopg2-pool==1.2
193
195
  pyasn1==0.6.1
194
196
  pyasn1_modules==0.4.2
195
197
  pycparser==2.23
@@ -200,6 +202,7 @@ pydash==8.0.5
200
202
  Pygments==2.19.2
201
203
  PyJHora==4.5.5
202
204
  PyJWT==2.10.1
205
+ pymongo==4.16.0
203
206
  PyNaCl==1.6.0
204
207
  pyOpenSSL==25.3.0
205
208
  pyotp==2.9.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.2.2
3
+ Version: 2.2.3
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
@@ -74,6 +74,7 @@ Requires-Dist: isort>=7.0.0; extra == "dev"
74
74
  Requires-Dist: mypy>=1.19.1; extra == "dev"
75
75
  Provides-Extra: test
76
76
  Requires-Dist: pytest>=9.0.2; extra == "test"
77
+ Requires-Dist: pytest-asyncio>=1.2.0; extra == "test"
77
78
  Requires-Dist: pytest-cov>=7.0.0; extra == "test"
78
79
  Requires-Dist: pytest-mock>=3.15.1; extra == "test"
79
80
  Requires-Dist: moto>=5.1.21; extra == "test"
@@ -19,10 +19,10 @@ src/polydb/__init__.py
19
19
  src/polydb/advanced_query.py
20
20
  src/polydb/batch.py
21
21
  src/polydb/cache.py
22
+ src/polydb/cloudDatabaseFactory.py
22
23
  src/polydb/databaseFactory.py
23
24
  src/polydb/decorators.py
24
25
  src/polydb/errors.py
25
- src/polydb/factory.py
26
26
  src/polydb/json_safe.py
27
27
  src/polydb/models.py
28
28
  src/polydb/monitoring.py
@@ -43,14 +43,16 @@ src/polydb/adapters/AzureTableStorageAdapter.py
43
43
  src/polydb/adapters/DynamoDBAdapter.py
44
44
  src/polydb/adapters/EFSAdapter.py
45
45
  src/polydb/adapters/FirestoreAdapter.py
46
+ src/polydb/adapters/GCPPubSubAdapter.py
46
47
  src/polydb/adapters/GCPStorageAdapter.py
47
48
  src/polydb/adapters/MongoDBAdapter.py
48
49
  src/polydb/adapters/PostgreSQLAdapter.py
49
- src/polydb/adapters/PubSubAdapter.py
50
50
  src/polydb/adapters/S3Adapter.py
51
51
  src/polydb/adapters/S3CompatibleAdapter.py
52
52
  src/polydb/adapters/SQSAdapter.py
53
+ src/polydb/adapters/VercelBlobAdapter.py
53
54
  src/polydb/adapters/VercelKVAdapter.py
55
+ src/polydb/adapters/VercelQueueAdapter.py
54
56
  src/polydb/adapters/__init__.py
55
57
  src/polydb/audit/AuditStorage.py
56
58
  src/polydb/audit/__init__.py
@@ -61,4 +63,12 @@ src/polydb/base/NoSQLKVAdapter.py
61
63
  src/polydb/base/ObjectStorageAdapter.py
62
64
  src/polydb/base/QueueAdapter.py
63
65
  src/polydb/base/SharedFilesAdapter.py
64
- src/polydb/base/__init__.py
66
+ src/polydb/base/__init__.py
67
+ tests/test_aws.py
68
+ tests/test_azure.py
69
+ tests/test_cloud_factory.py
70
+ tests/test_gcp.py
71
+ tests/test_mongodb.py
72
+ tests/test_multi_engine.py
73
+ tests/test_postgresql.py
74
+ tests/test_vercel.py
@@ -56,6 +56,7 @@ pika>=1.3.2
56
56
 
57
57
  [test]
58
58
  pytest>=9.0.2
59
+ pytest-asyncio>=1.2.0
59
60
  pytest-cov>=7.0.0
60
61
  pytest-mock>=3.15.1
61
62
  moto>=5.1.21
@@ -4,9 +4,9 @@ PolyDB - Enterprise Cloud-Independent Database Abstraction
4
4
  Full LINQ support, field-level audit, cache, soft delete, overflow storage
5
5
  """
6
6
 
7
- __version__ = "2.2.2"
7
+ __version__ = "2.2.3"
8
8
 
9
- from .factory import CloudDatabaseFactory
9
+ from .cloudDatabaseFactory import CloudDatabaseFactory
10
10
  from .databaseFactory import DatabaseFactory
11
11
  from .models import CloudProvider, PartitionConfig
12
12
  from .decorators import polydb_model
@@ -0,0 +1,141 @@
1
+ # src/polydb/adapters/AzureBlobStorageAdapter.py
2
+
3
+ import os
4
+ import threading
5
+ from typing import List, Optional
6
+
7
+ from azure.storage.blob import BlobServiceClient, ContainerClient
8
+ from azure.core.exceptions import ResourceExistsError, ResourceNotFoundError
9
+
10
+ from ..base.ObjectStorageAdapter import ObjectStorageAdapter
11
+ from ..errors import ConnectionError, StorageError
12
+ from ..retry import retry
13
+
14
+
15
+ class AzureBlobStorageAdapter(ObjectStorageAdapter):
16
+ """
17
+ Production-grade Azure Blob Storage adapter.
18
+
19
+ Features
20
+ - Thread-safe client initialization
21
+ - Container auto-creation
22
+ - Retry support
23
+ - Structured logging
24
+ - Connection reuse
25
+ """
26
+
27
+ def __init__(self, connection_string: str = "", container_name: str = ""):
28
+ super().__init__()
29
+
30
+ self.connection_string = connection_string or os.getenv("AZURE_STORAGE_CONNECTION_STRING")
31
+ self.container_name = container_name or os.getenv("AZURE_CONTAINER_NAME", "polydb")
32
+
33
+ if not self.connection_string:
34
+ raise ConnectionError("AZURE_STORAGE_CONNECTION_STRING is not configured")
35
+
36
+ self._client: Optional[BlobServiceClient] = None
37
+ self._container: Optional[ContainerClient] = None
38
+ self._lock = threading.Lock()
39
+
40
+ self._initialize_client()
41
+
42
+ def _initialize_client(self) -> None:
43
+ """Initialize Azure Blob client and container"""
44
+ try:
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)
51
+
52
+ self._container = self._client.get_container_client(self.container_name)
53
+
54
+ try:
55
+ self._container.create_container()
56
+ self.logger.info(f"Created container: {self.container_name}")
57
+ except ResourceExistsError:
58
+ pass
59
+
60
+ self.logger.info(
61
+ f"Azure Blob Storage initialized (container={self.container_name})"
62
+ )
63
+
64
+ except Exception as e:
65
+ raise ConnectionError(f"Failed to initialize Azure Blob Storage: {e}")
66
+
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
72
+
73
+ @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
74
+ def _put_raw(self, key: str, data: bytes) -> str:
75
+ """Upload blob"""
76
+ try:
77
+ container = self._require_container()
78
+
79
+ blob_client = container.get_blob_client(key)
80
+ blob_client.upload_blob(data, overwrite=True)
81
+
82
+ self.logger.debug(f"Uploaded blob key={key}")
83
+
84
+ return key
85
+
86
+ except Exception as e:
87
+ raise StorageError(f"Azure Blob put failed: {e}")
88
+
89
+ @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
90
+ def get(self, key: str) -> bytes | None:
91
+ """Download blob"""
92
+ try:
93
+ container = self._require_container()
94
+
95
+ blob_client = container.get_blob_client(key)
96
+
97
+ downloader = blob_client.download_blob()
98
+ data = downloader.readall()
99
+
100
+ self.logger.debug(f"Downloaded blob key={key}")
101
+
102
+ return data
103
+
104
+ except ResourceNotFoundError:
105
+ return None
106
+ except Exception as e:
107
+ raise StorageError(f"Azure Blob get failed: {e}")
108
+
109
+ @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
110
+ def delete(self, key: str) -> bool:
111
+ """Delete blob"""
112
+ try:
113
+ container = self._require_container()
114
+
115
+ blob_client = container.get_blob_client(key)
116
+ blob_client.delete_blob(delete_snapshots="include")
117
+
118
+ self.logger.debug(f"Deleted blob key={key}")
119
+
120
+ return True
121
+
122
+ except ResourceNotFoundError:
123
+ return False
124
+ except Exception as e:
125
+ raise StorageError(f"Azure Blob delete failed: {e}")
126
+
127
+ @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
128
+ def list(self, prefix: str = "") -> List[str]:
129
+ """List blobs"""
130
+ try:
131
+ container = self._require_container()
132
+
133
+ blobs = container.list_blobs(name_starts_with=prefix)
134
+ results = [blob.name for blob in blobs]
135
+
136
+ self.logger.debug(f"Listed {len(results)} blobs prefix={prefix}")
137
+
138
+ return results
139
+
140
+ except Exception as e:
141
+ raise StorageError(f"Azure Blob list failed: {e}")
@@ -0,0 +1,184 @@
1
+ # src/polydb/adapters/AzureFileStorageAdapter.py
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
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
11
+
12
+ from ..base.SharedFilesAdapter import SharedFilesAdapter
13
+ from ..errors import ConnectionError, StorageError
14
+ from ..retry import retry
15
+
16
+
17
+ class AzureFileStorageAdapter(SharedFilesAdapter):
18
+ """
19
+ Production-grade Azure File Storage adapter.
20
+
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
27
+ """
28
+
29
+ def __init__(self, connection_string: str = "", share_name: str = ""):
30
+ super().__init__()
31
+
32
+ self.connection_string = (
33
+ connection_string or os.getenv("AZURE_STORAGE_CONNECTION_STRING") or ""
34
+ )
35
+
36
+ self.share_name = share_name or os.getenv("AZURE_SHARE_NAME", "polydb")
37
+
38
+ if not self.connection_string:
39
+ raise ConnectionError("AZURE_STORAGE_CONNECTION_STRING not configured")
40
+
41
+ self._client: Optional[ShareServiceClient] = None
42
+ self._share = None
43
+ self._lock = threading.Lock()
44
+
45
+ self._initialize_client()
46
+
47
+ # --------------------------------------------------
48
+ # Client initialization
49
+ # --------------------------------------------------
50
+
51
+ def _initialize_client(self):
52
+ try:
53
+ with self._lock:
54
+ if self._client:
55
+ return
56
+
57
+ self._client = ShareServiceClient.from_connection_string(self.connection_string)
58
+
59
+ self._share = self._client.get_share_client(self.share_name)
60
+
61
+ try:
62
+ self._share.create_share()
63
+ except ResourceExistsError:
64
+ pass
65
+
66
+ self.logger.info("Initialized Azure File Storage client")
67
+
68
+ except Exception as e:
69
+ raise ConnectionError(f"Failed to initialize Azure File Storage: {str(e)}")
70
+
71
+ # --------------------------------------------------
72
+ # Helpers
73
+ # --------------------------------------------------
74
+
75
+ def _split_path(self, path: str):
76
+ if "/" not in path:
77
+ return "", path
78
+
79
+ directory, filename = path.rsplit("/", 1)
80
+ return directory, filename
81
+
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
+
94
+ return dir_client
95
+
96
+ # --------------------------------------------------
97
+ # Core operations
98
+ # --------------------------------------------------
99
+
100
+ @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
101
+ def upload(self, path: str, data: bytes) -> str:
102
+ """Upload file"""
103
+ try:
104
+ directory, filename = self._split_path(path)
105
+
106
+ dir_client = self._ensure_directory(directory)
107
+ file_client = dir_client.get_file_client(filename)
108
+
109
+ file_client.create_file(len(data))
110
+ file_client.upload_file(data)
111
+
112
+ return path
113
+
114
+ except Exception as e:
115
+ raise StorageError(f"Azure File upload failed: {str(e)}")
116
+
117
+ @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
118
+ def download(self, path: str) -> bytes:
119
+ """Download file"""
120
+ try:
121
+ 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 "")
125
+ file_client = dir_client.get_file_client(filename)
126
+
127
+ return file_client.download_file().readall()
128
+
129
+ except ResourceNotFoundError:
130
+ raise StorageError(f"File not found: {path}")
131
+ except Exception as e:
132
+ raise StorageError(f"Azure File download failed: {str(e)}")
133
+
134
+ @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
135
+ def delete(self, path: str) -> bool:
136
+ """Delete file"""
137
+ try:
138
+ 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 "")
142
+ file_client = dir_client.get_file_client(filename)
143
+
144
+ file_client.delete_file()
145
+ return True
146
+
147
+ except ResourceNotFoundError:
148
+ return False
149
+ except Exception as e:
150
+ raise StorageError(f"Azure File delete failed: {str(e)}")
151
+
152
+ @retry(max_attempts=3, delay=1.0, exceptions=(StorageError,))
153
+ def list(self, directory: str = "") -> List[str]:
154
+ """List files"""
155
+ 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
+
167
+ except Exception as e:
168
+ raise StorageError(f"Azure File list failed: {str(e)}")
169
+
170
+ # --------------------------------------------------
171
+ # Backward compatibility
172
+ # --------------------------------------------------
173
+
174
+ def write(self, path: str, data: bytes) -> bool:
175
+ """Alias for upload"""
176
+ self.upload(path, data)
177
+ return True
178
+
179
+ def read(self, path: str) -> bytes | None:
180
+ """Alias for download"""
181
+ try:
182
+ return self.download(path)
183
+ except StorageError:
184
+ return None