altcodepro-polydb-python 2.5.9__py3-none-any.whl → 2.5.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {altcodepro_polydb_python-2.5.9.dist-info → altcodepro_polydb_python-2.5.11.dist-info}/METADATA +1 -1
  2. altcodepro_polydb_python-2.5.11.dist-info/RECORD +74 -0
  3. polydb/PolyDB.py +1 -1
  4. polydb/__init__.py +25 -14
  5. polydb/adapters/AzureBlobStorageAdapter.py +2 -1
  6. polydb/adapters/AzureQueueAdapter.py +3 -5
  7. polydb/adapters/AzureTableStorageAdapter.py +27 -9
  8. polydb/adapters/BlockchainBlobAdapter.py +0 -1
  9. polydb/adapters/BlockchainFileAdapter.py +1 -1
  10. polydb/adapters/BlockchainKVAdapter.py +20 -5
  11. polydb/adapters/BlockchainQueueAdapter.py +0 -1
  12. polydb/adapters/DynamoDBAdapter.py +2 -6
  13. polydb/adapters/EFSAdapter.py +1 -1
  14. polydb/adapters/GCPFilestoreAdapter.py +1 -1
  15. polydb/adapters/GCPPubSubAdapter.py +2 -2
  16. polydb/adapters/GCPStorageAdapter.py +1 -1
  17. polydb/adapters/KafkaQueueAdapter.py +10 -16
  18. polydb/adapters/MongoDBAdapter.py +3 -5
  19. polydb/adapters/PostgreSQLAdapter.py +38 -13
  20. polydb/adapters/RabbitMQAdapter.py +2 -2
  21. polydb/adapters/S3Adapter.py +1 -3
  22. polydb/adapters/S3CompatibleAdapter.py +1 -1
  23. polydb/adapters/SQSAdapter.py +1 -2
  24. polydb/adapters/VercelBlobAdapter.py +1 -1
  25. polydb/adapters/VercelKVAdapter.py +6 -5
  26. polydb/adapters/VercelQueueAdapter.py +18 -4
  27. polydb/advanced_query.py +5 -4
  28. polydb/aio.py +198 -0
  29. polydb/audit/AuditStorage.py +14 -19
  30. polydb/audit/__init__.py +2 -2
  31. polydb/audit/context.py +6 -5
  32. polydb/audit/manager.py +3 -3
  33. polydb/audit/models.py +3 -2
  34. polydb/base/NoSQLKVAdapter.py +22 -14
  35. polydb/base/ObjectStorageAdapter.py +3 -2
  36. polydb/base/QueueAdapter.py +2 -3
  37. polydb/base/SecretsAdapter.py +2 -1
  38. polydb/base/SharedFilesAdapter.py +2 -3
  39. polydb/batch.py +38 -49
  40. polydb/cache.py +5 -2
  41. polydb/cloudDatabaseFactory.py +59 -22
  42. polydb/databaseFactory.py +7 -13
  43. polydb/errors.py +2 -0
  44. polydb/json_safe.py +1 -2
  45. polydb/models.py +1 -1
  46. polydb/monitoring.py +27 -15
  47. polydb/multitenancy.py +50 -59
  48. polydb/observability/logging.py +0 -1
  49. polydb/overflow_gc.py +179 -0
  50. polydb/query.py +3 -1
  51. polydb/retry.py +1 -1
  52. polydb/schema.py +45 -60
  53. polydb/security.py +7 -9
  54. polydb/services/__init__.py +2 -2
  55. polydb/services/compliance_service.py +10 -4
  56. polydb/services/security_service.py +9 -12
  57. polydb/types.py +28 -62
  58. polydb/utils.py +7 -8
  59. polydb/validation.py +50 -52
  60. altcodepro_polydb_python-2.5.9.dist-info/RECORD +0 -73
  61. polydb/decorators.py +0 -1040
  62. {altcodepro_polydb_python-2.5.9.dist-info → altcodepro_polydb_python-2.5.11.dist-info}/WHEEL +0 -0
  63. {altcodepro_polydb_python-2.5.9.dist-info → altcodepro_polydb_python-2.5.11.dist-info}/licenses/LICENSE +0 -0
  64. {altcodepro_polydb_python-2.5.9.dist-info → altcodepro_polydb_python-2.5.11.dist-info}/top_level.txt +0 -0
