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.
- {altcodepro_polydb_python-2.5.9.dist-info → altcodepro_polydb_python-2.5.11.dist-info}/METADATA +1 -1
- altcodepro_polydb_python-2.5.11.dist-info/RECORD +74 -0
- polydb/PolyDB.py +1 -1
- polydb/__init__.py +25 -14
- polydb/adapters/AzureBlobStorageAdapter.py +2 -1
- polydb/adapters/AzureQueueAdapter.py +3 -5
- polydb/adapters/AzureTableStorageAdapter.py +27 -9
- polydb/adapters/BlockchainBlobAdapter.py +0 -1
- polydb/adapters/BlockchainFileAdapter.py +1 -1
- polydb/adapters/BlockchainKVAdapter.py +20 -5
- polydb/adapters/BlockchainQueueAdapter.py +0 -1
- polydb/adapters/DynamoDBAdapter.py +2 -6
- polydb/adapters/EFSAdapter.py +1 -1
- polydb/adapters/GCPFilestoreAdapter.py +1 -1
- polydb/adapters/GCPPubSubAdapter.py +2 -2
- polydb/adapters/GCPStorageAdapter.py +1 -1
- polydb/adapters/KafkaQueueAdapter.py +10 -16
- polydb/adapters/MongoDBAdapter.py +3 -5
- polydb/adapters/PostgreSQLAdapter.py +38 -13
- polydb/adapters/RabbitMQAdapter.py +2 -2
- polydb/adapters/S3Adapter.py +1 -3
- polydb/adapters/S3CompatibleAdapter.py +1 -1
- polydb/adapters/SQSAdapter.py +1 -2
- polydb/adapters/VercelBlobAdapter.py +1 -1
- polydb/adapters/VercelKVAdapter.py +6 -5
- polydb/adapters/VercelQueueAdapter.py +18 -4
- polydb/advanced_query.py +5 -4
- polydb/aio.py +198 -0
- polydb/audit/AuditStorage.py +14 -19
- polydb/audit/__init__.py +2 -2
- polydb/audit/context.py +6 -5
- polydb/audit/manager.py +3 -3
- polydb/audit/models.py +3 -2
- polydb/base/NoSQLKVAdapter.py +22 -14
- polydb/base/ObjectStorageAdapter.py +3 -2
- polydb/base/QueueAdapter.py +2 -3
- polydb/base/SecretsAdapter.py +2 -1
- polydb/base/SharedFilesAdapter.py +2 -3
- polydb/batch.py +38 -49
- polydb/cache.py +5 -2
- polydb/cloudDatabaseFactory.py +59 -22
- polydb/databaseFactory.py +7 -13
- polydb/errors.py +2 -0
- polydb/json_safe.py +1 -2
- polydb/models.py +1 -1
- polydb/monitoring.py +27 -15
- polydb/multitenancy.py +50 -59
- polydb/observability/logging.py +0 -1
- polydb/overflow_gc.py +179 -0
- polydb/query.py +3 -1
- polydb/retry.py +1 -1
- polydb/schema.py +45 -60
- polydb/security.py +7 -9
- polydb/services/__init__.py +2 -2
- polydb/services/compliance_service.py +10 -4
- polydb/services/security_service.py +9 -12
- polydb/types.py +28 -62
- polydb/utils.py +7 -8
- polydb/validation.py +50 -52
- altcodepro_polydb_python-2.5.9.dist-info/RECORD +0 -73
- polydb/decorators.py +0 -1040
- {altcodepro_polydb_python-2.5.9.dist-info → altcodepro_polydb_python-2.5.11.dist-info}/WHEEL +0 -0
- {altcodepro_polydb_python-2.5.9.dist-info → altcodepro_polydb_python-2.5.11.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
polydb/cloudDatabaseFactory.py
CHANGED
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import threading
|
|
5
5
|
from typing import Dict, List, Optional
|
|
6
6
|
|
|
7
|
-
from .
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
|
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
|
|
228
|
-
return self.instances[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
|
364
|
-
|
|
|
365
|
-
|
|
|
366
|
-
|
|
|
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
polydb/models.py
CHANGED
polydb/monitoring.py
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Comprehensive monitoring, metrics, and observability
|
|
4
4
|
"""
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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)}
|