Убраны SPA, CORS, SMB, РАИСА, SQLAlchemy-обработчики и зависимости БД/SMB; настройки: ENV_FILE, SERVICE_NAME, NATS_ENABLED, WORKER_CONCURRENCY, PUBLISH_RETRIES, DONE_CACHE_SIZE, MINIO_SECURE. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
117 lines
4.4 KiB
Python
117 lines
4.4 KiB
Python
"""Точка входа 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=["*"])
|