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