Files
deepdata/runtime/deepdata-python/app/main.py
T

166 lines
5.0 KiB
Python

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(),
)