test
This commit is contained in:
parent
7decc3378e
commit
72b32d790f
3
.gitignore
vendored
3
.gitignore
vendored
@ -321,6 +321,7 @@ dist
|
||||
.pnp.*
|
||||
|
||||
# Custom ignore
|
||||
.local/
|
||||
.DS_Store
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
@ -338,4 +339,4 @@ graphify-out/
|
||||
/AGENTS.md
|
||||
|
||||
/uv.lock
|
||||
/pyproject.toml
|
||||
/pyproject.toml
|
||||
|
||||
58
api/LOCAL_MINIO.md
Normal file
58
api/LOCAL_MINIO.md
Normal file
@ -0,0 +1,58 @@
|
||||
# MinIO на Mac (Apple Silicon)
|
||||
|
||||
MinIO запускается отдельным процессом, данные сохраняются в `.local/minio/data`
|
||||
в корне проекта. Папка `.local/` и `api/.env` исключены из Git.
|
||||
|
||||
## Запуск
|
||||
|
||||
Из папки `api`, в активированном окружении:
|
||||
|
||||
```bash
|
||||
python run_minio.py
|
||||
```
|
||||
|
||||
Остановка — Ctrl+C. Данные сохраняются после перезапуска.
|
||||
|
||||
При первоначальной настройке сервер уже запущен в фоне. Его журнал находится
|
||||
в `.local/minio/server.log`, PID — в `.local/minio/minio.pid` (пути от корня
|
||||
проекта). Не запускай второй экземпляр на тех же портах. Фоновый процесс не
|
||||
настроен на автоматический запуск после перезагрузки Mac.
|
||||
|
||||
- S3 API: http://127.0.0.1:9000
|
||||
- Консоль: http://127.0.0.1:9001
|
||||
- Бакет: `dkb-files`
|
||||
- Логин и пароль: `OPENBAO__MINIO__MINIO_ADMIN_LOGIN` и
|
||||
`OPENBAO__MINIO__MINIO_ADMIN_PASSWORD` в `api/.env`.
|
||||
|
||||
Названия переменных окружения регистрозависимы; используй написание из примеров.
|
||||
|
||||
MinIO слушает только локальный интерфейс. Для локального HTTP в `.env` задано:
|
||||
|
||||
```dotenv
|
||||
OPENBAO__MINIO__MINIO_URL=127.0.0.1:9000
|
||||
OPENBAO__MINIO__MINIO_BUCKET=dkb-files
|
||||
OPENBAO__MINIO__MINIO_SECURE=false
|
||||
```
|
||||
|
||||
Адрес указывается без `http://`. Для TLS используется `MINIO_SECURE=true`
|
||||
(значение по умолчанию).
|
||||
|
||||
После запуска MinIO запусти API в другом терминале:
|
||||
|
||||
```bash
|
||||
uvicorn src.app:app --reload
|
||||
```
|
||||
|
||||
MinIO работает независимо от NATS и SMB. Загруженные файлы сохраняются, но
|
||||
задания распознавания остаются в очереди до настройки остальных интеграций.
|
||||
|
||||
## Повторная сборка бинарника
|
||||
|
||||
Из корня проекта, после установки Go (`brew install go`):
|
||||
|
||||
```bash
|
||||
mkdir -p .local/minio/bin
|
||||
GOBIN="$PWD/.local/minio/bin" go install github.com/minio/minio@RELEASE.2025-10-15T17-29-55Z
|
||||
```
|
||||
|
||||
Исходники: https://github.com/minio/minio
|
||||
33
api/run_minio.py
Normal file
33
api/run_minio.py
Normal file
@ -0,0 +1,33 @@
|
||||
"""Run local MinIO on macOS: python run_minio.py (Ctrl+C to stop)."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from dotenv import dotenv_values
|
||||
|
||||
|
||||
def main() -> None:
|
||||
api_dir = Path(__file__).resolve().parent
|
||||
local_dir = api_dir.parent / ".local" / "minio"
|
||||
binary = local_dir / "bin" / "minio"
|
||||
if not binary.is_file():
|
||||
raise SystemExit("MinIO не собран. См. LOCAL_MINIO.md")
|
||||
values = {**dotenv_values(api_dir / ".env"), **os.environ}
|
||||
prefix = "OPENBAO__MINIO__"
|
||||
credentials = [prefix + "MINIO_ADMIN_LOGIN", prefix + "MINIO_ADMIN_PASSWORD"]
|
||||
if any(not values.get(key) for key in credentials):
|
||||
raise SystemExit("Укажи логин и пароль MinIO в api/.env")
|
||||
data_dir = local_dir / "data"
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
env = dict(os.environ)
|
||||
env.update(
|
||||
MINIO_ROOT_USER=values[credentials[0]],
|
||||
MINIO_ROOT_PASSWORD=values[credentials[1]],
|
||||
)
|
||||
os.execve(binary, [str(binary), "server", str(data_dir),
|
||||
"--address", "127.0.0.1:9000",
|
||||
"--console-address", "127.0.0.1:9001"], env)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
121
api/src/app.py
121
api/src/app.py
@ -42,9 +42,6 @@ VERSION = "1.0.0"
|
||||
UP_TIME = datetime.now(timezone.utc)
|
||||
|
||||
|
||||
kp = nkeys.from_seed(settings.OPENBAO.NATS.NATS_SEED.encode('utf-8'))
|
||||
|
||||
|
||||
def jwt_cb():
|
||||
return settings.OPENBAO.NATS.NATS_JWT.encode('utf-8')
|
||||
|
||||
@ -52,6 +49,7 @@ def jwt_cb():
|
||||
def sig_cb(nonce):
|
||||
if (isinstance(nonce, str)):
|
||||
nonce = nonce.encode('utf-8')
|
||||
kp = nkeys.from_seed(settings.OPENBAO.NATS.NATS_SEED.encode('utf-8'))
|
||||
signed = kp.sign(nonce)
|
||||
return base64.b64encode(signed)
|
||||
|
||||
@ -94,80 +92,61 @@ async def lifespan(app):
|
||||
app.state.nats_subs = []
|
||||
app.state.storage = None
|
||||
app.state.nats = None
|
||||
app.state.poller = None
|
||||
app.state.pdf_poller = None
|
||||
s3_client = None
|
||||
smb_registered = False
|
||||
try:
|
||||
smbclient.register_session(settings.OPENBAO.SMB.SMB_HOST, settings.OPENBAO.SMB.SMB_LOGIN, settings.OPENBAO.SMB.SMB_PASSWORD)
|
||||
except:
|
||||
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(
|
||||
"Не удалось проверить наличие бакетa. error_category=s3_error, error_type={error_type}, error={error}",
|
||||
error_type=type(exc).__name__,
|
||||
error=repr(exc),
|
||||
)
|
||||
return
|
||||
storage = S3Storage(client=s3_client, bucket_name=settings.OPENBAO.MINIO.MINIO_BUCKET)
|
||||
app.state.storage = storage
|
||||
cache = AsyncInMemoryCache()
|
||||
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(
|
||||
"Не удалось проверить наличие бакетa. error_category=nats_error, error_type={error_type}, error={error}",
|
||||
error_type=type(exc).__name__,
|
||||
error=repr(exc),
|
||||
minio = settings.OPENBAO.MINIO
|
||||
if minio is not None:
|
||||
s3_client = Minio(
|
||||
minio.MINIO_URL,
|
||||
access_key=minio.MINIO_ADMIN_LOGIN,
|
||||
secret_key=minio.MINIO_ADMIN_PASSWORD,
|
||||
secure=minio.MINIO_SECURE,
|
||||
)
|
||||
return
|
||||
poller = PollWorker(nc, cache, storage)
|
||||
await poller.start()
|
||||
pdf_poller = PDFPollWorker(cache, storage)
|
||||
await pdf_poller.start()
|
||||
app.state.nats = nc
|
||||
app.state.poller = poller
|
||||
app.state.pdf_poller = pdf_poller
|
||||
if not settings.DEBUG:
|
||||
handler_cb = build_file_handler(nc, storage)
|
||||
if not await s3_client.bucket_exists(minio.MINIO_BUCKET):
|
||||
raise RuntimeError(f"Бакет MinIO {minio.MINIO_BUCKET!r} не создан")
|
||||
app.state.storage = S3Storage(s3_client, minio.MINIO_BUCKET)
|
||||
logger.info("MinIO подключён, бакет: {}", minio.MINIO_BUCKET)
|
||||
|
||||
missing = [name for name in ("MINIO", "NATS", "SMB")
|
||||
if getattr(settings.OPENBAO, name) is None]
|
||||
if missing:
|
||||
logger.info("Распознавание файлов отключено: не настроены {}", ", ".join(missing))
|
||||
else:
|
||||
smb = settings.OPENBAO.SMB
|
||||
smbclient.register_session(smb.SMB_HOST, smb.SMB_LOGIN, smb.SMB_PASSWORD)
|
||||
smb_registered = True
|
||||
nc = NATS()
|
||||
app.state.nats = nc
|
||||
if not settings.DEBUG:
|
||||
await nc.connect(
|
||||
settings.OPENBAO.NATS.NATS_URL,
|
||||
user_jwt_cb=jwt_cb, signature_cb=sig_cb,
|
||||
max_reconnect_attempts=2,
|
||||
)
|
||||
cache = AsyncInMemoryCache()
|
||||
app.state.poller = PollWorker(nc, cache, app.state.storage)
|
||||
await app.state.poller.start()
|
||||
app.state.pdf_poller = PDFPollWorker(cache, app.state.storage)
|
||||
await app.state.pdf_poller.start()
|
||||
|
||||
try:
|
||||
# sub_xls = await nc.subscribe('files.tabular_parsed', cb=handler_cb)
|
||||
# sub_img = await nc.subscribe('files.img_parsed', cb=handler_cb)
|
||||
#
|
||||
# app.state.nats_subs = [sub_xls, sub_img]
|
||||
logger.info("Подписка на топики завершена")
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Не удалось подписаться на тему. error_category=nats_error, error_type={error_type}, error={error}",
|
||||
error_type=type(exc).__name__,
|
||||
error=repr(exc),
|
||||
)
|
||||
try:
|
||||
logger.info("Приложение запущено")
|
||||
yield
|
||||
except Exception as exc:
|
||||
logger.exception("ОШИБКА ПРИ РАБОТЕ С СЕКРЕТАМИ error={error}", error=repr(exc))
|
||||
finally:
|
||||
for worker in (app.state.poller, app.state.pdf_poller):
|
||||
if worker is not None:
|
||||
await worker.stop()
|
||||
for sub in app.state.nats_subs:
|
||||
await sub.unsubscribe()
|
||||
if app.state.nats is not None:
|
||||
await app.state.nats.close()
|
||||
if s3_client is not None:
|
||||
await s3_client.close_session()
|
||||
if smb_registered:
|
||||
smbclient.reset_connection_cache()
|
||||
logger.info("Приложение закрывается")
|
||||
smbclient.reset_connection_cache()
|
||||
if not settings.DEBUG:
|
||||
for sub in app.state.nats_subs:
|
||||
logger.info("Отписываемся от топика")
|
||||
await sub.unsubscribe()
|
||||
if app.state.nats:
|
||||
await app.state.nats.close()
|
||||
if app.state.poller:
|
||||
await app.state.poller.stop()
|
||||
if app.state.storage:
|
||||
await app.state.storage.client.close_session()
|
||||
if app.state.pdf_poller:
|
||||
await app.state.pdf_poller.stop()
|
||||
logger.info("CLOSING APP AT ALL")
|
||||
|
||||
app = FastAPI(
|
||||
title="Fast API",
|
||||
@ -247,4 +226,4 @@ class SPAStaticFiles(StaticFiles):
|
||||
|
||||
app.mount("/", SPAStaticFiles(directory="web", html=True), name="web")
|
||||
|
||||
app.mount("/", StaticFiles(directory="web", html=True), name="web")
|
||||
app.mount("/", StaticFiles(directory="web", html=True), name="web")
|
||||
|
||||
@ -9,6 +9,7 @@ class MinioSettings(BaseSettings):
|
||||
MINIO_ADMIN_LOGIN: str
|
||||
MINIO_ADMIN_PASSWORD: str
|
||||
MINIO_BUCKET: str
|
||||
MINIO_SECURE: bool = True
|
||||
|
||||
class NATSSettings(BaseSettings):
|
||||
NATS_URL: str
|
||||
@ -23,9 +24,9 @@ class SMBSettings(BaseSettings):
|
||||
|
||||
class OpenBAOSettings(BaseSettings):
|
||||
DATABASE: DBSettings
|
||||
MINIO: MinioSettings
|
||||
NATS: NATSSettings
|
||||
SMB: SMBSettings
|
||||
MINIO: MinioSettings | None = None
|
||||
NATS: NATSSettings | None = None
|
||||
SMB: SMBSettings | None = None
|
||||
|
||||
class ServerSettings(BaseSettings):
|
||||
HOST: str = "0.0.0.0"
|
||||
@ -39,6 +40,7 @@ class Settings(BaseSettings):
|
||||
VERSION: str = '1.0.0'
|
||||
|
||||
class Config:
|
||||
case_sensitive = True
|
||||
extra = "ignore"
|
||||
env_file = ".env"
|
||||
env_nested_delimiter = "__"
|
||||
@ -47,4 +49,4 @@ class Settings(BaseSettings):
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
||||
settings = get_settings()
|
||||
settings = get_settings()
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from fastapi import UploadFile
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.inspection import inspect
|
||||
from loguru import logger
|
||||
@ -21,6 +21,8 @@ class FileService:
|
||||
self.session = session
|
||||
|
||||
async def save_file(self, file: UploadFile, legal_entity_id: uuid.UUID, quarter: str, year: int):
|
||||
if self.storage is None:
|
||||
raise HTTPException(status_code=503, detail="Обработка файлов не настроена")
|
||||
try:
|
||||
db_payload = {
|
||||
'original_name': file.filename,
|
||||
@ -70,6 +72,8 @@ class FileService:
|
||||
return [self._get_transformed_file_job_info(new_file) for new_file in result]
|
||||
|
||||
async def get_file_bytes(self, file_id):
|
||||
if self.storage is None:
|
||||
raise HTTPException(status_code=503, detail="Файловое хранилище не настроено")
|
||||
result = await self.file_repo.get_by_id(file_id)
|
||||
return (await self.storage.get_object(result.s3_key))
|
||||
|
||||
@ -82,4 +86,3 @@ class FileService:
|
||||
'original_name': stored_file_db_obj.original_name,
|
||||
'document_type': stored_file_db_obj.document_type
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user