"""Точка входа FastAPI сервиса tabular-parser. Что здесь есть: настройки, подключение S3 (MinIO), NATS (nkeys JWT; пустые seed/jwt — без аутентификации), middleware (gzip, proxy headers), healthcheck, обработчики исключений. Сервис без БД и фронта (DECISION-4). Воркер обработки `recognition.request` и поле `nats` в healthcheck добавляются позже (src/core/nats.py). """ import base64 from contextlib import asynccontextmanager from datetime import UTC, datetime import nkeys from fastapi import FastAPI from loguru import logger from miniopy_async import Minio from nats.aio.client import Client as NATS from starlette.middleware.gzip import GZipMiddleware from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware from src.api.router import router from src.core.clients import S3Storage from src.core.config import get_settings from src.core.exception_handlers import register_exception_handlers from src.core.logger import setup_logger settings = get_settings() VERSION = settings.VERSION UP_TIME = datetime.now(UTC) setup_logger(level="DEBUG" if settings.DEBUG else "INFO") async def _connect_nats() -> NATS | None: """Подключение к NATS (core, без JetStream); при ошибке — ``None``, приложение всё равно стартует.""" nats_settings = settings.OPENBAO.NATS options: dict = {"max_reconnect_attempts": 2} if nats_settings.NATS_SEED and nats_settings.NATS_JWT: kp = nkeys.from_seed(nats_settings.NATS_SEED.encode("utf-8")) def sig_cb(nonce: bytes | str) -> bytes: if isinstance(nonce, str): nonce = nonce.encode("utf-8") return base64.b64encode(kp.sign(nonce)) options |= {"user_jwt_cb": lambda: nats_settings.NATS_JWT.encode("utf-8"), "signature_cb": sig_cb} nc = NATS() try: await nc.connect(nats_settings.NATS_URL, **options) except Exception as exc: logger.exception( "Не удалось подключиться к NATS. error_category=nats_error, error_type={error_type}, error={error}", error_type=type(exc).__name__, error=repr(exc), ) return None logger.info("Подключение к NATS произошло") return nc async def _connect_s3() -> S3Storage | None: """Клиент S3 с проверкой бакета; при недоступности — ``None`` (приложение всё равно стартует).""" minio = settings.OPENBAO.MINIO s3_client = Minio( minio.MINIO_URL, access_key=minio.MINIO_ADMIN_LOGIN, secret_key=minio.MINIO_ADMIN_PASSWORD, secure=minio.MINIO_SECURE, cert_check=False, ) try: bucket_exists = await s3_client.bucket_exists(minio.MINIO_BUCKET) except Exception as exc: logger.exception( "Не удалось проверить наличие бакета. error_category=s3_error, error_type={error_type}, error={error}", error_type=type(exc).__name__, error=repr(exc), ) await s3_client.close_session() return None logger.info("Бакет проверен. bucket_exists={bucket_exists}", bucket_exists=bucket_exists) return S3Storage(client=s3_client, bucket_name=minio.MINIO_BUCKET) @asynccontextmanager async def lifespan(app: FastAPI): app.state.storage = await _connect_s3() app.state.nats = await _connect_nats() if settings.NATS_ENABLED else None if not settings.NATS_ENABLED: logger.info("NATS выключен (NATS_ENABLED=false)") try: logger.info("Приложение запущено") yield finally: logger.info("Приложение закрывается") if app.state.nats: await app.state.nats.close() if app.state.storage: await app.state.storage.client.close_session() app = FastAPI(title="tabular-parser", version=VERSION, root_path=settings.ROOT_PATH, lifespan=lifespan) @app.get("/healthcheck", status_code=200) async def healthcheck(): return {"uptime": UP_TIME, "version": VERSION} app.include_router(router) register_exception_handlers(app) app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=1) app.add_middleware(ProxyHeadersMiddleware, trusted_hosts=["*"])