altcodepro-polydb-python 2.3.11__tar.gz → 2.3.14__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.11/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.14}/PKG-INFO +3 -1
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/pyproject.toml +3 -1
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14/src/altcodepro_polydb_python.egg-info}/PKG-INFO +3 -1
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/altcodepro_polydb_python.egg-info/requires.txt +2 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/PolyDB.py +35 -35
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/AzureTableStorageAdapter.py +8 -2
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/PostgreSQLAdapter.py +25 -15
- altcodepro_polydb_python-2.3.14/src/polydb/audit/AuditStorage.py +164 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/models.py +41 -6
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/ObjectStorageAdapter.py +1 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/query.py +9 -6
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/retry.py +23 -12
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/security.py +7 -4
- altcodepro_polydb_python-2.3.11/src/polydb/audit/AuditStorage.py +0 -136
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/LICENSE +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/MANIFEST.in +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/README.md +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements-aws.txt +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements-azure.txt +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements-dev.txt +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements-gcp.txt +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements-generic.txt +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements.txt +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/setup.cfg +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/setup.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/EFSAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/FirestoreAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/MongoDBAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/S3Adapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/SQSAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/VercelFileAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/VercelKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/advanced_query.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/context.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/manager.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/NoSQLKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/QueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/SharedFilesAdapter.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/__init__.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/batch.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/cache.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/cloudDatabaseFactory.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/databaseFactory.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/decorators.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/errors.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/json_safe.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/models.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/monitoring.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/multitenancy.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/py.typed +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/registry.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/schema.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/types.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/utils.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/validation.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_aws.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_azure.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_blockchain.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_cloud_factory.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_gcp.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_mongodb.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_multi_engine.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_postgresql.py +0 -0
- {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/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.14
|
|
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
|
|
@@ -38,6 +38,8 @@ Requires-Dist: web3>=7.16.0
|
|
|
38
38
|
Requires-Dist: google-cloud-firestore>=2.27.0
|
|
39
39
|
Requires-Dist: google-cloud-pubsub>=2.38.0
|
|
40
40
|
Requires-Dist: pymongo>=4.17.0
|
|
41
|
+
Requires-Dist: build>=1.5.0
|
|
42
|
+
Requires-Dist: twine>=6.2.0
|
|
41
43
|
Provides-Extra: aws
|
|
42
44
|
Requires-Dist: boto3>=1.42.47; extra == "aws"
|
|
43
45
|
Requires-Dist: botocore>=1.42.47; extra == "aws"
|
|
@@ -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.14"
|
|
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"
|
|
@@ -57,6 +57,8 @@ dependencies = [
|
|
|
57
57
|
"google-cloud-firestore>=2.27.0",
|
|
58
58
|
"google-cloud-pubsub>=2.38.0",
|
|
59
59
|
"pymongo>=4.17.0",
|
|
60
|
+
"build>=1.5.0",
|
|
61
|
+
"twine>=6.2.0",
|
|
60
62
|
]
|
|
61
63
|
|
|
62
64
|
# Generic/Open-source stack (cheapest option)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: altcodepro-polydb-python
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.14
|
|
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
|
|
@@ -38,6 +38,8 @@ Requires-Dist: web3>=7.16.0
|
|
|
38
38
|
Requires-Dist: google-cloud-firestore>=2.27.0
|
|
39
39
|
Requires-Dist: google-cloud-pubsub>=2.38.0
|
|
40
40
|
Requires-Dist: pymongo>=4.17.0
|
|
41
|
+
Requires-Dist: build>=1.5.0
|
|
42
|
+
Requires-Dist: twine>=6.2.0
|
|
41
43
|
Provides-Extra: aws
|
|
42
44
|
Requires-Dist: boto3>=1.42.47; extra == "aws"
|
|
43
45
|
Requires-Dist: botocore>=1.42.47; extra == "aws"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
from typing import Any, Dict, List, Optional, Tuple, Type, Union
|
|
4
5
|
|
|
5
6
|
from .advanced_query import AdvancedQueryBuilder, QueryHelper
|
|
@@ -23,8 +24,6 @@ from .types import JsonDict, Lookup
|
|
|
23
24
|
from .utils import setup_logger
|
|
24
25
|
from .validation import ModelValidator, SchemaValidator
|
|
25
26
|
|
|
26
|
-
ModelRef = Union[Type, str]
|
|
27
|
-
|
|
28
27
|
|
|
29
28
|
class PolyDB:
|
|
30
29
|
"""
|
|
@@ -51,6 +50,7 @@ class PolyDB:
|
|
|
51
50
|
storage_configs: Optional[List[StorageConfig]] = None,
|
|
52
51
|
tenant_registry: Optional[TenantRegistry] = None,
|
|
53
52
|
partition_config: Optional[PartitionConfig] = None,
|
|
53
|
+
redis_cache_url: Optional[str] = None,
|
|
54
54
|
enable_retries: bool = True,
|
|
55
55
|
enable_audit: bool = True,
|
|
56
56
|
enable_audit_reads: bool = False,
|
|
@@ -59,6 +59,7 @@ class PolyDB:
|
|
|
59
59
|
enable_monitoring: bool = False,
|
|
60
60
|
enable_encryption: bool = False,
|
|
61
61
|
enable_rls: bool = False,
|
|
62
|
+
soft_delete: bool = True,
|
|
62
63
|
) -> None:
|
|
63
64
|
self.logger = setup_logger(self.__class__.__name__)
|
|
64
65
|
|
|
@@ -71,7 +72,6 @@ class PolyDB:
|
|
|
71
72
|
provider=provider,
|
|
72
73
|
cloud_factory=self.cloud,
|
|
73
74
|
engines=engines,
|
|
74
|
-
tenant_registry=tenant_registry,
|
|
75
75
|
enable_retries=enable_retries,
|
|
76
76
|
enable_audit=enable_audit,
|
|
77
77
|
enable_audit_reads=enable_audit_reads,
|
|
@@ -79,7 +79,10 @@ class PolyDB:
|
|
|
79
79
|
use_redis_cache=use_redis_cache,
|
|
80
80
|
enable_monitoring=enable_monitoring,
|
|
81
81
|
enable_encryption=enable_encryption,
|
|
82
|
-
|
|
82
|
+
soft_delete=soft_delete,
|
|
83
|
+
redis_cache_url=redis_cache_url
|
|
84
|
+
or os.getenv("REDIS_CACHE_URL")
|
|
85
|
+
or os.getenv("REDIS_URL"),
|
|
83
86
|
)
|
|
84
87
|
|
|
85
88
|
self.partition_config = partition_config
|
|
@@ -123,7 +126,7 @@ class PolyDB:
|
|
|
123
126
|
return self.cloud.get_object_storage(name)
|
|
124
127
|
|
|
125
128
|
def get_shared_files(self):
|
|
126
|
-
return self.cloud.
|
|
129
|
+
return self.cloud.get_files()
|
|
127
130
|
|
|
128
131
|
def get_queue(self):
|
|
129
132
|
return self.cloud.get_queue()
|
|
@@ -137,7 +140,7 @@ class PolyDB:
|
|
|
137
140
|
|
|
138
141
|
def create(
|
|
139
142
|
self,
|
|
140
|
-
model:
|
|
143
|
+
model: str,
|
|
141
144
|
data: JsonDict,
|
|
142
145
|
*,
|
|
143
146
|
engine_override: Optional[EngineOverride] = None,
|
|
@@ -146,7 +149,7 @@ class PolyDB:
|
|
|
146
149
|
|
|
147
150
|
def read(
|
|
148
151
|
self,
|
|
149
|
-
model:
|
|
152
|
+
model: str,
|
|
150
153
|
query: Optional[Lookup] = None,
|
|
151
154
|
*,
|
|
152
155
|
limit: Optional[int] = None,
|
|
@@ -169,7 +172,7 @@ class PolyDB:
|
|
|
169
172
|
|
|
170
173
|
def read_one(
|
|
171
174
|
self,
|
|
172
|
-
model:
|
|
175
|
+
model: str,
|
|
173
176
|
query: Lookup,
|
|
174
177
|
*,
|
|
175
178
|
no_cache: bool = False,
|
|
@@ -186,7 +189,7 @@ class PolyDB:
|
|
|
186
189
|
|
|
187
190
|
def get(
|
|
188
191
|
self,
|
|
189
|
-
model:
|
|
192
|
+
model: str,
|
|
190
193
|
entity_id: Any,
|
|
191
194
|
*,
|
|
192
195
|
include_deleted: bool = False,
|
|
@@ -203,7 +206,7 @@ class PolyDB:
|
|
|
203
206
|
|
|
204
207
|
def read_page(
|
|
205
208
|
self,
|
|
206
|
-
model:
|
|
209
|
+
model: str,
|
|
207
210
|
query: Lookup,
|
|
208
211
|
*,
|
|
209
212
|
page_size: int = 100,
|
|
@@ -222,7 +225,7 @@ class PolyDB:
|
|
|
222
225
|
|
|
223
226
|
def update(
|
|
224
227
|
self,
|
|
225
|
-
model:
|
|
228
|
+
model: str,
|
|
226
229
|
entity_id: Any,
|
|
227
230
|
data: JsonDict,
|
|
228
231
|
*,
|
|
@@ -241,7 +244,7 @@ class PolyDB:
|
|
|
241
244
|
|
|
242
245
|
def upsert(
|
|
243
246
|
self,
|
|
244
|
-
model:
|
|
247
|
+
model: str,
|
|
245
248
|
data: JsonDict,
|
|
246
249
|
*,
|
|
247
250
|
replace: bool = False,
|
|
@@ -256,7 +259,7 @@ class PolyDB:
|
|
|
256
259
|
|
|
257
260
|
def delete(
|
|
258
261
|
self,
|
|
259
|
-
model:
|
|
262
|
+
model: str,
|
|
260
263
|
entity_id: Any,
|
|
261
264
|
*,
|
|
262
265
|
etag: Optional[str] = None,
|
|
@@ -278,12 +281,12 @@ class PolyDB:
|
|
|
278
281
|
def query(self) -> QueryBuilder:
|
|
279
282
|
return QueryBuilder()
|
|
280
283
|
|
|
281
|
-
def advanced_query(self) -> AdvancedQueryBuilder:
|
|
282
|
-
return AdvancedQueryBuilder()
|
|
284
|
+
def advanced_query(self, table: str) -> AdvancedQueryBuilder:
|
|
285
|
+
return AdvancedQueryBuilder(table)
|
|
283
286
|
|
|
284
287
|
def query_linq(
|
|
285
288
|
self,
|
|
286
|
-
model:
|
|
289
|
+
model: str,
|
|
287
290
|
builder: QueryBuilder,
|
|
288
291
|
*,
|
|
289
292
|
engine_override: Optional[EngineOverride] = None,
|
|
@@ -426,7 +429,7 @@ class PolyDB:
|
|
|
426
429
|
queue_name: str = "default",
|
|
427
430
|
delay: Optional[int] = None,
|
|
428
431
|
) -> str:
|
|
429
|
-
queue = self.get_queue()
|
|
432
|
+
queue: Any = self.get_queue()
|
|
430
433
|
if hasattr(queue, "publish"):
|
|
431
434
|
return queue.publish(queue_name=queue_name, message=message, delay=delay)
|
|
432
435
|
return self.send_queue(message, queue_name=queue_name)
|
|
@@ -438,7 +441,7 @@ class PolyDB:
|
|
|
438
441
|
max_messages: int = 10,
|
|
439
442
|
wait_seconds: int = 5,
|
|
440
443
|
) -> List[Dict[str, Any]]:
|
|
441
|
-
queue = self.get_queue()
|
|
444
|
+
queue: Any = self.get_queue()
|
|
442
445
|
if hasattr(queue, "consume"):
|
|
443
446
|
return queue.consume(
|
|
444
447
|
queue_name=queue_name,
|
|
@@ -492,7 +495,7 @@ class PolyDB:
|
|
|
492
495
|
|
|
493
496
|
def set_cache(
|
|
494
497
|
self,
|
|
495
|
-
model:
|
|
498
|
+
model: str,
|
|
496
499
|
key: Any,
|
|
497
500
|
value: Any,
|
|
498
501
|
*,
|
|
@@ -504,7 +507,7 @@ class PolyDB:
|
|
|
504
507
|
|
|
505
508
|
def get_cache(
|
|
506
509
|
self,
|
|
507
|
-
model:
|
|
510
|
+
model: str,
|
|
508
511
|
key: Any,
|
|
509
512
|
) -> Optional[Any]:
|
|
510
513
|
if not self.cache:
|
|
@@ -513,7 +516,7 @@ class PolyDB:
|
|
|
513
516
|
|
|
514
517
|
def invalidate_cache(
|
|
515
518
|
self,
|
|
516
|
-
model:
|
|
519
|
+
model: str,
|
|
517
520
|
key: Optional[Any] = None,
|
|
518
521
|
) -> None:
|
|
519
522
|
if not self.cache:
|
|
@@ -530,7 +533,7 @@ class PolyDB:
|
|
|
530
533
|
|
|
531
534
|
def warm_model_cache(
|
|
532
535
|
self,
|
|
533
|
-
model:
|
|
536
|
+
model: str,
|
|
534
537
|
queries: List[Any],
|
|
535
538
|
*,
|
|
536
539
|
ttl: int = 300,
|
|
@@ -541,7 +544,7 @@ class PolyDB:
|
|
|
541
544
|
|
|
542
545
|
def warm_popular_queries(
|
|
543
546
|
self,
|
|
544
|
-
model:
|
|
547
|
+
model: str,
|
|
545
548
|
*,
|
|
546
549
|
limit: int = 20,
|
|
547
550
|
ttl: int = 300,
|
|
@@ -554,27 +557,27 @@ class PolyDB:
|
|
|
554
557
|
# BATCH
|
|
555
558
|
# ============================================================
|
|
556
559
|
|
|
557
|
-
def bulk_insert(self, model:
|
|
560
|
+
def bulk_insert(self, model: str, records: List[JsonDict]) -> BatchResult:
|
|
558
561
|
return self.batch.bulk_insert(model, records)
|
|
559
562
|
|
|
560
563
|
def bulk_update(
|
|
561
564
|
self,
|
|
562
|
-
model:
|
|
563
|
-
updates: List[
|
|
565
|
+
model: str,
|
|
566
|
+
updates: List[Dict[str, Any]], # {entity_id, data}
|
|
564
567
|
) -> BatchResult:
|
|
565
568
|
return self.batch.bulk_update(model, updates)
|
|
566
569
|
|
|
567
|
-
def bulk_delete(self, model:
|
|
570
|
+
def bulk_delete(self, model: str, entity_ids: List[Any]) -> BatchResult:
|
|
568
571
|
return self.batch.bulk_delete(model, entity_ids)
|
|
569
572
|
|
|
570
573
|
# ============================================================
|
|
571
574
|
# VALIDATION
|
|
572
575
|
# ============================================================
|
|
573
576
|
|
|
574
|
-
def validate_model(self, model:
|
|
577
|
+
def validate_model(self, model: Type):
|
|
575
578
|
return ModelValidator.validate_model(model)
|
|
576
579
|
|
|
577
|
-
def validate(self, model:
|
|
580
|
+
def validate(self, model: Type) -> None:
|
|
578
581
|
ModelValidator.validate_and_raise(model)
|
|
579
582
|
|
|
580
583
|
def validate_data(self, model: Any, data: JsonDict):
|
|
@@ -672,11 +675,7 @@ class PolyDB:
|
|
|
672
675
|
self.rls.add_policy(model, name, policy_func, apply_to)
|
|
673
676
|
|
|
674
677
|
def set_default_rls_filters(
|
|
675
|
-
self,
|
|
676
|
-
model: str,
|
|
677
|
-
*,
|
|
678
|
-
read_filters: Optional[Dict[str, Any]] = None,
|
|
679
|
-
write_filters: Optional[Dict[str, Any]] = None,
|
|
678
|
+
self, model: str, *, read_filters: Dict[str, Any], write_filters: Dict[str, Any]
|
|
680
679
|
) -> None:
|
|
681
680
|
if not self.rls:
|
|
682
681
|
raise RuntimeError("RLS is not enabled on this PolyDB instance.")
|
|
@@ -687,7 +686,8 @@ class PolyDB:
|
|
|
687
686
|
# ============================================================
|
|
688
687
|
|
|
689
688
|
def with_tenant(self, tenant_id: str) -> "PolyDB":
|
|
690
|
-
|
|
689
|
+
if self.tenant_registry:
|
|
690
|
+
TenantContext.set_tenant(tenant_id, self.tenant_registry)
|
|
691
691
|
return self
|
|
692
692
|
|
|
693
693
|
def get_tenant(self) -> Optional[TenantConfig]:
|
|
@@ -116,11 +116,17 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
|
|
|
116
116
|
if v is None:
|
|
117
117
|
return None
|
|
118
118
|
|
|
119
|
+
# Treat empty containers as absent — let the row omit the property.
|
|
120
|
+
if isinstance(v, (list, tuple, dict)) and len(v) == 0:
|
|
121
|
+
return None
|
|
122
|
+
|
|
119
123
|
if isinstance(v, bytes):
|
|
120
124
|
return _BYTES_PREFIX + base64.b64encode(v).decode("ascii")
|
|
121
125
|
|
|
122
|
-
if isinstance(v, (dict, list)):
|
|
123
|
-
return _JSON_PREFIX + json.dumps(
|
|
126
|
+
if isinstance(v, (dict, list, tuple)):
|
|
127
|
+
return _JSON_PREFIX + json.dumps(
|
|
128
|
+
list(v) if isinstance(v, tuple) else v, default=json_safe
|
|
129
|
+
)
|
|
124
130
|
|
|
125
131
|
if isinstance(v, UUID):
|
|
126
132
|
return str(v)
|
|
@@ -8,8 +8,6 @@ import hashlib
|
|
|
8
8
|
from contextlib import contextmanager
|
|
9
9
|
import json
|
|
10
10
|
from datetime import datetime, date
|
|
11
|
-
|
|
12
|
-
|
|
13
11
|
from ..errors import DatabaseError, ConnectionError
|
|
14
12
|
from ..retry import retry
|
|
15
13
|
from ..utils import validate_table_name, validate_column_name
|
|
@@ -102,34 +100,46 @@ class PostgreSQLAdapter:
|
|
|
102
100
|
|
|
103
101
|
def _serialize_value(self, v: Any) -> Any:
|
|
104
102
|
"""
|
|
105
|
-
Make
|
|
103
|
+
Make outgoing values safe for psycopg2 across mixed column types.
|
|
106
104
|
|
|
107
105
|
Rules:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
None / empty list / empty dict -> None (becomes NULL on any column)
|
|
107
|
+
list of primitives (str/int/...) -> native list (psycopg2 maps to TEXT[]/INT[])
|
|
108
|
+
list containing dicts -> Json(list) (for JSONB columns)
|
|
109
|
+
dict -> Json(dict)
|
|
110
|
+
datetime/date -> native
|
|
111
|
+
Decimal -> float
|
|
112
|
+
everything else -> as-is
|
|
113
113
|
"""
|
|
114
|
+
# NULL-ify empties so they're valid for TEXT[], JSONB, and plain columns alike.
|
|
114
115
|
from psycopg2.extras import Json
|
|
115
116
|
|
|
116
117
|
if v is None:
|
|
117
118
|
return None
|
|
119
|
+
if isinstance(v, (list, tuple)) and len(v) == 0:
|
|
120
|
+
return None
|
|
121
|
+
if isinstance(v, dict) and len(v) == 0:
|
|
122
|
+
return None
|
|
118
123
|
|
|
119
|
-
# Dict ->
|
|
124
|
+
# Dict -> JSONB
|
|
120
125
|
if isinstance(v, dict):
|
|
121
126
|
return Json(self._json_safe(v))
|
|
122
127
|
|
|
123
|
-
# List:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
# List: route by element type.
|
|
129
|
+
if isinstance(v, (list, tuple)):
|
|
130
|
+
v = list(v)
|
|
131
|
+
# If ANY element is a dict, treat as JSON payload (for JSONB columns).
|
|
132
|
+
if any(isinstance(x, dict) for x in v):
|
|
133
|
+
return Json(v)
|
|
134
|
+
# If ALL elements are primitives, send as native list for TEXT[]/INT[].
|
|
135
|
+
if all(isinstance(x, (str, int, float, bool, type(None))) for x in v):
|
|
136
|
+
return v
|
|
137
|
+
# Mixed / nested -> safest is JSONB
|
|
128
138
|
return Json(v)
|
|
129
139
|
|
|
130
140
|
# Datetime / date
|
|
131
141
|
if isinstance(v, (datetime, date)):
|
|
132
|
-
return v
|
|
142
|
+
return v
|
|
133
143
|
|
|
134
144
|
# Decimal
|
|
135
145
|
if isinstance(v, Decimal):
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# src/polydb/audit/AuditStorage.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
|
|
7
|
+
from .models import AuditRecord
|
|
8
|
+
from ..cloudDatabaseFactory import CloudDatabaseFactory
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuditStorage:
|
|
12
|
+
"""Audit log with distributed-safe hash chaining"""
|
|
13
|
+
|
|
14
|
+
_lock = threading.Lock()
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.factory = CloudDatabaseFactory()
|
|
18
|
+
self.sql = self.factory.get_sql()
|
|
19
|
+
self._ensure_table()
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _is_unique_violation(exc: Exception) -> bool:
|
|
23
|
+
s = str(exc).lower()
|
|
24
|
+
return "23505" in s or "duplicate key" in s or "unique constraint" in s
|
|
25
|
+
|
|
26
|
+
def _ensure_table(self):
|
|
27
|
+
"""Create audit table if not exists"""
|
|
28
|
+
try:
|
|
29
|
+
schema = """
|
|
30
|
+
CREATE TABLE IF NOT EXISTS polydb_audit_log (
|
|
31
|
+
audit_id UUID PRIMARY KEY,
|
|
32
|
+
timestamp TIMESTAMP NOT NULL,
|
|
33
|
+
tenant_id VARCHAR(255),
|
|
34
|
+
actor_id VARCHAR(255),
|
|
35
|
+
roles TEXT[],
|
|
36
|
+
action VARCHAR(50) NOT NULL,
|
|
37
|
+
model VARCHAR(255) NOT NULL,
|
|
38
|
+
entity_id VARCHAR(255),
|
|
39
|
+
storage_type VARCHAR(20) NOT NULL,
|
|
40
|
+
provider VARCHAR(50) NOT NULL,
|
|
41
|
+
success BOOLEAN NOT NULL,
|
|
42
|
+
before JSONB,
|
|
43
|
+
after JSONB,
|
|
44
|
+
changed_fields TEXT[],
|
|
45
|
+
trace_id VARCHAR(255),
|
|
46
|
+
request_id VARCHAR(255),
|
|
47
|
+
ip_address VARCHAR(45),
|
|
48
|
+
user_agent TEXT,
|
|
49
|
+
error TEXT,
|
|
50
|
+
hash VARCHAR(64) NOT NULL,
|
|
51
|
+
previous_hash VARCHAR(64) NOT NULL DEFAULT '',
|
|
52
|
+
CONSTRAINT uq_audit_chain UNIQUE (tenant_id, previous_hash),
|
|
53
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_audit_tenant_timestamp
|
|
57
|
+
ON polydb_audit_log(tenant_id, timestamp DESC);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_audit_model_entity
|
|
59
|
+
ON polydb_audit_log(model, entity_id);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_audit_actor
|
|
61
|
+
ON polydb_audit_log(actor_id, timestamp DESC);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_audit_hash_chain
|
|
63
|
+
ON polydb_audit_log(tenant_id, timestamp DESC, previous_hash);
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
self.sql.execute(schema)
|
|
67
|
+
except Exception:
|
|
68
|
+
# Table may already exist
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def get_last_hash(self, tenant_id: Optional[str]) -> Optional[str]:
|
|
72
|
+
"""Get most recent hash with strict ordering (distributed-safe)"""
|
|
73
|
+
with self._lock:
|
|
74
|
+
try:
|
|
75
|
+
from ..query import QueryBuilder, Operator
|
|
76
|
+
|
|
77
|
+
builder = QueryBuilder()
|
|
78
|
+
|
|
79
|
+
if tenant_id is not None:
|
|
80
|
+
builder.where("tenant_id", Operator.EQ, tenant_id)
|
|
81
|
+
|
|
82
|
+
builder.order_by("timestamp", descending=True).take(1)
|
|
83
|
+
|
|
84
|
+
results = self.sql.query_linq("polydb_audit_log", builder)
|
|
85
|
+
|
|
86
|
+
if results and len(results) > 0:
|
|
87
|
+
return results[0].get("hash")
|
|
88
|
+
|
|
89
|
+
return None
|
|
90
|
+
except Exception:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def persist(self, record: AuditRecord) -> None:
|
|
94
|
+
"""Append to the hash chain. Concurrency-safe ACROSS PROCESSES via the
|
|
95
|
+
UNIQUE(tenant_id, previous_hash) constraint + bounded retry: if two
|
|
96
|
+
writers race on the same predecessor, the loser re-reads the new tail
|
|
97
|
+
and re-chains instead of forking. (threading.Lock alone was only
|
|
98
|
+
process-local — the old "distributed-safe" claim was false.)"""
|
|
99
|
+
from dataclasses import asdict
|
|
100
|
+
from .models import compute_audit_hash
|
|
101
|
+
|
|
102
|
+
last_err: Optional[Exception] = None
|
|
103
|
+
for _ in range(8):
|
|
104
|
+
with self._lock:
|
|
105
|
+
prev = self.get_last_hash(record.tenant_id) or ""
|
|
106
|
+
record.previous_hash = prev
|
|
107
|
+
record.hash = compute_audit_hash(asdict(record))
|
|
108
|
+
row = {
|
|
109
|
+
"audit_id": record.audit_id,
|
|
110
|
+
"timestamp": record.timestamp,
|
|
111
|
+
"tenant_id": record.tenant_id,
|
|
112
|
+
"actor_id": record.actor_id,
|
|
113
|
+
"roles": record.roles or None,
|
|
114
|
+
"action": record.action,
|
|
115
|
+
"model": record.model,
|
|
116
|
+
"entity_id": record.entity_id,
|
|
117
|
+
"storage_type": record.storage_type,
|
|
118
|
+
"provider": record.provider,
|
|
119
|
+
"success": record.success,
|
|
120
|
+
"before": record.before,
|
|
121
|
+
"after": record.after,
|
|
122
|
+
"changed_fields": record.changed_fields or None,
|
|
123
|
+
"trace_id": record.trace_id,
|
|
124
|
+
"request_id": record.request_id,
|
|
125
|
+
"ip_address": record.ip_address,
|
|
126
|
+
"user_agent": record.user_agent,
|
|
127
|
+
"error": record.error,
|
|
128
|
+
"hash": record.hash,
|
|
129
|
+
"previous_hash": record.previous_hash,
|
|
130
|
+
}
|
|
131
|
+
try:
|
|
132
|
+
self.sql.insert("polydb_audit_log", row)
|
|
133
|
+
return
|
|
134
|
+
except Exception as e:
|
|
135
|
+
if self._is_unique_violation(e):
|
|
136
|
+
last_err = e
|
|
137
|
+
continue
|
|
138
|
+
raise
|
|
139
|
+
raise last_err or RuntimeError("audit persist failed after retries")
|
|
140
|
+
|
|
141
|
+
def verify_chain(self, tenant_id: Optional[str] = None) -> bool:
|
|
142
|
+
"""Verify BOTH chain linkage AND per-record content integrity.
|
|
143
|
+
The old version only checked previous_hash linkage, so editing
|
|
144
|
+
before/after/action while leaving `hash` intact passed silently."""
|
|
145
|
+
from ..query import QueryBuilder, Operator
|
|
146
|
+
from .models import compute_audit_hash
|
|
147
|
+
|
|
148
|
+
builder = QueryBuilder()
|
|
149
|
+
if tenant_id is not None:
|
|
150
|
+
builder.where("tenant_id", Operator.EQ, tenant_id)
|
|
151
|
+
builder.order_by("timestamp", descending=False)
|
|
152
|
+
|
|
153
|
+
records = self.sql.query_linq("polydb_audit_log", builder)
|
|
154
|
+
if not records:
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
prev = ""
|
|
158
|
+
for r in records:
|
|
159
|
+
if (r.get("previous_hash") or "") != prev:
|
|
160
|
+
return False
|
|
161
|
+
if r.get("hash") != compute_audit_hash(r): # content tamper check
|
|
162
|
+
return False
|
|
163
|
+
prev = r.get("hash")
|
|
164
|
+
return True
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/models.py
RENAMED
|
@@ -1,15 +1,52 @@
|
|
|
1
1
|
# src/polydb/audit/models.py
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass, asdict
|
|
4
|
-
from datetime import datetime
|
|
5
4
|
from typing import Any, Dict, List, Optional
|
|
6
5
|
import uuid
|
|
7
6
|
import hashlib
|
|
8
7
|
import json
|
|
9
|
-
|
|
8
|
+
from datetime import datetime, timezone
|
|
10
9
|
from ..json_safe import json_safe
|
|
11
10
|
|
|
12
11
|
|
|
12
|
+
def _iso(ts: Any) -> str:
|
|
13
|
+
return ts.isoformat() if hasattr(ts, "isoformat") else str(ts)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def canonical_audit_payload(src: Dict[str, Any]) -> str:
|
|
17
|
+
"""Deterministic JSON for the hash chain. Identical whether `src` is a
|
|
18
|
+
freshly-built record (asdict) or a row read back from Postgres, so the
|
|
19
|
+
create-time hash and the verify-time recomputed hash match.
|
|
20
|
+
Normalizes [] vs NULL and timestamp formatting; excludes `hash`."""
|
|
21
|
+
payload = {
|
|
22
|
+
"audit_id": src.get("audit_id"),
|
|
23
|
+
"timestamp": _iso(src.get("timestamp")),
|
|
24
|
+
"tenant_id": src.get("tenant_id"),
|
|
25
|
+
"actor_id": src.get("actor_id"),
|
|
26
|
+
"roles": list(src.get("roles") or []),
|
|
27
|
+
"action": src.get("action"),
|
|
28
|
+
"model": src.get("model"),
|
|
29
|
+
"entity_id": src.get("entity_id"),
|
|
30
|
+
"storage_type": src.get("storage_type"),
|
|
31
|
+
"provider": src.get("provider"),
|
|
32
|
+
"success": bool(src.get("success")),
|
|
33
|
+
"before": src.get("before"),
|
|
34
|
+
"after": src.get("after"),
|
|
35
|
+
"changed_fields": list(src.get("changed_fields") or []),
|
|
36
|
+
"trace_id": src.get("trace_id"),
|
|
37
|
+
"request_id": src.get("request_id"),
|
|
38
|
+
"ip_address": src.get("ip_address"),
|
|
39
|
+
"user_agent": src.get("user_agent"),
|
|
40
|
+
"error": src.get("error"),
|
|
41
|
+
"previous_hash": src.get("previous_hash") or "",
|
|
42
|
+
}
|
|
43
|
+
return json.dumps(payload, sort_keys=True, default=json_safe)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compute_audit_hash(src: Dict[str, Any]) -> str:
|
|
47
|
+
return hashlib.sha256(canonical_audit_payload(src).encode()).hexdigest()
|
|
48
|
+
|
|
49
|
+
|
|
13
50
|
@dataclass
|
|
14
51
|
class AuditRecord:
|
|
15
52
|
audit_id: str
|
|
@@ -55,7 +92,7 @@ class AuditRecord:
|
|
|
55
92
|
context,
|
|
56
93
|
previous_hash: Optional[str] = None,
|
|
57
94
|
):
|
|
58
|
-
now = datetime.
|
|
95
|
+
now = datetime.now(timezone.utc).replace(tzinfo=None).isoformat()
|
|
59
96
|
audit_id = str(uuid.uuid4())
|
|
60
97
|
|
|
61
98
|
record = cls(
|
|
@@ -81,8 +118,6 @@ class AuditRecord:
|
|
|
81
118
|
previous_hash=previous_hash,
|
|
82
119
|
)
|
|
83
120
|
|
|
84
|
-
record.hash =
|
|
85
|
-
json.dumps(asdict(record), sort_keys=True,default=json_safe).encode()
|
|
86
|
-
).hexdigest()
|
|
121
|
+
record.hash = compute_audit_hash(asdict(record))
|
|
87
122
|
|
|
88
123
|
return record
|
|
@@ -18,6 +18,7 @@ class ObjectStorageAdapter(ABC):
|
|
|
18
18
|
optimize: bool = True,
|
|
19
19
|
media_type: Optional[str] = None,
|
|
20
20
|
metadata: Dict[str, Any] | None = None,
|
|
21
|
+
container_name: Optional[str] = None,
|
|
21
22
|
) -> str:
|
|
22
23
|
"""Store object with optional optimization"""
|
|
23
24
|
if optimize and media_type:
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import Any, Dict, List, Optional, Union
|
|
6
6
|
from enum import Enum
|
|
7
|
+
from .utils import validate_column_name
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class Operator(Enum):
|
|
@@ -130,6 +131,7 @@ class QueryBuilder:
|
|
|
130
131
|
params = []
|
|
131
132
|
|
|
132
133
|
for f in self.filters:
|
|
134
|
+
validate_column_name(f.field)
|
|
133
135
|
|
|
134
136
|
if f.operator == Operator.EQ:
|
|
135
137
|
clauses.append(f"{f.field} = %s")
|
|
@@ -156,14 +158,15 @@ class QueryBuilder:
|
|
|
156
158
|
params.append(f.value)
|
|
157
159
|
|
|
158
160
|
elif f.operator == Operator.IN:
|
|
159
|
-
|
|
160
161
|
if isinstance(f.value, (list, tuple)):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
if not f.value:
|
|
163
|
+
clauses.append("1=0") # empty IN → match nothing
|
|
164
|
+
else:
|
|
165
|
+
placeholders = ",".join(["%s"] * len(f.value))
|
|
166
|
+
clauses.append(f"{f.field} IN ({placeholders})")
|
|
167
|
+
params.extend(f.value)
|
|
165
168
|
else:
|
|
166
|
-
clauses.append(f"{f.field}
|
|
169
|
+
clauses.append(f"{f.field} = %s") # scalar IN == equality
|
|
167
170
|
params.append(f.value)
|
|
168
171
|
|
|
169
172
|
elif f.operator == Operator.NOT_IN:
|
|
@@ -12,42 +12,49 @@ from typing import Callable, Optional, Tuple, Type
|
|
|
12
12
|
# Metrics hooks for enterprise monitoring
|
|
13
13
|
class MetricsHooks:
|
|
14
14
|
"""Metrics hooks that users can override for monitoring"""
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
@staticmethod
|
|
17
17
|
def on_query_start(operation: str, **kwargs):
|
|
18
18
|
"""Called when query starts"""
|
|
19
19
|
pass
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
@staticmethod
|
|
22
22
|
def on_query_end(operation: str, duration: float, success: bool, **kwargs):
|
|
23
23
|
"""Called when query ends"""
|
|
24
24
|
pass
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
@staticmethod
|
|
27
27
|
def on_error(operation: str, error: Exception, **kwargs):
|
|
28
28
|
"""Called when error occurs"""
|
|
29
29
|
pass
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def retry(
|
|
33
|
-
|
|
32
|
+
def retry(
|
|
33
|
+
max_attempts: int = 3,
|
|
34
|
+
delay: float = 1.0,
|
|
35
|
+
backoff: float = 2.0,
|
|
36
|
+
exceptions: Tuple[Type[Exception], ...] = (Exception,),
|
|
37
|
+
):
|
|
34
38
|
"""
|
|
35
39
|
Retry decorator with exponential backoff
|
|
36
|
-
|
|
40
|
+
|
|
37
41
|
Args:
|
|
38
42
|
max_attempts: Maximum number of retry attempts
|
|
39
43
|
delay: Initial delay between retries (seconds)
|
|
40
44
|
backoff: Backoff multiplier
|
|
41
45
|
exceptions: Tuple of exceptions to catch
|
|
42
46
|
"""
|
|
47
|
+
if max_attempts < 1:
|
|
48
|
+
raise ValueError("max_attempts must be >= 1")
|
|
49
|
+
|
|
43
50
|
def decorator(func: Callable) -> Callable:
|
|
44
51
|
@wraps(func)
|
|
45
52
|
def wrapper(*args, **kwargs):
|
|
46
53
|
attempt = 0
|
|
47
54
|
current_delay = delay
|
|
48
|
-
|
|
55
|
+
|
|
49
56
|
logger = logging.getLogger(__name__)
|
|
50
|
-
|
|
57
|
+
|
|
51
58
|
while attempt < max_attempts:
|
|
52
59
|
start_time = time.time()
|
|
53
60
|
try:
|
|
@@ -61,16 +68,20 @@ def retry(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0,
|
|
|
61
68
|
duration = time.time() - start_time
|
|
62
69
|
MetricsHooks.on_query_end(func.__name__, duration, False)
|
|
63
70
|
MetricsHooks.on_error(func.__name__, e)
|
|
64
|
-
|
|
71
|
+
|
|
65
72
|
if attempt >= max_attempts:
|
|
66
73
|
raise
|
|
67
|
-
|
|
74
|
+
|
|
68
75
|
logger.warning(
|
|
69
76
|
f"Attempt {attempt}/{max_attempts} failed for {func.__name__}: {str(e)}. "
|
|
70
77
|
f"Retrying in {current_delay}s..."
|
|
71
78
|
)
|
|
72
79
|
time.sleep(current_delay)
|
|
73
80
|
current_delay *= backoff
|
|
74
|
-
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
f"{func.__name__} exhausted {max_attempts} attempts without returning"
|
|
83
|
+
)
|
|
84
|
+
|
|
75
85
|
return wrapper
|
|
76
|
-
|
|
86
|
+
|
|
87
|
+
return decorator
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Security features: encryption, masking, row-level security
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
from typing import Dict, Any, List, Optional, Callable, Union
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
import hashlib
|
|
@@ -50,7 +51,7 @@ class FieldEncryption:
|
|
|
50
51
|
"""Encrypt arbitrary value (serialize if non-str)"""
|
|
51
52
|
if value is None:
|
|
52
53
|
return ""
|
|
53
|
-
data = json.dumps(value,default=json_safe) if not isinstance(value, str) else value
|
|
54
|
+
data = json.dumps(value, default=json_safe) if not isinstance(value, str) else value
|
|
54
55
|
try:
|
|
55
56
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
56
57
|
|
|
@@ -91,9 +92,11 @@ class FieldEncryption:
|
|
|
91
92
|
except ImportError:
|
|
92
93
|
raise ImportError("cryptography not installed")
|
|
93
94
|
except Exception as e:
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
# Fail loud. Returning ciphertext as if it were plaintext masks
|
|
96
|
+
# key-rotation errors / corruption and leaks the 'encrypted:' blob
|
|
97
|
+
# into application data.
|
|
98
|
+
logger.error("Field decryption failed: %s", e)
|
|
99
|
+
raise
|
|
97
100
|
|
|
98
101
|
def encrypt_fields(self, data: Dict[str, Any], fields: List[str]) -> Dict[str, Any]:
|
|
99
102
|
"""Encrypt specified fields in data dict"""
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# src/polydb/audit/AuditStorage.py
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import threading
|
|
5
|
-
from typing import Optional, Dict, Any
|
|
6
|
-
|
|
7
|
-
from .models import AuditRecord
|
|
8
|
-
from ..cloudDatabaseFactory import CloudDatabaseFactory
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class AuditStorage:
|
|
12
|
-
"""Audit log with distributed-safe hash chaining"""
|
|
13
|
-
|
|
14
|
-
_lock = threading.Lock()
|
|
15
|
-
|
|
16
|
-
def __init__(self):
|
|
17
|
-
self.factory = CloudDatabaseFactory()
|
|
18
|
-
self.sql = self.factory.get_sql()
|
|
19
|
-
self._ensure_table()
|
|
20
|
-
|
|
21
|
-
def _ensure_table(self):
|
|
22
|
-
"""Create audit table if not exists"""
|
|
23
|
-
try:
|
|
24
|
-
schema = """
|
|
25
|
-
CREATE TABLE IF NOT EXISTS polydb_audit_log (
|
|
26
|
-
audit_id UUID PRIMARY KEY,
|
|
27
|
-
timestamp TIMESTAMP NOT NULL,
|
|
28
|
-
tenant_id VARCHAR(255),
|
|
29
|
-
actor_id VARCHAR(255),
|
|
30
|
-
roles TEXT[],
|
|
31
|
-
action VARCHAR(50) NOT NULL,
|
|
32
|
-
model VARCHAR(255) NOT NULL,
|
|
33
|
-
entity_id VARCHAR(255),
|
|
34
|
-
storage_type VARCHAR(20) NOT NULL,
|
|
35
|
-
provider VARCHAR(50) NOT NULL,
|
|
36
|
-
success BOOLEAN NOT NULL,
|
|
37
|
-
before JSONB,
|
|
38
|
-
after JSONB,
|
|
39
|
-
changed_fields TEXT[],
|
|
40
|
-
trace_id VARCHAR(255),
|
|
41
|
-
request_id VARCHAR(255),
|
|
42
|
-
ip_address VARCHAR(45),
|
|
43
|
-
user_agent TEXT,
|
|
44
|
-
error TEXT,
|
|
45
|
-
hash VARCHAR(64) NOT NULL,
|
|
46
|
-
previous_hash VARCHAR(64),
|
|
47
|
-
created_at TIMESTAMP DEFAULT NOW()
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
CREATE INDEX IF NOT EXISTS idx_audit_tenant_timestamp
|
|
51
|
-
ON polydb_audit_log(tenant_id, timestamp DESC);
|
|
52
|
-
CREATE INDEX IF NOT EXISTS idx_audit_model_entity
|
|
53
|
-
ON polydb_audit_log(model, entity_id);
|
|
54
|
-
CREATE INDEX IF NOT EXISTS idx_audit_actor
|
|
55
|
-
ON polydb_audit_log(actor_id, timestamp DESC);
|
|
56
|
-
CREATE INDEX IF NOT EXISTS idx_audit_hash_chain
|
|
57
|
-
ON polydb_audit_log(tenant_id, timestamp DESC, previous_hash);
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
self.sql.execute(schema)
|
|
61
|
-
except Exception:
|
|
62
|
-
# Table may already exist
|
|
63
|
-
pass
|
|
64
|
-
|
|
65
|
-
def get_last_hash(self, tenant_id: Optional[str]) -> Optional[str]:
|
|
66
|
-
"""Get most recent hash with strict ordering (distributed-safe)"""
|
|
67
|
-
with self._lock:
|
|
68
|
-
try:
|
|
69
|
-
from ..query import QueryBuilder, Operator
|
|
70
|
-
|
|
71
|
-
builder = QueryBuilder()
|
|
72
|
-
|
|
73
|
-
if tenant_id is not None:
|
|
74
|
-
builder.where('tenant_id', Operator.EQ, tenant_id)
|
|
75
|
-
|
|
76
|
-
builder.order_by('timestamp', descending=True).take(1)
|
|
77
|
-
|
|
78
|
-
results = self.sql.query_linq('polydb_audit_log', builder)
|
|
79
|
-
|
|
80
|
-
if results and len(results) > 0:
|
|
81
|
-
return results[0].get('hash')
|
|
82
|
-
|
|
83
|
-
return None
|
|
84
|
-
except Exception:
|
|
85
|
-
return None
|
|
86
|
-
|
|
87
|
-
def persist(self, record: AuditRecord) -> None:
|
|
88
|
-
"""Persist with lock to ensure chain integrity"""
|
|
89
|
-
with self._lock:
|
|
90
|
-
self.sql.insert('polydb_audit_log', {
|
|
91
|
-
'audit_id': record.audit_id,
|
|
92
|
-
'timestamp': record.timestamp,
|
|
93
|
-
'tenant_id': record.tenant_id,
|
|
94
|
-
'actor_id': record.actor_id,
|
|
95
|
-
'roles': record.roles,
|
|
96
|
-
'action': record.action,
|
|
97
|
-
'model': record.model,
|
|
98
|
-
'entity_id': record.entity_id,
|
|
99
|
-
'storage_type': record.storage_type,
|
|
100
|
-
'provider': record.provider,
|
|
101
|
-
'success': record.success,
|
|
102
|
-
'before': record.before,
|
|
103
|
-
'after': record.after,
|
|
104
|
-
'changed_fields': record.changed_fields,
|
|
105
|
-
'trace_id': record.trace_id,
|
|
106
|
-
'request_id': record.request_id,
|
|
107
|
-
'ip_address': record.ip_address,
|
|
108
|
-
'user_agent': record.user_agent,
|
|
109
|
-
'error': record.error,
|
|
110
|
-
'hash': record.hash,
|
|
111
|
-
'previous_hash': record.previous_hash,
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
def verify_chain(self, tenant_id: Optional[str] = None) -> bool:
|
|
115
|
-
"""Verify hash chain integrity"""
|
|
116
|
-
from ..query import QueryBuilder, Operator
|
|
117
|
-
|
|
118
|
-
builder = QueryBuilder()
|
|
119
|
-
|
|
120
|
-
if tenant_id is not None:
|
|
121
|
-
builder.where('tenant_id', Operator.EQ, tenant_id)
|
|
122
|
-
|
|
123
|
-
builder.order_by('timestamp', descending=False)
|
|
124
|
-
|
|
125
|
-
records = self.sql.query_linq('polydb_audit_log', builder)
|
|
126
|
-
|
|
127
|
-
if not records:
|
|
128
|
-
return True
|
|
129
|
-
|
|
130
|
-
prev_hash = None
|
|
131
|
-
for record in records:
|
|
132
|
-
if record.get('previous_hash') != prev_hash:
|
|
133
|
-
return False
|
|
134
|
-
prev_hash = record.get('hash')
|
|
135
|
-
|
|
136
|
-
return True
|
|
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.11 → altcodepro_polydb_python-2.3.14}/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
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/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.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/__init__.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/advanced_query.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/__init__.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/context.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/QueueAdapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/databaseFactory.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/decorators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/monitoring.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/multitenancy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_blockchain.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_cloud_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_multi_engine.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_postgresql.py
RENAMED
|
File without changes
|
|
File without changes
|