altcodepro-polydb-python 2.1.0__tar.gz → 2.2.0__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 (66) hide show
  1. {altcodepro_polydb_python-2.1.0/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.2.0}/PKG-INFO +6 -5
  2. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/README.md +3 -3
  3. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/example_usage.py +6 -8
  4. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/pyproject.toml +18 -35
  5. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0/src/altcodepro_polydb_python.egg-info}/PKG-INFO +6 -5
  6. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +1 -0
  7. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/altcodepro_polydb_python.egg-info/requires.txt +2 -1
  8. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/__init__.py +25 -29
  9. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/AzureQueueAdapter.py +3 -1
  10. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/AzureTableStorageAdapter.py +2 -1
  11. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/DynamoDBAdapter.py +1 -1
  12. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/FirestoreAdapter.py +2 -1
  13. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/PostgreSQLAdapter.py +127 -47
  14. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/PubSubAdapter.py +3 -1
  15. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/SQSAdapter.py +3 -1
  16. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/VercelKVAdapter.py +4 -3
  17. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/audit/models.py +3 -1
  18. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/base/NoSQLKVAdapter.py +7 -5
  19. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/cache.py +3 -2
  20. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/databaseFactory.py +35 -9
  21. altcodepro_polydb_python-2.2.0/src/polydb/json_safe.py +8 -0
  22. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/multitenancy.py +2 -2
  23. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/security.py +3 -1
  24. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/LICENSE +0 -0
  25. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/MANIFEST.in +0 -0
  26. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/requirements-aws.txt +0 -0
  27. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/requirements-azure.txt +0 -0
  28. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/requirements-dev.txt +0 -0
  29. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/requirements-gcp.txt +0 -0
  30. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/requirements-generic.txt +0 -0
  31. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/requirements.txt +0 -0
  32. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/setup.cfg +0 -0
  33. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/setup.py +0 -0
  34. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
  35. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
  36. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
  37. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
  38. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/EFSAdapter.py +0 -0
  39. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
  40. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/MongoDBAdapter.py +0 -0
  41. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/S3Adapter.py +0 -0
  42. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
  43. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/adapters/__init__.py +0 -0
  44. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/advanced_query.py +0 -0
  45. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/audit/AuditStorage.py +0 -0
  46. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/audit/__init__.py +0 -0
  47. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/audit/context.py +0 -0
  48. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/audit/manager.py +0 -0
  49. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/base/ObjectStorageAdapter.py +0 -0
  50. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/base/QueueAdapter.py +0 -0
  51. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/base/SharedFilesAdapter.py +0 -0
  52. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/base/__init__.py +0 -0
  53. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/batch.py +0 -0
  54. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/decorators.py +0 -0
  55. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/errors.py +0 -0
  56. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/factory.py +0 -0
  57. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/models.py +0 -0
  58. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/monitoring.py +0 -0
  59. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/py.typed +0 -0
  60. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/query.py +0 -0
  61. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/registry.py +0 -0
  62. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/retry.py +0 -0
  63. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/schema.py +0 -0
  64. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/types.py +0 -0
  65. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/utils.py +0 -0
  66. {altcodepro_polydb_python-2.1.0 → altcodepro_polydb_python-2.2.0}/src/polydb/validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.1.0
3
+ Version: 2.2.0
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
@@ -25,6 +25,8 @@ Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: psycopg2-binary>=2.9.11
27
27
  Requires-Dist: tenacity>=9.1.4
28
+ Requires-Dist: redis>=6.4.0
29
+ Requires-Dist: python-dotenv>=1.1.1
28
30
  Provides-Extra: aws
29
31
  Requires-Dist: boto3>=1.42.47; extra == "aws"
30
32
  Requires-Dist: botocore>=1.42.47; extra == "aws"
@@ -65,7 +67,6 @@ Requires-Dist: google-cloud-storage>=3.9.0; extra == "all"
65
67
  Requires-Dist: pymongo>=4.16.0; extra == "all"
66
68
  Requires-Dist: pika>=1.3.2; extra == "all"
67
69
  Requires-Dist: requests>=2.32.5; extra == "all"
68
- Requires-Dist: redis>=6.4.0; extra == "all"
69
70
  Provides-Extra: dev
70
71
  Requires-Dist: black>=26.1.0; extra == "dev"
71
72
  Requires-Dist: flake8>=7.3.0; extra == "dev"
@@ -78,7 +79,7 @@ Requires-Dist: pytest-mock>=3.15.1; extra == "test"
78
79
  Requires-Dist: moto>=5.1.21; extra == "test"
79
80
  Dynamic: license-file
80
81
 
81
- # PolyDB v3.0 - Enterprise Database Abstraction Layer
82
+ # PolyDB v2.2.0 - Enterprise Database Abstraction Layer
82
83
 
83
84
  **Production-ready, cloud-independent database abstraction with full LINQ support, field-level audit, cache, and overflow storage**
84
85
 
@@ -243,8 +244,8 @@ users = db.read(User, {"role": "admin"})
243
244
  users = db.read(User, {"role": "admin"}, no_cache=True)
244
245
 
245
246
  # Manual invalidation
246
- from polydb.cache import CacheEngine
247
- cache = CacheEngine()
247
+ from polydb.cache import RedisCacheEngine
248
+ cache = RedisCacheEngine()
248
249
  cache.invalidate("User")
249
250
  cache.clear()
250
251
  ```
@@ -1,4 +1,4 @@
1
- # PolyDB v3.0 - Enterprise Database Abstraction Layer
1
+ # PolyDB v2.2.0 - Enterprise Database Abstraction Layer
2
2
 
3
3
  **Production-ready, cloud-independent database abstraction with full LINQ support, field-level audit, cache, and overflow storage**
4
4
 
@@ -163,8 +163,8 @@ users = db.read(User, {"role": "admin"})
163
163
  users = db.read(User, {"role": "admin"}, no_cache=True)
164
164
 
165
165
  # Manual invalidation
166
- from polydb.cache import CacheEngine
167
- cache = CacheEngine()
166
+ from polydb.cache import RedisCacheEngine
167
+ cache = RedisCacheEngine()
168
168
  cache.invalidate("User")
169
169
  cache.clear()
170
170
  ```
