142 lines
12 KiB
Markdown
142 lines
12 KiB
Markdown
# 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` раз.
|
||
|
||
Ошибки транспорта процесс не роняют, а превращаются в конверт `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` появляются в задачах 2.1 (`venv`, `lint`, `test`, `test-integration`, `check`, `up`) и 5.1 (`check-full`).
|
||
|
||
## Запуск локально против 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 |
|
||
| `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`, менять не нужно).
|