"""FastAPI app — Signal V2 Pull Worker.""" from __future__ import annotations import asyncio import logging from contextlib import asynccontextmanager from fastapi import FastAPI from signal_v2 import state as state_mod from signal_v2.config import get_settings from signal_v2.pull_worker import poll_loop from signal_v2.rate_limit import SignalDedup from signal_v2.stock_client import StockClient logger = logging.getLogger(__name__) class AppContext: client: StockClient | None = None dedup: SignalDedup | None = None shutdown: asyncio.Event | None = None poll_task: asyncio.Task | None = None _ctx = AppContext() @asynccontextmanager async def lifespan(app: FastAPI): settings = get_settings() if not settings.webai_api_key: logger.warning( "WEBAI_API_KEY not configured — stock API calls will fail with 401" ) _ctx.client = StockClient(settings.stock_api_url, settings.webai_api_key) _ctx.dedup = SignalDedup(settings.db_path) _ctx.shutdown = asyncio.Event() _ctx.poll_task = asyncio.create_task( poll_loop(_ctx.client, state_mod.state, _ctx.shutdown) ) yield # Shutdown if _ctx.shutdown is not None: _ctx.shutdown.set() if _ctx.poll_task is not None: try: await asyncio.wait_for(_ctx.poll_task, timeout=5.0) except asyncio.TimeoutError: _ctx.poll_task.cancel() try: await _ctx.poll_task except asyncio.CancelledError: pass if _ctx.client is not None: await _ctx.client.close() app = FastAPI( title="Signal V2 Pull Worker", version="0.1.0", lifespan=lifespan ) @app.get("/health") async def health(): settings = get_settings() return { "status": "online", "stock_api_url": settings.stock_api_url, "last_poll": state_mod.state.last_updated, "cache_size": _ctx.client.cache_size() if _ctx.client is not None else 0, }