@@ -3,16 +3,14 @@
3
3
  Complete PolyDB usage example with all features
4
4
  """
5
5
 
6
- from polydb import (
7
- DatabaseFactory,
8
- polydb_model,
9
- QueryBuilder,
10
- Operator,
11
- AuditContext,
12
- )
13
-
14
6
 
15
7
  # 1. Define models
8
+ from polydb.audit.context import AuditContext
9
+ from polydb.databaseFactory import DatabaseFactory
10
+ from polydb.decorators import polydb_model
11
+ from polydb.query import Operator, QueryBuilder
12
+
13
+
16
14
  @polydb_model
17
15
  class User:
18
16
  __polydb__ = {
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "altcodepro-polydb-python"
7
- version = "2.1.0"
7
+ version = "2.2.0"
8
8
  description = "Production-ready multi-cloud database abstraction layer with connection pooling, retry logic, and thread safety"
9
- readme = {file = "README.md", content-type = "text/markdown"}
9
+ readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.8"
11
11
  license-files = ["LICENSE"]
12
- authors = [{name = "AltCodePro"}]
12
+ authors = [{ name = "AltCodePro" }]
13
13
  keywords = [
14
14
  "database",
15
15
  "cloud",
@@ -23,7 +23,7 @@ keywords = [
23
23
  "postgres",
24
24
  "mongodb",
25
25
  "dynamodb",
26
- "s3"
26
+ "s3",
27
27
  ]
28
28
  classifiers = [
29
29
  "Development Status :: 4 - Beta",
@@ -43,23 +43,22 @@ classifiers = [
43
43
  # Core dependencies - always installed
44
44
  dependencies = [
45
45
  "psycopg2-binary>=2.9.11",
46
- "tenacity>=9.1.4"
46
+ "tenacity>=9.1.4",
47
+ "redis>=6.4.0",
48
+ "python-dotenv>=1.1.1",
47
49
  ]
48
50
 
49
51
  # Generic/Open-source stack (cheapest option)
50
52
  [project.optional-dependencies]
51
53
 
52
- aws = [
53
- "boto3>=1.42.47",
54
- "botocore>=1.42.47"
55
- ]
54
+ aws = ["boto3>=1.42.47", "botocore>=1.42.47"]
56
55
 
57
56
  azure = [
58
57
  "azure-core>=1.38.1",
59
58
  "azure-data-tables>=12.7.0",
60
59
  "azure-storage-blob>=12.28.0",
61
60
  "azure-storage-file-share>=12.24.0",
62
- "azure-storage-queue>=12.15.0"
61
+ "azure-storage-queue>=12.15.0",
63
62
  ]
64
63
 
65
64
  gcp = [
@@ -68,26 +67,16 @@ gcp = [
68
67
  "google-cloud-core>=2.5.0",
69
68
  "google-cloud-firestore>=2.23.0",
70
69
  "google-cloud-pubsub>=2.35.0",
71
- "google-cloud-storage>=3.9.0"
70
+ "google-cloud-storage>=3.9.0",
72
71
  ]
73
72
 
74
- mongodb = [
75
- "pymongo>=4.16.0"
76
- ]
73
+ mongodb = ["pymongo>=4.16.0"]
77
74
 
78
- rabbitmq = [
79
- "pika>=1.3.2"
80
- ]
75
+ rabbitmq = ["pika>=1.3.2"]
81
76
 
82
- vercel = [
83
- "requests>=2.32.5"
84
- ]
77
+ vercel = ["requests>=2.32.5"]
85
78
 
86
- generic = [
87
- "pymongo>=4.16.0",
88
- "pika>=1.3.2",
89
- "boto3>=1.42.47"
90
- ]
79
+ generic = ["pymongo>=4.16.0", "pika>=1.3.2", "boto3>=1.42.47"]
91
80
 
92
81
  all = [
93
82
  "boto3>=1.42.47",
@@ -103,17 +92,11 @@ all = [
103
92
  "pymongo>=4.16.0",
104
93
  "pika>=1.3.2",
105
94
  "requests>=2.32.5",
106
- "redis>=6.4.0"
107
95
  ]
108
96
 
109
97
 
110
98
  # Development dependencies
111
- dev = [
112
- "black>=26.1.0",
113
- "flake8>=7.3.0",
114
- "isort>=7.0.0",
115
- "mypy>=1.19.1"
116
- ]
99
+ dev = ["black>=26.1.0", "flake8>=7.3.0", "isort>=7.0.0", "mypy>=1.19.1"]
117
100
 
118
101
 
119
102
  # Testing with all providers
@@ -121,7 +104,7 @@ test = [
121
104
  "pytest>=9.0.2",
122
105
  "pytest-cov>=7.0.0",
123
106
  "pytest-mock>=3.15.1",
124
- "moto>=5.1.21"
107
+ "moto>=5.1.21",
125
108
  ]
126
109
 
127
110
  [project.urls]
@@ -131,7 +114,7 @@ Repository = "https://github.com/altcodepro/polydb-python"
131
114
  "Bug Tracker" = "https://github.com/altcodepro/polydb-python/issues"
132
115
 
133
116
  [tool.setuptools]
134
- package-dir = {"" = "src"}
117
+ package-dir = { "" = "src" }
135
118
 
136
119
  [tool.setuptools.packages.find]
137
120
  where = ["src"]
@@ -167,4 +150,4 @@ addopts = [
167
150
  "--cov=polydb",
168
151
  "--cov-report=term-missing",
169
152
  "--cov-report=html",
170
- ]
153
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: altcodepro-polydb-python
3
- Version: 2.1.0
3
+ Version: 2.2.0
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
@@ -25,6 +25,8 @@ Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: psycopg2-binary>=2.9.11
27
27
  Requires-Dist: tenacity>=9.1.4
28
+ Requires-Dist: redis>=6.4.0
29
+ Requires-Dist: python-dotenv>=1.1.1
28
30
  Provides-Extra: aws
29
31
  Requires-Dist: boto3>=1.42.47; extra == "aws"
30
32
  Requires-Dist: botocore>=1.42.47; extra == "aws"
@@ -65,7 +67,6 @@ Requires-Dist: google-cloud-storage>=3.9.0; extra == "all"
65
67
  Requires-Dist: pymongo>=4.16.0; extra == "all"
66
68
  Requires-Dist: pika>=1.3.2; extra == "all"
67
69
  Requires-Dist: requests>=2.32.5; extra == "all"
68
- Requires-Dist: redis>=6.4.0; extra == "all"
69
70
  Provides-Extra: dev
70
71
  Requires-Dist: black>=26.1.0; extra == "dev"
71
72
  Requires-Dist: flake8>=7.3.0; extra == "dev"
@@ -78,7 +79,7 @@ Requires-Dist: pytest-mock>=3.15.1; extra == "test"
78
79
  Requires-Dist: moto>=5.1.21; extra == "test"
79
80
  Dynamic: license-file
80
81
 
81
- # PolyDB v3.0 - Enterprise Database Abstraction Layer
82
+ # PolyDB v2.2.0 - Enterprise Database Abstraction Layer
82
83
 
83
84
  **Production-ready, cloud-independent database abstraction with full LINQ support, field-level audit, cache, and overflow storage**
84
85
 
@@ -243,8 +244,8 @@ users = db.read(User, {"role": "admin"})
243
244
  users = db.read(User, {"role": "admin"}, no_cache=True)
244
245
 
245
246
  # Manual invalidation
246
- from polydb.cache import CacheEngine
247
- cache = CacheEngine()
247
+ from polydb.cache import RedisCacheEngine
248
+ cache = RedisCacheEngine()
248
249
  cache.invalidate("User")
249
250
  cache.clear()
250
251
  ```
@@ -23,6 +23,7 @@ src/polydb/databaseFactory.py
23
23
  src/polydb/decorators.py
24
24
  src/polydb/errors.py
25
25
  src/polydb/factory.py
26
+ src/polydb/json_safe.py
26
27
  src/polydb/models.py
27
28
  src/polydb/monitoring.py
28
29
  src/polydb/multitenancy.py
@@ -1,5 +1,7 @@
1
1
  psycopg2-binary>=2.9.11
2
2
  tenacity>=9.1.4
3
+ redis>=6.4.0
4
+ python-dotenv>=1.1.1
3
5
 
4
6
  [all]
5
7
  boto3>=1.42.47
@@ -15,7 +17,6 @@ google-cloud-storage>=3.9.0
15
17
  pymongo>=4.16.0
16
18
  pika>=1.3.2
17
19
  requests>=2.32.5
18
- redis>=6.4.0
19
20
 
20
21
  [aws]
21
22
  boto3>=1.42.47
@@ -4,7 +4,7 @@ PolyDB - Enterprise Cloud-Independent Database Abstraction
4
4
  Full LINQ support, field-level audit, cache, soft delete, overflow storage
5
5
  """
