From 8685441fcd8593ab633d9e7b2ec06d12abe09dea Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sun, 4 Oct 2026 03:48:34 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B9=D0=BA=D0=B8=20=E2=80=94=20SERVICE=5FNAME=20=D1=82=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=BA=D0=BE=20tabular-parser,=20PUBLISH=5FRETRIES?= =?UTF-8?q?=20=E2=89=A5=200=20(M-1,=20M-3,=20M-4,=20M-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SERVICE_NAME: Literal["tabular-parser"] (R-35); в config.py, .env.example, README и CLAUDE.md исправлено «имя queue group» — queue group задаёт константа REQUEST_QUEUE_GROUP в core/nats.py. - PUBLISH_RETRIES: NonNegativeInt — 0 означает одну попытку без повторов, как и принимает NatsConnection. - Удалён неиспользуемый модульный settings = get_settings() из config.py. - pyproject.toml: убран override mypy для raisa_fastapi_protected_api (зависимость удалена D-1). - tests/conftest.py: фабрика minimal_settings (Settings из временного .env с минимальным окружением, окружение процесса скрыто); test_config.py — умолчания при минимальном окружении (M-8), PUBLISH_RETRIES=0/-1, SERVICE_NAME≠tabular-parser. Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 2 +- README.md | 6 +++-- api/.env.example | 5 ++-- api/pyproject.toml | 2 +- api/src/core/config.py | 11 ++++---- api/tests/conftest.py | 48 +++++++++++++++++++++++++++++++++++ api/tests/unit/test_config.py | 41 ++++++++++++++++++++++++++++++ 7 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 api/tests/unit/test_config.py diff --git a/CLAUDE.md b/CLAUDE.md index 0c22d9b..727ba05 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,7 @@ Stateless-сервис разбора XLS/XLSX/XLSB: ОСВ по счетам ( **Оставлено от шаблона:** `api/run_server.py` (точка входа платформы, `SERVER__HOST/PORT`), `api/src/app.py` (lifespan: подключение к MinIO и NATS, ошибка подключения логируется и не мешает старту; `GET /healthcheck`, gzip/proxy middleware), -`core/config.py` (pydantic-settings, `SERVICE_NAME` — `service.name` конвертов и имя queue group, `OPENBAO__MINIO__*` +`core/config.py` (pydantic-settings, `SERVICE_NAME` — только `service.name` конвертов (queue group — константа в `core/nats.py`), `OPENBAO__MINIO__*` включая `MINIO_SECURE`, `OPENBAO__NATS__*`, `DEBUG`, `ENV_FILE`, `NATS_ENABLED`, `WORKER_CONCURRENCY`, `PUBLISH_RETRIES`, `DONE_CACHE_SIZE`), `core/clients.py` (S3), `core/logger.py`, `core/exception_handlers.py` (без SQLAlchemy), `common/exceptions.py`, `pytest.ini`, `requirements*.txt`, `raisa_config.yml`, `setup/setup.sh` + `pip.conf` (без npm-шага). Шаблонный `core/nats_callback.py` (consumer `recognition.result` — зона diff --git a/README.md b/README.md index 79624dd..20c4885 100644 --- a/README.md +++ b/README.md @@ -135,9 +135,11 @@ curl -s localhost:8000/healthcheck | `ENV_FILE` | `.env` | путь к файлу окружения (от `api/`) | | `NATS_ENABLED` | `true` | `false` — не подключаться к NATS, воркер не стартует | | `WORKER_CONCURRENCY` | `1` | сколько заданий обрабатывается одновременно | -| `PUBLISH_RETRIES` | `3` | повторы публикации ack и результата в NATS после первой попытки (пауза 2 с) | +| `PUBLISH_RETRIES` | `3` | повторы публикации ack и результата в NATS после первой попытки (пауза 2 с; `0` — без повторов) | | `DONE_CACHE_SIZE` | `1000` | размер LRU обработанных `job_id` (дедупликация внутри процесса) | | `SERVER__HOST` | `0.0.0.0` | адрес привязки HTTP-сервера | | `SERVER__PORT` | `8000` | порт HTTP-сервера | -Имя сервиса в конвертах контракта (`service.name`) и имя queue group — `tabular-parser` (`SERVICE_NAME`, менять не нужно). +`SERVICE_NAME` — имя сервиса в конвертах контракта (`service.name`); допустимо только `tabular-parser`, иное значение — +ошибка конфигурации при старте. Queue group подписки NATS — константа `tabular-parser` в `src/core/nats.py`, настройкой +не меняется. diff --git a/api/.env.example b/api/.env.example index fa55ae2..8614ffe 100644 --- a/api/.env.example +++ b/api/.env.example @@ -25,13 +25,14 @@ OPENBAO__NATS__NATS_JWT= # Префикс путей приложения за прокси и версия в OpenAPI ROOT_PATH=/ VERSION=1.0.0 -# `service.name` конвертов контракта и имя queue group NATS; менять не нужно, пока контракт v1.x +# `service.name` конвертов контракта; допустимо только tabular-parser (иное — ошибка старта). Queue group NATS — +# константа в коде (`tabular-parser`), этой настройкой не меняется SERVICE_NAME=tabular-parser # false — не подключаться к NATS (воркер не стартует; /healthcheck отдаёт nats = off) NATS_ENABLED=true # Число заданий, обрабатываемых одновременно WORKER_CONCURRENCY=1 -# Повторы публикации ack и результата в NATS после первой попытки (3 — всего до 4 попыток, пауза 2 с) +# Повторы публикации ack и результата в NATS после первой попытки (3 — всего до 4 попыток, пауза 2 с; 0 — без повторов) PUBLISH_RETRIES=3 # Размер process-local LRU обработанных job_id (защита от дублей доставки) DONE_CACHE_SIZE=1000 diff --git a/api/pyproject.toml b/api/pyproject.toml index 048aae1..53b4910 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -20,7 +20,7 @@ explicit_package_bases = true # Сторонние пакеты без типов (или недоступные вне банка) [[tool.mypy.overrides]] -module = ["nkeys.*", "nats.*", "raisa_fastapi_protected_api", "raisa_fastapi_protected_api.*"] +module = ["nkeys.*", "nats.*"] ignore_missing_imports = true # miniopy_async: пакет типизирован с ошибкой (Minio не виден mypy при реэкспорте) — типы не подтягиваем, Minio = Any diff --git a/api/src/core/config.py b/api/src/core/config.py index 191c48e..f81a927 100644 --- a/api/src/core/config.py +++ b/api/src/core/config.py @@ -1,6 +1,7 @@ import os +from typing import Literal -from pydantic import PositiveInt +from pydantic import NonNegativeInt, PositiveInt from pydantic_settings import BaseSettings, SettingsConfigDict @@ -41,10 +42,11 @@ class Settings(BaseSettings): ROOT_PATH: str = "/" VERSION: str = "1.0.0" - SERVICE_NAME: str = "tabular-parser" # `service.name` конвертов контракта и имя queue group NATS + # `service.name` конвертов контракта (enum ServiceName); queue group NATS — константа REQUEST_QUEUE_GROUP в nats.py + SERVICE_NAME: Literal["tabular-parser"] = "tabular-parser" NATS_ENABLED: bool = True # false — приложение не подключается к NATS (воркер не стартует) WORKER_CONCURRENCY: PositiveInt = 1 # число заданий, обрабатываемых одновременно - PUBLISH_RETRIES: PositiveInt = 3 # повторы публикации ack/результата в NATS после первой попытки (до 4 попыток) + PUBLISH_RETRIES: NonNegativeInt = 3 # повторы публикации ack/результата после первой попытки (0 — без повторов) DONE_CACHE_SIZE: PositiveInt = 1000 # размер process-local LRU обработанных job_id model_config = SettingsConfigDict( @@ -57,6 +59,3 @@ class Settings(BaseSettings): def get_settings() -> Settings: return Settings() - - -settings = get_settings() diff --git a/api/tests/conftest.py b/api/tests/conftest.py index e69de29..90c2304 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -0,0 +1,48 @@ +"""Общие фикстуры тестов.""" + +import os +from collections.abc import Callable +from pathlib import Path + +import pytest + +from src.core.config import Settings + +# Переменные, которые читает Settings: убираются из окружения процесса, чтобы тест видел только свой файл +_SETTINGS_ENV_PREFIXES = ( + "OPENBAO__", + "SERVER__", + "DEBUG", + "ROOT_PATH", + "VERSION", + "SERVICE_NAME", + "NATS_ENABLED", + "WORKER_CONCURRENCY", + "PUBLISH_RETRIES", + "DONE_CACHE_SIZE", +) + +# Минимальное окружение spec «Старт с минимальным окружением»: MinIO, адрес NATS и DEBUG; без seed/jwt, БД и SMB +MINIMAL_ENV = """\ +DEBUG=true +OPENBAO__MINIO__MINIO_URL=localhost:9000 +OPENBAO__MINIO__MINIO_ADMIN_LOGIN=minioadmin +OPENBAO__MINIO__MINIO_ADMIN_PASSWORD=minioadmin +OPENBAO__MINIO__MINIO_BUCKET=arm-ka +OPENBAO__NATS__NATS_URL=nats://localhost:4222 +""" + + +@pytest.fixture +def minimal_settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Callable[[str], Settings]: + """Фабрика Settings только из временного `.env` с `MINIMAL_ENV` и строками `extra` (окружение процесса скрыто).""" + for name in list(os.environ): + if name.upper().startswith(_SETTINGS_ENV_PREFIXES): + monkeypatch.delenv(name) + + def build(extra: str = "") -> Settings: + env_file = tmp_path / ".env" + env_file.write_text(MINIMAL_ENV + extra, encoding="utf-8") + return Settings(_env_file=env_file) # type: ignore[call-arg] + + return build diff --git a/api/tests/unit/test_config.py b/api/tests/unit/test_config.py new file mode 100644 index 0000000..cbb420c --- /dev/null +++ b/api/tests/unit/test_config.py @@ -0,0 +1,41 @@ +"""Unit-тесты настроек `src/core/config.py` (spec «Старт с минимальным окружением»; R-35).""" + +from collections.abc import Callable + +import pytest +from pydantic import ValidationError + +from src.core.config import Settings + +SettingsFactory = Callable[..., Settings] + + +def test_minimal_environment_gives_defaults(minimal_settings: SettingsFactory) -> None: + settings = minimal_settings() + + assert settings.DEBUG is True + assert settings.OPENBAO.MINIO.MINIO_BUCKET == "arm-ka" + assert settings.OPENBAO.NATS.NATS_URL == "nats://localhost:4222" + assert settings.OPENBAO.NATS.NATS_SEED == "" + assert settings.OPENBAO.NATS.NATS_JWT == "" + assert settings.NATS_ENABLED is True + assert settings.SERVICE_NAME == "tabular-parser" + assert settings.WORKER_CONCURRENCY == 1 + assert settings.PUBLISH_RETRIES == 3 # noqa: PLR2004 - значение по умолчанию из spec + assert settings.DONE_CACHE_SIZE == 1000 # noqa: PLR2004 - значение по умолчанию из spec + + +def test_publish_retries_zero_is_allowed(minimal_settings: SettingsFactory) -> None: + """0 — без повторов: одна попытка публикации (NatsConnection принимает publish_retries ≥ 0).""" + assert minimal_settings("PUBLISH_RETRIES=0\n").PUBLISH_RETRIES == 0 + + +def test_publish_retries_negative_is_rejected(minimal_settings: SettingsFactory) -> None: + with pytest.raises(ValidationError, match="PUBLISH_RETRIES"): + minimal_settings("PUBLISH_RETRIES=-1\n") + + +def test_service_name_other_than_contract_value_is_rejected(minimal_settings: SettingsFactory) -> None: + """SERVICE_NAME — `service.name` конвертов контракта: другое значение — ошибка конфигурации при старте.""" + with pytest.raises(ValidationError, match="SERVICE_NAME"): + minimal_settings("SERVICE_NAME=scan-gateway\n")