polydb/batch.py CHANGED
@@ -2,8 +2,10 @@
2
2
  """
3
3
  Batch operations for high-throughput scenarios
4
4
  """
5
- from typing import List, Dict, Any, Optional, Union
5
+
6
6
  from dataclasses import dataclass
7
+ from typing import Any, Dict, List, Optional, Union
8
+
7
9
  from .types import JsonDict
8
10
 
9
11
 
@@ -18,86 +20,76 @@ class BatchResult:
18
20
 
19
21
  class BatchOperations:
20
22
  """Batch CRUD operations"""
21
-
23
+
22
24
  def __init__(self, factory):
23
25
  self.factory = factory
24
-
26
+
25
27
  def bulk_insert(
26
28
  self,
27
29
  model: Union[type, str],
28
30
  records: List[JsonDict],
29
31
  *,
30
32
  chunk_size: int = 100,
31
- fail_fast: bool = False
33
+ fail_fast: bool = False,
32
34
  ) -> BatchResult:
33
35
  """Bulk insert with chunking"""
34
36
  succeeded = []
35
37
  failed = []
36
-
38
+
37
39
  for i in range(0, len(records), chunk_size):
38
- chunk = records[i:i + chunk_size]
39
-
40
+ chunk = records[i : i + chunk_size]
41
+
40
42
  for record in chunk:
41
43
  try:
42
44
  result = self.factory.create(model, record)
43
45
  succeeded.append(result)
44
46
  except Exception as e:
45
- failed.append({
46
- 'data': record,
47
- 'error': str(e)
48
- })
49
-
47
+ failed.append({"data": record, "error": str(e)})
48
+
50
49
  if fail_fast:
51
50
  break
52
-
51
+
53
52
  return BatchResult(
54
53
  succeeded=succeeded,
55
54
  failed=failed,
56
55
  total=len(records),
57
56
  success_count=len(succeeded),
58
- error_count=len(failed)
57
+ error_count=len(failed),
59
58
  )
60
-
59
+
61
60
  def bulk_update(
62
61
  self,
63
62
  model: Union[type, str],
64
63
  updates: List[Dict[str, Any]], # {entity_id, data}
65
64
  *,
66
65
  chunk_size: int = 100,
67
- fail_fast: bool = False
66
+ fail_fast: bool = False,
68
67
  ) -> BatchResult:
69
68
  """Bulk update"""
70
69
  succeeded = []
71
70
  failed = []
72
-
71
+
73
72
  for i in range(0, len(updates), chunk_size):
74
- chunk = updates[i:i + chunk_size]
75
-
73
+ chunk = updates[i : i + chunk_size]
74
+
76
75
  for update in chunk:
77
76
  try:
78
- result = self.factory.update(
79
- model,
80
- update['entity_id'],
81
- update['data']
82
- )
77
+ result = self.factory.update(model, update["entity_id"], update["data"])
83
78
  succeeded.append(result)
84
79
  except Exception as e:
85
- failed.append({
86
- 'data': update,
87
- 'error': str(e)
88
- })
89
-
80
+ failed.append({"data": update, "error": str(e)})
81
+
90
82
  if fail_fast:
91
83
  break
92
-
84
+
93
85
  return BatchResult(
94
86
  succeeded=succeeded,
95
87
  failed=failed,
96
88
  total=len(updates),
97
89
  success_count=len(succeeded),
98
- error_count=len(failed)
90
+ error_count=len(failed),
99
91
  )
100
-
92
+
101
93
  def bulk_delete(
102
94
  self,
103
95
  model: Union[type, str],
@@ -105,59 +97,56 @@ class BatchOperations:
105
97
  *,
106
98
  chunk_size: int = 100,
107
99
  fail_fast: bool = False,
108
- hard: bool = False
100
+ hard: bool = False,
109
101
  ) -> BatchResult:
110
102
  """Bulk delete"""
111
103
  succeeded = []
112
104
  failed = []