6
6
 
7
- __version__ = "3.0.0"
7
+ __version__ = "2.2.0"
8
8
 
9
9
  from .factory import CloudDatabaseFactory
10
10
  from .databaseFactory import DatabaseFactory
@@ -12,7 +12,7 @@ from .models import CloudProvider, PartitionConfig
12
12
  from .decorators import polydb_model
13
13
  from .query import QueryBuilder, Operator
14
14
  from .audit.context import AuditContext
15
- from .cache import CacheEngine
15
+ from .cache import RedisCacheEngine as CacheEngine
16
16
  from .errors import (
17
17
  CloudDBError,
18
18
  DatabaseError,
@@ -31,34 +31,30 @@ from .errors import (
31
31
 
32
32
  __all__ = [
33
33
  # Factories
34
- 'CloudDatabaseFactory',
35
- 'DatabaseFactory',
36
-
34
+ "CloudDatabaseFactory",
35
+ "DatabaseFactory",
37
36
  # Models & Config
38
- 'CloudProvider',
39
- 'PartitionConfig',
40
- 'polydb_model',
41
-
37
+ "CloudProvider",
38
+ "PartitionConfig",
39
+ "polydb_model",
42
40
  # Query
43
- 'QueryBuilder',
44
- 'Operator',
45
-
41
+ "QueryBuilder",
42
+ "Operator",
46
43
  # Audit & Cache
47
- 'AuditContext',
48
- 'CacheEngine',
49
-
44
+ "AuditContext",
45
+ "CacheEngine",
50
46
  # Errors
51
- 'CloudDBError',
52
- 'DatabaseError',
53
- 'NoSQLError',
54
- 'StorageError',
55
- 'QueueError',
56
- 'ConnectionError',
57
- 'ValidationError',
58
- 'PolyDBError',
59
- 'ModelNotRegisteredError',
60
- 'InvalidModelMetadataError',
61
- 'UnsupportedStorageTypeError',
62
- 'AdapterConfigurationError',
63
- 'OperationNotSupportedError',
64
- ]
47
+ "CloudDBError",
48
+ "DatabaseError",
49
+ "NoSQLError",
50
+ "StorageError",
51
+ "QueueError",
52
+ "ConnectionError",
53
+ "ValidationError",
54
+ "PolyDBError",
55
+ "ModelNotRegisteredError",
56
+ "InvalidModelMetadataError",
57
+ "UnsupportedStorageTypeError",
58
+ "AdapterConfigurationError",
59
+ "OperationNotSupportedError",
60
+ ]
@@ -6,6 +6,8 @@ import os
6
6
  import threading
7
7
  from typing import Any, Dict, List
8
8
 
9
+ from src.polydb.json_safe import json_safe
10
+
9
11
  class AzureQueueAdapter(QueueAdapter):
10
12
  """Azure Queue Storage with client reuse"""
11
13
 
@@ -36,7 +38,7 @@ class AzureQueueAdapter(QueueAdapter):
36
38
 
37
39
  if self._client:
38
40
  queue_client = self._client.get_queue_client(queue_name)
39
- response = queue_client.send_message(json.dumps(message))
41
+ response = queue_client.send_message(json.dumps(message,default=json_safe))
40
42
  return response.id
41
43
  return ""
42
44
  except Exception as e:
@@ -3,6 +3,7 @@ import os
3
3
  import threading
4
4
  from typing import Any, Dict, List, Optional
5
5
  from polydb.base.NoSQLKVAdapter import NoSQLKVAdapter
6
+ from src.polydb.json_safe import json_safe
6
7
  from ..errors import NoSQLError, ConnectionError
7
8
  from ..retry import retry
8
9
  from ..types import JsonDict
@@ -56,7 +57,7 @@ class AzureTableStorageAdapter(NoSQLKVAdapter):
56
57
  data_copy['RowKey'] = rk
57
58
 
58
59
  # Check size
59
- data_bytes = json.dumps(data_copy).encode()
60
+ data_bytes = json.dumps(data_copy,default=json_safe).encode()
60
61
  data_size = len(data_bytes)
61
62
 
62
63
  if data_size > self.AZURE_TABLE_MAX_SIZE:
@@ -64,7 +64,7 @@ class DynamoDBAdapter(NoSQLKVAdapter):
64
64
  data_copy['SK'] = rk
65
65
 
66
66
  # Check size
67
- data_bytes = json.dumps(data_copy).encode()
67
+ data_bytes = json.dumps(data_copy,default=json_safe).encode()
68
68
  data_size = len(data_bytes)
69
69
 
70
70
  if data_size > self.DYNAMODB_MAX_SIZE:
@@ -6,6 +6,7 @@ from google.cloud import firestore
6
6
  from google.cloud import storage
7
7
  from google.cloud.firestore import Client
8
8
  from polydb.base.NoSQLKVAdapter import NoSQLKVAdapter
9
+ from src.polydb.json_safe import json_safe
9
10
 
10
11
  from ..errors import NoSQLError, ConnectionError
11
12
  from ..retry import retry
@@ -66,7 +67,7 @@ class FirestoreAdapter(NoSQLKVAdapter):
66
67
  data_copy['_rk'] = rk
67
68
 
68
69
  # Check size
69
- data_bytes = json.dumps(data_copy).encode()
70
+ data_bytes = json.dumps(data_copy,default=json_safe).encode()
70
71
  data_size = len(data_bytes)
71
72
 
72
73
  if data_size > self.FIRESTORE_MAX_SIZE:
@@ -21,8 +21,10 @@ class PostgreSQLAdapter:
21
21
  self.logger = setup_logger(__name__)
22
22
  self.connection_string = os.getenv(
23
23
  "POSTGRES_CONNECTION_STRING",
24
- os.getenv("POSTGRES_URL", "postgresql://user:password@localhost:5432/database"),
24
+ os.getenv("POSTGRES_URL", ""),
25
25
  )
26
+ if not self.connection_string:
27
+ raise ConnectionError("POSTGRES_CONNECTION_STRING or POSTGRES_URL must be set")
26
28
  self._pool = None
27
29
  self._lock = threading.Lock()
28
30
  self._initialize_pool()
@@ -42,24 +44,46 @@ class PostgreSQLAdapter:
42
44
  except Exception as e:
43
45
  raise ConnectionError(f"Failed to initialize PostgreSQL pool: {str(e)}")
44
46
 
45
- def _get_connection(self):
47
+ def _get_connection(self) -> Any:
46
48
  if not self._pool:
47
49
  self._initialize_pool()
48
50
  return self._pool.getconn() # type: ignore
49
51
 
50
- def _return_connection(self, conn):
52
+ def _return_connection(self, conn: Any):
51
53
  if self._pool and conn:
52
54
  self._pool.putconn(conn)
53
55
 
56
+ def begin_transaction(self) -> Any:
57
+ """Begin a transaction and return the connection handle."""
58
+ conn = self._get_connection()
59
+ conn.autocommit = False # Ensure transaction mode
60
+ return conn
61
+
62
+ def commit(self, tx: Any):
63
+ """Commit the transaction using the provided connection."""
64
+ if tx:
65
+ tx.commit()
66
+ self._return_connection(tx)
67
+
68
+ def rollback(self, tx: Any):
69
+ """Rollback the transaction using the provided connection."""
70
+ if tx:
71
+ tx.rollback()
72
+ self._return_connection(tx)
73
+
54
74
  @retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
55
- def insert(self, table: str, data: JsonDict) -> JsonDict:
75
+ def insert(self, table: str, data: JsonDict, tx: Optional[Any] = None) -> JsonDict:
56
76
  table = validate_table_name(table)
57
77
  for k in data.keys():
58
78
  validate_column_name(k)
59
79
 
60
- conn = None
61
- try:
80
+ conn = tx
81
+ own_conn = False
82
+ if not conn:
62
83
  conn = self._get_connection()
84
+ own_conn = True
85
+
86
+ try:
63
87
  cursor = conn.cursor()
64
88
 
65
89
  columns = ", ".join(data.keys())
@@ -71,15 +95,17 @@ class PostgreSQLAdapter:
71
95
  columns_list = [desc[0] for desc in cursor.description]
72
96
  result = dict(zip(columns_list, result_row))
73
97
 
74
- conn.commit()
98
+ if own_conn:
99
+ conn.commit()
100
+
75
101
  cursor.close()
76
102
  return result
77
103
  except Exception as e:
78
- if conn:
104
+ if own_conn:
79
105
  conn.rollback()
80
106
  raise DatabaseError(f"Insert failed: {str(e)}")
81
107
  finally:
82
- if conn:
108
+ if own_conn and conn:
83
109
  self._return_connection(conn)
84
110
 
85
111
  @retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
@@ -89,12 +115,16 @@ class PostgreSQLAdapter:
89
115
  query: Optional[Lookup] = None,
90
116
  limit: Optional[int] = None,
91
117
  offset: Optional[int] = None,
118
+ tx: Optional[Any] = None,
92
119
  ) -> List[JsonDict]:
