altcodepro-polydb-python 2.2.8__tar.gz → 2.3.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.
- {altcodepro_polydb_python-2.2.8/src/altcodepro_polydb_python.egg-info → altcodepro_polydb_python-2.3.0}/PKG-INFO +1 -1
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/pyproject.toml +1 -1
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0/src/altcodepro_polydb_python.egg-info}/PKG-INFO +1 -1
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/AzureQueueAdapter.py +14 -2
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/cloudDatabaseFactory.py +13 -2
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/databaseFactory.py +6 -7
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/monitoring.py +72 -87
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/LICENSE +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/MANIFEST.in +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/README.md +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/example_usage.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/requirements-aws.txt +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/requirements-azure.txt +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/requirements-dev.txt +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/requirements-gcp.txt +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/requirements-generic.txt +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/requirements.txt +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/setup.cfg +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/setup.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/altcodepro_polydb_python.egg-info/SOURCES.txt +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/altcodepro_polydb_python.egg-info/dependency_links.txt +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/altcodepro_polydb_python.egg-info/requires.txt +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/altcodepro_polydb_python.egg-info/top_level.txt +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/PolyDB.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/__init__.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/AzureBlobStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/AzureFileStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/AzureTableStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/BlockchainBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/BlockchainKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/BlockchainQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/DynamoDBAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/EFSAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/FirestoreAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/GCPPubSubAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/GCPStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/MongoDBAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/PostgreSQLAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/S3Adapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/S3CompatibleAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/SQSAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/VercelBlobAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/VercelKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/VercelQueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/__init__.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/advanced_query.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/audit/AuditStorage.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/audit/__init__.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/audit/context.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/audit/manager.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/audit/models.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/base/NoSQLKVAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/base/ObjectStorageAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/base/QueueAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/base/SharedFilesAdapter.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/base/__init__.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/batch.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/cache.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/decorators.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/errors.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/json_safe.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/models.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/multitenancy.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/py.typed +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/query.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/registry.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/retry.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/schema.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/security.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/types.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/utils.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/validation.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/tests/test_aws.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/tests/test_azure.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/tests/test_blockchain.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/tests/test_cloud_factory.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/tests/test_gcp.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/tests/test_mongodb.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/tests/test_multi_engine.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/tests/test_postgresql.py +0 -0
- {altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/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
|
+
Version: 2.3.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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "altcodepro-polydb-python"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
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.8"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: altcodepro-polydb-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import threading
|
|
5
5
|
import json
|
|
6
|
+
import re
|
|
7
|
+
|
|
6
8
|
from typing import Any, Dict, List, Optional
|
|
7
9
|
|
|
8
10
|
from azure.storage.queue import QueueServiceClient, QueueClient
|
|
@@ -40,6 +42,12 @@ class AzureQueueAdapter(QueueAdapter):
|
|
|
40
42
|
|
|
41
43
|
self._initialize_client()
|
|
42
44
|
|
|
45
|
+
def _normalize_queue_name(self, name: str) -> str:
|
|
46
|
+
name = name.lower()
|
|
47
|
+
name = re.sub(r"[^a-z0-9-]", "-", name) # replace invalid chars
|
|
48
|
+
name = re.sub(r"-+", "-", name) # collapse multiple dashes
|
|
49
|
+
return name.strip("-")
|
|
50
|
+
|
|
43
51
|
def _initialize_client(self) -> None:
|
|
44
52
|
"""Initialize Azure Queue client"""
|
|
45
53
|
try:
|
|
@@ -59,7 +67,7 @@ class AzureQueueAdapter(QueueAdapter):
|
|
|
59
67
|
"""Get or create queue client"""
|
|
60
68
|
if self._client is None:
|
|
61
69
|
raise ConnectionError("Azure Queue client not initialized")
|
|
62
|
-
|
|
70
|
+
queue_name = self._normalize_queue_name(queue_name)
|
|
63
71
|
if queue_name not in self._queues:
|
|
64
72
|
queue_client = self._client.get_queue_client(queue_name)
|
|
65
73
|
|
|
@@ -76,6 +84,7 @@ class AzureQueueAdapter(QueueAdapter):
|
|
|
76
84
|
def send(self, message: Dict[str, Any], queue_name: str = "default") -> str:
|
|
77
85
|
"""Send message to queue"""
|
|
78
86
|
try:
|
|
87
|
+
queue_name = self._normalize_queue_name(queue_name)
|
|
79
88
|
queue_client = self._get_queue(queue_name)
|
|
80
89
|
|
|
81
90
|
response = queue_client.send_message(json.dumps(message, default=json_safe))
|
|
@@ -89,6 +98,8 @@ class AzureQueueAdapter(QueueAdapter):
|
|
|
89
98
|
def receive(self, queue_name: str = "default", max_messages: int = 1) -> List[Dict[str, Any]]:
|
|
90
99
|
"""Receive messages"""
|
|
91
100
|
try:
|
|
101
|
+
|
|
102
|
+
queue_name = self._normalize_queue_name(queue_name)
|
|
92
103
|
queue_client = self._get_queue(queue_name)
|
|
93
104
|
|
|
94
105
|
messages = queue_client.receive_messages(max_messages=max_messages)
|
|
@@ -113,6 +124,7 @@ class AzureQueueAdapter(QueueAdapter):
|
|
|
113
124
|
def delete(self, message_id: str, queue_name: str = "default", pop_receipt: str = "") -> bool:
|
|
114
125
|
"""Delete message from queue"""
|
|
115
126
|
try:
|
|
127
|
+
queue_name = self._normalize_queue_name(queue_name)
|
|
116
128
|
queue_client = self._get_queue(queue_name)
|
|
117
129
|
|
|
118
130
|
queue_client.delete_message(message_id, pop_receipt)
|
|
@@ -144,7 +156,7 @@ class AzureQueueAdapter(QueueAdapter):
|
|
|
144
156
|
"""
|
|
145
157
|
if not message_id:
|
|
146
158
|
raise QueueError("AzureQueueAdapter.ack requires message_id")
|
|
147
|
-
|
|
159
|
+
queue_name = self._normalize_queue_name(queue_name)
|
|
148
160
|
return self.delete(
|
|
149
161
|
message_id=message_id,
|
|
150
162
|
queue_name=queue_name,
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/cloudDatabaseFactory.py
RENAMED
|
@@ -4,7 +4,8 @@ import os
|
|
|
4
4
|
import threading
|
|
5
5
|
from typing import Dict, List, Optional
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
from .adapters.AzureFileStorageAdapter import AzureFileStorageAdapter
|
|
8
|
+
from .adapters.EFSAdapter import EFSAdapter
|
|
8
9
|
from .adapters.PostgreSQLAdapter import PostgreSQLAdapter
|
|
9
10
|
from .adapters.AzureBlobStorageAdapter import AzureBlobStorageAdapter
|
|
10
11
|
from .adapters.BlockchainBlobAdapter import BlockchainBlobAdapter
|
|
@@ -441,7 +442,17 @@ class CloudDatabaseFactory:
|
|
|
441
442
|
self.instances["queue"] = instance
|
|
442
443
|
return instance
|
|
443
444
|
|
|
444
|
-
def get_files(
|
|
445
|
+
def get_files(
|
|
446
|
+
self, name: str = "files"
|
|
447
|
+
) -> (
|
|
448
|
+
AzureFileStorageAdapter
|
|
449
|
+
| EFSAdapter
|
|
450
|
+
| GCPStorageAdapter
|
|
451
|
+
| AzureBlobStorageAdapter
|
|
452
|
+
| S3CompatibleAdapter
|
|
453
|
+
| VercelBlobAdapter
|
|
454
|
+
| BlockchainBlobAdapter
|
|
455
|
+
):
|
|
445
456
|
with self._lock:
|
|
446
457
|
if name in self.instances:
|
|
447
458
|
return self.instances[name]
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/databaseFactory.py
RENAMED
|
@@ -110,8 +110,6 @@ def _extract_meta(model: Union[type, str]) -> ModelMeta:
|
|
|
110
110
|
provider=raw.get("provider"),
|
|
111
111
|
cache=raw.get("cache", False),
|
|
112
112
|
cache_ttl=raw.get("cache_ttl"),
|
|
113
|
-
encrypted_fields=raw.get("encrypted_fields", []),
|
|
114
|
-
soft_delete=raw.get("soft_delete", False),
|
|
115
113
|
)
|
|
116
114
|
# Default for dynamic/string models
|
|
117
115
|
return ModelMeta(storage="nosql", table=None, collection=None)
|
|
@@ -147,6 +145,7 @@ class DatabaseFactory:
|
|
|
147
145
|
# Multi-engine
|
|
148
146
|
engines: Optional[List[EngineConfig]] = None,
|
|
149
147
|
# Feature flags
|
|
148
|
+
redis_cache_url: Optional[str] = None,
|
|
150
149
|
enable_retries: bool = True,
|
|
151
150
|
enable_audit: bool = True,
|
|
152
151
|
enable_audit_reads: bool = False,
|
|
@@ -170,7 +169,7 @@ class DatabaseFactory:
|
|
|
170
169
|
self._cache: Optional[RedisCacheEngine] = None
|
|
171
170
|
self.cache_warmer: Optional[CacheWarmer] = None
|
|
172
171
|
if enable_cache and use_redis_cache:
|
|
173
|
-
redis_url = os.getenv("REDIS_CACHE_URL")
|
|
172
|
+
redis_url = redis_cache_url or os.getenv("REDIS_CACHE_URL") or os.getenv("REDIS_URL")
|
|
174
173
|
if redis_url:
|
|
175
174
|
self._cache = RedisCacheEngine(redis_url=redis_url)
|
|
176
175
|
self.cache_warmer = CacheWarmer(self, self._cache)
|
|
@@ -480,13 +479,13 @@ class DatabaseFactory:
|
|
|
480
479
|
)
|
|
481
480
|
if self.encryption and encrypted_fields:
|
|
482
481
|
raw = [self.encryption.decrypt_fields(r, encrypted_fields) for r in raw]
|
|
483
|
-
if use_external_cache and not no_cache:
|
|
482
|
+
if self._cache and use_external_cache and not no_cache:
|
|
484
483
|
ttl = cache_ttl or getattr(meta, "cache_ttl", 300)
|
|
485
484
|
self._cache.set(name, query or {}, raw, ttl)
|
|
486
485
|
return raw
|
|
487
486
|
|
|
488
487
|
# Check external cache first
|
|
489
|
-
if use_external_cache and not no_cache:
|
|
488
|
+
if self._cache and use_external_cache and not no_cache:
|
|
490
489
|
cached = self._cache.get(name, query or {})
|
|
491
490
|
if cached is not None:
|
|
492
491
|
return cached
|
|
@@ -923,11 +922,11 @@ class DatabaseFactory:
|
|
|
923
922
|
self, path: str, data: Union[bytes, str], *, adapter_name: str = "files"
|
|
924
923
|
) -> bool:
|
|
925
924
|
files = self._engines[0].cloud_factory.get_files(adapter_name)
|
|
926
|
-
return files.write(path, data.encode() if isinstance(data, str) else data)
|
|
925
|
+
return files.write(path, data.encode() if isinstance(data, str) else data) # type: ignore
|
|
927
926
|
|
|
928
927
|
def read_file(self, path: str, *, adapter_name: str = "files") -> Optional[bytes]:
|
|
929
928
|
files = self._engines[0].cloud_factory.get_files(adapter_name)
|
|
930
|
-
return files.read(path)
|
|
929
|
+
return files.read(path) # type: ignore
|
|
931
930
|
|
|
932
931
|
def delete_file(self, path: str, *, adapter_name: str = "files") -> bool:
|
|
933
932
|
files = self._engines[0].cloud_factory.get_files(adapter_name)
|
|
@@ -14,6 +14,7 @@ import logging
|
|
|
14
14
|
@dataclass
|
|
15
15
|
class QueryMetrics:
|
|
16
16
|
"""Metrics for a single query"""
|
|
17
|
+
|
|
17
18
|
operation: str
|
|
18
19
|
model: str
|
|
19
20
|
duration_ms: float
|
|
@@ -29,12 +30,13 @@ class QueryMetrics:
|
|
|
29
30
|
@dataclass
|
|
30
31
|
class AggregatedMetrics:
|
|
31
32
|
"""Aggregated metrics over time window"""
|
|
33
|
+
|
|
32
34
|
total_queries: int = 0
|
|
33
35
|
successful_queries: int = 0
|
|
34
36
|
failed_queries: int = 0
|
|
35
37
|
total_duration_ms: float = 0.0
|
|
36
38
|
avg_duration_ms: float = 0.0
|
|
37
|
-
min_duration_ms: float = float(
|
|
39
|
+
min_duration_ms: float = float("inf")
|
|
38
40
|
max_duration_ms: float = 0.0
|
|
39
41
|
cache_hit_rate: float = 0.0
|
|
40
42
|
queries_by_operation: Dict[str, int] = field(default_factory=dict)
|
|
@@ -44,117 +46,113 @@ class AggregatedMetrics:
|
|
|
44
46
|
|
|
45
47
|
class MetricsCollector:
|
|
46
48
|
"""Collects and aggregates metrics"""
|
|
47
|
-
|
|
49
|
+
|
|
48
50
|
def __init__(self, slow_query_threshold_ms: float = 1000.0):
|
|
49
51
|
self.slow_query_threshold = slow_query_threshold_ms
|
|
50
52
|
self._metrics: List[QueryMetrics] = []
|
|
51
53
|
self._lock = threading.Lock()
|
|
52
54
|
self._hooks: List[Callable] = []
|
|
53
55
|
self.logger = logging.getLogger(__name__)
|
|
54
|
-
|
|
56
|
+
|
|
55
57
|
def record(self, metric: QueryMetrics):
|
|
56
58
|
"""Record a query metric"""
|
|
57
59
|
with self._lock:
|
|
58
60
|
self._metrics.append(metric)
|
|
59
|
-
|
|
61
|
+
|
|
60
62
|
# Log slow queries
|
|
61
63
|
if metric.duration_ms > self.slow_query_threshold:
|
|
62
64
|
self.logger.warning(
|
|
63
65
|
f"Slow query detected: {metric.operation} on {metric.model} "
|
|
64
66
|
f"took {metric.duration_ms:.2f}ms"
|
|
65
67
|
)
|
|
66
|
-
|
|
68
|
+
|
|
67
69
|
# Trigger hooks
|
|
68
70
|
for hook in self._hooks:
|
|
69
71
|
try:
|
|
70
72
|
hook(metric)
|
|
71
73
|
except Exception as e:
|
|
72
74
|
self.logger.error(f"Metrics hook failed: {e}")
|
|
73
|
-
|
|
75
|
+
|
|
74
76
|
def register_hook(self, hook: Callable[[QueryMetrics], None]):
|
|
75
77
|
"""Register a metrics hook"""
|
|
76
78
|
self._hooks.append(hook)
|
|
77
|
-
|
|
79
|
+
|
|
78
80
|
def get_metrics(
|
|
79
81
|
self,
|
|
80
82
|
since: Optional[datetime] = None,
|
|
81
83
|
model: Optional[str] = None,
|
|
82
|
-
operation: Optional[str] = None
|
|
84
|
+
operation: Optional[str] = None,
|
|
83
85
|
) -> List[QueryMetrics]:
|
|
84
86
|
"""Get filtered metrics"""
|
|
85
87
|
with self._lock:
|
|
86
88
|
metrics = self._metrics.copy()
|
|
87
|
-
|
|
89
|
+
|
|
88
90
|
if since:
|
|
89
91
|
metrics = [m for m in metrics if m.timestamp >= since]
|
|
90
|
-
|
|
92
|
+
|
|
91
93
|
if model:
|
|
92
94
|
metrics = [m for m in metrics if m.model == model]
|
|
93
|
-
|
|
95
|
+
|
|
94
96
|
if operation:
|
|
95
97
|
metrics = [m for m in metrics if m.operation == operation]
|
|
96
|
-
|
|
98
|
+
|
|
97
99
|
return metrics
|
|
98
|
-
|
|
100
|
+
|
|
99
101
|
def aggregate(
|
|
100
|
-
self,
|
|
101
|
-
since: Optional[datetime] = None,
|
|
102
|
-
model: Optional[str] = None
|
|
102
|
+
self, since: Optional[datetime] = None, model: Optional[str] = None
|
|
103
103
|
) -> AggregatedMetrics:
|
|
104
104
|
"""Generate aggregated metrics"""
|
|
105
105
|
metrics = self.get_metrics(since=since, model=model)
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
if not metrics:
|
|
108
108
|
return AggregatedMetrics()
|
|
109
|
-
|
|
109
|
+
|
|
110
110
|
agg = AggregatedMetrics()
|
|
111
111
|
agg.total_queries = len(metrics)
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
durations = []
|
|
114
114
|
cache_hits = 0
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
for m in metrics:
|
|
117
117
|
if m.success:
|
|
118
118
|
agg.successful_queries += 1
|
|
119
119
|
else:
|
|
120
120
|
agg.failed_queries += 1
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
durations.append(m.duration_ms)
|
|
123
123
|
agg.total_duration_ms += m.duration_ms
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
if m.cache_hit:
|
|
126
126
|
cache_hits += 1
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
# Count by operation
|
|
129
|
-
agg.queries_by_operation[m.operation] =
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
agg.queries_by_operation[m.operation] = agg.queries_by_operation.get(m.operation, 0) + 1
|
|
130
|
+
|
|
132
131
|
# Count by model
|
|
133
|
-
agg.queries_by_model[m.model] =
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
agg.queries_by_model[m.model] = agg.queries_by_model.get(m.model, 0) + 1
|
|
133
|
+
|
|
136
134
|
# Track slow queries
|
|
137
135
|
if m.duration_ms > self.slow_query_threshold:
|
|
138
136
|
agg.slow_queries.append(m)
|
|
139
|
-
|
|
137
|
+
|
|
140
138
|
agg.avg_duration_ms = agg.total_duration_ms / agg.total_queries
|
|
141
139
|
agg.min_duration_ms = min(durations)
|
|
142
140
|
agg.max_duration_ms = max(durations)
|
|
143
141
|
agg.cache_hit_rate = cache_hits / agg.total_queries
|
|
144
|
-
|
|
142
|
+
|
|
145
143
|
return agg
|
|
146
|
-
|
|
144
|
+
|
|
147
145
|
def clear_old_metrics(self, older_than: timedelta):
|
|
148
146
|
"""Clear metrics older than specified duration"""
|
|
149
147
|
cutoff = datetime.utcnow() - older_than
|
|
150
|
-
|
|
148
|
+
|
|
151
149
|
with self._lock:
|
|
152
150
|
self._metrics = [m for m in self._metrics if m.timestamp >= cutoff]
|
|
153
|
-
|
|
151
|
+
|
|
154
152
|
def export_prometheus(self) -> str:
|
|
155
153
|
"""Export metrics in Prometheus format"""
|
|
156
154
|
agg = self.aggregate()
|
|
157
|
-
|
|
155
|
+
|
|
158
156
|
lines = [
|
|
159
157
|
f"# HELP polydb_queries_total Total number of queries",
|
|
160
158
|
f"# TYPE polydb_queries_total counter",
|
|
@@ -177,20 +175,20 @@ class MetricsCollector:
|
|
|
177
175
|
f"# TYPE polydb_cache_hit_rate gauge",
|
|
178
176
|
f"polydb_cache_hit_rate {agg.cache_hit_rate}",
|
|
179
177
|
]
|
|
180
|
-
|
|
178
|
+
|
|
181
179
|
return "\n".join(lines)
|
|
182
180
|
|
|
183
181
|
|
|
184
182
|
class PerformanceMonitor:
|
|
185
183
|
"""Context manager for automatic query timing"""
|
|
186
|
-
|
|
184
|
+
|
|
187
185
|
def __init__(
|
|
188
186
|
self,
|
|
189
187
|
collector: MetricsCollector,
|
|
190
188
|
operation: str,
|
|
191
189
|
model: str,
|
|
192
190
|
tenant_id: Optional[str] = None,
|
|
193
|
-
actor_id: Optional[str] = None
|
|
191
|
+
actor_id: Optional[str] = None,
|
|
194
192
|
):
|
|
195
193
|
self.collector = collector
|
|
196
194
|
self.operation = operation
|
|
@@ -200,21 +198,22 @@ class PerformanceMonitor:
|
|
|
200
198
|
self.start_time = None
|
|
201
199
|
self.success = False
|
|
202
200
|
self.error = None
|
|
203
|
-
self.rows_affected =
|
|
201
|
+
self.rows_affected = 0
|
|
202
|
+
self.rows_returned = 0
|
|
204
203
|
self.cache_hit = False
|
|
205
|
-
|
|
204
|
+
|
|
206
205
|
def __enter__(self):
|
|
207
206
|
self.start_time = time.perf_counter()
|
|
208
207
|
return self
|
|
209
|
-
|
|
208
|
+
|
|
210
209
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
211
|
-
duration_ms = (time.perf_counter() - self.start_time) * 1000
|
|
212
|
-
|
|
210
|
+
duration_ms = (time.perf_counter() - self.start_time) * 1000 # type: ignore
|
|
211
|
+
|
|
213
212
|
if exc_type is None:
|
|
214
213
|
self.success = True
|
|
215
214
|
else:
|
|
216
215
|
self.error = str(exc_val)
|
|
217
|
-
|
|
216
|
+
|
|
218
217
|
metric = QueryMetrics(
|
|
219
218
|
operation=self.operation,
|
|
220
219
|
model=self.model,
|
|
@@ -224,40 +223,36 @@ class PerformanceMonitor:
|
|
|
224
223
|
tenant_id=self.tenant_id,
|
|
225
224
|
actor_id=self.actor_id,
|
|
226
225
|
rows_affected=self.rows_affected,
|
|
227
|
-
cache_hit=self.cache_hit
|
|
226
|
+
cache_hit=self.cache_hit,
|
|
228
227
|
)
|
|
229
|
-
|
|
228
|
+
|
|
230
229
|
self.collector.record(metric)
|
|
231
|
-
|
|
230
|
+
|
|
232
231
|
return False
|
|
233
232
|
|
|
234
233
|
|
|
235
234
|
class HealthCheck:
|
|
236
235
|
"""System health monitoring"""
|
|
237
|
-
|
|
236
|
+
|
|
238
237
|
def __init__(self, factory):
|
|
239
238
|
self.factory = factory
|
|
240
239
|
self.logger = logging.getLogger(__name__)
|
|
241
|
-
|
|
240
|
+
|
|
242
241
|
def check_sql_health(self) -> Dict[str, Any]:
|
|
243
242
|
"""Check SQL database health"""
|
|
244
243
|
try:
|
|
245
244
|
start = time.perf_counter()
|
|
246
245
|
self.factory._sql.execute("SELECT 1", fetch_one=True)
|
|
247
246
|
duration_ms = (time.perf_counter() - start) * 1000
|
|
248
|
-
|
|
247
|
+
|
|
249
248
|
return {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
249
|
+
"status": "healthy",
|
|
250
|
+
"latency_ms": duration_ms,
|
|
251
|
+
"provider": self.factory._provider_name,
|
|
253
252
|
}
|
|
254
253
|
except Exception as e:
|
|
255
|
-
return {
|
|
256
|
-
|
|
257
|
-
'error': str(e),
|
|
258
|
-
'provider': self.factory._provider_name
|
|
259
|
-
}
|
|
260
|
-
|
|
254
|
+
return {"status": "unhealthy", "error": str(e), "provider": self.factory._provider_name}
|
|
255
|
+
|
|
261
256
|
def check_nosql_health(self) -> Dict[str, Any]:
|
|
262
257
|
"""Check NoSQL database health"""
|
|
263
258
|
try:
|
|
@@ -265,49 +260,39 @@ class HealthCheck:
|
|
|
265
260
|
start = time.perf_counter()
|
|
266
261
|
# This would need a test model
|
|
267
262
|
duration_ms = (time.perf_counter() - start) * 1000
|
|
268
|
-
|
|
263
|
+
|
|
269
264
|
return {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
265
|
+
"status": "healthy",
|
|
266
|
+
"latency_ms": duration_ms,
|
|
267
|
+
"provider": self.factory._provider_name,
|
|
273
268
|
}
|
|
274
269
|
except Exception as e:
|
|
275
|
-
return {
|
|
276
|
-
|
|
277
|
-
'error': str(e),
|
|
278
|
-
'provider': self.factory._provider_name
|
|
279
|
-
}
|
|
280
|
-
|
|
270
|
+
return {"status": "unhealthy", "error": str(e), "provider": self.factory._provider_name}
|
|
271
|
+
|
|
281
272
|
def check_cache_health(self) -> Dict[str, Any]:
|
|
282
273
|
"""Check cache health"""
|
|
283
274
|
if not self.factory._cache:
|
|
284
|
-
return {
|
|
285
|
-
|
|
275
|
+
return {"status": "disabled"}
|
|
276
|
+
|
|
286
277
|
try:
|
|
287
278
|
# Test cache operations
|
|
288
279
|
test_key = "_health_check"
|
|
289
280
|
test_value = {"test": True}
|
|
290
|
-
|
|
281
|
+
|
|
291
282
|
start = time.perf_counter()
|
|
292
283
|
self.factory._cache.set(test_key, {}, test_value, 10)
|
|
293
284
|
retrieved = self.factory._cache.get(test_key, {})
|
|
294
285
|
duration_ms = (time.perf_counter() - start) * 1000
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
'status': 'healthy',
|
|
298
|
-
'latency_ms': duration_ms
|
|
299
|
-
}
|
|
286
|
+
|
|
287
|
+
return {"status": "healthy", "latency_ms": duration_ms}
|
|
300
288
|
except Exception as e:
|
|
301
|
-
return {
|
|
302
|
-
|
|
303
|
-
'error': str(e)
|
|
304
|
-
}
|
|
305
|
-
|
|
289
|
+
return {"status": "unhealthy", "error": str(e)}
|
|
290
|
+
|
|
306
291
|
def full_health_check(self) -> Dict[str, Any]:
|
|
307
292
|
"""Complete system health check"""
|
|
308
293
|
return {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
294
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
295
|
+
"sql": self.check_sql_health(),
|
|
296
|
+
"nosql": self.check_nosql_health(),
|
|
297
|
+
"cache": self.check_cache_health(),
|
|
298
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/EFSAdapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/S3Adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/SQSAdapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/adapters/__init__.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/advanced_query.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/audit/AuditStorage.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/audit/__init__.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/audit/context.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/audit/manager.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/audit/models.py
RENAMED
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/base/NoSQLKVAdapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/base/QueueAdapter.py
RENAMED
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/base/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/src/polydb/multitenancy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/tests/test_cloud_factory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{altcodepro_polydb_python-2.2.8 → altcodepro_polydb_python-2.3.0}/tests/test_multi_engine.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|