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,315 @@
|
|
|
1
|
+
"""Discovery Lifecycle Manager.
|
|
2
|
+
|
|
3
|
+
Manages the lifecycle of discovered providers including TTL tracking,
|
|
4
|
+
quarantine management, and graceful deregistration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from typing import Awaitable, Callable, Dict, List, Optional, Set
|
|
10
|
+
|
|
11
|
+
from mcp_hangar.domain.discovery.discovered_provider import DiscoveredProvider
|
|
12
|
+
|
|
13
|
+
from ...logging_config import get_logger
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Type alias for registry callback
|
|
19
|
+
RegistryCallback = Callable[[str, str], Awaitable[None]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DiscoveryLifecycleManager:
|
|
23
|
+
"""Manages lifecycle of discovered providers.
|
|
24
|
+
|
|
25
|
+
Responsibilities:
|
|
26
|
+
- Track provider TTLs and expiration
|
|
27
|
+
- Manage quarantine state
|
|
28
|
+
- Handle graceful deregistration
|
|
29
|
+
- Provide manual approval workflow
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
manager = DiscoveryLifecycleManager(default_ttl=90)
|
|
33
|
+
manager.add_provider(provider)
|
|
34
|
+
|
|
35
|
+
# Periodic check
|
|
36
|
+
expired = await manager.check_expirations()
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
default_ttl: int = 90,
|
|
42
|
+
check_interval: int = 10,
|
|
43
|
+
drain_timeout: int = 30,
|
|
44
|
+
on_deregister: Optional[RegistryCallback] = None,
|
|
45
|
+
):
|
|
46
|
+
"""Initialize lifecycle manager.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
default_ttl: Default TTL in seconds (3x refresh interval)
|
|
50
|
+
check_interval: Interval between expiration checks
|
|
51
|
+
drain_timeout: Timeout for graceful connection draining
|
|
52
|
+
on_deregister: Callback when provider should be deregistered
|
|
53
|
+
"""
|
|
54
|
+
self.default_ttl = default_ttl
|
|
55
|
+
self.check_interval = check_interval
|
|
56
|
+
self.drain_timeout = drain_timeout
|
|
57
|
+
self.on_deregister = on_deregister
|
|
58
|
+
|
|
59
|
+
# Active providers
|
|
60
|
+
self._providers: Dict[str, DiscoveredProvider] = {}
|
|
61
|
+
|
|
62
|
+
# Quarantined providers: name -> (provider, reason, timestamp)
|
|
63
|
+
self._quarantine: Dict[str, tuple[DiscoveredProvider, str, datetime]] = {}
|
|
64
|
+
|
|
65
|
+
# Providers being drained (graceful shutdown)
|
|
66
|
+
self._draining: Set[str] = set()
|
|
67
|
+
|
|
68
|
+
# Lifecycle task
|
|
69
|
+
self._running = False
|
|
70
|
+
self._lifecycle_task: Optional[asyncio.Task] = None
|
|
71
|
+
|
|
72
|
+
async def start(self) -> None:
|
|
73
|
+
"""Start lifecycle management loop."""
|
|
74
|
+
if self._running:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
self._running = True
|
|
78
|
+
self._lifecycle_task = asyncio.create_task(self._lifecycle_loop())
|
|
79
|
+
logger.info(f"Lifecycle manager started (ttl={self.default_ttl}s, interval={self.check_interval}s)")
|
|
80
|
+
|
|
81
|
+
async def stop(self) -> None:
|
|
82
|
+
"""Stop lifecycle management."""
|
|
83
|
+
self._running = False
|
|
84
|
+
|
|
85
|
+
if self._lifecycle_task:
|
|
86
|
+
self._lifecycle_task.cancel()
|
|
87
|
+
try:
|
|
88
|
+
await self._lifecycle_task
|
|
89
|
+
except asyncio.CancelledError:
|
|
90
|
+
pass
|
|
91
|
+
self._lifecycle_task = None
|
|
92
|
+
|
|
93
|
+
logger.info("Lifecycle manager stopped")
|
|
94
|
+
|
|
95
|
+
async def _lifecycle_loop(self) -> None:
|
|
96
|
+
"""Periodic check for expired providers."""
|
|
97
|
+
while self._running:
|
|
98
|
+
try:
|
|
99
|
+
await self._check_expirations()
|
|
100
|
+
await asyncio.sleep(self.check_interval)
|
|
101
|
+
except asyncio.CancelledError:
|
|
102
|
+
break
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error(f"Error in lifecycle loop: {e}")
|
|
105
|
+
await asyncio.sleep(self.check_interval)
|
|
106
|
+
|
|
107
|
+
async def _check_expirations(self) -> List[str]:
|
|
108
|
+
"""Check and handle expired providers.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of expired provider names
|
|
112
|
+
"""
|
|
113
|
+
expired = []
|
|
114
|
+
|
|
115
|
+
for name, provider in list(self._providers.items()):
|
|
116
|
+
if provider.is_expired():
|
|
117
|
+
expired.append(name)
|
|
118
|
+
logger.info(f"Provider '{name}' expired (last seen: {provider.last_seen_at}). Starting deregistration.")
|
|
119
|
+
await self._deregister(name, "ttl_expired")
|
|
120
|
+
|
|
121
|
+
return expired
|
|
122
|
+
|
|
123
|
+
async def _deregister(self, name: str, reason: str) -> None:
|
|
124
|
+
"""Deregister a provider with optional draining.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
name: Provider name
|
|
128
|
+
reason: Reason for deregistration
|
|
129
|
+
"""
|
|
130
|
+
if name in self._draining:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
provider = self._providers.pop(name, None)
|
|
134
|
+
if not provider:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# Mark as draining
|
|
138
|
+
self._draining.add(name)
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
# Callback to main registry
|
|
142
|
+
if self.on_deregister:
|
|
143
|
+
await self.on_deregister(name, reason)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.error(f"Error deregistering provider {name}: {e}")
|
|
146
|
+
finally:
|
|
147
|
+
self._draining.discard(name)
|
|
148
|
+
|
|
149
|
+
def add_provider(self, provider: DiscoveredProvider) -> None:
|
|
150
|
+
"""Add provider to lifecycle tracking.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
provider: Provider to track
|
|
154
|
+
"""
|
|
155
|
+
self._providers[provider.name] = provider
|
|
156
|
+
logger.debug(f"Added provider to lifecycle tracking: {provider.name}")
|
|
157
|
+
|
|
158
|
+
def update_seen(self, name: str) -> Optional[DiscoveredProvider]:
|
|
159
|
+
"""Update last_seen for a provider.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
name: Provider name
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Updated provider, or None if not found
|
|
166
|
+
"""
|
|
167
|
+
if name in self._providers:
|
|
168
|
+
old_provider = self._providers[name]
|
|
169
|
+
updated = old_provider.with_updated_seen_time()
|
|
170
|
+
self._providers[name] = updated
|
|
171
|
+
return updated
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
def update_provider(self, provider: DiscoveredProvider) -> None:
|
|
175
|
+
"""Update provider configuration.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
provider: Updated provider
|
|
179
|
+
"""
|
|
180
|
+
self._providers[provider.name] = provider
|
|
181
|
+
logger.debug(f"Updated provider in lifecycle tracking: {provider.name}")
|
|
182
|
+
|
|
183
|
+
def remove_provider(self, name: str) -> Optional[DiscoveredProvider]:
|
|
184
|
+
"""Remove provider from tracking.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
name: Provider name
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Removed provider, or None if not found
|
|
191
|
+
"""
|
|
192
|
+
return self._providers.pop(name, None)
|
|
193
|
+
|
|
194
|
+
def get_provider(self, name: str) -> Optional[DiscoveredProvider]:
|
|
195
|
+
"""Get a tracked provider.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
name: Provider name
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Provider, or None if not found
|
|
202
|
+
"""
|
|
203
|
+
return self._providers.get(name)
|
|
204
|
+
|
|
205
|
+
def get_all_providers(self) -> Dict[str, DiscoveredProvider]:
|
|
206
|
+
"""Get all tracked providers.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Dictionary of name -> provider
|
|
210
|
+
"""
|
|
211
|
+
return dict(self._providers)
|
|
212
|
+
|
|
213
|
+
# Quarantine management
|
|
214
|
+
|
|
215
|
+
def quarantine(self, provider: DiscoveredProvider, reason: str) -> None:
|
|
216
|
+
"""Move provider to quarantine.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
provider: Provider to quarantine
|
|
220
|
+
reason: Reason for quarantine
|
|
221
|
+
"""
|
|
222
|
+
self._quarantine[provider.name] = (provider, reason, datetime.now(timezone.utc))
|
|
223
|
+
# Remove from active tracking
|
|
224
|
+
self._providers.pop(provider.name, None)
|
|
225
|
+
logger.warning(f"Provider '{provider.name}' quarantined: {reason}")
|
|
226
|
+
|
|
227
|
+
def approve(self, name: str) -> Optional[DiscoveredProvider]:
|
|
228
|
+
"""Approve quarantined provider for registration.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
name: Provider name
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Approved provider, or None if not in quarantine
|
|
235
|
+
"""
|
|
236
|
+
if name in self._quarantine:
|
|
237
|
+
provider, reason, _ = self._quarantine.pop(name)
|
|
238
|
+
# Add back to active tracking
|
|
239
|
+
self._providers[provider.name] = provider
|
|
240
|
+
logger.info(f"Approved quarantined provider: {name}")
|
|
241
|
+
return provider
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
def reject(self, name: str) -> Optional[DiscoveredProvider]:
|
|
245
|
+
"""Reject and remove quarantined provider.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
name: Provider name
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Rejected provider, or None if not in quarantine
|
|
252
|
+
"""
|
|
253
|
+
if name in self._quarantine:
|
|
254
|
+
provider, _, _ = self._quarantine.pop(name)
|
|
255
|
+
logger.info(f"Rejected quarantined provider: {name}")
|
|
256
|
+
return provider
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
def get_quarantined(self) -> Dict[str, tuple[DiscoveredProvider, str, datetime]]:
|
|
260
|
+
"""Get all quarantined providers.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Dictionary of name -> (provider, reason, quarantine_time)
|
|
264
|
+
"""
|
|
265
|
+
return dict(self._quarantine)
|
|
266
|
+
|
|
267
|
+
def is_quarantined(self, name: str) -> bool:
|
|
268
|
+
"""Check if provider is quarantined.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
name: Provider name
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
True if quarantined
|
|
275
|
+
"""
|
|
276
|
+
return name in self._quarantine
|
|
277
|
+
|
|
278
|
+
# Stats and status
|
|
279
|
+
|
|
280
|
+
def get_stats(self) -> Dict[str, int]:
|
|
281
|
+
"""Get lifecycle statistics.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Dictionary with counts
|
|
285
|
+
"""
|
|
286
|
+
return {
|
|
287
|
+
"active": len(self._providers),
|
|
288
|
+
"quarantined": len(self._quarantine),
|
|
289
|
+
"draining": len(self._draining),
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
def get_expiring_soon(self, threshold_seconds: int = 30) -> List[DiscoveredProvider]:
|
|
293
|
+
"""Get providers expiring soon.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
threshold_seconds: Time threshold for "soon"
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
List of providers expiring within threshold
|
|
300
|
+
"""
|
|
301
|
+
expiring = []
|
|
302
|
+
now = datetime.now(timezone.utc)
|
|
303
|
+
|
|
304
|
+
for provider in self._providers.values():
|
|
305
|
+
last_seen = provider.last_seen_at
|
|
306
|
+
if last_seen.tzinfo is None:
|
|
307
|
+
last_seen = last_seen.replace(tzinfo=timezone.utc)
|
|
308
|
+
|
|
309
|
+
elapsed = (now - last_seen).total_seconds()
|
|
310
|
+
remaining = provider.ttl_seconds - elapsed
|
|
311
|
+
|
|
312
|
+
if remaining <= threshold_seconds:
|
|
313
|
+
expiring.append(provider)
|
|
314
|
+
|
|
315
|
+
return expiring
|