93
120
  table = validate_table_name(table)
94
- conn = None
121
+ conn = tx
122
+ own_conn = False
123
+ if not conn:
124
+ conn = self._get_connection()
125
+ own_conn = True
95
126
 
96
127
  try:
97
- conn = self._get_connection()
98
128
  cursor = conn.cursor()
99
129
 
100
130
  sql = f"SELECT * FROM {table}"
@@ -131,15 +161,20 @@ class PostgreSQLAdapter:
131
161
  except Exception as e:
132
162
  raise DatabaseError(f"Select failed: {str(e)}")
133
163
  finally:
134
- if conn:
164
+ if own_conn and conn:
135
165
  self._return_connection(conn)
136
166
 
137
167
  @retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
138
168
  def select_page(
139
- self, table: str, query: Lookup, page_size: int, continuation_token: Optional[str] = None
169
+ self,
170
+ table: str,
171
+ query: Lookup,
172
+ page_size: int,
173
+ continuation_token: Optional[str] = None,
174
+ tx: Optional[Any] = None,
140
175
  ) -> Tuple[List[JsonDict], Optional[str]]:
141
176
  offset = int(continuation_token) if continuation_token else 0
142
- results = self.select(table, query, limit=page_size + 1, offset=offset)
177
+ results = self.select(table, query, limit=page_size + 1, offset=offset, tx=tx)
143
178
 
