from contextlib import asynccontextmanager from typing import Literal from urllib.parse import urlparse, urlunparse from fastapi import FastAPI, Depends from fastapi_mcp import FastApiMCP from fastapi.responses import RedirectResponse from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.exception import ( register_exception_handlers, SuccessResponse, ExceptionHandlingMiddleware, ErrorCodes, BusinessError, ) from app.core.logging import setup_logging, get_logger from app.db.session import AsyncSessionLocal, get_db from app.middleware import UserContextMiddleware from app.module import router from app.module.collection.schedule import ( load_scheduled_collection_tasks, set_collection_scheduler, ) from app.module.shared.schedule import Scheduler from app.module.generation.service.task_executor import init_executor, shutdown_executor from app.module.system.service.log_pvc_monitor import schedule_log_pvc_monitor setup_logging() logger = get_logger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): # @startup logger.info("DeepData Python Backend starting...") # Database connection validation try: async with AsyncSessionLocal() as session: await session.execute(text("SELECT 1")) def mask_db_url(url: str) -> Literal[b""] | str: parsed = urlparse(url) if parsed.password is not None: # 重建 netloc,将密码替换为 **** netloc = f"{parsed.username}:*****@{parsed.hostname}" if parsed.port: netloc += f":{parsed.port}" return urlunparse(parsed._replace(netloc=netloc)) return url # 使用示例 logger.info(f"Database: {mask_db_url(settings.database_url)}") except Exception as e: logger.error(f"Database connection validation failed: {e}") logger.debug(f"Connection details: {settings.database_url}") raise # Label Studio # TODO Add actual connectivity check if needed logger.info(f"Label Studio: {settings.label_studio_base_url}") # Collection scheduler collection_scheduler = Scheduler(name="collection scheduler") collection_scheduler.start() set_collection_scheduler(collection_scheduler) await load_scheduled_collection_tasks() schedule_log_pvc_monitor(collection_scheduler) # Initialize generation task executor init_executor(max_workers=10, max_concurrent_tasks=5) logger.info("Generation task executor initialized") # 恢复未完成任务的轮询 + 启动全局状态轮询协程 from app.module.cleaning.service.cleaning_task_scheduler import get_scheduler scheduler = get_scheduler() await scheduler.startup() logger.info("Cleaning task status polling started") yield # @shutdown collection_scheduler.shutdown() shutdown_executor() logger.info("DeepData Python Backend shutting down ...\n\n") # 创建FastAPI应用 app = FastAPI( title=settings.app_name, description=settings.app_description, version=settings.app_version, debug=settings.debug, lifespan=lifespan, ) # 注册全局异常捕获中间件(最外层,确保捕获所有异常) # 这样即使 debug=True,也不会泄露堆栈信息给客户端 app.add_middleware(ExceptionHandlingMiddleware) # 注册用户上下文中间件 app.add_middleware(UserContextMiddleware) # 注册路由 app.include_router(router) # 注册全局异常处理器 register_exception_handlers(app) # 测试端点:验证异常处理 @app.get("/test-success", include_in_schema=False) async def test_success(): """测试成功响应""" return SuccessResponse(data={"message": "Test successful"}) @app.get("/test-business-error", include_in_schema=False) async def test_business_error(): """测试业务错误响应""" raise BusinessError(ErrorCodes.ANNOTATION_TASK_NOT_FOUND) @app.get("/test-system-error", include_in_schema=False) async def test_system_error(): """测试系统错误响应""" raise SystemError(ErrorCodes.DATABASE_ERROR) # 根路径 @app.get("/", include_in_schema=False) async def root(): """根路径,返回服务信息""" return SuccessResponse( data={ "message": f"{settings.app_name} is running", "version": settings.app_version, "docs_url": "/redoc", "label_studio_url": settings.label_studio_base_url } ) mcp = FastApiMCP( app, name="DeepData MCP", description="DeepData python mcp server", include_tags=["mcp"], ) mcp.mount_http(mount_path="/api/mcp") if __name__ == "__main__": import uvicorn uvicorn.run( "app.main:app", host=settings.host, port=settings.port, reload=settings.debug, log_level=settings.log_level.lower(), )