113
-
105
+
114
106
  for i in range(0, len(entity_ids), chunk_size):
115
- chunk = entity_ids[i:i + chunk_size]
116
-
107
+ chunk = entity_ids[i : i + chunk_size]
108
+
117
109
  for entity_id in chunk:
118
110
  try:
119
111
  result = self.factory.delete(model, entity_id, hard=hard)
120
112
  succeeded.append(result)
121
113
  except Exception as e:
122
- failed.append({
123
- 'data': {'entity_id': entity_id},
124
- 'error': str(e)
125
- })
126
-
114
+ failed.append({"data": {"entity_id": entity_id}, "error": str(e)})
115
+
127
116
  if fail_fast:
128
117
  break
129
-
118
+
130
119
  return BatchResult(
131
120
  succeeded=succeeded,
132
121
  failed=failed,
133
122
  total=len(entity_ids),
134
123
  success_count=len(succeeded),
135
- error_count=len(failed)
124
+ error_count=len(failed),
136
125
  )
137
126
 
138
127
 
139
128
  class TransactionManager:
140
129
  """Transaction support for SQL operations"""
141
-
130
+
142
131
  def __init__(self, sql_adapter):
143
132
  self.sql = sql_adapter
144
-
133
+
145
134
  def __enter__(self):
146
135
  self.conn = self.sql._get_connection()
147
136
  self.conn.autocommit = False
148
137
  return self
149
-
138
+
150
139
  def __exit__(self, exc_type, exc_val, exc_tb):
151
140
  if exc_type is None:
152
141
  self.conn.commit()
153
142
  else:
154
143
  self.conn.rollback()
155
-
144
+
156
145
  self.sql._return_connection(self.conn)
157
146
  return False
158
-
147
+
159
148
  def execute(self, sql: str, params: Optional[List[Any]] = None):
160
149
  """Execute within transaction"""
161
150
  cursor = self.conn.cursor()
162
151
  cursor.execute(sql, params or [])
163
- cursor.close()
152
+ cursor.close()
polydb/cache.py CHANGED
@@ -2,12 +2,15 @@
2
2
  """
3
3
  Advanced caching with Redis support and strategies
4
4
  """
5
- from typing import Any, Dict, Optional, List
6
- import json
5
+
7
6
  import hashlib
7
+ import json
8
8
  import threading
9
9
  from enum import Enum
10
+ from typing import Any, Dict, List, Optional
11
+
10
12
  import redis
13
+
11
14
  from .json_safe import json_safe
12
15
 
13
16
 
@@ -4,7 +4,7 @@ import os
4
4
  import threading
5
5
  from typing import Dict, List, Optional
6
6
 
7
- from .base import SharedFilesAdapter
7
+ from .errors import UnsupportedStorageTypeError
8
8
 
9
9
  # Deliberately NOT importing adapter classes at module level (they used to
10
10
  # be, redundantly -- every branch below already does its own lazy, local
@@ -27,7 +27,6 @@ from .models import (
27
27
  DynamoDBConfig,
28
28
  EFSFileConfig,
29
29
  FirestoreConfig,
30
- GCPFileConfig,
31
30
  GCPPubSubConfig,
32
31
  GCPSecretManagerConfig,
33
32
  GCPStorageConfig,
@@ -115,11 +114,14 @@ class CloudDatabaseFactory:
115
114
  name: str = "azure",
116
115
  container_name: Optional[str] = None,
117
116
  ) -> (
118
- AzureBlobStorageAdapter
119
- | S3CompatibleAdapter
120
- | GCPStorageAdapter
121
- | VercelBlobAdapter
122
- | BlockchainBlobAdapter
117
+ # noqa: F821 -- deliberately never imported at module level (see
118
+ # this file's own top comment); real only under `from __future__
119
+ # import annotations`, which this module has.
120
+ AzureBlobStorageAdapter # noqa: F821
121
+ | S3CompatibleAdapter # noqa: F821
122
+ | GCPStorageAdapter # noqa: F821
123
+ | VercelBlobAdapter # noqa: F821
124
+ | BlockchainBlobAdapter # noqa: F821
123
125
  ):
124
126
  with self._lock:
