import logging import sys from contextlib import asynccontextmanager from pathlib import Path import uvicorn from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from loguru import logger from app.api import router as api_router from app.config import settings from app.database import check_database, engine from app.dependencies import build_container def _configure_uvicorn_access_log_filters() -> None: """第三方或 Demo 若轮询 pending-confirmation,无条目时 404 为常态;压低 uvicorn access 刷屏。""" class _SuppressPendingPoll404(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: try: msg = record.getMessage() except Exception: return True if "/pending-confirmation" in msg and "GET" in msg and " 404 " in msg: return False return True logging.getLogger("uvicorn.access").addFilter(_SuppressPendingPoll404()) def configure_logging() -> None: """集中配置 loguru sink;由 create_app 显式调用,避免 import-time 副作用。""" logger.remove() logger.add( sys.stderr, format=( "{time:YYYY-MM-DD HH:mm:ss} | " "{level: <8} | " "{name}:{function} - {message}" ), ) @asynccontextmanager async def lifespan(app: FastAPI): await check_database() logger.info( "Database connection verified; ensure schema is applied with " "`alembic upgrade head` before serving traffic" ) container = build_container(settings) app.state.container = container await container.start() try: yield finally: await container.shutdown() await engine.dispose() logger.info("Database engine disposed") def create_app() -> FastAPI: configure_logging() _configure_uvicorn_access_log_filters() application = FastAPI( title="Operation Room Monitor", lifespan=lifespan, ) if settings.demo_cors_enabled: origins = settings.parsed_demo_cors_origins() if origins: application.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=origins != ["*"], allow_methods=["*"], allow_headers=["*"], ) logger.info("CORS enabled for demo client; origins={}", origins) application.include_router(api_router) if settings.demo_orchestrator_enabled: from app.routers import demo_orch application.include_router(demo_orch.router) logger.info( "Demo orchestrator enabled: POST /internal/demo/orchestrate-and-start", ) else: logger.info( "Demo orchestrator disabled (DEMO_ORCHESTRATOR_ENABLED=false): " "GET /internal/demo/orchestrator-status for status; " "POST /internal/demo/orchestrate-and-start is not registered", ) return application app = create_app() def main() -> None: root = Path(__file__).resolve().parent kwargs: dict = { "host": settings.server_host, "port": settings.server_port, } if settings.server_reload: kwargs["reload"] = True kwargs["reload_dirs"] = [str(root)] kwargs["reload_includes"] = ["*.py"] kwargs["reload_excludes"] = [ "**/.venv/**", "**/__pycache__/**", "**/web/**", "**/scripts/demo_client/**", "**/.git/**", ] uvicorn.run("main:app", **kwargs) if __name__ == "__main__": main()