altcodepro-polydb-python 2.5.20__tar.gz → 2.5.21__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 (129) hide show
  1. {altcodepro_polydb_python-2.5.20/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.5.21}/PKG-INFO +1 -1
  2. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/pyproject.toml +1 -1
  3. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
  4. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +2 -0
  5. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/AzureKeyVaultAdapter.py +16 -2
  6. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/AzureTableStorageAdapter.py +61 -4
  7. altcodepro_polydb_python-2.5.21/tests/test_azure_keyvault_credential.py +34 -0
  8. altcodepro_polydb_python-2.5.21/tests/test_azure_table_self_heal.py +64 -0
  9. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/LICENSE +0 -0
  10. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/MANIFEST.in +0 -0
  11. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/README.md +0 -0
  12. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/requirements-aws.txt +0 -0
  13. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/requirements-azure.txt +0 -0
  14. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/requirements-dev.txt +0 -0
  15. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/requirements-gcp.txt +0 -0
  16. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/requirements-generic.txt +0 -0
  17. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/requirements.txt +0 -0
  18. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/setup.cfg +0 -0
  19. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/setup.py +0 -0
  20. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  21. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
  22. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  23. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/PolyDB.py +0 -0
  24. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/__init__.py +0 -0
  25. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/AWSSecretsManagerAdapter.py +0 -0
  26. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  27. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  28. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
  29. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
  30. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
  31. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
  32. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
  33. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
  34. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/EFSAdapter.py +0 -0
  35. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/FirestoreAdapter.py +0 -0
  36. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/GCPSecretManagerAdapter.py +0 -0
  39. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  40. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/KafkaQueueAdapter.py +0 -0
  41. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  42. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/PostgreSQLAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/RabbitMQAdapter.py +0 -0
  44. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/S3Adapter.py +0 -0
  45. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  46. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/SQSAdapter.py +0 -0
  47. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/VaultAdapter.py +0 -0
  48. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  49. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/VercelFileAdapter.py +0 -0
  50. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  51. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  52. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/adapters/__init__.py +0 -0
  53. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/advanced_query.py +0 -0
  54. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/aio.py +0 -0
  55. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/audit/AuditStorage.py +0 -0
  56. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/audit/__init__.py +0 -0
  57. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/audit/context.py +0 -0
  58. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/audit/manager.py +0 -0
  59. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/audit/models.py +0 -0
  60. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/base/NoSQLKVAdapter.py +0 -0
  61. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  62. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/base/QueueAdapter.py +0 -0
  63. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/base/SecretsAdapter.py +0 -0
  64. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/base/SharedFilesAdapter.py +0 -0
  65. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/base/__init__.py +0 -0
  66. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/batch.py +0 -0
  67. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/cache.py +0 -0
  68. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/cloudDatabaseFactory.py +0 -0
  69. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/databaseFactory.py +0 -0
  70. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/errors.py +0 -0
  71. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/json_safe.py +0 -0
  72. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/models.py +0 -0
  73. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/monitoring.py +0 -0
  74. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/multitenancy.py +0 -0
  75. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/observability/__init__.py +0 -0
  76. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/observability/logging.py +0 -0
  77. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/overflow_gc.py +0 -0
  78. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/py.typed +0 -0
  79. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/query.py +0 -0
  80. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/registry.py +0 -0
  81. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/retry.py +0 -0
  82. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/schema.py +0 -0
  83. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/security.py +0 -0
  84. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/services/__init__.py +0 -0
  85. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/services/compliance_service.py +0 -0
  86. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/services/security_service.py +0 -0
  87. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/types.py +0 -0
  88. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/utils.py +0 -0
  89. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/src/polydb/validation.py +0 -0
  90. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_advanced_query_injection.py +0 -0
  91. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_async_api.py +0 -0
  92. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_atomic_decrement.py +0 -0
  93. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_audit_chain_cutover.py +0 -0
  94. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_audit_hmac.py +0 -0
  95. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_aws.py +0 -0
  96. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_azure.py +0 -0
  97. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_azure_queue_receipt_handle.py +0 -0
  98. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_azure_table_empty_filter.py +0 -0
  99. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_azure_table_id_property_persisted.py +0 -0
  100. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_azure_table_id_return_value_sanitization_mismatch.py +0 -0
  101. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_azure_table_large_record_patch_roundtrip.py +0 -0
  102. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_azure_table_unpack_entity.py +0 -0
  103. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_blockchain.py +0 -0
  104. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_blockchain_overflow_guard.py +0 -0
  105. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_cloud_factory.py +0 -0
  106. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_factory_wiring.py +0 -0
  107. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_field_encryption.py +0 -0
  108. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_gcp.py +0 -0
  109. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_kafka.py +0 -0
  110. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_like_pattern_escaping.py +0 -0
  111. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_mongodb.py +0 -0
  112. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_monitoring_health.py +0 -0
  113. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_multi_engine.py +0 -0
  114. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_multitenancy_ddl_injection.py +0 -0
  115. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_nosql_kv_overflow_write_path.py +0 -0
  116. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_nosql_operator_injection.py +0 -0
  117. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_observability.py +0 -0
  118. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_overflow_gc.py +0 -0
  119. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_postgresql.py +0 -0
  120. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_postgresql_deserialize_row.py +0 -0
  121. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_queue_extend_delay_cancel.py +0 -0
  122. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_rabbitmq.py +0 -0
  123. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_read_projection.py +0 -0
  124. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_redis_kv.py +0 -0
  125. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_schema_ddl_injection.py +0 -0
  126. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_secrets_aws.py +0 -0
  127. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_secrets_vault.py +0 -0
  128. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/tests/test_session_vars.py +0 -0
  129. {altcodepro_polydb_python-2.5.20 → altcodepro_polydb_python-2.5.21}/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.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "altcodepro-polydb-python"