125
127
  # cache per (name, container) so different containers don't collide
@@ -223,9 +225,16 @@ class CloudDatabaseFactory:
223
225
  partition_config: Optional[PartitionConfig] = None,
224
226
  name: str = "kv",
225
227
  ):
228
+ # Cache per (name, partition_config) -- two calls with the same `name`
229
+ # but different partition configs used to silently share the first
230
+ # adapter built, with the second caller's partition_config thrown
231
+ # away. Mirrors the object::/secrets:: composite-key pattern used
232
+ # elsewhere in this file.
233
+ cache_key = f"kv::{name}::{self._partition_config_key(partition_config)}"
234
+
226
235
  with self._lock:
227
- if name in self.instances:
228
- return self.instances[name]
236
+ if cache_key in self.instances:
237
+ return self.instances[cache_key]
229
238
 
230
239
  cfg = self.configs.get(name)
231
240
  if not cfg:
@@ -240,7 +249,6 @@ class CloudDatabaseFactory:
240
249
 
241
250
  if isinstance(cfg, AzureTableConfig):
242
251
  connection_string = cfg.connection_string or ""
243
- table_name = cfg.table_name
244
252
  container_name = cfg.container_name
245
253
 
246
254
  instance = AzureTableStorageAdapter(
@@ -335,7 +343,7 @@ class CloudDatabaseFactory:
335
343
  )
336
344
 
337
345
  # ---------------- MONGODB ----------------
338
- else:
346
+ elif cfg.provider == CloudProvider.MONGODB:
339
347
  from .adapters.MongoDBAdapter import MongoDBAdapter
340
348
 
341
349
  mongo_uri = ""
@@ -351,19 +359,50 @@ class CloudDatabaseFactory:
351
359
  db_name=db_name,
352
360
  )
353
361
 
354
- self.instances[name] = instance
362
+ # ---------------- NO REAL KV BACKEND FOR THIS PROVIDER ----------
363
+ # Providers like POSTGRESQL / S3_COMPATIBLE / VAULT / KAFKA /
364
+ # RABBITMQ have no NoSQL KV adapter of their own. This branch
365
+ # used to be a bare `else -> MongoDBAdapter("", "")`, which built
366
+ # a Mongo client pointed at an empty URI and failed obscurely on
367
+ # first real use instead of here, at construction time, with a
368
+ # message naming the actual problem.
369
+ else:
370
+ raise UnsupportedStorageTypeError(
371
+ f"get_nosql_kv: provider {cfg.provider.value!r} has no NoSQL KV adapter. "
372
+ "Register an explicit StorageConfig for a supported KV provider "
373
+ "(azure, aws, gcp, vercel, mongodb, blockchain) under this `name`, "
374
+ "or use get_sql() for a PostgreSQL-only model."
375
+ )
376
+
377
+ self.instances[cache_key] = instance
355
378
  return instance
356
379
 
380
+ @staticmethod
381
+ def _partition_config_key(partition_config: Optional[PartitionConfig]) -> str:
382
+ """Stable string identity for a PartitionConfig, for use in a cache key."""
383
+ if partition_config is None:
384
+ return ""
385
+ return "|".join(
386
+ [
387
+ partition_config.partition_key_template,
388
+ partition_config.row_key_template or "",
389
+ ",".join(partition_config.composite_keys or []),
390
+ str(partition_config.auto_generate),
391
+ ]
392
+ )
393
+
357
394
  def get_queue(
358
395
  self, name="azure_queue"
359
396
  ) -> (
360
- AzureQueueAdapter
361
- | SQSAdapter
362
- | GCPPubSubAdapter
363
- | VercelQueueAdapter
364
- | BlockchainQueueAdapter
365
- | KafkaQueueAdapter
366
- | RabbitMQAdapter
397
+ # noqa: F821 -- same deliberate not-imported-at-module-level
398
+ # pattern as get_object_storage's own return annotation above.
399
+ AzureQueueAdapter # noqa: F821
400
+ | SQSAdapter # noqa: F821
401
+ | GCPPubSubAdapter # noqa: F821
402
+ | VercelQueueAdapter # noqa: F821
403
+ | BlockchainQueueAdapter # noqa: F821
404
+ | KafkaQueueAdapter # noqa: F821
405
+ | RabbitMQAdapter # noqa: F821
367
406
  ):
