altcodepro-polydb-python 2.5.20__py3-none-any.whl → 2.5.21__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.5.20
3
+ Version: 2.5.21
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
@@ -1,4 +1,4 @@
1
- altcodepro_polydb_python-2.5.20.dist-info/licenses/LICENSE,sha256=9X8GLocsBwy-5aR5JGOt2SAMDDPs9Qv-YnqmHBHOXrw,1067
1
+ altcodepro_polydb_python-2.5.21.dist-info/licenses/LICENSE,sha256=9X8GLocsBwy-5aR5JGOt2SAMDDPs9Qv-YnqmHBHOXrw,1067
2
2
  polydb/PolyDB.py,sha256=Lp0QtG7D89Z6P6nt9fVoT8rVHAy8xlE2kFRSS70r-mk,23672
3
3
  polydb/__init__.py,sha256=fNrKwNBgwMJKrBhs_eaRu4XpQ3olREipIOiwBmv38b8,1876
4
4
  polydb/advanced_query.py,sha256=M9sKmdF2PsLZWSc-zKpXRcwaDumbYYTsibmL_IAjKP4,6931
@@ -25,9 +25,9 @@ polydb/validation.py,sha256=DK9-AFndTr0cLodpc3KX0wnABDpw2vPB4H6hp67BJKE,4570
25
25
  polydb/adapters/AWSSecretsManagerAdapter.py,sha256=W4B3xhQFcBAlNlwQ4uTgcDmRrOmZr6v-suC4qihTpJw,2747
26
26
  polydb/adapters/AzureBlobStorageAdapter.py,sha256=ZTovSC_CVduGnIoIQUUeNxDQlf-IbFDBRzjGDk8U9Io,7084
27
27
  polydb/adapters/AzureFileStorageAdapter.py,sha256=VZNprqlBXCuWUgtqClNT-NrQmRf-XFYEiRA2BLbf-Sc,7046
28
- polydb/adapters/AzureKeyVaultAdapter.py,sha256=7Nizs9qGh6cytbZNgV78f-odOO4Z6sUlwm_HQYqt-pc,2339
28
+ polydb/adapters/AzureKeyVaultAdapter.py,sha256=EtsrxI3X75nORnO0KbvKTVXJfliHd-gt3y9Mo8Npugs,3365
29
29
  polydb/adapters/AzureQueueAdapter.py,sha256=1e2HiFvkH1BI_JKS9fgays2J41kOfH6pUPbZDe1wmWA,13658
30
- polydb/adapters/AzureTableStorageAdapter.py,sha256=u2DsUIMOjYnBX_2G4abaLCjRaVOCORQQnwC4e0XrJ0Q,35567
30
+ polydb/adapters/AzureTableStorageAdapter.py,sha256=oW80ZeCsj8hkIlmlrv404rsB8m5TA9_nHL5OM57WQSg,38068
31
31
  polydb/adapters/BlockchainBlobAdapter.py,sha256=85H3PxiL9rGDtK-sTsW26jOuUQ_SGipLSPu3s-juZRA,3298
32
32
  polydb/adapters/BlockchainFileAdapter.py,sha256=lZbRyC18LghZcVh5QyIRP7tCftNF_Ep1FKG0KRkvMmk,8335
33
33
  polydb/adapters/BlockchainKVAdapter.py,sha256=pvpiPshDE5Z_IRcSBvBwgAXT--86lgEyIsEUeqcmoE8,5422
@@ -68,7 +68,7 @@ polydb/observability/logging.py,sha256=FWYky-Mg1IhLpwuaKuLIsTROBZc9bI_-AO-y53nvE
68
68
  polydb/services/__init__.py,sha256=Zs3PZbHutgPdPjJatygvtYvKPZMBt1qKSqESbFRPHJs,241
69
69
  polydb/services/compliance_service.py,sha256=UMjCbQ4z13-w6pT4RLQpMi_u1zbShEpiz8kawA2WsSU,5075
70
70
  polydb/services/security_service.py,sha256=gGo_j9Ca_rgvxS7s6C5JUktugQBj1PV1DXpMnOYAGbI,4992
71
- altcodepro_polydb_python-2.5.20.dist-info/METADATA,sha256=1UKkHliLCxQix8uakLf4qjXErKRQHzbJnlBZmqYZy3w,15599
72
- altcodepro_polydb_python-2.5.20.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
73
- altcodepro_polydb_python-2.5.20.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
74
- altcodepro_polydb_python-2.5.20.dist-info/RECORD,,
71
+ altcodepro_polydb_python-2.5.21.dist-info/METADATA,sha256=SbnlRNiCE6ZiK4aVCr5-Znz4R4xUJpDQyh10PQYpO34,15599
72
+ altcodepro_polydb_python-2.5.21.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
73
+ altcodepro_polydb_python-2.5.21.dist-info/top_level.txt,sha256=WgLFWJoYjUhwvyPxJFl6jYLrVFuBJDX3OABf4ocwk_E,7
74
+ altcodepro_polydb_python-2.5.21.dist-info/RECORD,,
@@ -19,10 +19,24 @@ class AzureKeyVaultAdapter(SecretsAdapter):
19
19
  self._initialize_client()
20
20
 
21
21
  def _initialize_client(self) -> None:
22
- from azure.identity import DefaultAzureCredential
23
22
  from azure.keyvault.secrets import SecretClient
24
23
 
