feat: consume Repo Manager classification publisher
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / pytest-smoke (push) Failing after 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
This commit is contained in:
tegwick 2026-09-01 00:49:30 +02:00
parent b2f430ffb1
commit f582773f5a
6 changed files with 201 additions and 3 deletions

View file

@ -1,6 +1,7 @@
from __future__ import annotations
from contextlib import asynccontextmanager
import asyncio
from contextlib import asynccontextmanager, suppress
from fastapi import FastAPI, Response, status
@ -17,6 +18,7 @@ from hub_core.runtime.repository_navigation import (
from hub_core.runtime.repository_navigation_routes import (
create_repository_navigation_router,
)
from hub_core.runtime.repo_manager_client import HTTPRepoProjectionClient
from hub_core.runtime.store import InMemoryPortStore, PortStore
from hub_core.runtime.validation import ContractValidator
from hub_core.runtime.workload_projection import (
@ -37,8 +39,20 @@ def create_app(
resolved_settings = settings or RuntimeSettings.from_env()
resolved_store = port_store or _create_store(resolved_settings)
owns_store = port_store is None
resolved_repo_projection_client = repo_projection_client
owns_repo_projection_client = False
if (
resolved_repo_projection_client is None
and resolved_settings.repo_manager_base_url is not None
):
resolved_repo_projection_client = HTTPRepoProjectionClient(
resolved_settings.repo_manager_base_url,
api_token=resolved_settings.repo_manager_api_token,
timeout_seconds=resolved_settings.repo_manager_timeout_seconds,
)
owns_repo_projection_client = True
repository_navigation = RepositoryNavigationService(
client=repo_projection_client,
client=resolved_repo_projection_client,
store=resolved_store,
)
workload_projection = WorkloadProjectionService(
@ -48,19 +62,33 @@ def create_app(
@asynccontextmanager
async def lifespan(_: FastAPI):
if repo_projection_client is not None:
refresh_task: asyncio.Task[None] | None = None
if resolved_repo_projection_client is not None:
try:
await repository_navigation.refresh()
except ProjectionRejected:
# The readiness dependency reports the rejected or absent
# projection while the API remains available for diagnosis.
pass
if resolved_settings.repo_projection_refresh_seconds > 0:
refresh_task = asyncio.create_task(
_refresh_repository_projection(
repository_navigation,
resolved_settings.repo_projection_refresh_seconds,
)
)
if workload_projection_client is not None:
try:
await workload_projection.refresh()
except WorkloadProjectionRejected:
pass
yield
if refresh_task is not None:
refresh_task.cancel()
with suppress(asyncio.CancelledError):
await refresh_task
if owns_repo_projection_client:
await resolved_repo_projection_client.aclose() # type: ignore[union-attr]
if owns_store and (closer := getattr(resolved_store, "aclose", None)):
await closer()
@ -117,6 +145,19 @@ def create_app(
return app
async def _refresh_repository_projection(
service: RepositoryNavigationService,
interval_seconds: float,
) -> None:
while True:
await asyncio.sleep(interval_seconds)
try:
await service.refresh()
except ProjectionRejected:
# The service records stale state and diagnostics; retry on schedule.
pass
def _create_store(settings: RuntimeSettings) -> PortStore:
if settings.backend == "memory":
return InMemoryPortStore()