Andrey Epifancev 85839a0fbf docs: решение R-10 (расположение .env.example/.env.test) в design.md
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 02:30:19 +04:00

12 KiB
Raw Blame History

Design: Срез 0 — скелет tabular-parser

Context

См. proposal.md. Исходное состояние — копия каркаса arm-core (124f63b): FastAPI с lifespan (SMB, MinIO, NATS), SPA-mount, Alembic, пустые пакеты models/repositories/services. Контракт v1.0, ack processing и правила Nats-Msg-Id — родительский change в docs. Платформа: Python 3.11, NATS без JetStream, Nexus недоступен до прода, CI в Gitea нет. Сервис должен остаться запускаемым платформой так же, как шаблон (api/run_server.py, SERVER__*, raisa_config.yml).

Goals / Non-Goals

Goals: сервис без БД/фронта; транспорт NATS ↔ S3 проверен интеграционным тестом, включая повторный запрос; парсер — заменяемый интерфейс. Non-Goals: см. proposal; масштабирование (несколько инстансов — queue group даёт это бесплатно, но не тестируется).

Decisions

D-1. Вырезаем, а не переписываем

Удаляются каталоги и зависимости, перечисленные в proposal; app.py упрощается до: настройки, lifespan (MinIO, NATS, запуск воркера), /healthcheck, обработчики ошибок, логирование. Остающиеся файлы шаблона не переименовываются — чтобы diff с arm-core и будущие обновления шаблона были читаемы.

D-2. Воркер — подписка core NATS с queue group, ack сразу, обработка в очереди задач

nc.subscribe("recognition.tabular.request", queue="tabular-parser", cb=on_request). В on_request: валидация → если job_id уже в process-local множестве «в работе/обработано» — только ack; иначе добавить в множество, опубликовать ack processing, положить задание во внутреннюю asyncio.Queue. Рабочая корутина (одна на инстанцию, WORKER_CONCURRENCY=1) берёт задания из очереди и выполняет конвейер S3 → парсер → S3 → конверт результата. Множество обработанных job_id ограничено по размеру (DONE_CACHE_SIZE=1000, LRU) — защита от повторов ядра после результата. Почему ack сразу: ядро без JetStream публикует повторно через 30 с без ack; долгий разбор не должен вызывать повторы. Альтернатива — pull-семантика JetStream: недоступна.

D-3. Парсер — протокол Parser.parse(data: bytes, file_type) -> ParseOutcome

ParseOutcome = status, documents (модели контракта), metrics, error. NullParser всегда отдаёт error NOT_IMPLEMENTED с сообщением «разбор XLS/XLSX/XLSB реализуется в Срезе 2». Воркер не знает о реализации; Срез 2 подменяет фабрику. Так транспорт и парсер тестируются раздельно, парсер — без NATS/S3.

D-4. Ключи S3 и имя сервиса

Исходник читается по request.s3.key (роль uploads), результат пишется в results/{file_id}/{job_id}/result.json того же бакета; service = {name: "tabular-parser", version: settings.VERSION}. Клиент S3 — S3Storage шаблона, дополненный get_bytes/put_json.

D-5. Любая ошибка транспорта — результат, не молчание

Таблица: запрос не проходит pydantic → SCHEMA_MISMATCH (без ack — нечего подтверждать, но результат публикуется с job_id, если он читаем); MAJOR версии запроса ≠ 1 → SCHEMA_MISMATCH; S3 get упал → FILE_READ_ERROR; исключение парсера → UNKNOWN_ERROR с текстом исключения в error.message (без трассировки). Публикуется конверт без result_s3; metrics.duration_ms заполняется всегда. Публикация результата повторяется до PUBLISH_RETRIES=3 раз с паузой 2 с; если не удалось — лог уровня error, задание дойдёт до timeout в ядре.

D-6. Соединение с NATS — как у ядра, без JetStream

Пустые NATS_SEED/NATS_JWT → без учётных данных (dev); иначе nkeys/JWT шаблона. Автопереподключение nats-py (max_reconnect_attempts=-1); при разрыве подписка восстанавливается клиентом. /healthcheck отдаёт nats = ok | down. Флаг NATS_ENABLED (по умолчанию true) — чтобы unit-тесты поднимали приложение без NATS.

D-7. Тесты в два уровня