25
- self._client = SecretClient(vault_url=self.vault_url, credential=DefaultAzureCredential())
24
+ self._client = SecretClient(vault_url=self.vault_url, credential=self._build_credential())
25
+
26
+ @staticmethod
27
+ def _build_credential():
28
+ """Inside Azure Container Apps / App Service / Functions a managed identity is available
29
+ (IDENTITY_ENDPOINT / MSI_ENDPOINT). DefaultAzureCredential tries EnvironmentCredential FIRST, and a deployment that
30
+ also exports AZURE_CLIENT_ID + AZURE_CLIENT_SECRET (for some other purpose) then fails with AADSTS7000232
31
+ "MSI identity should not use ClientSecretCredential". So when a managed identity exists it goes first
32
+ (AZURE_CLIENT_ID, if set, names a user-assigned identity); the normal chain stays as the fallback, so local
33
+ development (az login, environment variables) behaves as before."""
34
+ from azure.identity import ChainedTokenCredential, DefaultAzureCredential, ManagedIdentityCredential
35
+
36
+ if os.getenv("IDENTITY_ENDPOINT") or os.getenv("MSI_ENDPOINT"):
37
+ client_id = os.getenv("AZURE_CLIENT_ID") or None
38
+ return ChainedTokenCredential(ManagedIdentityCredential(client_id=client_id), DefaultAzureCredential())
39
+ return DefaultAzureCredential()
26
40
 
27
41
  # Key Vault secret names may only contain alphanumerics and dashes --
28
42
  # our secret keys (e.g. "tenant-42/stripe/api_key") use slashes, so
@@ -47,6 +47,59 @@ _RETRY_TOTAL = int(os.getenv("AZURE_TABLE_RETRY_TOTAL", "3"))
47
47
  logging.getLogger("azure").setLevel(logging.ERROR)
48
48
 
49
49
 
50
+
51
+ def _is_table_missing(exc: BaseException) -> bool:
52
+ text = f"{type(exc).__name__} {exc}"
53
+ return "TableNotFound" in text or "The table specified does not exist" in text
54
+
55
+
56
+ class _SelfHealingTableClient:
57
+ """Wraps an Azure TableClient so a table that disappears while the process is running (deleted by hand, a data
58
+ reset, a restore) is created again and the operation retried once, instead of failing with TableNotFound until
59
+ the process restarts. Lazy iterators (query_entities / list_entities) are healed only if the failure happens
60
+ before the first row was produced."""
61
+
62
+ _LAZY = ("query_entities", "list_entities")
63
+
64
+ def __init__(self, adapter: "AzureTableStorageAdapter", table_name: str, client: Any):
65
+ self._adapter = adapter
66
+ self._table_name = table_name
67
+ self._client = client
68
+
69
+ def _heal(self) -> None:
70
+ self._adapter._ensured_tables.discard(self._table_name)
71
+ self._adapter._client.create_table_if_not_exists(self._table_name)
72
+ self._adapter._ensured_tables.add(self._table_name)
73
+ logger.warning(f"Azure Table {self._table_name} was missing; created it again")
74
+
75
+ def __getattr__(self, name: str):
76
+ target = getattr(self._client, name)
77
+ if not callable(target):
78
+ return target
79
+ if name in self._LAZY:
80
+ def lazy(*args, **kwargs):
81
+ produced = False
82
+ try:
83
+ for item in target(*args, **kwargs):
84
+ produced = True
85
+ yield item
86
+ except Exception as exc: # noqa: BLE001
87
+ if produced or not _is_table_missing(exc):
88
+ raise
89
+ self._heal()
90
+ yield from getattr(self._client, name)(*args, **kwargs)
91
+ return lazy
92
+
93
+ def call(*args, **kwargs):
94
+ try:
95
+ return target(*args, **kwargs)
96
+ except Exception as exc: # noqa: BLE001
97
+ if not _is_table_missing(exc):
98
+ raise
99
+ self._heal()
100
+ return getattr(self._client, name)(*args, **kwargs)
101
+ return call
102
+
50
103
  class AzureTableStorageAdapter(NoSQLKVAdapter):
51
104
  """
52
105
  Azure Table Storage adapter with:
@@ -441,17 +494,21 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
441
494
  try:
442
495
  self._client.create_table_if_not_exists(table_name)
443
496
  logger.info(f"✅ Azure Table ensured/created: {table_name}")
497
+ self._ensured_tables.add(table_name)
444
498
  except Exception as e:
445
499
  msg = str(e)
446
- if "TableAlreadyExists" not in msg and "already exists" not in msg.lower():
447
- logger.warning(f"Could not create table {table_name}: {e}")
448
- self._ensured_tables.add(table_name)
500
+ if "TableAlreadyExists" in msg or "already exists" in msg.lower():
501
+ self._ensured_tables.add(table_name)
502
+ else:
503
+ # Do NOT remember a failed creation as "ensured": the next call tries again instead of
504
+ # failing with TableNotFound for the rest of the process's life.
505
+ logger.error(f"Could not create table {table_name}: {e}")
449
506
 
450
507
  if cached is None:
451
508
  cached = self._client.get_table_client(table_name=table_name)
452
509
  self._table_clients_cache[table_name] = cached
453
510
 
454
- return cached
511
+ return _SelfHealingTableClient(self, table_name, cached)
455
512
 
456
513
  def _restore_overflow_properties(self, entity_dict: JsonDict) -> JsonDict:
457
514
  restored = {}