368
407
  with self._lock:
369
408
  if name in self.instances:
@@ -598,9 +637,7 @@ class CloudDatabaseFactory:
598
637
  url, token, mount_point = None, None, "secret"
599
638
  if isinstance(cfg, VaultConfig):
600
639
  url, token, mount_point = cfg.url, cfg.token, cfg.mount_point
601
- instance = VaultAdapter(
602
- url=url or "", token=token or "", mount_point=mount_point
603
- )
640
+ instance = VaultAdapter(url=url or "", token=token or "", mount_point=mount_point)
604
641
 
605
642
  self.instances[cache_key] = instance
606
643
  return instance
polydb/databaseFactory.py CHANGED
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
 
15
15
  import logging
16
16
  import os
17
+ import re as _re
17
18
  from dataclasses import dataclass, field
18
19
  from datetime import datetime
19
20
  from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
@@ -21,21 +22,18 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
21
22
  from tenacity import retry, stop_after_attempt, wait_exponential
22
23
 
23
24
  from .adapters.PostgreSQLAdapter import PostgreSQLAdapter
24
-
25
+ from .audit.context import AuditContext
26
+ from .audit.manager import AuditManager
25
27
  from .base.NoSQLKVAdapter import NoSQLKVAdapter
26
-
27
28
  from .batch import BatchOperations
28
29
  from .cache import CacheWarmer, RedisCacheEngine
30
+ from .cloudDatabaseFactory import CloudDatabaseFactory
31
+ from .errors import AdapterConfigurationError
32
+ from .models import PageRequest, PageResult
29
33
  from .monitoring import HealthCheck, MetricsCollector, PerformanceMonitor
34
+ from .query import QueryBuilder
30
35
  from .security import DataMasking, FieldEncryption
31
- from .errors import AdapterConfigurationError
32
36
  from .types import JsonDict, Lookup, ModelMeta
33
- from .audit.manager import AuditManager
34
- from .audit.context import AuditContext
35
- from .query import Operator, QueryBuilder
36
- from .cloudDatabaseFactory import CloudDatabaseFactory
37
- from .models import PageRequest, PageResult
38
- import re as _re
39
37
 
40
38
  logger = logging.getLogger(__name__)
41
39
 
@@ -382,7 +380,6 @@ class DatabaseFactory:
382
380
  adapters = self._adapters_for(model, meta, engine_override)
383
381
  after_plain = None
384
382
  success = False
385
- error: Optional[str] = None
386
383
  entity_id: Optional[Any] = None
387
384
 
388
385
  def _op() -> JsonDict:
@@ -559,7 +556,6 @@ class DatabaseFactory:
559
556
  )
560
557
  after_plain = None
561
558
  success = False
562
- error: Optional[str] = None
563
559
 
564
560
  def _op() -> JsonDict:
565
561
  nonlocal after_plain, success
@@ -687,7 +683,6 @@ class DatabaseFactory:
687
683
  adapters = self._adapters_for(model, meta, engine_override)
688
684
  after_plain = None
689
685
  success = False
690
- error: Optional[str] = None
691
686
 
692
687
  def _op() -> JsonDict:
693
688
  nonlocal after_plain, success
@@ -763,7 +758,6 @@ class DatabaseFactory:
763
758
  session_vars=session_vars,
764
759
  )
765
760
  success = False
766
- error: Optional[str] = None
767
761
 
768
762
  def _op() -> JsonDict:
769
763
  nonlocal success
polydb/errors.py CHANGED
@@ -2,6 +2,7 @@
2
2
  """
3
3
  Structured exceptions for cloud database operations
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
 
@@ -52,6 +53,7 @@ class ValidationError(CloudDBError):
52
53
 
53
54
  pass
54
55
 
56
+
55
57
  class PolyDBError(Exception):
56
58
  """Base exception for polydb."""
57
59
 
polydb/json_safe.py CHANGED
@@ -1,8 +1,7 @@
1
- import json
2
1
  from datetime import datetime
3
2
 
3
+
4
4
  def json_safe(obj):
5
5
  if isinstance(obj, datetime):
6
6
  return obj.isoformat()
7
7
  return str(obj)
8
-
polydb/models.py CHANGED
@@ -4,8 +4,8 @@ Data models and configurations
4
4
  """