144
179
  has_more = len(results) > page_size
145
180
  if has_more:
@@ -149,14 +184,24 @@ class PostgreSQLAdapter:
149
184
  return results, next_token
150
185
 
151
186
  @retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
152
- def update(self, table: str, entity_id: Union[Any, Lookup], data: JsonDict) -> JsonDict:
187
+ def update(
188
+ self,
189
+ table: str,
190
+ entity_id: Union[Any, Lookup],
191
+ data: JsonDict,
192
+ tx: Optional[Any] = None,
193
+ ) -> JsonDict:
153
194
  table = validate_table_name(table)
154
195
  for k in data.keys():
155
196
  validate_column_name(k)
156
197
 
157
- conn = None
158
- try:
198
+ conn = tx
199
+ own_conn = False
200
+ if not conn:
159
201
  conn = self._get_connection()
202
+ own_conn = True
203
+
204
+ try:
160
205
  cursor = conn.cursor()
161
206
 
162
207
  set_clause = ", ".join([f"{k} = %s" for k in data.keys()])
@@ -183,26 +228,32 @@ class PostgreSQLAdapter:
183
228
  columns = [desc[0] for desc in cursor.description]
184
229
  result = dict(zip(columns, result_row))
185
230
 
186
- conn.commit()
231
+ if own_conn:
232
+ conn.commit()
233
+
187
234
  cursor.close()
188
235
  return result
189
236
  except Exception as e:
190
- if conn:
237
+ if own_conn:
191
238
  conn.rollback()
192
239
  raise DatabaseError(f"Update failed: {str(e)}")
193
240
  finally:
194
- if conn:
241
+ if own_conn and conn:
195
242
  self._return_connection(conn)
196
243
 
197
244
  @retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
198
- def upsert(self, table: str, data: JsonDict) -> JsonDict:
245
+ def upsert(self, table: str, data: JsonDict, tx: Optional[Any] = None) -> JsonDict:
199
246
  table = validate_table_name(table)
200
247
  for k in data.keys():
201
248
  validate_column_name(k)
202
249
 
203
- conn = None
204
- try:
250
+ conn = tx
251
+ own_conn = False
252
+ if not conn:
205
253
  conn = self._get_connection()
254
+ own_conn = True
255
+
256
+ try:
206
257
  cursor = conn.cursor()
207
258
 
208
259
  columns = ", ".join(data.keys())
@@ -226,24 +277,31 @@ class PostgreSQLAdapter:
226
277
  columns_list = [desc[0] for desc in cursor.description]
227
278
  result = dict(zip(columns_list, result_row))
228
279
 
229
- conn.commit()
280
+ if own_conn:
281
+ conn.commit()
282
+
230
283
  cursor.close()
231
284
  return result
232
285
  except Exception as e:
233
- if conn:
286
+ if own_conn:
234
287
  conn.rollback()
235
288
  raise DatabaseError(f"Upsert failed: {str(e)}")
236
289
  finally:
237
- if conn:
290
+ if own_conn and conn:
238
291
  self._return_connection(conn)
239
292
 
240
293
  @retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
241
- def delete(self, table: str, entity_id: Union[Any, Lookup]) -> JsonDict:
294
+ def delete(
295
+ self, table: str, entity_id: Union[Any, Lookup], tx: Optional[Any] = None
296
+ ) -> JsonDict:
242
297
  table = validate_table_name(table)
243
- conn = None
298
+ conn = tx
299
+ own_conn = False
300
+ if not conn:
301
+ conn = self._get_connection()
302
+ own_conn = True
244
303
 
245
304
  try:
246
- conn = self._get_connection()
247
305
  cursor = conn.cursor()
248
306
 
249
307
  params = []
@@ -268,24 +326,31 @@ class PostgreSQLAdapter:
268
326
  columns = [desc[0] for desc in cursor.description]
269
327
  result = dict(zip(columns, result_row))
270
328
 
271
- conn.commit()
329
+ if own_conn:
330
+ conn.commit()
331
+
272
332
  cursor.close()
273
333
  return result
274
334
  except Exception as e:
275
- if conn:
335
+ if own_conn:
276
336
  conn.rollback()
277
337
  raise DatabaseError(f"Delete failed: {str(e)}")
278
338
  finally:
