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

100 lines
12 KiB
Markdown
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.

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