5
5
 
6
6
  from dataclasses import dataclass, field
7
- from typing import Optional, List, Callable, Any, Dict
8
7
  from enum import Enum
8
+ from typing import Any, Callable, Dict, List, Optional
9
9
 
10
10
 
11
11
  class CloudProvider(Enum):
polydb/monitoring.py CHANGED
@@ -2,13 +2,13 @@
2
2
  """
3
3
  Comprehensive monitoring, metrics, and observability
4
4
  """
5
- from typing import Dict, Any, Optional, List, Callable
6
- from dataclasses import dataclass, field
7
- from datetime import datetime, timedelta
8
- from collections import defaultdict
5
+
6
+ import logging
9
7
  import threading
10
8
  import time
11
- import logging
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timedelta
11
+ from typing import Any, Callable, Dict, List, Optional
12
12
 
13
13
 
14
14
  @dataclass
@@ -154,25 +154,25 @@ class MetricsCollector:
154
154
  agg = self.aggregate()
155
155
 
156
156
  lines = [
157
- f"# HELP polydb_queries_total Total number of queries",
158
- f"# TYPE polydb_queries_total counter",
157
+ "# HELP polydb_queries_total Total number of queries",
158
+ "# TYPE polydb_queries_total counter",
159
159
  f"polydb_queries_total {agg.total_queries}",
160
160
  "",
161
- f"# HELP polydb_queries_successful Successful queries",
162
- f"# TYPE polydb_queries_successful counter",
161
+ "# HELP polydb_queries_successful Successful queries",
162
+ "# TYPE polydb_queries_successful counter",
163
163
  f"polydb_queries_successful {agg.successful_queries}",
164
164
  "",
165
- f"# HELP polydb_queries_failed Failed queries",
166
- f"# TYPE polydb_queries_failed counter",
165
+ "# HELP polydb_queries_failed Failed queries",
166
+ "# TYPE polydb_queries_failed counter",
167
167
  f"polydb_queries_failed {agg.failed_queries}",
168
168
  "",
169
- f"# HELP polydb_query_duration_ms Query duration",
170
- f"# TYPE polydb_query_duration_ms summary",
169
+ "# HELP polydb_query_duration_ms Query duration",
170
+ "# TYPE polydb_query_duration_ms summary",
171
171
  f"polydb_query_duration_ms_sum {agg.total_duration_ms}",
172
172
  f"polydb_query_duration_ms_count {agg.total_queries}",
173
173
  "",
174
- f"# HELP polydb_cache_hit_rate Cache hit rate",
175
- f"# TYPE polydb_cache_hit_rate gauge",
174
+ "# HELP polydb_cache_hit_rate Cache hit rate",
175
+ "# TYPE polydb_cache_hit_rate gauge",
176
176
  f"polydb_cache_hit_rate {agg.cache_hit_rate}",
177
177
  ]
178
178
 
@@ -284,6 +284,18 @@ class HealthCheck:
284
284
  retrieved = self.factory._cache.get(test_key, {})
285
285
  duration_ms = (time.perf_counter() - start) * 1000
286
286
 
287
+ # A real, found-by-adding-CI-lint gap: `retrieved` was read
288
+ # back and then never actually checked against what was
289
+ # written -- set()/get() not raising is not the same thing as
290
+ # the cache round-tripping the value correctly, so a cache
291
+ # silently returning stale/empty/wrong data still reported
292
+ # "healthy" here as long as neither call raised.
293
+ if retrieved != test_value:
294
+ return {
295
+ "status": "unhealthy",
296
+ "error": f"cache round-trip mismatch: wrote {test_value!r}, read back {retrieved!r}",
297
+ }
298
+
287
299
  return {"status": "healthy", "latency_ms": duration_ms}
288
300
  except Exception as e:
289
301
  return {"status": "unhealthy", "error": str(e)}