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.
Files changed (84) hide show
  1. {altcodepro_polydb_python-2.3.11/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.14}/PKG-INFO +3 -1
  2. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/pyproject.toml +3 -1
  3. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14/src/altcodepro_polydb_python.egg-info}/PKG-INFO +3 -1
  4. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/altcodepro_polydb_python.egg-info/requires.txt +2 -0
  5. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/PolyDB.py +35 -35
  6. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/AzureTableStorageAdapter.py +8 -2
  7. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/PostgreSQLAdapter.py +25 -15
  8. altcodepro_polydb_python-2.3.14/src/polydb/audit/AuditStorage.py +164 -0
  9. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/models.py +41 -6
  10. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/ObjectStorageAdapter.py +1 -0
  11. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/query.py +9 -6
  12. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/retry.py +23 -12
  13. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/security.py +7 -4
  14. altcodepro_polydb_python-2.3.11/src/polydb/audit/AuditStorage.py +0 -136
  15. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/LICENSE +0 -0
  16. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/MANIFEST.in +0 -0
  17. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/README.md +0 -0
  18. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements-aws.txt +0 -0
  19. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements-azure.txt +0 -0
  20. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements-dev.txt +0 -0
  21. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements-gcp.txt +0 -0
  22. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements-generic.txt +0 -0
  23. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/requirements.txt +0 -0
  24. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/setup.cfg +0 -0
  25. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/setup.py +0 -0
  26. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +0 -0
  27. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  28. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  29. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/__init__.py +0 -0
  30. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  31. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  32. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/AzureQueueAdapter.py +0 -0
  33. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
  34. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/BlockchainFileAdapter.py +0 -0
  35. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
  36. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/EFSAdapter.py +0 -0
  39. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/FirestoreAdapter.py +0 -0
  40. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/GCPFilestoreAdapter.py +0 -0
  41. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
  42. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  44. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/S3Adapter.py +0 -0
  45. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  46. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/SQSAdapter.py +0 -0
  47. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
  48. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/VercelFileAdapter.py +0 -0
  49. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/VercelKVAdapter.py +0 -0
  50. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
  51. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/adapters/__init__.py +0 -0
  52. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/advanced_query.py +0 -0
  53. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/__init__.py +0 -0
  54. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/context.py +0 -0
  55. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/audit/manager.py +0 -0
  56. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/NoSQLKVAdapter.py +0 -0
  57. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/QueueAdapter.py +0 -0
  58. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/SharedFilesAdapter.py +0 -0
  59. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/base/__init__.py +0 -0
  60. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/batch.py +0 -0
  61. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/cache.py +0 -0
  62. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/cloudDatabaseFactory.py +0 -0
  63. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/databaseFactory.py +0 -0
  64. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/decorators.py +0 -0
  65. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/errors.py +0 -0
  66. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/json_safe.py +0 -0
  67. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/models.py +0 -0
  68. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/monitoring.py +0 -0
  69. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/multitenancy.py +0 -0
  70. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/py.typed +0 -0
  71. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/registry.py +0 -0
  72. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/schema.py +0 -0
  73. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/types.py +0 -0
  74. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/utils.py +0 -0
  75. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/src/polydb/validation.py +0 -0
  76. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_aws.py +0 -0
  77. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_azure.py +0 -0
  78. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_blockchain.py +0 -0
  79. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_cloud_factory.py +0 -0
  80. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_gcp.py +0 -0
  81. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_mongodb.py +0 -0
  82. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_multi_engine.py +0 -0
  83. {altcodepro_polydb_python-2.3.11 → altcodepro_polydb_python-2.3.14}/tests/test_postgresql.py +0 -0
  84. {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.11
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.11"
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.11
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"
@@ -13,6 +13,8 @@ web3>=7.16.0
13
13
  google-cloud-firestore>=2.27.0
14
14
  google-cloud-pubsub>=2.38.0
15
15
  pymongo>=4.17.0
16
+ build>=1.5.0
17
+ twine>=6.2.0
16
18
 
17
19
  [all]
18
20
  boto3>=1.42.47
@@ -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
- enable_rls=enable_rls,
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.get_shared_files()
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef,
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: ModelRef, records: List[JsonDict]) -> BatchResult:
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: ModelRef,
563
- updates: List[Tuple[Any, JsonDict]],
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: ModelRef, entity_ids: List[Any]) -> BatchResult:
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: ModelRef):
577
+ def validate_model(self, model: Type):
575
578
  return ModelValidator.validate_model(model)
576
579
 
577
- def validate(self, model: ModelRef) -> None:
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
- TenantContext.set_tenant(tenant_id, self.tenant_registry)
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(v, default=json_safe)
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 all outgoing values safe for psycopg2.
103
+ Make outgoing values safe for psycopg2 across mixed column types.
106
104
 
107
105
  Rules:
108
- - dict -> Json()
109
- - list -> leave as list (so TEXT[] works)
110
- - datetime/date -> pass as native (psycopg2 handles it)
111
- - Decimal -> convert to float
112
- - everything else -> pass as-is
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 -> JSON/JSONB
124
+ # Dict -> JSONB
120
125
  if isinstance(v, dict):
121
126
  return Json(self._json_safe(v))
122
127
 
123
- # List:
124
- # DO NOT wrap in Json() automatically.
125
- # If column is JSONB, Postgres will still accept Json(list).
126
- # But for TEXT[] columns we must send Python list.
127
- if isinstance(v, list):
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 # psycopg2 handles natively
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
@@ -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.utcnow().isoformat()
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 = hashlib.sha256(
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
- placeholders = ",".join(["%s"] * len(f.value))
162
- clauses.append(f"{f.field} IN ({placeholders})")
163
- params.extend(f.value)
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} LIKE %s")
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(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0,
33
- exceptions: Tuple[Type[Exception], ...] = (Exception,)):
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
- return decorator
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
- # On decrypt failure, return masked or original to avoid crashes
95
- logger.warning(f"Decryption failed: {e}. Returning original value.")
96
- return encrypted_data
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