279
- if conn:
339
+ if own_conn and conn:
280
340
  self._return_connection(conn)
281
341
 
282
342
  @retry(max_attempts=3, delay=1.0, exceptions=(DatabaseError,))
283
- def query_linq(self, table: str, builder: QueryBuilder) -> Union[List[JsonDict], int]:
343
+ def query_linq(
344
+ self, table: str, builder: QueryBuilder, tx: Optional[Any] = None
345
+ ) -> Union[List[JsonDict], int]:
284
346
  table = validate_table_name(table)
285
- conn = None
347
+ conn = tx
348
+ own_conn = False
349
+ if not conn:
350
+ conn = self._get_connection()
351
+ own_conn = True
286
352
 
287
353
  try:
288
- conn = self._get_connection()
289
354
  cursor = conn.cursor()
290
355
 
291
356
  if builder.count_only:
@@ -336,17 +401,24 @@ class PostgreSQLAdapter:
336
401
  cursor.execute(sql, params)
337
402
 
338
403
  if builder.count_only:
339
- return cursor.fetchone()[0]
404
+ result = cursor.fetchone()[0]
405
+ else:
406
+ columns = [desc[0] for desc in cursor.description]
407
+ results = [dict(zip(columns, row)) for row in cursor.fetchall()]
408
+ result = results
340
409
 
341
- columns = [desc[0] for desc in cursor.description]
342
- results = [dict(zip(columns, row)) for row in cursor.fetchall()]
343
410
  cursor.close()
344
411
 
345
- return results
412
+ if own_conn:
413
+ conn.commit()
414
+
415
+ return result
346
416
  except Exception as e:
417
+ if own_conn:
418
+ conn.rollback()
347
419
  raise DatabaseError(f"LINQ query failed: {str(e)}")
348
420
  finally:
349
- if conn:
421
+ if own_conn and conn:
350
422
  self._return_connection(conn)
351
423
 
352
424
  def __del__(self):
@@ -361,14 +433,19 @@ class PostgreSQLAdapter:
361
433
  self,
362
434
  sql: str,
363
435
  params: Optional[List[Any]] = None,
436
+ tx: Optional[Any] = None,
364
437
  *,
365
438
  fetch: bool = False,
366
439
  fetch_one: bool = False,
367
440
  ) -> Union[None, JsonDict, List[JsonDict]]:
368
- conn = None
441
+ conn = tx
442
+ own_conn = False
443
+ if not conn:
444
+ conn = self._get_connection()
445
+ own_conn = True
446
+
369
447
  cursor = None
370
448
  try:
371
- conn = self._get_connection()
372
449
  cursor = conn.cursor()
373
450
 
374
451
  self.logger.debug("Executing raw SQL: %s", sql)
@@ -381,22 +458,25 @@ class PostgreSQLAdapter:
381
458
  if row:
382
459
  columns = [desc[0] for desc in cursor.description]
383
460
  result = dict(zip(columns, row))
384
- conn.commit()
461
+ if own_conn:
462
+ conn.commit()
385
463
  return result
386
464
 
387
465
  if fetch:
388
466
  rows = cursor.fetchall()
389
467
  columns = [desc[0] for desc in cursor.description]
390
468
  results = [dict(zip(columns, r)) for r in rows]
391
- conn.commit()
469
+ if own_conn:
470
+ conn.commit()
392
471
  return results
393
472
 
394
473
  # Non-fetch execution (DDL/DML)
395
- conn.commit()
474
+ if own_conn:
475
+ conn.commit()
396
476
  return None
397
477
 
398
478
  except Exception as e:
399
- if conn:
479
+ if own_conn:
400
480
  try:
401
481
  conn.rollback()
402
482
  except Exception:
@@ -409,7 +489,7 @@ class PostgreSQLAdapter:
409
489
  cursor.close()
410
490
  except Exception:
411
491
  pass
412
- if conn:
492
+ if own_conn and conn:
413
493
  self._return_connection(conn)
414
494
 
415
495
  @contextmanager
@@ -7,6 +7,8 @@ import os
7
7
  import threading
8
8
  from typing import Any, Dict, List
9
9
 
10
+ from src.polydb.json_safe import json_safe
11
+
10
12
  class PubSubAdapter(QueueAdapter):
11
13
  """GCP Pub/Sub with client reuse"""
12
14
 
@@ -42,7 +44,7 @@ class PubSubAdapter(QueueAdapter):
42
44
  topic_path = self._publisher.topic_path(
43
45
  self.project_id, queue_name or self.topic_name
44
46
  )
45
- data = json.dumps(message).encode("utf-8")
47
+ data = json.dumps(message,default=json_safe).encode("utf-8")
46
48
  future = self._publisher.publish(topic_path, data)
47
49
  return future.result()
48
50
  return ""
@@ -12,6 +12,8 @@ import os
12
12
  import threading
13
13
  from typing import Any, Dict, List
14
14
 
15
+ from src.polydb.json_safe import json_safe
16
+
15
17
 
16
18
  class SQSAdapter(QueueAdapter):
17
19
  """AWS SQS with client reuse"""
@@ -45,7 +47,7 @@ class SQSAdapter(QueueAdapter):
45
47
  self._initialize_client()
46
48
  if self._client:
47
49
  response = self._client.send_message(
48
- QueueUrl=self.queue_url, MessageBody=json.dumps(message)
50
+ QueueUrl=self.queue_url, MessageBody=json.dumps(message,default=json_safe)
49
51
  )
50
52
  return response["MessageId"]
51
53
  return ""
@@ -3,6 +3,7 @@ import os
3
3
  import threading
4
4
  from typing import Any, Dict, List, Optional
5
5
  from polydb.base.NoSQLKVAdapter import NoSQLKVAdapter
6
+ from src.polydb.json_safe import json_safe
6
7
  from ..errors import NoSQLError, StorageError
7
8
  from ..retry import retry
8
9
  from ..types import JsonDict
