mcp-hangar 0.2.0__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.
- mcp_hangar/__init__.py +139 -0
- mcp_hangar/application/__init__.py +1 -0
- mcp_hangar/application/commands/__init__.py +67 -0
- mcp_hangar/application/commands/auth_commands.py +118 -0
- mcp_hangar/application/commands/auth_handlers.py +296 -0
- mcp_hangar/application/commands/commands.py +59 -0
- mcp_hangar/application/commands/handlers.py +189 -0
- mcp_hangar/application/discovery/__init__.py +21 -0
- mcp_hangar/application/discovery/discovery_metrics.py +283 -0
- mcp_hangar/application/discovery/discovery_orchestrator.py +497 -0
- mcp_hangar/application/discovery/lifecycle_manager.py +315 -0
- mcp_hangar/application/discovery/security_validator.py +414 -0
- mcp_hangar/application/event_handlers/__init__.py +50 -0
- mcp_hangar/application/event_handlers/alert_handler.py +191 -0
- mcp_hangar/application/event_handlers/audit_handler.py +203 -0
- mcp_hangar/application/event_handlers/knowledge_base_handler.py +120 -0
- mcp_hangar/application/event_handlers/logging_handler.py +69 -0
- mcp_hangar/application/event_handlers/metrics_handler.py +152 -0
- mcp_hangar/application/event_handlers/persistent_audit_store.py +217 -0
- mcp_hangar/application/event_handlers/security_handler.py +604 -0
- mcp_hangar/application/mcp/tooling.py +158 -0
- mcp_hangar/application/ports/__init__.py +9 -0
- mcp_hangar/application/ports/observability.py +237 -0
- mcp_hangar/application/queries/__init__.py +52 -0
- mcp_hangar/application/queries/auth_handlers.py +237 -0
- mcp_hangar/application/queries/auth_queries.py +118 -0
- mcp_hangar/application/queries/handlers.py +227 -0
- mcp_hangar/application/read_models/__init__.py +11 -0
- mcp_hangar/application/read_models/provider_views.py +139 -0
- mcp_hangar/application/sagas/__init__.py +11 -0
- mcp_hangar/application/sagas/group_rebalance_saga.py +137 -0
- mcp_hangar/application/sagas/provider_failover_saga.py +266 -0
- mcp_hangar/application/sagas/provider_recovery_saga.py +172 -0
- mcp_hangar/application/services/__init__.py +9 -0
- mcp_hangar/application/services/provider_service.py +208 -0
- mcp_hangar/application/services/traced_provider_service.py +211 -0
- mcp_hangar/bootstrap/runtime.py +328 -0
- mcp_hangar/context.py +178 -0
- mcp_hangar/domain/__init__.py +117 -0
- mcp_hangar/domain/contracts/__init__.py +57 -0
- mcp_hangar/domain/contracts/authentication.py +225 -0
- mcp_hangar/domain/contracts/authorization.py +229 -0
- mcp_hangar/domain/contracts/event_store.py +178 -0
- mcp_hangar/domain/contracts/metrics_publisher.py +59 -0
- mcp_hangar/domain/contracts/persistence.py +383 -0
- mcp_hangar/domain/contracts/provider_runtime.py +146 -0
- mcp_hangar/domain/discovery/__init__.py +20 -0
- mcp_hangar/domain/discovery/conflict_resolver.py +267 -0
- mcp_hangar/domain/discovery/discovered_provider.py +185 -0
- mcp_hangar/domain/discovery/discovery_service.py +412 -0
- mcp_hangar/domain/discovery/discovery_source.py +192 -0
- mcp_hangar/domain/events.py +433 -0
- mcp_hangar/domain/exceptions.py +525 -0
- mcp_hangar/domain/model/__init__.py +70 -0
- mcp_hangar/domain/model/aggregate.py +58 -0
- mcp_hangar/domain/model/circuit_breaker.py +152 -0
- mcp_hangar/domain/model/event_sourced_api_key.py +413 -0
- mcp_hangar/domain/model/event_sourced_provider.py +423 -0
- mcp_hangar/domain/model/event_sourced_role_assignment.py +268 -0
- mcp_hangar/domain/model/health_tracker.py +183 -0
- mcp_hangar/domain/model/load_balancer.py +185 -0
- mcp_hangar/domain/model/provider.py +810 -0
- mcp_hangar/domain/model/provider_group.py +656 -0
- mcp_hangar/domain/model/tool_catalog.py +105 -0
- mcp_hangar/domain/policies/__init__.py +19 -0
- mcp_hangar/domain/policies/provider_health.py +187 -0
- mcp_hangar/domain/repository.py +249 -0
- mcp_hangar/domain/security/__init__.py +85 -0
- mcp_hangar/domain/security/input_validator.py +710 -0
- mcp_hangar/domain/security/rate_limiter.py +387 -0
- mcp_hangar/domain/security/roles.py +237 -0
- mcp_hangar/domain/security/sanitizer.py +387 -0
- mcp_hangar/domain/security/secrets.py +501 -0
- mcp_hangar/domain/services/__init__.py +20 -0
- mcp_hangar/domain/services/audit_service.py +376 -0
- mcp_hangar/domain/services/image_builder.py +328 -0
- mcp_hangar/domain/services/provider_launcher.py +1046 -0
- mcp_hangar/domain/value_objects.py +1138 -0
- mcp_hangar/errors.py +818 -0
- mcp_hangar/fastmcp_server.py +1105 -0
- mcp_hangar/gc.py +134 -0
- mcp_hangar/infrastructure/__init__.py +79 -0
- mcp_hangar/infrastructure/async_executor.py +133 -0
- mcp_hangar/infrastructure/auth/__init__.py +37 -0
- mcp_hangar/infrastructure/auth/api_key_authenticator.py +388 -0
- mcp_hangar/infrastructure/auth/event_sourced_store.py +567 -0
- mcp_hangar/infrastructure/auth/jwt_authenticator.py +360 -0
- mcp_hangar/infrastructure/auth/middleware.py +340 -0
- mcp_hangar/infrastructure/auth/opa_authorizer.py +243 -0
- mcp_hangar/infrastructure/auth/postgres_store.py +659 -0
- mcp_hangar/infrastructure/auth/projections.py +366 -0
- mcp_hangar/infrastructure/auth/rate_limiter.py +311 -0
- mcp_hangar/infrastructure/auth/rbac_authorizer.py +323 -0
- mcp_hangar/infrastructure/auth/sqlite_store.py +624 -0
- mcp_hangar/infrastructure/command_bus.py +112 -0
- mcp_hangar/infrastructure/discovery/__init__.py +110 -0
- mcp_hangar/infrastructure/discovery/docker_source.py +289 -0
- mcp_hangar/infrastructure/discovery/entrypoint_source.py +249 -0
- mcp_hangar/infrastructure/discovery/filesystem_source.py +383 -0
- mcp_hangar/infrastructure/discovery/kubernetes_source.py +247 -0
- mcp_hangar/infrastructure/event_bus.py +260 -0
- mcp_hangar/infrastructure/event_sourced_repository.py +443 -0
- mcp_hangar/infrastructure/event_store.py +396 -0
- mcp_hangar/infrastructure/knowledge_base/__init__.py +259 -0
- mcp_hangar/infrastructure/knowledge_base/contracts.py +202 -0
- mcp_hangar/infrastructure/knowledge_base/memory.py +177 -0
- mcp_hangar/infrastructure/knowledge_base/postgres.py +545 -0
- mcp_hangar/infrastructure/knowledge_base/sqlite.py +513 -0
- mcp_hangar/infrastructure/metrics_publisher.py +36 -0
- mcp_hangar/infrastructure/observability/__init__.py +10 -0
- mcp_hangar/infrastructure/observability/langfuse_adapter.py +534 -0
- mcp_hangar/infrastructure/persistence/__init__.py +33 -0
- mcp_hangar/infrastructure/persistence/audit_repository.py +371 -0
- mcp_hangar/infrastructure/persistence/config_repository.py +398 -0
- mcp_hangar/infrastructure/persistence/database.py +333 -0
- mcp_hangar/infrastructure/persistence/database_common.py +330 -0
- mcp_hangar/infrastructure/persistence/event_serializer.py +280 -0
- mcp_hangar/infrastructure/persistence/event_upcaster.py +166 -0
- mcp_hangar/infrastructure/persistence/in_memory_event_store.py +150 -0
- mcp_hangar/infrastructure/persistence/recovery_service.py +312 -0
- mcp_hangar/infrastructure/persistence/sqlite_event_store.py +386 -0
- mcp_hangar/infrastructure/persistence/unit_of_work.py +409 -0
- mcp_hangar/infrastructure/persistence/upcasters/README.md +13 -0
- mcp_hangar/infrastructure/persistence/upcasters/__init__.py +7 -0
- mcp_hangar/infrastructure/query_bus.py +153 -0
- mcp_hangar/infrastructure/saga_manager.py +401 -0
- mcp_hangar/logging_config.py +209 -0
- mcp_hangar/metrics.py +1007 -0
- mcp_hangar/models.py +31 -0
- mcp_hangar/observability/__init__.py +54 -0
- mcp_hangar/observability/health.py +487 -0
- mcp_hangar/observability/metrics.py +319 -0
- mcp_hangar/observability/tracing.py +433 -0
- mcp_hangar/progress.py +542 -0
- mcp_hangar/retry.py +613 -0
- mcp_hangar/server/__init__.py +120 -0
- mcp_hangar/server/__main__.py +6 -0
- mcp_hangar/server/auth_bootstrap.py +340 -0
- mcp_hangar/server/auth_cli.py +335 -0
- mcp_hangar/server/auth_config.py +305 -0
- mcp_hangar/server/bootstrap.py +735 -0
- mcp_hangar/server/cli.py +161 -0
- mcp_hangar/server/config.py +224 -0
- mcp_hangar/server/context.py +215 -0
- mcp_hangar/server/http_auth_middleware.py +165 -0
- mcp_hangar/server/lifecycle.py +467 -0
- mcp_hangar/server/state.py +117 -0
- mcp_hangar/server/tools/__init__.py +16 -0
- mcp_hangar/server/tools/discovery.py +186 -0
- mcp_hangar/server/tools/groups.py +75 -0
- mcp_hangar/server/tools/health.py +301 -0
- mcp_hangar/server/tools/provider.py +939 -0
- mcp_hangar/server/tools/registry.py +320 -0
- mcp_hangar/server/validation.py +113 -0
- mcp_hangar/stdio_client.py +229 -0
- mcp_hangar-0.2.0.dist-info/METADATA +347 -0
- mcp_hangar-0.2.0.dist-info/RECORD +160 -0
- mcp_hangar-0.2.0.dist-info/WHEEL +4 -0
- mcp_hangar-0.2.0.dist-info/entry_points.txt +2 -0
- mcp_hangar-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Knowledge Base abstractions and contracts.
|
|
2
|
+
|
|
3
|
+
Defines interfaces for knowledge base operations that can be implemented
|
|
4
|
+
by different backends (PostgreSQL, SQLite, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class KnowledgeBaseDriver(Enum):
|
|
15
|
+
"""Supported knowledge base drivers."""
|
|
16
|
+
|
|
17
|
+
POSTGRES = "postgres"
|
|
18
|
+
SQLITE = "sqlite"
|
|
19
|
+
MEMORY = "memory" # For testing
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class KnowledgeBaseConfig:
|
|
24
|
+
"""Knowledge base configuration."""
|
|
25
|
+
|
|
26
|
+
enabled: bool = False
|
|
27
|
+
driver: KnowledgeBaseDriver = KnowledgeBaseDriver.SQLITE
|
|
28
|
+
dsn: str = ""
|
|
29
|
+
pool_size: int = 5
|
|
30
|
+
cache_ttl_s: int = 300
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_dict(cls, data: dict) -> "KnowledgeBaseConfig":
|
|
34
|
+
"""Create config from dictionary."""
|
|
35
|
+
if not data.get("enabled", False):
|
|
36
|
+
return cls(enabled=False)
|
|
37
|
+
|
|
38
|
+
dsn = data.get("dsn", "")
|
|
39
|
+
|
|
40
|
+
# Auto-detect driver from DSN
|
|
41
|
+
if dsn.startswith("postgresql://") or dsn.startswith("postgres://"):
|
|
42
|
+
driver = KnowledgeBaseDriver.POSTGRES
|
|
43
|
+
elif dsn.startswith("sqlite://") or dsn.endswith(".db"):
|
|
44
|
+
driver = KnowledgeBaseDriver.SQLITE
|
|
45
|
+
else:
|
|
46
|
+
# Explicit driver override
|
|
47
|
+
driver_str = data.get("driver", "sqlite")
|
|
48
|
+
driver = KnowledgeBaseDriver(driver_str)
|
|
49
|
+
|
|
50
|
+
return cls(
|
|
51
|
+
enabled=True,
|
|
52
|
+
driver=driver,
|
|
53
|
+
dsn=dsn,
|
|
54
|
+
pool_size=data.get("pool_size", 5),
|
|
55
|
+
cache_ttl_s=data.get("cache_ttl_s", 300),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class AuditEntry:
|
|
61
|
+
"""Audit log entry."""
|
|
62
|
+
|
|
63
|
+
event_type: str
|
|
64
|
+
provider: Optional[str] = None
|
|
65
|
+
tool: Optional[str] = None
|
|
66
|
+
arguments: Optional[dict] = None
|
|
67
|
+
result_summary: Optional[str] = None
|
|
68
|
+
duration_ms: Optional[int] = None
|
|
69
|
+
success: bool = True
|
|
70
|
+
error_message: Optional[str] = None
|
|
71
|
+
correlation_id: Optional[str] = None
|
|
72
|
+
timestamp: Optional[datetime] = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class ProviderStateEntry:
|
|
77
|
+
"""Provider state history entry."""
|
|
78
|
+
|
|
79
|
+
provider_id: str
|
|
80
|
+
old_state: Optional[str]
|
|
81
|
+
new_state: str
|
|
82
|
+
reason: Optional[str] = None
|
|
83
|
+
timestamp: Optional[datetime] = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class MetricEntry:
|
|
88
|
+
"""Provider metric entry."""
|
|
89
|
+
|
|
90
|
+
provider_id: str
|
|
91
|
+
metric_name: str
|
|
92
|
+
metric_value: float
|
|
93
|
+
labels: Optional[dict] = None
|
|
94
|
+
timestamp: Optional[datetime] = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class IKnowledgeBase(ABC):
|
|
98
|
+
"""Abstract interface for knowledge base operations.
|
|
99
|
+
|
|
100
|
+
Implementations must be thread-safe and handle their own connection management.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
@abstractmethod
|
|
104
|
+
async def initialize(self) -> bool:
|
|
105
|
+
"""Initialize the knowledge base (create tables, run migrations).
|
|
106
|
+
|
|
107
|
+
Returns True if successful.
|
|
108
|
+
"""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
@abstractmethod
|
|
112
|
+
async def close(self) -> None:
|
|
113
|
+
"""Close connections and cleanup resources."""
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
async def is_healthy(self) -> bool:
|
|
118
|
+
"""Check if knowledge base is operational."""
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
# === Cache Operations ===
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
async def cache_get(self, provider: str, tool: str, arguments: dict) -> Optional[dict]:
|
|
125
|
+
"""Get cached tool result. Returns None if not found or expired."""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
async def cache_set(
|
|
130
|
+
self,
|
|
131
|
+
provider: str,
|
|
132
|
+
tool: str,
|
|
133
|
+
arguments: dict,
|
|
134
|
+
result: Any,
|
|
135
|
+
ttl_s: Optional[int] = None,
|
|
136
|
+
) -> bool:
|
|
137
|
+
"""Cache tool result. Returns True if successful."""
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
async def cache_invalidate(self, provider: Optional[str] = None, tool: Optional[str] = None) -> int:
|
|
142
|
+
"""Invalidate cache entries. Returns count of invalidated entries."""
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
@abstractmethod
|
|
146
|
+
async def cache_cleanup(self) -> int:
|
|
147
|
+
"""Remove expired cache entries. Returns count of removed entries."""
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
# === Audit Operations ===
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
async def audit_log(self, entry: AuditEntry) -> bool:
|
|
154
|
+
"""Log audit entry. Returns True if successful."""
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
@abstractmethod
|
|
158
|
+
async def audit_query(
|
|
159
|
+
self,
|
|
160
|
+
provider: Optional[str] = None,
|
|
161
|
+
tool: Optional[str] = None,
|
|
162
|
+
success: Optional[bool] = None,
|
|
163
|
+
since: Optional[datetime] = None,
|
|
164
|
+
limit: int = 100,
|
|
165
|
+
) -> list[AuditEntry]:
|
|
166
|
+
"""Query audit log entries."""
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
@abstractmethod
|
|
170
|
+
async def audit_stats(self, hours: int = 24) -> dict:
|
|
171
|
+
"""Get audit statistics for last N hours."""
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
# === Provider State Operations ===
|
|
175
|
+
|
|
176
|
+
@abstractmethod
|
|
177
|
+
async def record_state_change(self, entry: ProviderStateEntry) -> bool:
|
|
178
|
+
"""Record provider state change. Returns True if successful."""
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
@abstractmethod
|
|
182
|
+
async def get_state_history(self, provider_id: str, limit: int = 100) -> list[ProviderStateEntry]:
|
|
183
|
+
"""Get provider state history."""
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
# === Metrics Operations ===
|
|
187
|
+
|
|
188
|
+
@abstractmethod
|
|
189
|
+
async def record_metric(self, entry: MetricEntry) -> bool:
|
|
190
|
+
"""Record provider metric. Returns True if successful."""
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
@abstractmethod
|
|
194
|
+
async def get_metrics(
|
|
195
|
+
self,
|
|
196
|
+
provider_id: str,
|
|
197
|
+
metric_name: Optional[str] = None,
|
|
198
|
+
since: Optional[datetime] = None,
|
|
199
|
+
limit: int = 100,
|
|
200
|
+
) -> list[MetricEntry]:
|
|
201
|
+
"""Get provider metrics."""
|
|
202
|
+
pass
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""In-memory implementation of IKnowledgeBase for testing."""
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from ...logging_config import get_logger
|
|
10
|
+
from .contracts import AuditEntry, IKnowledgeBase, KnowledgeBaseConfig, MetricEntry, ProviderStateEntry
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MemoryKnowledgeBase(IKnowledgeBase):
|
|
16
|
+
"""In-memory implementation for testing."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, config: KnowledgeBaseConfig):
|
|
19
|
+
self._config = config
|
|
20
|
+
self._cache: dict[str, tuple[Any, datetime]] = {} # key -> (result, expires_at)
|
|
21
|
+
self._audit_log: list[AuditEntry] = []
|
|
22
|
+
self._state_history: dict[str, list[ProviderStateEntry]] = defaultdict(list)
|
|
23
|
+
self._metrics: dict[str, list[MetricEntry]] = defaultdict(list)
|
|
24
|
+
self._initialized = False
|
|
25
|
+
|
|
26
|
+
async def initialize(self) -> bool:
|
|
27
|
+
self._initialized = True
|
|
28
|
+
logger.info("memory_kb_initialized")
|
|
29
|
+
return True
|
|
30
|
+
|
|
31
|
+
async def close(self) -> None:
|
|
32
|
+
self._cache.clear()
|
|
33
|
+
self._audit_log.clear()
|
|
34
|
+
self._state_history.clear()
|
|
35
|
+
self._metrics.clear()
|
|
36
|
+
self._initialized = False
|
|
37
|
+
logger.info("memory_kb_closed")
|
|
38
|
+
|
|
39
|
+
async def is_healthy(self) -> bool:
|
|
40
|
+
return self._initialized
|
|
41
|
+
|
|
42
|
+
def _hash_arguments(self, arguments: dict) -> str:
|
|
43
|
+
serialized = json.dumps(arguments, sort_keys=True, default=str)
|
|
44
|
+
return hashlib.sha256(serialized.encode()).hexdigest()[:32]
|
|
45
|
+
|
|
46
|
+
def _cache_key(self, provider: str, tool: str, arguments: dict) -> str:
|
|
47
|
+
return f"{provider}:{tool}:{self._hash_arguments(arguments)}"
|
|
48
|
+
|
|
49
|
+
# === Cache Operations ===
|
|
50
|
+
|
|
51
|
+
async def cache_get(self, provider: str, tool: str, arguments: dict) -> Optional[dict]:
|
|
52
|
+
key = self._cache_key(provider, tool, arguments)
|
|
53
|
+
if key in self._cache:
|
|
54
|
+
result, expires_at = self._cache[key]
|
|
55
|
+
if expires_at > datetime.now(timezone.utc):
|
|
56
|
+
logger.debug("cache_hit", provider=provider, tool=tool)
|
|
57
|
+
return result
|
|
58
|
+
else:
|
|
59
|
+
del self._cache[key]
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
async def cache_set(
|
|
63
|
+
self,
|
|
64
|
+
provider: str,
|
|
65
|
+
tool: str,
|
|
66
|
+
arguments: dict,
|
|
67
|
+
result: Any,
|
|
68
|
+
ttl_s: Optional[int] = None,
|
|
69
|
+
) -> bool:
|
|
70
|
+
key = self._cache_key(provider, tool, arguments)
|
|
71
|
+
ttl = ttl_s or self._config.cache_ttl_s
|
|
72
|
+
expires_at = datetime.now(timezone.utc) + timedelta(seconds=ttl)
|
|
73
|
+
self._cache[key] = (result, expires_at)
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
async def cache_invalidate(self, provider: Optional[str] = None, tool: Optional[str] = None) -> int:
|
|
77
|
+
if not provider and not tool:
|
|
78
|
+
count = len(self._cache)
|
|
79
|
+
self._cache.clear()
|
|
80
|
+
return count
|
|
81
|
+
|
|
82
|
+
prefix = f"{provider}:" if provider else ""
|
|
83
|
+
keys_to_delete = [k for k in self._cache if k.startswith(prefix) and (not tool or f":{tool}:" in k)]
|
|
84
|
+
for k in keys_to_delete:
|
|
85
|
+
del self._cache[k]
|
|
86
|
+
return len(keys_to_delete)
|
|
87
|
+
|
|
88
|
+
async def cache_cleanup(self) -> int:
|
|
89
|
+
now = datetime.now(timezone.utc)
|
|
90
|
+
expired = [k for k, (_, exp) in self._cache.items() if exp < now]
|
|
91
|
+
for k in expired:
|
|
92
|
+
del self._cache[k]
|
|
93
|
+
logger.info("cache_cleanup", deleted=len(expired))
|
|
94
|
+
return len(expired)
|
|
95
|
+
|
|
96
|
+
# === Audit Operations ===
|
|
97
|
+
|
|
98
|
+
async def audit_log(self, entry: AuditEntry) -> bool:
|
|
99
|
+
entry.timestamp = entry.timestamp or datetime.now(timezone.utc)
|
|
100
|
+
self._audit_log.append(entry)
|
|
101
|
+
return True
|
|
102
|
+
|
|
103
|
+
async def audit_query(
|
|
104
|
+
self,
|
|
105
|
+
provider: Optional[str] = None,
|
|
106
|
+
tool: Optional[str] = None,
|
|
107
|
+
success: Optional[bool] = None,
|
|
108
|
+
since: Optional[datetime] = None,
|
|
109
|
+
limit: int = 100,
|
|
110
|
+
) -> list[AuditEntry]:
|
|
111
|
+
results = []
|
|
112
|
+
for entry in reversed(self._audit_log):
|
|
113
|
+
if provider and entry.provider != provider:
|
|
114
|
+
continue
|
|
115
|
+
if tool and entry.tool != tool:
|
|
116
|
+
continue
|
|
117
|
+
if success is not None and entry.success != success:
|
|
118
|
+
continue
|
|
119
|
+
if since and entry.timestamp and entry.timestamp < since:
|
|
120
|
+
continue
|
|
121
|
+
results.append(entry)
|
|
122
|
+
if len(results) >= limit:
|
|
123
|
+
break
|
|
124
|
+
return results
|
|
125
|
+
|
|
126
|
+
async def audit_stats(self, hours: int = 24) -> dict:
|
|
127
|
+
since = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
128
|
+
recent = [e for e in self._audit_log if e.timestamp and e.timestamp >= since]
|
|
129
|
+
|
|
130
|
+
durations = [e.duration_ms for e in recent if e.duration_ms is not None]
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"total": len(recent),
|
|
134
|
+
"success_count": sum(1 for e in recent if e.success),
|
|
135
|
+
"error_count": sum(1 for e in recent if not e.success),
|
|
136
|
+
"providers": len(set(e.provider for e in recent if e.provider)),
|
|
137
|
+
"tools": len(set(e.tool for e in recent if e.tool)),
|
|
138
|
+
"avg_duration_ms": sum(durations) / len(durations) if durations else None,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# === Provider State Operations ===
|
|
142
|
+
|
|
143
|
+
async def record_state_change(self, entry: ProviderStateEntry) -> bool:
|
|
144
|
+
entry.timestamp = entry.timestamp or datetime.now(timezone.utc)
|
|
145
|
+
self._state_history[entry.provider_id].append(entry)
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
async def get_state_history(self, provider_id: str, limit: int = 100) -> list[ProviderStateEntry]:
|
|
149
|
+
history = self._state_history.get(provider_id, [])
|
|
150
|
+
return list(reversed(history[-limit:]))
|
|
151
|
+
|
|
152
|
+
# === Metrics Operations ===
|
|
153
|
+
|
|
154
|
+
async def record_metric(self, entry: MetricEntry) -> bool:
|
|
155
|
+
entry.timestamp = entry.timestamp or datetime.now(timezone.utc)
|
|
156
|
+
self._metrics[entry.provider_id].append(entry)
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
async def get_metrics(
|
|
160
|
+
self,
|
|
161
|
+
provider_id: str,
|
|
162
|
+
metric_name: Optional[str] = None,
|
|
163
|
+
since: Optional[datetime] = None,
|
|
164
|
+
limit: int = 100,
|
|
165
|
+
) -> list[MetricEntry]:
|
|
166
|
+
metrics = self._metrics.get(provider_id, [])
|
|
167
|
+
|
|
168
|
+
results = []
|
|
169
|
+
for m in reversed(metrics):
|
|
170
|
+
if metric_name and m.metric_name != metric_name:
|
|
171
|
+
continue
|
|
172
|
+
if since and m.timestamp and m.timestamp < since:
|
|
173
|
+
continue
|
|
174
|
+
results.append(m)
|
|
175
|
+
if len(results) >= limit:
|
|
176
|
+
break
|
|
177
|
+
return results
|