fix: настройки — SERVICE_NAME только tabular-parser, PUBLISH_RETRIES ≥ 0 (M-1, M-3, M-4, M-5)

- 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 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-04 03:48:34 +04:00
parent bf2f60b3b5
commit 8685441fcd
7 changed files with 103 additions and 12 deletions

View File

@ -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` — зона

View File

@ -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`, настройкой
не меняется.

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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")