altcodepro-polydb-python 2.3.14__tar.gz → 2.3.16__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {altcodepro_polydb_python-2.3.14/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.16}/PKG-INFO +1 -1
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/pyproject.toml +1 -1
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/AzureTableStorageAdapter.py +129 -24
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/PostgreSQLAdapter.py +125 -6
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/LICENSE +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/MANIFEST.in +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/README.md +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/requirements-aws.txt +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/requirements-azure.txt +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/requirements-dev.txt +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/requirements-gcp.txt +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/requirements-generic.txt +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/requirements.txt +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/setup.cfg +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/setup.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/PolyDB.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/EFSAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/FirestoreAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/MongoDBAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/S3Adapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/SQSAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/VercelFileAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/VercelKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/advanced_query.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/audit/AuditStorage.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/audit/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/audit/context.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/audit/manager.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/audit/models.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/base/NoSQLKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/base/ObjectStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/base/QueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/base/SharedFilesAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/base/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/batch.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/cache.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/cloudDatabaseFactory.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/databaseFactory.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/decorators.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/errors.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/json_safe.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/models.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/monitoring.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/multitenancy.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/py.typed +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/query.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/registry.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/retry.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/schema.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/security.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/types.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/utils.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/validation.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_aws.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_azure.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_blockchain.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_cloud_factory.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_gcp.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_mongodb.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_multi_engine.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_postgresql.py +0 -0
- {altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_vercel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: altcodepro-polydb-python
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.16
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "altcodepro-polydb-python"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.16"
|
|
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"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: altcodepro-polydb-python
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.16
|
|
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
|
|
@@ -31,6 +31,13 @@ _BASE64_RE = re.compile(r"^[A-Za-z0-9+/]*={0,2}$")
|
|
|
31
31
|
# ensures model isolation across the same table
|
|
32
32
|
_MODEL_FIELD = "__polydb_model__"
|
|
33
33
|
|
|
34
|
+
# ─── NEW: HTTP transport tuning ──────────────────────────────────────────────
|
|
35
|
+
# Default Azure SDK has no read timeout. A sync call from an async event loop
|
|
36
|
+
# can park forever. These are overridable via env so ops can tune per env.
|
|
37
|
+
_CONNECT_TIMEOUT = float(os.getenv("AZURE_TABLE_CONNECT_TIMEOUT", "10"))
|
|
38
|
+
_READ_TIMEOUT = float(os.getenv("AZURE_TABLE_READ_TIMEOUT", "30"))
|
|
39
|
+
_RETRY_TOTAL = int(os.getenv("AZURE_TABLE_RETRY_TOTAL", "3"))
|
|
40
|
+
|
|
34
41
|
logging.getLogger("azure").setLevel(logging.ERROR)
|
|
35
42
|
|
|
36
43
|
|
|
@@ -43,6 +50,15 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
43
50
|
- Blob overflow for entities > 1MB
|
|
44
51
|
- Model isolation using __polydb_model__
|
|
45
52
|
- Always returns id (derived from RowKey if missing)
|
|
53
|
+
|
|
54
|
+
Performance / safety guarantees:
|
|
55
|
+
- One TableServiceClient per adapter instance.
|
|
56
|
+
- One TableClient cached per table for the adapter's lifetime.
|
|
57
|
+
- create_table_if_not_exists runs ONCE per table per adapter lifetime.
|
|
58
|
+
- Explicit connect/read timeouts on the HTTP transport so a slow or
|
|
59
|
+
mis-configured endpoint fails fast instead of hanging the caller.
|
|
60
|
+
- No mutation of instance state inside read/write paths (was the
|
|
61
|
+
previous `self._table_client = ...` race hazard).
|
|
46
62
|
"""
|
|
47
63
|
|
|
48
64
|
AZURE_TABLE_MAX_SIZE = 60 * 1024 # 1MB
|
|
@@ -67,6 +83,16 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
67
83
|
self._client: Any = None
|
|
68
84
|
self._blob_service = None
|
|
69
85
|
self._client_lock = threading.Lock()
|
|
86
|
+
|
|
87
|
+
# ─── NEW: caches ────────────────────────────────────────────────────
|
|
88
|
+
# _ensured_tables: table names for which create_table_if_not_exists
|
|
89
|
+
# has already succeeded on this adapter instance.
|
|
90
|
+
# _table_clients_cache: TableClient handles by table name.
|
|
91
|
+
# _cache_lock: protects both caches from concurrent get/put.
|
|
92
|
+
self._ensured_tables: set[str] = set()
|
|
93
|
+
self._table_clients_cache: Dict[str, Any] = {}
|
|
94
|
+
self._cache_lock = threading.Lock()
|
|
95
|
+
|
|
70
96
|
self._initialize_client()
|
|
71
97
|
|
|
72
98
|
def _initialize_client(self):
|
|
@@ -76,9 +102,22 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
76
102
|
|
|
77
103
|
with self._client_lock:
|
|
78
104
|
if not self._client:
|
|
79
|
-
|
|
105
|
+
# ─── CHANGED: explicit transport timeouts + retry budget ──
|
|
106
|
+
# connection_timeout / read_timeout are honoured by the
|
|
107
|
+
# azure-core RequestsTransport. retry_total caps the
|
|
108
|
+
# SDK's built-in retry policy so a hard failure surfaces
|
|
109
|
+
# in seconds, not minutes.
|
|
110
|
+
self._client = TableServiceClient.from_connection_string(
|
|
111
|
+
self.connection_string,
|
|
112
|
+
connection_timeout=_CONNECT_TIMEOUT,
|
|
113
|
+
read_timeout=_READ_TIMEOUT,
|
|
114
|
+
retry_total=_RETRY_TOTAL,
|
|
115
|
+
)
|
|
80
116
|
self._blob_service = BlobServiceClient.from_connection_string(
|
|
81
|
-
self.connection_string
|
|
117
|
+
self.connection_string,
|
|
118
|
+
connection_timeout=_CONNECT_TIMEOUT,
|
|
119
|
+
read_timeout=_READ_TIMEOUT,
|
|
120
|
+
retry_total=_RETRY_TOTAL,
|
|
82
121
|
)
|
|
83
122
|
|
|
84
123
|
try:
|
|
@@ -379,22 +418,50 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
379
418
|
|
|
380
419
|
def _get_table_client(self, model: type):
|
|
381
420
|
"""
|
|
382
|
-
|
|
383
|
-
|
|
421
|
+
Return a TableClient for the model, ensuring the table exists.
|
|
422
|
+
|
|
423
|
+
─── CHANGED ────────────────────────────────────────────────────────
|
|
424
|
+
Both the table existence check AND the TableClient handle are now
|
|
425
|
+
cached per adapter instance. The previous version ran
|
|
426
|
+
create_table_if_not_exists on every read/write/query/delete — a
|
|
427
|
+
hot-loop seed of 100 records produced ~300 unnecessary network
|
|
428
|
+
round-trips. Now:
|
|
429
|
+
|
|
430
|
+
- First call for a table: 1 HTTP call (create_table_if_not_exists)
|
|
431
|
+
+ 1 cheap local TableClient construction.
|
|
432
|
+
- All subsequent calls: O(1) dict lookup, no network.
|
|
384
433
|
"""
|
|
385
434
|
table_name = self._get_table_name(model)
|
|
386
435
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
436
|
+
# Fast path: already ensured + client cached.
|
|
437
|
+
cached = self._table_clients_cache.get(table_name)
|
|
438
|
+
if cached is not None and table_name in self._ensured_tables:
|
|
439
|
+
return cached
|
|
440
|
+
|
|
441
|
+
with self._cache_lock:
|
|
442
|
+
# Re-check inside the lock to avoid duplicate ensures under
|
|
443
|
+
# concurrent first-touch.
|
|
444
|
+
cached = self._table_clients_cache.get(table_name)
|
|
445
|
+
already_ensured = table_name in self._ensured_tables
|
|
446
|
+
|
|
447
|
+
if not already_ensured:
|
|
448
|
+
try:
|
|
449
|
+
self._client.create_table_if_not_exists(table_name)
|
|
450
|
+
logger.info(f"✅ Azure Table ensured/created: {table_name}")
|
|
451
|
+
except Exception as e:
|
|
452
|
+
# TableAlreadyExists is normal and safe to ignore
|
|
453
|
+
msg = str(e)
|
|
454
|
+
if "TableAlreadyExists" not in msg and "already exists" not in msg.lower():
|
|
455
|
+
logger.warning(f"Could not create table {table_name}: {e}")
|
|
456
|
+
# Mark as ensured regardless — either it exists now or we
|
|
457
|
+
# logged the failure; we won't retry on every op.
|
|
458
|
+
self._ensured_tables.add(table_name)
|
|
459
|
+
|
|
460
|
+
if cached is None:
|
|
461
|
+
cached = self._client.get_table_client(table_name=table_name)
|
|
462
|
+
self._table_clients_cache[table_name] = cached
|
|
395
463
|
|
|
396
|
-
|
|
397
|
-
return self._client.get_table_client(table_name=table_name)
|
|
464
|
+
return cached
|
|
398
465
|
|
|
399
466
|
def _restore_overflow_properties(self, entity_dict: JsonDict) -> JsonDict:
|
|
400
467
|
"""Detect and restore any large properties stored in Blob Storage.
|
|
@@ -456,7 +523,12 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
456
523
|
@retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
|
|
457
524
|
def _put_raw(self, model: type, pk: str, rk: str, data: JsonDict) -> JsonDict:
|
|
458
525
|
try:
|
|
459
|
-
self._table_client
|
|
526
|
+
# ─── CHANGED: local variable, not self._table_client ────────────
|
|
527
|
+
# The old pattern `self._table_client = self._get_table_client(...)`
|
|
528
|
+
# was a race hazard: two concurrent writes on different models
|
|
529
|
+
# could swap each other's client mid-call. Local variable is
|
|
530
|
+
# safe and identical in cost since the client is now cached.
|
|
531
|
+
table_client = self._get_table_client(model)
|
|
460
532
|
safe_pk = self._sanitize_pk_rk(pk)
|
|
461
533
|
safe_rk = self._sanitize_pk_rk(rk)
|
|
462
534
|
# Pack entity (encoded for Azure Table)
|
|
@@ -506,8 +578,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
506
578
|
else:
|
|
507
579
|
reference_entity[k] = v
|
|
508
580
|
|
|
509
|
-
|
|
510
|
-
self._table_client.upsert_entity(reference_entity)
|
|
581
|
+
table_client.upsert_entity(reference_entity)
|
|
511
582
|
|
|
512
583
|
restored = self._unpack_entity(entity)
|
|
513
584
|
restored["id"] = safe_rk
|
|
@@ -525,9 +596,10 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
525
596
|
try:
|
|
526
597
|
safe_pk = self._sanitize_pk_rk(pk)
|
|
527
598
|
safe_rk = self._sanitize_pk_rk(rk)
|
|
528
|
-
|
|
599
|
+
# ─── CHANGED: local variable ────────────────────────────────────
|
|
600
|
+
table_client = self._get_table_client(model)
|
|
529
601
|
|
|
530
|
-
entity =
|
|
602
|
+
entity = table_client.get_entity(safe_pk, safe_rk)
|
|
531
603
|
entity_dict = dict(entity)
|
|
532
604
|
|
|
533
605
|
# Model isolation check
|
|
@@ -553,7 +625,8 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
553
625
|
self, model: type, filters: Dict[str, Any], limit: Optional[int]
|
|
554
626
|
) -> List[JsonDict]:
|
|
555
627
|
try:
|
|
556
|
-
|
|
628
|
+
# ─── CHANGED: local variable ────────────────────────────────────
|
|
629
|
+
table_client = self._get_table_client(model)
|
|
557
630
|
|
|
558
631
|
# Build query filter (your original logic kept unchanged)
|
|
559
632
|
eff_filters = dict(filters or {})
|
|
@@ -590,7 +663,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
590
663
|
|
|
591
664
|
query_filter = " and ".join(parts) if parts else None
|
|
592
665
|
|
|
593
|
-
entities =
|
|
666
|
+
entities = table_client.query_entities(query_filter=query_filter)
|
|
594
667
|
|
|
595
668
|
results: List[JsonDict] = []
|
|
596
669
|
count = 0
|
|
@@ -620,13 +693,14 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
620
693
|
@retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
|
|
621
694
|
def _delete_raw(self, model: type, pk: str, rk: str, etag: Optional[str]) -> JsonDict:
|
|
622
695
|
try:
|
|
623
|
-
|
|
696
|
+
# ─── CHANGED: local variable ────────────────────────────────────
|
|
697
|
+
table_client = self._get_table_client(model)
|
|
624
698
|
safe_pk = self._sanitize_pk_rk(pk)
|
|
625
699
|
safe_rk = self._sanitize_pk_rk(rk)
|
|
626
700
|
|
|
627
701
|
# read to check model + overflow
|
|
628
702
|
try:
|
|
629
|
-
entity =
|
|
703
|
+
entity = table_client.get_entity(safe_pk, safe_rk)
|
|
630
704
|
entity_dict = dict(entity)
|
|
631
705
|
|
|
632
706
|
if entity_dict.get(_MODEL_FIELD) != model.__qualname__:
|
|
@@ -639,8 +713,39 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
639
713
|
except Exception:
|
|
640
714
|
pass
|
|
641
715
|
|
|
642
|
-
|
|
716
|
+
table_client.delete_entity(safe_pk, safe_rk, etag=etag)
|
|
643
717
|
return {"deleted": True, "PartitionKey": safe_pk, "RowKey": safe_rk, "id": safe_rk}
|
|
644
718
|
|
|
645
719
|
except Exception as e:
|
|
646
720
|
raise NoSQLError(f"Azure Table delete failed: {str(e)}")
|
|
721
|
+
|
|
722
|
+
# -----------------------------
|
|
723
|
+
# Lifecycle
|
|
724
|
+
# -----------------------------
|
|
725
|
+
def close(self) -> None:
|
|
726
|
+
"""Close cached clients. Safe to call multiple times.
|
|
727
|
+
|
|
728
|
+
─── NEW ─────────────────────────────────────────────────────────────
|
|
729
|
+
Hook this into PolyDB shutdown so HTTP sessions don't leak on
|
|
730
|
+
application exit. Caches are cleared so a re-init starts clean.
|
|
731
|
+
"""
|
|
732
|
+
with self._cache_lock:
|
|
733
|
+
for tc in self._table_clients_cache.values():
|
|
734
|
+
try:
|
|
735
|
+
tc.close()
|
|
736
|
+
except Exception:
|
|
737
|
+
pass
|
|
738
|
+
self._table_clients_cache.clear()
|
|
739
|
+
self._ensured_tables.clear()
|
|
740
|
+
|
|
741
|
+
with self._client_lock:
|
|
742
|
+
if self._client is not None:
|
|
743
|
+
try:
|
|
744
|
+
self._client.close()
|
|
745
|
+
except Exception:
|
|
746
|
+
pass
|
|
747
|
+
if self._blob_service is not None:
|
|
748
|
+
try:
|
|
749
|
+
self._blob_service.close()
|
|
750
|
+
except Exception:
|
|
751
|
+
pass
|
|
@@ -8,6 +8,9 @@ import hashlib
|
|
|
8
8
|
from contextlib import contextmanager
|
|
9
9
|
import json
|
|
10
10
|
from datetime import datetime, date
|
|
11
|
+
|
|
12
|
+
import psycopg2.extensions
|
|
13
|
+
|
|
11
14
|
from ..errors import DatabaseError, ConnectionError
|
|
12
15
|
from ..retry import retry
|
|
13
16
|
from ..utils import validate_table_name, validate_column_name
|
|
@@ -32,37 +35,145 @@ class PostgreSQLAdapter:
|
|
|
32
35
|
self._lock = threading.Lock()
|
|
33
36
|
self._initialize_pool()
|
|
34
37
|
|
|
38
|
+
# ---------------------------------------------------------------------
|
|
39
|
+
# CONNECTION HYGIENE HELPERS
|
|
40
|
+
# ---------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def _is_idle(self, conn) -> bool:
|
|
43
|
+
"""True iff the connection is not inside a (possibly aborted) transaction."""
|
|
44
|
+
try:
|
|
45
|
+
return conn.info.transaction_status == psycopg2.extensions.TRANSACTION_STATUS_IDLE
|
|
46
|
+
except Exception:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
def _drain_transaction(self, conn) -> None:
|
|
50
|
+
"""Force the connection back to IDLE so it's safe to change session
|
|
51
|
+
settings (e.g. autocommit). Safe to call when already idle."""
|
|
52
|
+
if self._is_idle(conn):
|
|
53
|
+
return
|
|
54
|
+
try:
|
|
55
|
+
conn.rollback()
|
|
56
|
+
except Exception:
|
|
57
|
+
# Last resort: caller will surface a real error on next use.
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
def _ping_connection(self, conn) -> bool:
|
|
61
|
+
"""Test if connection is still alive.
|
|
62
|
+
|
|
63
|
+
psycopg2 auto-starts a transaction on the first statement after
|
|
64
|
+
commit/rollback, so we MUST rollback after the SELECT 1 — otherwise
|
|
65
|
+
the next caller inherits an active transaction and any attempt to
|
|
66
|
+
toggle autocommit raises ``set_session cannot be used inside a
|
|
67
|
+
transaction``.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
with conn.cursor() as cur:
|
|
71
|
+
cur.execute("SELECT 1")
|
|
72
|
+
# Clean up the auto-started transaction so the connection
|
|
73
|
+
# leaves this method in IDLE state.
|
|
74
|
+
try:
|
|
75
|
+
conn.rollback()
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
return True
|
|
79
|
+
except Exception:
|
|
80
|
+
# Best-effort cleanup on a failed ping. The connection is
|
|
81
|
+
# almost certainly broken; the caller will close it.
|
|
82
|
+
try:
|
|
83
|
+
conn.rollback()
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
return False
|
|
87
|
+
|
|
35
88
|
def _initialize_pool(self):
|
|
36
89
|
try:
|
|
37
90
|
import psycopg2.pool
|
|
91
|
+
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
|
|
92
|
+
|
|
93
|
+
# Enhance connection string with better Azure settings
|
|
94
|
+
dsn = self.connection_string
|
|
95
|
+
if "postgresql://" in dsn:
|
|
96
|
+
parsed = urlparse(dsn)
|
|
97
|
+
query = parse_qs(parsed.query)
|
|
98
|
+
query.setdefault("connect_timeout", ["30"])
|
|
99
|
+
query.setdefault("keepalives", ["1"])
|
|
100
|
+
query.setdefault("keepalives_idle", ["30"])
|
|
101
|
+
query.setdefault("keepalives_interval", ["10"])
|
|
102
|
+
query.setdefault("keepalives_count", ["5"])
|
|
103
|
+
|
|
104
|
+
new_query = urlencode(query, doseq=True)
|
|
105
|
+
parsed = parsed._replace(query=new_query)
|
|
106
|
+
dsn = urlunparse(parsed)
|
|
38
107
|
|
|
39
108
|
with self._lock:
|
|
40
109
|
if not self._pool:
|
|
41
110
|
self._pool = psycopg2.pool.ThreadedConnectionPool(
|
|
42
|
-
minconn=int(os.getenv("POSTGRES_MIN_CONNECTIONS", "
|
|
43
|
-
maxconn=int(os.getenv("POSTGRES_MAX_CONNECTIONS", "
|
|
44
|
-
dsn=
|
|
111
|
+
minconn=int(os.getenv("POSTGRES_MIN_CONNECTIONS", "5")),
|
|
112
|
+
maxconn=int(os.getenv("POSTGRES_MAX_CONNECTIONS", "30")),
|
|
113
|
+
dsn=dsn,
|
|
45
114
|
)
|
|
46
|
-
self.logger.info("PostgreSQL pool initialized")
|
|
115
|
+
self.logger.info("PostgreSQL pool initialized with Azure optimizations")
|
|
47
116
|
except Exception as e:
|
|
48
117
|
raise ConnectionError(f"Failed to initialize PostgreSQL pool: {str(e)}")
|
|
49
118
|
|
|
50
119
|
def _get_connection(self) -> Any:
|
|
51
120
|
if not self._pool:
|
|
52
121
|
self._initialize_pool()
|
|
53
|
-
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
conn = self._pool.getconn() # type: ignore
|
|
125
|
+
|
|
126
|
+
# Critical: Validate connection for Azure transient issues.
|
|
127
|
+
# _ping_connection rolls back its own SELECT 1 so the connection
|
|
128
|
+
# is returned to callers in IDLE state.
|
|
129
|
+
if not self._ping_connection(conn):
|
|
130
|
+
self.logger.warning("Stale connection detected from pool, closing and retrying")
|
|
131
|
+
self._pool.putconn(conn, close=True) # type: ignore
|
|
132
|
+
conn = self._pool.getconn() # type: ignore # Get fresh connection
|
|
133
|
+
# New connection might still have been used before — make sure it's idle.
|
|
134
|
+
self._drain_transaction(conn)
|
|
135
|
+
|
|
136
|
+
return conn
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
self.logger.error(f"Failed to acquire connection from pool: {e}")
|
|
140
|
+
raise ConnectionError(f"Could not obtain database connection: {e}") from e
|
|
54
141
|
|
|
55
142
|
def _return_connection(self, conn: Any):
|
|
143
|
+
"""Return a connection to the pool, defensively draining any
|
|
144
|
+
leftover transaction state. Belt-and-suspenders: every operation
|
|
145
|
+
in this adapter already commits/rolls-back before returning, but
|
|
146
|
+
if any path ever forgets, the pool still gets a clean connection.
|
|
147
|
+
"""
|
|
56
148
|
if self._pool and conn:
|
|
149
|
+
self._drain_transaction(conn)
|
|
57
150
|
self._pool.putconn(conn)
|
|
58
151
|
|
|
59
152
|
# ---------------------------------------------------------------------
|
|
60
153
|
# TRANSACTIONS
|
|
61
154
|
# ---------------------------------------------------------------------
|
|
155
|
+
def reset_pool(self):
|
|
156
|
+
"""Reset the entire pool (call during startup or after major failures)"""
|
|
157
|
+
with self._lock:
|
|
158
|
+
if self._pool:
|
|
159
|
+
try:
|
|
160
|
+
self._pool.closeall()
|
|
161
|
+
except:
|
|
162
|
+
pass
|
|
163
|
+
self._pool = None
|
|
164
|
+
self._initialize_pool()
|
|
62
165
|
|
|
63
166
|
def begin_transaction(self) -> Any:
|
|
64
|
-
"""Begin a transaction and return the connection handle.
|
|
167
|
+
"""Begin a transaction and return the connection handle.
|
|
168
|
+
|
|
169
|
+
Defensively drains any leftover transaction state on the pooled
|
|
170
|
+
connection before toggling autocommit. Without this, a connection
|
|
171
|
+
that's still in TRANSACTION_STATUS_INTRANS (from a previous user
|
|
172
|
+
or from the pool's connection check) causes psycopg2 to raise
|
|
173
|
+
``set_session cannot be used inside a transaction``.
|
|
174
|
+
"""
|
|
65
175
|
conn = self._get_connection()
|
|
176
|
+
self._drain_transaction(conn)
|
|
66
177
|
conn.autocommit = False
|
|
67
178
|
return conn
|
|
68
179
|
|
|
@@ -269,8 +380,16 @@ class PostgreSQLAdapter:
|
|
|
269
380
|
results = [self._deserialize_row(dict(zip(columns, row))) for row in cursor.fetchall()]
|
|
270
381
|
cursor.close()
|
|
271
382
|
|
|
383
|
+
if own_conn:
|
|
384
|
+
conn.commit()
|
|
385
|
+
|
|
272
386
|
return results
|
|
273
387
|
except Exception as e:
|
|
388
|
+
if own_conn:
|
|
389
|
+
try:
|
|
390
|
+
conn.rollback()
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
274
393
|
raise DatabaseError(f"Select failed: {str(e)}")
|
|
275
394
|
finally:
|
|
276
395
|
if own_conn and conn:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/requirements-generic.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/S3Adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/adapters/__init__.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/advanced_query.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/audit/AuditStorage.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/audit/__init__.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/audit/context.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/audit/manager.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/audit/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/base/QueueAdapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/base/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/databaseFactory.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/decorators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/monitoring.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/multitenancy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/src/polydb/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_blockchain.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_cloud_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_multi_engine.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.14 → altcodepro_polydb_python-2.3.16}/tests/test_postgresql.py
RENAMED
|
File without changes
|
|
File without changes
|