arm-tabular-parser/README.md

142 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.

# tabular-parser — разбор XLS/XLSX/XLSB для АРМ КА
Stateless-сервис распознавания табличных файлов (ОСВ по счетам, ББ, ОФР из Excel) для автоматизированного рабочего
места кредитного аналитика ДКБ (АО «Россельхозбанк»). Сервис **не имеет доступа к БД и фронта** и видит только две
внешние системы: NATS (core, без JetStream) и S3 (MinIO, один бакет). Он получает задание, читает исходный файл,
возвращает структуру файла и ничего не знает про организации, периметры и план счетов — нормализацией занимается
`arm-core`.
Обмен с `arm-core` идёт по контракту **v1.0**: схемы — [`../docs/contracts/`](../docs/contracts/), копия — в
`api/contracts/` (добавляется задачей 3.1 среза и проверяется на идентичность в `make check`).
**Срез 0 — скелет.** Транспорт работает полностью (подписка, ack, чтение S3, публикация результата, ошибки транспорта),
а сам разбор заменён заглушкой: на любой корректный запрос сервис отвечает конвертом `status = error` с
`error.code = NOT_IMPLEMENTED`. Настоящий парсер (ОСВ — Срез 2, формы ББ/ОФР — позже) подключается через интерфейс
`Parser` без изменений воркера.
Репозиторий — часть workspace АРМ КА. Общий контекст, модель, правила предметной области и контракты — в соседних
репозиториях:
- [`../CLAUDE.md`](../CLAUDE.md) — инварианты и процесс работы всего workspace;
- [`../docs/`](../docs/) — контекст (`context/`), контракты (`contracts/`), окружение разработки (`dev/`);
- [`CLAUDE.md`](CLAUDE.md) — соглашения этого репозитория.
Репозитории должны лежать рядом: `tabular-parser/` и `docs/` — соседние каталоги (так же устроено зеркало `.wt/<срез>/`).
Пути `../docs/...` ниже и в `Makefile` рассчитаны на это.
## Как обрабатывается запрос
1. `arm-core` публикует запрос в NATS, subject `recognition.tabular.request` (схема `recognition-request`), исходный
файл лежит в S3 под ключом `uploads/{file_id}/{job_id}/<имя>`.
2. Сервис подписан на этот subject в queue group `tabular-parser` — несколько экземпляров делят нагрузку между собой.
3. Запрос валидируется по схеме. Если `job_id` ещё не обработан этим процессом (process-local LRU), сервис **до чтения
файла** публикует в `recognition.result` конверт `status = processing` (ack) с заголовком
`Nats-Msg-Id = ack:<job_id>:<attempt>`. Повторный запрос с тем же `job_id` получает повторный ack и второй раз не
обрабатывается.
4. Файл читается из S3 (`s3.bucket` / `s3.key`) и передаётся парсеру («байты + тип файла → документы, метрики или ошибка»).
5. Результат (схема `recognition-result`) записывается в S3 под ключом `results/{file_id}/{job_id}/result.json`.
6. В `recognition.result` публикуется конверт результата (схема `recognition-result-message`; статусы
`processing | done | needs_check | error`) с заголовком `Nats-Msg-Id = result:<job_id>:<attempt>`. Публикация
при неудаче повторяется до `PUBLISH_RETRIES` раз (после первой попытки, с паузой 2 с).
Ошибки транспорта процесс не роняют, а превращаются в конверт `status = error`: нечитаемый или несовместимый запрос —
`SCHEMA_MISMATCH` (ack при этом не публикуется), объект S3 не читается — `FILE_READ_ERROR`, исключение парсера —
`UNKNOWN_ERROR`, заглушка парсера в Срезе 0 — `NOT_IMPLEMENTED`.
## Состав репозитория
```
api/ бэкенд (все команды разработки — отсюда или через make из корня)
run_server.py точка входа платформы (uvicorn; SERVER__HOST, SERVER__PORT)
src/app.py FastAPI: lifespan (подключение к MinIO и NATS), GET /healthcheck, middleware, обработчики ошибок
src/core/ config.py (настройки), clients.py (S3), logger.py, exception_handlers.py
src/parser/ интерфейс Parser и реализации (добавляется задачей 4.1)
src/contract/ pydantic-модели контракта v1.0 (добавляется задачей 3.1)
contracts/ копии JSON-схем контракта (добавляется задачей 3.1)
rules/ словари синонимов *.yaml для парсера (добавляется задачей 4.1)
tests/ pytest: unit и integration (маркер `integration`)
setup/ pip.conf и setup.sh (создание venv без uv)
raisa_config.yml конфигурация платформы РАИСА
Makefile команды разработки (добавляется задачей 2.1)
Dockerfile образ сервиса, python:3.11-slim (добавляется задачей 2.2)
openspec/ требования и дельта текущего среза (openspec/changes/slice-0-skeleton/)
```
## Предварительные требования
- **Python 3.11** (версия платформы) и **[`uv`](https://docs.astral.sh/uv/)** — `make venv` создаёт окружение и ставит
зависимости через `uv`; `uv` сам подберёт интерпретатор 3.11, системный `python3` другой версии не подойдёт.
- **Docker** и compose v2 — для NATS и MinIO (compose лежит в `../docs/dev/`; он же поднимает PostgreSQL для
`arm-core`, этому сервису БД не нужна).
- `make`.
Пакеты `raisa_*` из Nexus банка вне банка недоступны: `make venv` пропускает строки `raisa_*` в `requirements.txt`.
## Команды `make`
Выполняются из корня репозитория; инструменты берутся из `api/.venv`.
| Команда | Что делает |
|---|---|
| `make venv` | создаёт `api/.venv` (Python 3.11) и ставит `requirements.txt` и `requirements-dev.txt` через `uv` |
| `make lint` | `ruff check .`, `ruff format --check .`, `mypy src` |
| `make test` | `pytest -m "not integration"` — без Docker |
| `make test-integration` | `pytest -m integration` — против NATS и MinIO из compose |
| `make check` | `make lint` + `make test` (включая идентичность копии контракта с `../docs/contracts`) |
| `make check-full` | `make check` + `make test-integration` + `docker build`; запускать перед PR |
| `make up` | `docker compose -f ../docs/dev/docker-compose.yml up -d --wait postgres nats minio` |
Все цели `Makefile` (включая `check-full`) создаются задачей 2.1; `docker build` в `check-full` заработает после задачи 2.2 (`Dockerfile`).
## Запуск локально против compose
```bash
make venv
cp ../docs/dev/.env.example ../docs/dev/.env # один раз: учётные данные и порты compose (в git не попадает)
make up # NATS (4222), MinIO (9000, консоль 9001), бакет arm-ka
cp api/.env.example api/.env # один раз: настройки сервиса (в git не попадает)
cd api && .venv/bin/python run_server.py
```
Значения из `api/.env.example` уже совпадают с `../docs/dev/.env` (стандартные порты и учётные данные compose); править
`api/.env` нужно, только если вы меняли порты или пароли compose. Файл `api/.env.example` создаётся задачей 2.2.
Подробности про compose, смену портов и сброс — [`../docs/dev/README.md`](../docs/dev/README.md).
Сервис слушает `0.0.0.0:8000` (`SERVER__HOST`, `SERVER__PORT`). Проверка:
```bash
curl -s localhost:8000/healthcheck
```
В ответе — `uptime`, `version` и (после добавления воркера, задача 4.3) `nats`: `ok` — подключён, `down` — включён
(`NATS_ENABLED=true`), но связи нет, `off` — выключен.
Если NATS или MinIO недоступны при старте, приложение всё равно поднимается: ошибка логируется, соединение с NATS
восстанавливается автоматически (бесконечное автопереподключение).
## Переменные окружения
Настройки читает `pydantic-settings` (`api/src/core/config.py`). Вложенные поля задаются через двойное подчёркивание.
Файл окружения выбирается переменной `ENV_FILE` (по умолчанию `.env` в каталоге `api/`, тесты используют `.env.test`).
| Переменная | По умолчанию | Назначение |
|---|---|---|
| `OPENBAO__MINIO__MINIO_URL` | — (обязательна) | адрес MinIO без схемы, `host:port` |
| `OPENBAO__MINIO__MINIO_ADMIN_LOGIN` | — (обязательна) | логин S3 |
| `OPENBAO__MINIO__MINIO_ADMIN_PASSWORD` | — (обязательна) | пароль S3 |
| `OPENBAO__MINIO__MINIO_BUCKET` | — (обязательна) | бакет (`arm-ka`) |
| `OPENBAO__MINIO__MINIO_SECURE` | `true` | `true` — https (платформа), `false` — http (MinIO из compose) |
| `OPENBAO__NATS__NATS_URL` | — (обязательна) | адрес NATS, `nats://host:4222` |
| `OPENBAO__NATS__NATS_SEED` | пусто | nkeys seed; пусто вместе с JWT — подключение без аутентификации (dev) |
| `OPENBAO__NATS__NATS_JWT` | пусто | JWT пользователя NATS |
| `DEBUG` | `false` | уровень логов DEBUG |
| `ENV_FILE` | `.env` | путь к файлу окружения (от `api/`) |
| `NATS_ENABLED` | `true` | `false` — не подключаться к NATS, воркер не стартует |
| `WORKER_CONCURRENCY` | `1` | сколько заданий обрабатывается одновременно |
| `PUBLISH_RETRIES` | `3` | повторы публикации ack и результата в NATS после первой попытки (пауза 2 с) |
| `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`, менять не нужно).