2026-03-17 14:36:31 +08:00
|
|
|
"""
|
|
|
|
|
YG-Dataset Backend Application
|
|
|
|
|
FastAPI-based API server for dataset generation platform
|
|
|
|
|
"""
|
|
|
|
|
|
2026-03-17 17:30:11 +08:00
|
|
|
import logging
|
|
|
|
|
import time
|
|
|
|
|
import uuid
|
2026-03-17 14:36:31 +08:00
|
|
|
from contextlib import asynccontextmanager
|
2026-03-17 17:30:11 +08:00
|
|
|
from fastapi import FastAPI, Request
|
2026-03-17 14:36:31 +08:00
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
2026-03-17 17:30:11 +08:00
|
|
|
from fastapi.responses import JSONResponse
|
|
|
|
|
from fastapi.exceptions import RequestValidationError
|
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
|
|
|
from sqlalchemy.exc import SQLAlchemyError
|
2026-03-17 14:36:31 +08:00
|
|
|
|
|
|
|
|
from app.api.v1 import api_router
|
2026-03-17 17:30:11 +08:00
|
|
|
from app.api.response import ApiResponse
|
2026-03-17 14:36:31 +08:00
|
|
|
from app.core.config import settings
|
2026-03-17 17:30:11 +08:00
|
|
|
from app.core.database import init_db, close_db
|
|
|
|
|
from app.core.exceptions import AppException
|
|
|
|
|
from app.core.logging import logger
|
|
|
|
|
|
2026-03-17 23:02:43 +08:00
|
|
|
# Import all models to register them with Base.metadata
|
|
|
|
|
from app.models.models import * # noqa: F401, F403
|
|
|
|
|
|
2026-03-17 17:30:11 +08:00
|
|
|
|
|
|
|
|
class RequestIDMiddleware(BaseHTTPMiddleware):
|
|
|
|
|
"""Middleware to add request ID to each request"""
|
|
|
|
|
|
|
|
|
|
async def dispatch(self, request: Request, call_next):
|
|
|
|
|
request_id = str(uuid.uuid4())
|
|
|
|
|
request.state.request_id = request_id
|
|
|
|
|
|
|
|
|
|
# Add request ID to response headers
|
|
|
|
|
response = await call_next(request)
|
|
|
|
|
response.headers["X-Request-ID"] = request_id
|
|
|
|
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TimingMiddleware(BaseHTTPMiddleware):
|
|
|
|
|
"""Middleware to measure request processing time"""
|
|
|
|
|
|
|
|
|
|
async def dispatch(self, request: Request, call_next):
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
# Log request
|
|
|
|
|
logger.info(f"→ {request.method} {request.url.path}")
|
|
|
|
|
|
|
|
|
|
response = await call_next(request)
|
|
|
|
|
|
|
|
|
|
process_time = time.time() - start_time
|
|
|
|
|
response.headers["X-Process-Time"] = str(process_time)
|
|
|
|
|
|
|
|
|
|
# Log response
|
|
|
|
|
logger.info(f"← {request.method} {request.url.path} | Status: {response.status_code} | Time: {process_time:.3f}s")
|
|
|
|
|
|
|
|
|
|
return response
|
2026-03-17 14:36:31 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
async def lifespan(app: FastAPI):
|
|
|
|
|
"""Application lifespan events"""
|
|
|
|
|
# Startup
|
2026-03-17 17:30:11 +08:00
|
|
|
logger.info("Starting YG-Dataset application...")
|
2026-03-17 14:36:31 +08:00
|
|
|
await init_db()
|
2026-03-17 17:30:11 +08:00
|
|
|
logger.info("Database initialized successfully")
|
2026-03-17 14:36:31 +08:00
|
|
|
yield
|
|
|
|
|
# Shutdown
|
2026-03-17 17:30:11 +08:00
|
|
|
logger.info("Shutting down YG-Dataset application...")
|
|
|
|
|
await close_db()
|
|
|
|
|
logger.info("Database connections closed")
|
2026-03-17 14:36:31 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI(
|
|
|
|
|
title="YG-Dataset API",
|
|
|
|
|
description="Dataset Generation Platform API",
|
|
|
|
|
version="1.0.0",
|
|
|
|
|
lifespan=lifespan,
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-17 17:30:11 +08:00
|
|
|
# Add custom middleware (order matters: last added = first executed)
|
|
|
|
|
app.add_middleware(TimingMiddleware)
|
|
|
|
|
app.add_middleware(RequestIDMiddleware)
|
|
|
|
|
|
|
|
|
|
# CORS - Configure properly for production
|
|
|
|
|
# For development, you can use ["*"] but for production, specify exact origins
|
2026-03-17 23:02:43 +08:00
|
|
|
ALLOWED_ORIGINS = ["*"]
|
2026-03-17 17:30:11 +08:00
|
|
|
|
2026-03-17 14:36:31 +08:00
|
|
|
app.add_middleware(
|
|
|
|
|
CORSMiddleware,
|
2026-03-17 17:30:11 +08:00
|
|
|
allow_origins=ALLOWED_ORIGINS,
|
2026-03-17 14:36:31 +08:00
|
|
|
allow_credentials=True,
|
2026-03-17 17:30:11 +08:00
|
|
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
2026-03-17 14:36:31 +08:00
|
|
|
allow_headers=["*"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-17 17:30:11 +08:00
|
|
|
|
|
|
|
|
# Exception handlers
|
|
|
|
|
@app.exception_handler(AppException)
|
|
|
|
|
async def app_exception_handler(request: Request, exc: AppException):
|
|
|
|
|
"""Handle custom application exceptions"""
|
|
|
|
|
logger.warning(f"App exception: {exc.message} | Code: {exc.code}")
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=exc.status_code,
|
|
|
|
|
content=ApiResponse.fail(
|
|
|
|
|
message=exc.message,
|
|
|
|
|
error={"code": exc.code, "details": exc.details}
|
2026-03-18 10:44:09 +08:00
|
|
|
).model_dump(mode='json')
|
2026-03-17 17:30:11 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(RequestValidationError)
|
|
|
|
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
|
|
|
"""Handle validation exceptions"""
|
|
|
|
|
errors = []
|
|
|
|
|
for error in exc.errors():
|
|
|
|
|
errors.append({
|
|
|
|
|
"field": ".".join(str(loc) for loc in error["loc"]),
|
|
|
|
|
"message": error["msg"],
|
|
|
|
|
"type": error["type"]
|
|
|
|
|
})
|
|
|
|
|
logger.warning(f"Validation error: {errors}")
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=422,
|
|
|
|
|
content=ApiResponse.fail(
|
|
|
|
|
message="Validation error",
|
|
|
|
|
error={"code": "VALIDATION_ERROR", "details": {"errors": errors}}
|
2026-03-18 10:44:09 +08:00
|
|
|
).model_dump(mode='json')
|
2026-03-17 17:30:11 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(SQLAlchemyError)
|
|
|
|
|
async def database_exception_handler(request: Request, exc: SQLAlchemyError):
|
|
|
|
|
"""Handle database exceptions"""
|
|
|
|
|
logger.error(f"Database error: {str(exc)}", exc_info=True)
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=500,
|
|
|
|
|
content=ApiResponse.fail(
|
|
|
|
|
message="Database operation failed",
|
|
|
|
|
error={"code": "DATABASE_ERROR"}
|
2026-03-18 10:44:09 +08:00
|
|
|
).model_dump(mode='json')
|
2026-03-17 17:30:11 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
|
|
|
async def general_exception_handler(request: Request, exc: Exception):
|
|
|
|
|
"""Handle unhandled exceptions"""
|
|
|
|
|
logger.error(f"Unhandled exception: {str(exc)}", exc_info=True)
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=500,
|
|
|
|
|
content=ApiResponse.fail(
|
|
|
|
|
message="Internal server error",
|
|
|
|
|
error={"code": "INTERNAL_ERROR"}
|
2026-03-18 10:44:09 +08:00
|
|
|
).model_dump(mode='json')
|
2026-03-17 17:30:11 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-03-17 14:36:31 +08:00
|
|
|
# Include API routes
|
|
|
|
|
app.include_router(api_router, prefix="/api/v1")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/health")
|
|
|
|
|
async def health_check():
|
|
|
|
|
"""Health check endpoint"""
|
2026-03-17 17:30:11 +08:00
|
|
|
return ApiResponse.ok(
|
|
|
|
|
data={"status": "healthy", "version": "1.0.0"},
|
|
|
|
|
message="Service is running"
|
|
|
|
|
)
|
2026-03-17 14:36:31 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
import uvicorn
|
|
|
|
|
uvicorn.run(
|
|
|
|
|
"app.main:app",
|
|
|
|
|
host=settings.HOST,
|
|
|
|
|
port=settings.PORT,
|
|
|
|
|
reload=settings.DEBUG,
|
|
|
|
|
)
|