"""FastAPI application entry point with lifespan management.""" # --------------------------------------------------------------------------- # SSL + proxy injection — MUST happen before any HTTP client imports # --------------------------------------------------------------------------- import os as _os import ssl as _ssl from pathlib import Path as _Path _COMBINED_CERT = _Path(__file__).resolve().parent.parent / "combined-ca-bundle.pem" if _COMBINED_CERT.exists(): _cert_path = str(_COMBINED_CERT) # Env vars for libraries that respect them (requests, urllib3) _os.environ["SSL_CERT_FILE"] = _cert_path _os.environ["REQUESTS_CA_BUNDLE"] = _cert_path _os.environ["CURL_CA_BUNDLE"] = _cert_path # Monkey-patch ssl.create_default_context so that ALL libraries # (aiohttp, httpx, google-genai, alpaca-py, etc.) automatically # use our combined CA bundle that includes the corporate root cert. _original_create_default_context = _ssl.create_default_context def _patched_create_default_context( purpose=_ssl.Purpose.SERVER_AUTH, *, cafile=None, capath=None, cadata=None ): ctx = _original_create_default_context( purpose, cafile=cafile, capath=capath, cadata=cadata ) # Always load our combined bundle on top of whatever was loaded ctx.load_verify_locations(cafile=_cert_path) return ctx _ssl.create_default_context = _patched_create_default_context # Also patch aiohttp's cached SSL context objects directly, since # aiohttp creates them at import time and may have already cached # a context without our corporate CA bundle. try: import aiohttp.connector as _aio_conn if hasattr(_aio_conn, '_SSL_CONTEXT_VERIFIED') and _aio_conn._SSL_CONTEXT_VERIFIED is not None: _aio_conn._SSL_CONTEXT_VERIFIED.load_verify_locations(cafile=_cert_path) if hasattr(_aio_conn, '_SSL_CONTEXT_UNVERIFIED') and _aio_conn._SSL_CONTEXT_UNVERIFIED is not None: _aio_conn._SSL_CONTEXT_UNVERIFIED.load_verify_locations(cafile=_cert_path) except ImportError: pass # Corporate proxy — needed when Kiro spawns the process (no .zshrc sourced) _PROXY = "http://aproxy.corproot.net:8080" _NO_PROXY = "corproot.net,sharedtcs.net,127.0.0.1,localhost,bix.swisscom.com,swisscom.com" _os.environ.setdefault("HTTP_PROXY", _PROXY) _os.environ.setdefault("HTTPS_PROXY", _PROXY) _os.environ.setdefault("NO_PROXY", _NO_PROXY) import logging import sys from contextlib import asynccontextmanager from collections.abc import AsyncGenerator from fastapi import FastAPI from passlib.hash import bcrypt from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database import async_session_factory, engine from app.middleware import register_exception_handlers from app.models.user import User from app.scheduler import configure_scheduler, scheduler from app.routers.admin import router as admin_router from app.routers.auth import router as auth_router from app.routers.health import router as health_router from app.routers.ingestion import router as ingestion_router from app.routers.ohlcv import router as ohlcv_router from app.routers.indicators import router as indicators_router from app.routers.fundamentals import router as fundamentals_router from app.routers.scores import router as scores_router from app.routers.trades import router as trades_router from app.routers.watchlist import router as watchlist_router from app.routers.sentiment import router as sentiment_router from app.routers.sr_levels import router as sr_levels_router from app.routers.tickers import router as tickers_router def _configure_logging() -> None: """Set up structured JSON-style logging.""" handler = logging.StreamHandler(sys.stdout) handler.setFormatter( logging.Formatter( '{"time":"%(asctime)s","level":"%(levelname)s",' '"logger":"%(name)s","message":"%(message)s"}' ) ) root = logging.getLogger() root.handlers.clear() root.addHandler(handler) root.setLevel(settings.log_level.upper()) async def _create_default_admin(session: AsyncSession) -> None: """Create the default admin account if no admin user exists.""" result = await session.execute( select(User).where(User.role == "admin") ) if result.scalar_one_or_none() is None: admin = User( username="admin", password_hash=bcrypt.hash("admin"), role="admin", has_access=True, ) session.add(admin) await session.commit() logging.getLogger(__name__).info("Default admin account created") @asynccontextmanager async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: """Manage startup and shutdown lifecycle.""" logger = logging.getLogger(__name__) _configure_logging() logger.info("Starting Stock Data Backend") async with async_session_factory() as session: await _create_default_admin(session) configure_scheduler() scheduler.start() logger.info("Scheduler started") yield scheduler.shutdown(wait=False) logger.info("Scheduler stopped") await engine.dispose() logger.info("Shutting down") app = FastAPI( title="Stock Data Backend", version="0.1.0", lifespan=lifespan, ) register_exception_handlers(app) app.include_router(health_router, prefix="/api/v1") app.include_router(auth_router, prefix="/api/v1") app.include_router(admin_router, prefix="/api/v1") app.include_router(tickers_router, prefix="/api/v1") app.include_router(ohlcv_router, prefix="/api/v1") app.include_router(ingestion_router, prefix="/api/v1") app.include_router(indicators_router, prefix="/api/v1") app.include_router(sr_levels_router, prefix="/api/v1") app.include_router(sentiment_router, prefix="/api/v1") app.include_router(fundamentals_router, prefix="/api/v1") app.include_router(scores_router, prefix="/api/v1") app.include_router(trades_router, prefix="/api/v1") app.include_router(watchlist_router, prefix="/api/v1")