arm-core/api/src/app.py

190 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Точка входа FastAPI. Каркас из шаблона команды: инфраструктура без доменного кода.
Что здесь есть: настройки, подключение S3 (MinIO), NATS (nkeys JWT), SMB (горячие папки ИС Сканирование),
middleware (gzip, proxy headers, CORS, РАИСА при DEBUG=False), healthcheck, раздача собранного SPA.
Что добавляет Срез 0: роутеры в src/api/router.py, outbox-паблишер и consumer recognition.result
(docs/context/03-architecture.md), зависимость require(permission).
"""
import base64
import os
from contextlib import asynccontextmanager
from datetime import datetime, timezone
import nkeys
import smbclient
from fastapi import FastAPI
from fastapi.exceptions import HTTPException, StarletteHTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
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
settings = get_settings()
VERSION = settings.VERSION
UP_TIME = datetime.now(timezone.utc)
# --- NATS: аутентификация по nkeys/JWT ---------------------------------------------------------
kp = nkeys.from_seed(settings.OPENBAO.NATS.NATS_SEED.encode("utf-8"))
def jwt_cb():
return settings.OPENBAO.NATS.NATS_JWT.encode("utf-8")
def sig_cb(nonce):
if isinstance(nonce, str):
nonce = nonce.encode("utf-8")
return base64.b64encode(kp.sign(nonce))
# --- РАИСА: защищённый API (только вне DEBUG) ---------------------------------------------------
protected_middleware_available = False
if not settings.DEBUG:
try:
from raisa_fastapi_protected_api import ( # pyright: ignore[reportMissingImports]
AuthorizationMiddleware,
OpenEndpoint,
ProtectedOAuthSettings,
ProtectedRolesSettings,
ProtectedSettings,
SearchType,
)
protected_middleware_available = True
except ImportError:
protected_middleware_available = False
def _normalize_root_path(value: str) -> str:
if not value:
return ""
value = value.strip()
if value == "/":
return ""
return "/" + value.strip("/")
_APP_ROOT_PATH = _normalize_root_path(settings.ROOT_PATH)
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.nats_subs = []
app.state.storage = None
app.state.nats = None
try:
smbclient.register_session(
settings.OPENBAO.SMB.SMB_HOST, settings.OPENBAO.SMB.SMB_LOGIN, settings.OPENBAO.SMB.SMB_PASSWORD
)
except Exception:
logger.exception("Ошибка при регистрации сессии SMB")
s3_client = Minio(
settings.OPENBAO.MINIO.MINIO_URL,
access_key=settings.OPENBAO.MINIO.MINIO_ADMIN_LOGIN,
secret_key=settings.OPENBAO.MINIO.MINIO_ADMIN_PASSWORD,
cert_check=False,
)
try:
bucket_exists = await s3_client.bucket_exists(settings.OPENBAO.MINIO.MINIO_BUCKET)
logger.info("Бакет проверен. bucket_exists={bucket_exists}", bucket_exists=bucket_exists)
except Exception as exc:
logger.exception(
"Не удалось проверить наличие бакета. error_category=s3_error, error_type={error_type}, error={error}",
error_type=type(exc).__name__,
error=repr(exc),
)
return
app.state.storage = S3Storage(client=s3_client, bucket_name=settings.OPENBAO.MINIO.MINIO_BUCKET)
nc = NATS()
if not settings.DEBUG:
try:
await nc.connect(
settings.OPENBAO.NATS.NATS_URL, user_jwt_cb=jwt_cb, signature_cb=sig_cb, max_reconnect_attempts=2
)
logger.info("Подключение к NATS произошло")
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
app.state.nats = nc
# Подписки (recognition.result) и outbox-паблишер — Срез 0, см. docs/context/03-architecture.md
try:
logger.info("Приложение запущено")
yield
finally:
logger.info("Приложение закрывается")
smbclient.reset_connection_cache()
for sub in app.state.nats_subs:
await sub.unsubscribe()
if app.state.nats and not settings.DEBUG:
await app.state.nats.close()
if app.state.storage:
await app.state.storage.client.close_session()
app = FastAPI(title="АРМ КА", 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=["*"])
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --- Раздача собранного SPA (web/) ---------------------------------------------------------------
os.makedirs("web", exist_ok=True)
def sep_static(path: str, stat_dir: str) -> str:
sep_path = path.split("/")
index = sep_path.index(stat_dir)
return "/".join(sep_path[index:])
class SPAStaticFiles(StaticFiles):
async def get_response(self, path: str, scope):
try:
return await super().get_response(path, scope)
except (HTTPException, StarletteHTTPException) as ex:
if ex.status_code == 404:
if "images" in path:
return await super().get_response(sep_static(path, "images"), scope)
if "static" in path:
return await super().get_response(sep_static(path, "static"), scope)
return await super().get_response("index.html", scope)
raise
app.mount("/", SPAStaticFiles(directory="web", html=True), name="web")