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.
- {altcodepro_polydb_python-2.2.2/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.2.3}/PKG-INFO +2 -1
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/example_usage.py +42 -38
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/pyproject.toml +2 -1
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements.txt +3 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3/src/altcodepro_polydb_python.egg-info}/PKG-INFO +2 -1
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +13 -3
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/altcodepro_polydb_python.egg-info/requires.txt +1 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/__init__.py +2 -2
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/AzureBlobStorageAdapter.py +141 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/AzureFileStorageAdapter.py +184 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/AzureQueueAdapter.py +125 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/AzureTableStorageAdapter.py +526 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/DynamoDBAdapter.py +503 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/FirestoreAdapter.py +366 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/GCPPubSubAdapter.py +217 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/GCPStorageAdapter.py +180 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/MongoDBAdapter.py +256 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/adapters/PostgreSQLAdapter.py +285 -83
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/S3Adapter.py +175 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/SQSAdapter.py +163 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/VercelBlobAdapter.py +161 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/VercelKVAdapter.py +320 -0
- altcodepro_polydb_python-2.2.3/src/polydb/adapters/VercelQueueAdapter.py +61 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/audit/AuditStorage.py +1 -1
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/base/NoSQLKVAdapter.py +113 -101
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/base/ObjectStorageAdapter.py +23 -2
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/base/QueueAdapter.py +2 -2
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/base/SharedFilesAdapter.py +2 -2
- altcodepro_polydb_python-2.2.2/src/polydb/factory.py → altcodepro_polydb_python-2.2.3/src/polydb/cloudDatabaseFactory.py +49 -24
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/databaseFactory.py +434 -101
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/query.py +111 -42
- altcodepro_polydb_python-2.2.3/tests/test_aws.py +231 -0
- altcodepro_polydb_python-2.2.3/tests/test_azure.py +210 -0
- altcodepro_polydb_python-2.2.3/tests/test_cloud_factory.py +298 -0
- altcodepro_polydb_python-2.2.3/tests/test_gcp.py +247 -0
- altcodepro_polydb_python-2.2.3/tests/test_mongodb.py +283 -0
- altcodepro_polydb_python-2.2.3/tests/test_multi_engine.py +488 -0
- altcodepro_polydb_python-2.2.3/tests/test_postgresql.py +546 -0
- altcodepro_polydb_python-2.2.3/tests/test_vercel.py +99 -0
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -77
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/AzureFileStorageAdapter.py +0 -79
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/AzureQueueAdapter.py +0 -63
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/AzureTableStorageAdapter.py +0 -183
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/DynamoDBAdapter.py +0 -216
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/FirestoreAdapter.py +0 -194
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/GCPStorageAdapter.py +0 -81
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/MongoDBAdapter.py +0 -136
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/PubSubAdapter.py +0 -85
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/S3Adapter.py +0 -86
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/SQSAdapter.py +0 -86
- altcodepro_polydb_python-2.2.2/src/polydb/adapters/VercelKVAdapter.py +0 -328
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/LICENSE +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/MANIFEST.in +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/README.md +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements-aws.txt +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements-azure.txt +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements-dev.txt +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements-gcp.txt +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/requirements-generic.txt +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/setup.cfg +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/setup.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/adapters/EFSAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/adapters/__init__.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/advanced_query.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/audit/__init__.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/audit/context.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/audit/manager.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/audit/models.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/base/__init__.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/batch.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/cache.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/decorators.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/errors.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/json_safe.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/models.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/monitoring.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/multitenancy.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/py.typed +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/registry.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/retry.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/schema.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/security.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/types.py +0 -0
- {altcodepro_polydb_python-2.2.2 → altcodepro_polydb_python-2.2.3}/src/polydb/utils.py +0 -0
- {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.
|
|
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(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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(
|
|
101
|
-
|
|
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(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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.
|
|
7
|
+
__version__ = "2.2.3"
|
|
8
8
|
|
|
9
|
-
from .
|
|
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
|