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,398 @@
|
|
|
1
|
+
"""Provider configuration repository implementations.
|
|
2
|
+
|
|
3
|
+
Provides both in-memory and SQLite implementations of IProviderConfigRepository.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
import json
|
|
8
|
+
import threading
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from ...domain.contracts.persistence import ConcurrentModificationError, PersistenceError, ProviderConfigSnapshot
|
|
12
|
+
from ...logging_config import get_logger
|
|
13
|
+
from .database import Database
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InMemoryProviderConfigRepository:
|
|
19
|
+
"""In-memory implementation of provider config repository.
|
|
20
|
+
|
|
21
|
+
Useful for testing and development. Data is lost on restart.
|
|
22
|
+
Thread-safe implementation.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
"""Initialize empty in-memory repository."""
|
|
27
|
+
self._configs: Dict[str, ProviderConfigSnapshot] = {}
|
|
28
|
+
self._versions: Dict[str, int] = {}
|
|
29
|
+
self._lock = threading.RLock()
|
|
30
|
+
|
|
31
|
+
async def save(self, config: ProviderConfigSnapshot) -> None:
|
|
32
|
+
"""Save provider configuration."""
|
|
33
|
+
with self._lock:
|
|
34
|
+
now = datetime.now(timezone.utc)
|
|
35
|
+
|
|
36
|
+
# Update timestamps
|
|
37
|
+
if config.provider_id in self._configs:
|
|
38
|
+
# Update existing
|
|
39
|
+
new_config = ProviderConfigSnapshot(
|
|
40
|
+
**{
|
|
41
|
+
**config.to_dict(),
|
|
42
|
+
"created_at": self._configs[config.provider_id].created_at,
|
|
43
|
+
"updated_at": now,
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
self._versions[config.provider_id] = self._versions.get(config.provider_id, 0) + 1
|
|
47
|
+
else:
|
|
48
|
+
# Create new
|
|
49
|
+
new_config = ProviderConfigSnapshot(
|
|
50
|
+
**{
|
|
51
|
+
**config.to_dict(),
|
|
52
|
+
"created_at": now,
|
|
53
|
+
"updated_at": now,
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
self._versions[config.provider_id] = 1
|
|
57
|
+
|
|
58
|
+
self._configs[config.provider_id] = new_config
|
|
59
|
+
logger.debug(f"Saved config for provider: {config.provider_id}")
|
|
60
|
+
|
|
61
|
+
async def get(self, provider_id: str) -> Optional[ProviderConfigSnapshot]:
|
|
62
|
+
"""Retrieve provider configuration by ID."""
|
|
63
|
+
with self._lock:
|
|
64
|
+
return self._configs.get(provider_id)
|
|
65
|
+
|
|
66
|
+
async def get_all(self) -> List[ProviderConfigSnapshot]:
|
|
67
|
+
"""Retrieve all provider configurations."""
|
|
68
|
+
with self._lock:
|
|
69
|
+
return list(self._configs.values())
|
|
70
|
+
|
|
71
|
+
async def delete(self, provider_id: str) -> bool:
|
|
72
|
+
"""Delete provider configuration."""
|
|
73
|
+
with self._lock:
|
|
74
|
+
if provider_id in self._configs:
|
|
75
|
+
del self._configs[provider_id]
|
|
76
|
+
self._versions.pop(provider_id, None)
|
|
77
|
+
logger.debug(f"Deleted config for provider: {provider_id}")
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
async def exists(self, provider_id: str) -> bool:
|
|
82
|
+
"""Check if provider configuration exists."""
|
|
83
|
+
with self._lock:
|
|
84
|
+
return provider_id in self._configs
|
|
85
|
+
|
|
86
|
+
def clear(self) -> None:
|
|
87
|
+
"""Clear all configurations (for testing)."""
|
|
88
|
+
with self._lock:
|
|
89
|
+
self._configs.clear()
|
|
90
|
+
self._versions.clear()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class SQLiteProviderConfigRepository:
|
|
94
|
+
"""SQLite implementation of provider config repository.
|
|
95
|
+
|
|
96
|
+
Provides durable storage with optimistic concurrency control.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, database: Database):
|
|
100
|
+
"""Initialize with database connection.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
database: Database instance for connections
|
|
104
|
+
"""
|
|
105
|
+
self._db = database
|
|
106
|
+
|
|
107
|
+
async def save(self, config: ProviderConfigSnapshot) -> None:
|
|
108
|
+
"""Save provider configuration with optimistic locking.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
config: Provider configuration to save
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ConcurrentModificationError: If version conflict detected
|
|
115
|
+
PersistenceError: If save operation fails
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
async with self._db.transaction() as conn:
|
|
119
|
+
# Check existing version
|
|
120
|
+
cursor = await conn.execute(
|
|
121
|
+
"SELECT version FROM provider_configs WHERE provider_id = ?",
|
|
122
|
+
(config.provider_id,),
|
|
123
|
+
)
|
|
124
|
+
row = await cursor.fetchone()
|
|
125
|
+
|
|
126
|
+
config_json = json.dumps(config.to_dict())
|
|
127
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
128
|
+
|
|
129
|
+
if row is None:
|
|
130
|
+
# Insert new config
|
|
131
|
+
await conn.execute(
|
|
132
|
+
"""
|
|
133
|
+
INSERT INTO provider_configs
|
|
134
|
+
(provider_id, mode, config_json, enabled, version, created_at, updated_at)
|
|
135
|
+
VALUES (?, ?, ?, ?, 1, ?, ?)
|
|
136
|
+
""",
|
|
137
|
+
(
|
|
138
|
+
config.provider_id,
|
|
139
|
+
config.mode,
|
|
140
|
+
config_json,
|
|
141
|
+
1 if config.enabled else 0,
|
|
142
|
+
now,
|
|
143
|
+
now,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
logger.debug(f"Inserted new config for provider: {config.provider_id}")
|
|
147
|
+
else:
|
|
148
|
+
# Update existing config with version increment
|
|
149
|
+
current_version = row[0]
|
|
150
|
+
new_version = current_version + 1
|
|
151
|
+
|
|
152
|
+
result = await conn.execute(
|
|
153
|
+
"""
|
|
154
|
+
UPDATE provider_configs
|
|
155
|
+
SET mode = ?, config_json = ?, enabled = ?,
|
|
156
|
+
version = ?, updated_at = ?
|
|
157
|
+
WHERE provider_id = ? AND version = ?
|
|
158
|
+
""",
|
|
159
|
+
(
|
|
160
|
+
config.mode,
|
|
161
|
+
config_json,
|
|
162
|
+
1 if config.enabled else 0,
|
|
163
|
+
new_version,
|
|
164
|
+
now,
|
|
165
|
+
config.provider_id,
|
|
166
|
+
current_version,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if result.rowcount == 0:
|
|
171
|
+
raise ConcurrentModificationError(
|
|
172
|
+
config.provider_id,
|
|
173
|
+
current_version,
|
|
174
|
+
current_version + 1,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
logger.debug(
|
|
178
|
+
f"Updated config for provider: {config.provider_id} "
|
|
179
|
+
f"(version {current_version} -> {new_version})"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
except ConcurrentModificationError:
|
|
183
|
+
raise
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Failed to save provider config: {e}")
|
|
186
|
+
raise PersistenceError(f"Failed to save provider config: {e}") from e
|
|
187
|
+
|
|
188
|
+
async def get(self, provider_id: str) -> Optional[ProviderConfigSnapshot]:
|
|
189
|
+
"""Retrieve provider configuration by ID.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
provider_id: Provider identifier
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Configuration snapshot if found, None otherwise
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
async with self._db.connection() as conn:
|
|
199
|
+
cursor = await conn.execute(
|
|
200
|
+
"SELECT config_json FROM provider_configs WHERE provider_id = ?",
|
|
201
|
+
(provider_id,),
|
|
202
|
+
)
|
|
203
|
+
row = await cursor.fetchone()
|
|
204
|
+
|
|
205
|
+
if row is None:
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
config_data = json.loads(row[0])
|
|
209
|
+
return ProviderConfigSnapshot.from_dict(config_data)
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.error(f"Failed to get provider config: {e}")
|
|
213
|
+
raise PersistenceError(f"Failed to get provider config: {e}") from e
|
|
214
|
+
|
|
215
|
+
async def get_all(self) -> List[ProviderConfigSnapshot]:
|
|
216
|
+
"""Retrieve all provider configurations.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
List of all stored configurations
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
async with self._db.connection() as conn:
|
|
223
|
+
cursor = await conn.execute("SELECT config_json FROM provider_configs WHERE enabled = 1")
|
|
224
|
+
rows = await cursor.fetchall()
|
|
225
|
+
|
|
226
|
+
configs = []
|
|
227
|
+
for row in rows:
|
|
228
|
+
try:
|
|
229
|
+
config_data = json.loads(row[0])
|
|
230
|
+
configs.append(ProviderConfigSnapshot.from_dict(config_data))
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.warning(f"Failed to deserialize config: {e}")
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
return configs
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.error(f"Failed to get all provider configs: {e}")
|
|
239
|
+
raise PersistenceError(f"Failed to get all provider configs: {e}") from e
|
|
240
|
+
|
|
241
|
+
async def delete(self, provider_id: str) -> bool:
|
|
242
|
+
"""Delete provider configuration (soft delete by disabling).
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
provider_id: Provider identifier
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
True if deleted, False if not found
|
|
249
|
+
"""
|
|
250
|
+
try:
|
|
251
|
+
async with self._db.transaction() as conn:
|
|
252
|
+
# Soft delete - mark as disabled
|
|
253
|
+
result = await conn.execute(
|
|
254
|
+
"""
|
|
255
|
+
UPDATE provider_configs
|
|
256
|
+
SET enabled = 0, updated_at = ?
|
|
257
|
+
WHERE provider_id = ? AND enabled = 1
|
|
258
|
+
""",
|
|
259
|
+
(datetime.now(timezone.utc).isoformat(), provider_id),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
deleted = result.rowcount > 0
|
|
263
|
+
if deleted:
|
|
264
|
+
logger.debug(f"Soft-deleted config for provider: {provider_id}")
|
|
265
|
+
|
|
266
|
+
return deleted
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
logger.error(f"Failed to delete provider config: {e}")
|
|
270
|
+
raise PersistenceError(f"Failed to delete provider config: {e}") from e
|
|
271
|
+
|
|
272
|
+
async def hard_delete(self, provider_id: str) -> bool:
|
|
273
|
+
"""Permanently delete provider configuration.
|
|
274
|
+
|
|
275
|
+
Use with caution - this removes all history.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
provider_id: Provider identifier
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
True if deleted, False if not found
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
async with self._db.transaction() as conn:
|
|
285
|
+
result = await conn.execute(
|
|
286
|
+
"DELETE FROM provider_configs WHERE provider_id = ?",
|
|
287
|
+
(provider_id,),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
deleted = result.rowcount > 0
|
|
291
|
+
if deleted:
|
|
292
|
+
logger.info(f"Hard-deleted config for provider: {provider_id}")
|
|
293
|
+
|
|
294
|
+
return deleted
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.error(f"Failed to hard-delete provider config: {e}")
|
|
298
|
+
raise PersistenceError(f"Failed to hard-delete provider config: {e}") from e
|
|
299
|
+
|
|
300
|
+
async def exists(self, provider_id: str) -> bool:
|
|
301
|
+
"""Check if provider configuration exists.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
provider_id: Provider identifier
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
True if exists and enabled, False otherwise
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
async with self._db.connection() as conn:
|
|
311
|
+
cursor = await conn.execute(
|
|
312
|
+
"SELECT 1 FROM provider_configs WHERE provider_id = ? AND enabled = 1",
|
|
313
|
+
(provider_id,),
|
|
314
|
+
)
|
|
315
|
+
row = await cursor.fetchone()
|
|
316
|
+
return row is not None
|
|
317
|
+
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.error(f"Failed to check provider existence: {e}")
|
|
320
|
+
raise PersistenceError(f"Failed to check provider existence: {e}") from e
|
|
321
|
+
|
|
322
|
+
async def get_with_version(self, provider_id: str) -> Optional[tuple[ProviderConfigSnapshot, int]]:
|
|
323
|
+
"""Get configuration with its version for optimistic locking.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
provider_id: Provider identifier
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Tuple of (config, version) if found, None otherwise
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
async with self._db.connection() as conn:
|
|
333
|
+
cursor = await conn.execute(
|
|
334
|
+
"SELECT config_json, version FROM provider_configs WHERE provider_id = ?",
|
|
335
|
+
(provider_id,),
|
|
336
|
+
)
|
|
337
|
+
row = await cursor.fetchone()
|
|
338
|
+
|
|
339
|
+
if row is None:
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
config_data = json.loads(row[0])
|
|
343
|
+
return (ProviderConfigSnapshot.from_dict(config_data), row[1])
|
|
344
|
+
|
|
345
|
+
except Exception as e:
|
|
346
|
+
logger.error(f"Failed to get provider config with version: {e}")
|
|
347
|
+
raise PersistenceError(f"Failed to get provider config with version: {e}") from e
|
|
348
|
+
|
|
349
|
+
async def update_last_started(self, provider_id: str) -> None:
|
|
350
|
+
"""Update the last_started_at timestamp.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
provider_id: Provider identifier
|
|
354
|
+
"""
|
|
355
|
+
try:
|
|
356
|
+
async with self._db.transaction() as conn:
|
|
357
|
+
await conn.execute(
|
|
358
|
+
"""
|
|
359
|
+
UPDATE provider_configs
|
|
360
|
+
SET last_started_at = ?, updated_at = ?
|
|
361
|
+
WHERE provider_id = ?
|
|
362
|
+
""",
|
|
363
|
+
(
|
|
364
|
+
datetime.now(timezone.utc).isoformat(),
|
|
365
|
+
datetime.now(timezone.utc).isoformat(),
|
|
366
|
+
provider_id,
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
except Exception as e:
|
|
371
|
+
logger.error(f"Failed to update last_started_at: {e}")
|
|
372
|
+
# Non-critical operation, don't raise
|
|
373
|
+
|
|
374
|
+
async def update_failure_count(self, provider_id: str, consecutive_failures: int) -> None:
|
|
375
|
+
"""Update the consecutive failure count.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
provider_id: Provider identifier
|
|
379
|
+
consecutive_failures: Current failure count
|
|
380
|
+
"""
|
|
381
|
+
try:
|
|
382
|
+
async with self._db.transaction() as conn:
|
|
383
|
+
await conn.execute(
|
|
384
|
+
"""
|
|
385
|
+
UPDATE provider_configs
|
|
386
|
+
SET consecutive_failures = ?, updated_at = ?
|
|
387
|
+
WHERE provider_id = ?
|
|
388
|
+
""",
|
|
389
|
+
(
|
|
390
|
+
consecutive_failures,
|
|
391
|
+
datetime.now(timezone.utc).isoformat(),
|
|
392
|
+
provider_id,
|
|
393
|
+
),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
except Exception as e:
|
|
397
|
+
logger.error(f"Failed to update failure count: {e}")
|
|
398
|
+
# Non-critical operation, don't raise
|