@@ -36,7 +37,7 @@ class VercelKVAdapter(NoSQLKVAdapter):
36
37
  data_copy['_rk'] = rk
37
38
 
38
39
  # Check size
39
- data_bytes = json.dumps(data_copy).encode()
40
+ data_bytes = json.dumps(data_copy,default=json_safe).encode()
40
41
  data_size = len(data_bytes)
41
42
 
42
43
  if data_size > self.VERCEL_KV_MAX_SIZE:
@@ -69,7 +70,7 @@ class VercelKVAdapter(NoSQLKVAdapter):
69
70
  response = requests.post(
70
71
  f"{self.kv_url}/set/{key}",
71
72
  headers={'Authorization': f'Bearer {self.kv_token}'},
72
- json={'value': json.dumps(reference_data)},
73
+ json={'value': json.dumps(reference_data,default=json_safe)},
73
74
  timeout=self.timeout
74
75
  )
75
76
  response.raise_for_status()
@@ -78,7 +79,7 @@ class VercelKVAdapter(NoSQLKVAdapter):
78
79
  response = requests.post(
79
80
  f"{self.kv_url}/set/{key}",
80
81
  headers={'Authorization': f'Bearer {self.kv_token}'},
81
- json={'value': json.dumps(data_copy)},
82
+ json={'value': json.dumps(data_copy,default=json_safe)},
82
83
  timeout=self.timeout
83
84
  )
84
85
  response.raise_for_status()
@@ -7,6 +7,8 @@ import uuid
7
7
  import hashlib
8
8
  import json
9
9
 
10
+ from src.polydb.json_safe import json_safe
11
+
10
12
 
11
13
  @dataclass
12
14
  class AuditRecord:
@@ -80,7 +82,7 @@ class AuditRecord:
80
82
  )
81
83
 
82
84
  record.hash = hashlib.sha256(
83
- json.dumps(asdict(record), sort_keys=True).encode()
85
+ json.dumps(asdict(record), sort_keys=True,default=json_safe).encode()
84
86
  ).hexdigest()
85
87
 
86
88
  return record
@@ -6,6 +6,8 @@ import json
6
6
  import threading
7
7
  from typing import Any, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
8
8
 
9
+ from src.polydb.json_safe import json_safe
10
+
9
11
  from ..errors import NoSQLError, StorageError
10
12
  from ..retry import retry
11
13
  from ..query import QueryBuilder, Operator
@@ -36,7 +38,7 @@ class NoSQLKVAdapter:
36
38
  try:
37
39
  pk = self.partition_config.partition_key_template.format(**data)
38
40
  except KeyError:
39
- pk = f"default_{data.get(pk_field, hashlib.md5(json.dumps(data, sort_keys=True).encode()).hexdigest()[:8])}"
41
+ pk = f"default_{data.get(pk_field, hashlib.md5(json.dumps(data, sort_keys=True,default=json_safe).encode()).hexdigest()[:8])}"
40
42
  else:
41
43
  pk = str(data.get(pk_field, 'default'))
42
44
 
@@ -46,16 +48,16 @@ class NoSQLKVAdapter:
46
48
  try:
47
49
  rk = self.partition_config.row_key_template.format(**data)
48
50
  except KeyError:
49
- rk = hashlib.md5(json.dumps(data, sort_keys=True).encode()).hexdigest()
51
+ rk = hashlib.md5(json.dumps(data, sort_keys=True,default=json_safe).encode()).hexdigest()
50
52
  else:
51
- rk = data.get('id', hashlib.md5(json.dumps(data, sort_keys=True).encode()).hexdigest())
53
+ rk = data.get('id', hashlib.md5(json.dumps(data, sort_keys=True,default=json_safe).encode()).hexdigest())
52
54
 
53
55
  return str(pk), str(rk)
54
56
 
55
57
  @retry(max_attempts=3, delay=1.0, exceptions=(NoSQLError,))
56
58
  def _check_overflow(self, data: JsonDict) -> Tuple[JsonDict, Optional[str]]:
57
59
  """Check size and store in blob if needed"""
58
- data_bytes = json.dumps(data).encode()
60
+ data_bytes = json.dumps(data,default=json_safe).encode()
59
61
  data_size = len(data_bytes)
60
62
 
61
63
  if data_size > self.max_size:
@@ -292,7 +294,7 @@ class NoSQLKVAdapter:
292
294
  seen = set()
293
295
  unique = []
294
296
  for r in results:
295
- key = json.dumps(r, sort_keys=True)
297
+ key = json.dumps(r, sort_keys=True,default=json_safe)
296
298
  if key not in seen:
297
299
  seen.add(key)
298
300
  unique.append(r)
@@ -8,6 +8,7 @@ import hashlib
8
8
  import threading
9
9
  from enum import Enum
10
10
  import redis
11
+ from src.polydb.json_safe import json_safe
11
12
 
12
13
  class CacheStrategy(Enum):
13
14
  """Cache invalidation strategies"""
@@ -58,7 +59,7 @@ class RedisCacheEngine:
58
59
 
59
60
  def _make_key(self, model: str, query: Dict[str, Any]) -> str:
60
61
  """Generate cache key"""
61
- query_str = json.dumps(query, sort_keys=True)
62
+ query_str = json.dumps(query, sort_keys=True,default=json_safe)
62
63
  query_hash = hashlib.md5(query_str.encode()).hexdigest()
63
64
  return f"{self.prefix}{model}:{query_hash}"
64
65
 
@@ -94,7 +95,7 @@ class RedisCacheEngine:
94
95
  ttl = ttl or self.default_ttl
95
96
 
96
97
  try:
97
- data = json.dumps(value)
98
+ data = json.dumps(value,default=json_safe)
98
99
  self._client.setex(key, ttl, data)
99
100
 
100
101
  # Initialize access count
@@ -17,6 +17,7 @@ from .types import JsonDict, Lookup, ModelMeta
17
17
  from .audit.manager import AuditManager
18
18
  from .audit.context import AuditContext
19
19
  from .query import Operator, QueryBuilder
