"""Точка входа FastAPI приложения — Академический помощник (anti-plagiarism).""" import logging from contextlib import asynccontextmanager from typing import AsyncGenerator from fastapi import FastAPI, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.api import auth, documents, reports, search, tasks from app.config import settings from app.core.websocket_manager import ws_manager from app.database import engine logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: """ Lifecycle менеджер приложения. Выполняет инициализацию при старте и очистку при остановке. """ logger.info("Запуск Академического помощника...") # Проверка соединений при старте try: async with engine.connect() as conn: await conn.execute(__import__("sqlalchemy").text("SELECT 1")) logger.info("PostgreSQL: соединение установлено") except Exception as e: logger.error(f"PostgreSQL: ошибка соединения: {e}") yield # Очистка при остановке logger.info("Остановка приложения...") await engine.dispose() app = FastAPI( title="Академический помощник API", description=( "Сервис антиплагиата и поиска академических источников. " "Семантический поиск через FAISS GPU + Elasticsearch, " "проверка плагиата в 4 уровня, ГОСТ библиография." ), version="0.1.0", lifespan=lifespan, docs_url="/api/docs", redoc_url="/api/redoc", openapi_url="/api/openapi.json", ) # ─── CORS ───────────────────────────────────────────────────────────────────── app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ─── Роутеры ────────────────────────────────────────────────────────────────── app.include_router(auth.router, prefix="/api") app.include_router(tasks.router, prefix="/api") app.include_router(search.router, prefix="/api") app.include_router(documents.router, prefix="/api") app.include_router(reports.router, prefix="/api") # ─── WebSocket ──────────────────────────────────────────────────────────────── @app.websocket("/ws/tasks/{task_id}") async def websocket_task_updates(websocket: WebSocket, task_id: str) -> None: """ WebSocket endpoint для real-time обновлений статуса задачи. Клиент подключается и получает обновления при изменении статуса задачи. Соединение закрывается при получении статуса done/failed. """ await ws_manager.connect(task_id, websocket) try: while True: # Ждём входящих сообщений (ping/pong для поддержания соединения) data = await websocket.receive_text() if data == "ping": await websocket.send_text("pong") except WebSocketDisconnect: ws_manager.disconnect(task_id, websocket) logger.info(f"WebSocket клиент отключился от задачи {task_id!r}") # ─── Health check ────────────────────────────────────────────────────────────── @app.get("/health", tags=["monitoring"]) async def health_check() -> JSONResponse: """Проверка работоспособности сервиса.""" return JSONResponse( content={ "status": "ok", "service": "api", "version": "0.1.0", } ) @app.get("/", include_in_schema=False) async def root() -> JSONResponse: """Корневой endpoint — редирект к документации.""" return JSONResponse( content={"message": "Академический помощник API", "docs": "/api/docs"} )