import asyncio import logging from collections.abc import AsyncIterator from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from .flows.api import approvals_router, flows_router from .flows.sync import run_sync_blocking, watch_forever from .settings import settings logger = logging.getLogger(__name__) def _blocking_startup_sync() -> None: """One-shot reconciliation at startup (fresh session, never fatal).""" try: report = run_sync_blocking(Path(settings.flows_dir)) logger.info( "startup flow sync: +%d ~%d -%d skipped=%d errors=%d", len(report.added), len(report.updated), len(report.deleted), len(report.skipped), len(report.errors), ) for filename, reason in report.errors.items(): logger.warning("startup flow sync: %s: %s", filename, reason) except FileNotFoundError as exc: logger.error("startup flow sync skipped: %s", exc) except Exception: # noqa: BLE001 - API must start even if sync fails logger.exception("startup flow sync failed") @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncIterator[None]: task: asyncio.Task | None = None stop: asyncio.Event | None = None if settings.flows_watch_enabled: await asyncio.to_thread(_blocking_startup_sync) stop = asyncio.Event() task = asyncio.create_task( watch_forever(stop, Path(settings.flows_dir)), name="flows-watcher" ) yield if task is not None and stop is not None: stop.set() task.cancel() try: await task except (asyncio.CancelledError, Exception): # noqa: BLE001 - shutdown pass app = FastAPI( title="Kirche Einfach API", description="KI-gestützte Verwaltungsplattform für Kirche", version="0.1.0", docs_url="/api/docs", redoc_url="/api/redoc", openapi_url="/api/openapi.json", lifespan=lifespan, ) app.include_router(flows_router) app.include_router(approvals_router) @app.get("/") async def root(): return {"message": "Kirche Einfach API", "version": settings.version} @app.get("/health") async def health(): return {"status": "ok"}