20
+ from dotenv import load_dotenv
20
21
 
21
22
  logger = logging.getLogger(__name__)
22
23
 
@@ -65,6 +66,11 @@ class DatabaseFactory:
65
66
  # Redis cache (only if explicitly enabled + URL present)
66
67
  self._cache: Optional[RedisCacheEngine] = None
67
68
  self.cache_warmer: Optional[CacheWarmer] = None
69
+ try:
70
+ load_dotenv()
71
+ except Exception:
72
+ pass
73
+
68
74
  if enable_cache and use_redis_cache:
69
75
  redis_url = os.getenv("REDIS_CACHE_URL")
70
76
  if redis_url:
@@ -108,17 +114,28 @@ class DatabaseFactory:
108
114
  return model.__name__ if isinstance(model, type) else str(model)
109
115
 
110
116
  def _current_tenant_id(self) -> Optional[str]:
111
- tenant = TenantContext.get_tenant()
112
- return tenant.tenant_id if tenant else None
117
+ # Prefer TenantContext if present
118
+ try:
119
+ tenant = TenantContext.get_tenant()
120
+ if tenant and tenant.tenant_id:
121
+ return tenant.tenant_id
122
+ except Exception:
123
+ pass
124
+
125
+ # Fallback to AuditContext
126
+ return AuditContext.tenant_id.get()
113
127
 
114
128
  def _current_actor_id(self) -> Optional[str]:
115
129
  return AuditContext.actor_id.get()
116
130
 
117
131
  def _inject_tenant(self, data: JsonDict) -> JsonDict:
118
132
  tenant_id = self._current_tenant_id()
119
- if tenant_id and "tenant_id" not in data:
120
- data = dict(data)
121
- data["tenant_id"] = tenant_id
133
+
134
+ if not tenant_id:
135
+ raise ValueError("Tenant ID is required but not set in AuditContext or TenantContext")
136
+
137
+ data = dict(data)
138
+ data.setdefault("tenant_id", tenant_id)
122
139
  return data
123
140
 
124
141
  def _inject_audit_fields(self, data: JsonDict, is_create: bool = False) -> JsonDict:
@@ -293,15 +310,17 @@ class DatabaseFactory:
293
310
  meta = self._meta(model)
294
311
  tenant_id = self._current_tenant_id()
295
312
  actor_id = self._current_actor_id()
296
-
297
313
  query = self._apply_soft_delete_filter(query if not include_deleted else None)
298
-
299
314
  # Multi-tenancy & RLS filters
300
315
  if self.tenant_enforcer:
301
316
  query = self.tenant_enforcer.enforce_read(model_name, query or {})
302
317
  if self.rls:
303
318
  query = self.rls.enforce_read(model_name, query or {})
304
-
319
+ # Inject tenant filter (mandatory isolation)
320
+ tenant_id = self._current_tenant_id()
321
+ if tenant_id:
322
+ query = dict(query or {})
323
+ query.setdefault("tenant_id", tenant_id)
305
324
  use_external_cache = self._enable_cache and self._cache and getattr(meta, "cache", False)
306
325
  encrypted_fields = getattr(meta, "encrypted_fields", [])
307
326
 
@@ -408,7 +427,11 @@ class DatabaseFactory:
408
427
  query = self.tenant_enforcer.enforce_read(model_name, query or {})
409
428
  if self.rls:
410
429
  query = self.rls.enforce_read(model_name, query or {})
411
-
430
+ # Inject tenant filter (mandatory isolation)
431
+ tenant_id = self._current_tenant_id()
432
+ if tenant_id:
433
+ query = dict(query or {})
434
+ query.setdefault("tenant_id", tenant_id)
412
435
  encrypted_fields = getattr(meta, "encrypted_fields", [])
413
436
 
414
437
  def _op() -> Tuple[List[JsonDict], Optional[str]]:
@@ -497,6 +520,9 @@ class DatabaseFactory:
497
520
  nonlocal after_plain, success
498
521
 
499
522
  if meta.storage == "sql" and meta.table:
523
+ if tenant_id:
524
+ if isinstance(entity_id, dict):
525
+ entity_id.setdefault("tenant_id", tenant_id)
500
526
  result = self._sql.update(meta.table, entity_id, data)
501
527
  else:
502
528
  model_type = self._model_type(model)
@@ -0,0 +1,8 @@
1
+ import json
2
+ from datetime import datetime
3
+
4
+ def json_safe(obj):
5
+ if isinstance(obj, datetime):
6
+ return obj.isoformat()
7
+ return str(obj)
8
+
@@ -4,7 +4,7 @@ Multi-tenancy enforcement and isolation
4
4
  """
5
5
  from typing import Dict, Any, List, Optional, Callable
6
6
  from contextvars import ContextVar
7
- from dataclasses import dataclass
7
+ from dataclasses import dataclass, field
8
8
  from enum import Enum
9
9
 
10
10
 
@@ -24,7 +24,7 @@ class TenantConfig:
24
24
  database_name: Optional[str] = None
25
25
  max_connections: int = 10
26
26
  storage_quota_gb: Optional[float] = None
27
- features: List[str] = []
27
+ features: List[str] = field(default_factory=list)
28
28
 
29
29
 
30
30
  class TenantRegistry:
@@ -11,6 +11,8 @@ import json
11
11
  from functools import wraps
12
12
  import logging
13
13
 
14
+ from src.polydb.json_safe import json_safe
15
+
14
16
  logger = logging.getLogger(__name__)
15
17
 
16
18
 
@@ -48,7 +50,7 @@ class FieldEncryption:
48
50
  """Encrypt arbitrary value (serialize if non-str)"""
49
51
  if value is None:
50
52
  return ""
51
- data = json.dumps(value) if not isinstance(value, str) else value
53
+ data = json.dumps(value,default=json_safe) if not isinstance(value, str) else value
52
54
  try:
53
55
  from cryptography.hazmat.primitives.ciphers.aead import AESGCM
54
56