"""FastAPI application entry point with lifespan management.""" 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")