From 0bb94da3c43e435870332b545a8819a81ff63753 Mon Sep 17 00:00:00 2001 From: Yuzviak Date: Mon, 11 May 2026 09:04:56 +0300 Subject: [PATCH] feat(01-08): rewire app.py lifespan and deps.py to use build_pipeline - app.py: replace inline component wiring with build_pipeline(env=cfg.env) - Store processor as app.state.processor (and backwards-compat pipeline_components) - RuntimeConfig replaces get_settings(); MAVLink stop() on shutdown - deps.py: get_flight_processor prefers app.state.processor from lifespan - Falls back to build_pipeline() for test contexts without lifespan - Per-request repo/streamer swap preserved --- src/gps_denied/api/deps.py | 25 +++++++++---- src/gps_denied/app.py | 76 ++++++++++++++------------------------ 2 files changed, 45 insertions(+), 56 deletions(-) diff --git a/src/gps_denied/api/deps.py b/src/gps_denied/api/deps.py index 269a7fe..6d37e23 100644 --- a/src/gps_denied/api/deps.py +++ b/src/gps_denied/api/deps.py @@ -5,7 +5,7 @@ from fastapi import Depends, HTTPException, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.ext.asyncio import AsyncSession -from gps_denied.config import get_settings +from gps_denied.config import RuntimeConfig, get_settings from gps_denied.core.processor import FlightProcessor from gps_denied.core.sse import SSEEventStreamer from gps_denied.db.engine import get_session @@ -65,14 +65,23 @@ async def get_flight_processor( ) -> FlightProcessor: global _processor if _processor is None: - eskf_config = getattr(request.app.state, "eskf_config", None) - _processor = FlightProcessor(repo, sse, eskf_config=eskf_config) - # Підключаємо pipeline компоненти з lifespan - components = getattr(request.app.state, "pipeline_components", None) - if components: - _processor.attach_components(**components) - # Оновлюємо repo (нова сесія на кожен запит) + # Prefer the processor already built by lifespan (via build_pipeline) + lifespan_processor = getattr(request.app.state, "processor", None) + if lifespan_processor is not None: + _processor = lifespan_processor + else: + # Fallback: build pipeline directly (e.g. in tests without lifespan) + from gps_denied.pipeline import build_pipeline + _settings = RuntimeConfig() + _processor = build_pipeline( + env=_settings.env, + config=_settings, + repository=repo, + streamer=sse, + ) + # Оновлюємо repo та streamer (нова сесія на кожен запит) _processor.repository = repo + _processor.streamer = sse return _processor diff --git a/src/gps_denied/app.py b/src/gps_denied/app.py index 7c187bd..e49db2f 100644 --- a/src/gps_denied/app.py +++ b/src/gps_denied/app.py @@ -7,70 +7,50 @@ from fastapi import FastAPI from gps_denied import __version__ from gps_denied.api.routers import flights -from gps_denied.config import get_settings +from gps_denied.config import RuntimeConfig +from gps_denied.pipeline import build_pipeline logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): - """Initialise core pipeline components on startup.""" - from gps_denied.core.chunk_manager import RouteChunkManager - from gps_denied.core.coordinates import CoordinateTransformer - from gps_denied.core.gpr import GlobalPlaceRecognition - from gps_denied.core.graph import FactorGraphOptimizer - from gps_denied.core.mavlink import MAVLinkBridge - from gps_denied.core.metric import MetricRefinement - from gps_denied.core.models import ModelManager - from gps_denied.core.recovery import FailureRecoveryCoordinator - from gps_denied.core.rotation import ImageRotationManager - from gps_denied.core.satellite import SatelliteDataManager - from gps_denied.core.vo import create_vo_backend - from gps_denied.schemas.eskf import ESKFConfig - from gps_denied.schemas.graph import FactorGraphConfig + """Initialise core pipeline components on startup via build_pipeline.""" + cfg = RuntimeConfig() + processor = build_pipeline(env=cfg.env, config=cfg) - settings = get_settings() + # Retrieve MAVLink bridge from processor internals for lifecycle management + mavlink = processor._mavlink - mm = ModelManager(engine_dir=str(settings.models.weights_dir)) - vo = create_vo_backend(model_manager=mm) - gpr = GlobalPlaceRecognition(mm) - metric = MetricRefinement(mm) - graph = FactorGraphOptimizer(FactorGraphConfig()) - chunk_mgr = RouteChunkManager(graph) - recovery = FailureRecoveryCoordinator(chunk_mgr, gpr, metric) - rotation = ImageRotationManager(mm) - coord = CoordinateTransformer() - satellite = SatelliteDataManager(tile_dir=settings.satellite.tile_dir) - mavlink = MAVLinkBridge( - connection_string=settings.mavlink.connection, - output_hz=settings.mavlink.output_hz, - telemetry_hz=settings.mavlink.telemetry_hz, - ) - - # ESKF config from env vars (per-airframe tuning) - eskf_config = ESKFConfig(**settings.eskf.model_dump()) - - # Store on app.state so deps can access them + app.state.processor = processor + app.state.config = cfg + # Keep backwards-compat key so any code reading pipeline_components still works app.state.pipeline_components = { - "vo": vo, "gpr": gpr, "metric": metric, - "graph": graph, "recovery": recovery, - "chunk_mgr": chunk_mgr, "rotation": rotation, - "coord": coord, "satellite": satellite, + "vo": processor._vo, + "gpr": processor._gpr, + "metric": processor._metric, + "graph": processor._graph, + "recovery": processor._recovery, + "chunk_mgr": processor._chunk_mgr, + "rotation": processor._rotation, + "coord": processor._coord, + "satellite": processor._satellite, "mavlink": mavlink, } - app.state.eskf_config = eskf_config + app.state.eskf_config = processor._eskf_config logger.info( - "Pipeline ready — MAVLink: %s, tiles: %s", - settings.mavlink.connection, settings.satellite.tile_dir, + "Pipeline ready — env=%s, MAVLink: %s, tiles: %s", + cfg.env, cfg.mavlink.connection, cfg.satellite.tile_dir, ) yield - # Cleanup - try: - await mavlink.stop() - except Exception: - pass + # Cleanup MAVLink on shutdown + if mavlink is not None: + try: + await mavlink.stop() + except Exception: + pass app.state.pipeline_components = None