7
- version = "2.5.20"
7
+ version = "2.5.21"
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.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
@@ -90,11 +90,13 @@ tests/test_audit_chain_cutover.py
90
90
  tests/test_audit_hmac.py
91
91
  tests/test_aws.py
92
92
  tests/test_azure.py
93
+ tests/test_azure_keyvault_credential.py
93
94
  tests/test_azure_queue_receipt_handle.py
94
95
  tests/test_azure_table_empty_filter.py
95
96
  tests/test_azure_table_id_property_persisted.py
96
97
  tests/test_azure_table_id_return_value_sanitization_mismatch.py
97
98
  tests/test_azure_table_large_record_patch_roundtrip.py
99
+ tests/test_azure_table_self_heal.py
98
100
  tests/test_azure_table_unpack_entity.py
99
101
  tests/test_blockchain.py
100
102
  tests/test_blockchain_overflow_guard.py
@@ -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 = {}
@@ -0,0 +1,34 @@
1
+ """Inside a managed-identity host the managed identity is tried first, so a stray AZURE_CLIENT_SECRET cannot break Key Vault."""
2
+ import sys
3
+ import types
4
+ from unittest.mock import MagicMock
5
+
6
+ import pytest
7
+
8
+
9
+ @pytest.fixture
10
+ def fake_identity(monkeypatch):
11
+ mod = types.ModuleType("azure.identity")
12
+ mod.ChainedTokenCredential = MagicMock(name="Chained")
13
+ mod.DefaultAzureCredential = MagicMock(name="Default")
14
+ mod.ManagedIdentityCredential = MagicMock(name="MI")
15
+ monkeypatch.setitem(sys.modules, "azure.identity", mod)
16
+ return mod
17
+
18
+
19
+ def test_a_managed_identity_host_puts_the_managed_identity_first(fake_identity, monkeypatch):
20
+ from polydb.adapters.AzureKeyVaultAdapter import AzureKeyVaultAdapter
21
+ monkeypatch.setenv("IDENTITY_ENDPOINT", "http://169.254.0.1/msi")
22
+ monkeypatch.setenv("AZURE_CLIENT_ID", "mi-client-id")
23
+ AzureKeyVaultAdapter._build_credential()
24
+ fake_identity.ManagedIdentityCredential.assert_called_once_with(client_id="mi-client-id")
25
+ chain_args = fake_identity.ChainedTokenCredential.call_args.args
26
+ assert chain_args[0] is fake_identity.ManagedIdentityCredential.return_value and chain_args[1] is fake_identity.DefaultAzureCredential.return_value
27
+
28
+
29
+ def test_elsewhere_the_default_chain_is_unchanged(fake_identity, monkeypatch):
30
+ from polydb.adapters.AzureKeyVaultAdapter import AzureKeyVaultAdapter
31
+ monkeypatch.delenv("IDENTITY_ENDPOINT", raising=False)
32
+ monkeypatch.delenv("MSI_ENDPOINT", raising=False)
33
+ assert AzureKeyVaultAdapter._build_credential() is fake_identity.DefaultAzureCredential.return_value
34
+ fake_identity.ManagedIdentityCredential.assert_not_called()
@@ -0,0 +1,64 @@
1
+ """A missing Azure Table is created again and the call retried; a failed creation is not remembered as success."""
2
+ from unittest.mock import MagicMock
3
+
4
+ from polydb.adapters.AzureTableStorageAdapter import _SelfHealingTableClient, _is_table_missing
5
+
6
+
7
+ class _Err(Exception):
8
+ pass
9
+
10
+
11
+ def _adapter():
12
+ a = MagicMock()
13
+ a._ensured_tables = {"t"}
14
+ return a
15
+
16
+
17
+ def test_detects_the_missing_table_error():
18
+ assert _is_table_missing(_Err("ErrorCode:TableNotFound"))
19
+ assert _is_table_missing(_Err("The table specified does not exist."))
20
+ assert not _is_table_missing(_Err("timeout"))
21
+
22
+
23
+ def test_a_call_on_a_missing_table_creates_it_and_retries_once():
24
+ client = MagicMock()
25
+ client.get_entity.side_effect = [_Err("TableNotFound"), {"ok": 1}]
26
+ a = _adapter()
27
+ got = _SelfHealingTableClient(a, "t", client).get_entity(partition_key="p", row_key="r")
28
+ assert got == {"ok": 1} and a._client.create_table_if_not_exists.called and "t" in a._ensured_tables
29
+
30
+
31
+ def test_other_errors_are_not_swallowed():
32
+ client = MagicMock()
33
+ client.get_entity.side_effect = _Err("boom")
34
+ try:
35
+ _SelfHealingTableClient(_adapter(), "t", client).get_entity()
36
+ raise AssertionError("should raise")
37
+ except _Err:
38
+ pass
39
+
40
+
41
+ def test_a_lazy_query_on_a_missing_table_heals_before_the_first_row():
42
+ client = MagicMock()
43
+ def gone():
44
+ raise _Err("TableNotFound")
45
+ yield
46
+ client.query_entities.side_effect = [gone(), iter([{"a": 1}, {"a": 2}])]
47
+ a = _adapter()
48
+ rows = list(_SelfHealingTableClient(a, "t", client).query_entities("x"))
49
+ assert rows == [{"a": 1}, {"a": 2}] and a._client.create_table_if_not_exists.called
50
+
51
+
52
+ def test_a_failure_after_rows_were_produced_is_raised_not_replayed():
53
+ client = MagicMock()
54
+ def partial():
55
+ yield {"a": 1}
56
+ raise _Err("TableNotFound")
57
+ client.query_entities.return_value = partial()
58
+ it = _SelfHealingTableClient(_adapter(), "t", client).query_entities("x")
59
+ assert next(it) == {"a": 1}
60
+ try:
61
+ next(it)
62
+ raise AssertionError("should raise")
63
+ except _Err:
64
+ pass