Unit (tests/unit): модели контракта и примеры docs; обработчик запроса с фейками S3Storage/Publisher (протоколы) — сценарии D-5, повторный запрос, ack до обработки. Интеграционные (tests/integration, маркер integration): compose-NATS/MinIO — реальный запрос из примера request-tabular.json → ack и результат в recognition.result, файл в бакете.

D-8. Dev без Nexus, Python 3.11

Dockerfile — python:3.11-slim; make venv ставит requirements.txt без строк raisa_* (в этом сервисе их уже нет после D-1, но цель одинакова во всех репозиториях). Конфигураций CI нет: make check / make check-full.

Risks / Trade-offs

  • [Повторный запрос ядра после ack при долгой обработке] → ack публикуется до чтения S3; ядро повторяет только без ack.
  • [Инстанция упала после ack, до результата] → ядро закроет задание по reply_deadline (timeout), replay — Срез 3+.
  • [NOT_IMPLEMENTED в проде, если Срез 2 задержится] → код ошибки честный, ядро показывает его аналитику.
  • [Удалённые зависимости понадобятся позже] → возвращаются по одной строке в requirements.txt.

Migration Plan

Нет данных. Удаление каталогов — одним коммитом «chore: вырезан фронт и БД из шаблона» до функциональных изменений, чтобы ревью было читаемым. Откат — revert коммита.

Open Questions

Общие с родительским change (версия Node не нужна этому сервису; параметры NATS банка — к деплою). Специфичных нет.

Decisions made without discussion

№ Дата Решение Обоснование Цена ошибки
1 2026-10-03 D-1: вырезать фронт/БД/SMB, не переименовывая оставшееся читаемый diff с arm-core нет
2 2026-10-03 D-2: queue group tabular-parser, ack до обработки, одна рабочая корутина, LRU обработанных job_id NATS без JetStream (ответ 03.10); ядро повторяет без ack смена параметров; при JetStream — pull-consumer
3 2026-10-03 D-3: парсер-заглушка отвечает NOT_IMPLEMENTED (код добавлен в контракт родительским change'ем) честный статус вместо маскировки под UNSUPPORTED_FORMAT нет
4 2026-10-03 D-4: результат в results/{file_id}/{job_id}/result.json того же бакета схема ключей 03-architecture.md, один бакет смена шаблона ключа в одном месте
5 2026-10-03 D-5: транспортные ошибки превращаются в результат; публикация с 3 повторами ядро не должно ждать таймаута, если сервис жив нет
6 2026-10-03 D-8: python:3.11-slim, make check/check-full вместо CI ответы пользователя смена тега
7 2026-10-03 Каталог словарей — api/rules/ (относительно рабочего каталога api/) все команды шаблона идут из api/ перенос каталога
8 2026-10-04 R-1: проверка старта в задаче 1.2 — через gitignored api/.env/окружение; .env.example и повторная проверка старта — в 2.2 1.2 идёт раньше 2.2 по плану перенос проверки
9 2026-10-04 R-2: маркеры pytest и filterwarnings в api/pytest.ini; pyproject.toml — только ruff и mypy pytest.ini приоритетнее [tool.pytest]; как в arm-core правка конфига
10 2026-10-04 R-3: .env.test содержит dev-значения compose (localhost, minioadmin, arm-ka, пустые NATS seed/jwt) как в arm-core; dev-пароли публичны в docs нет
11 2026-10-04 R-4: шаблонный core/nats_callback.py удаляется в задаче 4.3 заменён воркером; consumer recognition.result — зона ядра восстановить из git
12 2026-10-04 R-5: критерий 1.1 «только удаления и .gitignore» читается как «удаления, .gitignore, setup/setup.sh» npm-шаги setup.sh входят в текст задачи нет
13 2026-10-04 R-6: из requirements.txt убираются и транзитивные сироты удалённых пакетов, если без них всё работает сервис без БД/SMB не должен тянуть их вернуть строку
14 2026-10-04 R-7: README (1.3) может описывать файлы из 2.1/2.2; окончательная сверка существования — в 5.2 и финальном ревью порядок задач правка README
15 2026-10-04 R-8: make lint = ruff check + ruff format --check + mypy src; остальные файлы шаблона форматируются в коммите 2.1 как arm-core отключить format-check
16 2026-10-04 R-9: трейлер коммитов Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> указание вызывающего, Global Constraints плана нет
17 2026-10-04 R-10: .env.example и .env.test лежат в api/ (рядом с pytest.ini, как в arm-core) команды идут из api/, pytest.ini задаёт ENV_FILE=.env.test относительно api/ правка двух строк README