Andrey Epifancev e705d75520 feat(parser): контрольные суммы §1.4, статусы, OsvParser как парсер по умолчанию, лимиты
checks.py: тождество сальдо по каждой строке, суммы родителя = Σ детей того же data_type
(CURRENCY/QUANTITY исключены; обороты — по колонкам, сальдо — по колонкам или по нетто при
свёрнутом сальдо родителя), «Итого» = корневой счёт (R-13: нет корня — «Итого» пусто/0;
R-20: нет «Итого» — false и предупреждение).
osv.py: OsvParser — лимит размера, чтение по сигнатуре, листы по содержимому (формы ББ/ОФР и
прочие — пропуск с предупреждением), карта исходов D-6, кооперативный дедлайн, metrics
(rows без TOTAL, warnings с «sheet=<имя> hierarchy=<стратегия>» первой записью по листу).
get_parser(settings, rules_dir=…) → OsvParser (словари грузятся при создании: битый словарь —
падение старта); PARSE_MAX_BYTES / PARSE_TIMEOUT_SECONDS в Settings и .env.*; app.py передаёт
настройки. test_null_parser.py → test_parser_factory.py.
Тесты: юнит-карта статусов, «Испорченная сумма», «Пустая ОСВ», два листа ОСВ, новый синоним
без релиза кода (R-8); property-тест на 66 реальных файлах (все done, < 5 с, схема v1.1).
test_app и интеграционный round-trip ждут ошибку реального парсера вместо NOT_IMPLEMENTED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
2026-10-05 14:37:21 +04:00

tabular-parser — разбор XLS/XLSX/XLSB для АРМ КА

Stateless-сервис распознавания табличных файлов (ОСВ по счетам, ББ, ОФР из Excel) для автоматизированного рабочего места кредитного аналитика ДКБ (АО «Россельхозбанк»). Сервис не имеет доступа к БД и фронта и видит только две внешние системы: NATS (core, без JetStream) и S3 (MinIO, один бакет). Он получает задание, читает исходный файл, возвращает структуру файла и ничего не знает про организации, периметры и план счетов — нормализацией занимается arm-core.

Обмен с arm-core идёт по контракту v1.1: схемы — ../docs/contracts/, копия — в api/contracts/ (идентичность с источником проверяется в make check).

Срез 0 — скелет. Транспорт работает полностью (подписка, ack, чтение S3, публикация результата, ошибки транспорта), а сам разбор заменён заглушкой: на любой корректный запрос сервис отвечает конвертом status = error с error.code = NOT_IMPLEMENTED. Настоящий парсер (ОСВ — Срез 2, формы ББ/ОФР — позже) подключается через интерфейс Parser без изменений воркера.

Репозиторий — часть workspace АРМ КА. Общий контекст, модель, правила предметной области и контракты — в соседних репозиториях:

  • ../CLAUDE.md — инварианты и процесс работы всего workspace;
  • ../docs/ — контекст (context/), контракты (contracts/), окружение разработки (dev/);
  • 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), nats.py (соединение NATS), worker.py (воркер
                      распознавания), logger.py, exception_handlers.py
  src/parser/         интерфейс Parser и заглушка NullParser (разбор — Срез 2)
  src/contract/       pydantic-модели контракта v1.1
  contracts/          копии JSON-схем контракта и примеры
  rules/              словари синонимов *.yaml для парсера (пока только README; словари — Срез 2)
  tests/              pytest: unit и integration (маркер `integration`)
setup/                pip.conf и setup.sh (создание venv без uv)
raisa_config.yml      конфигурация платформы РАИСА
Makefile              команды разработки
Dockerfile            образ сервиса, python:3.11-slim
openspec/             требования и дельта текущего среза (openspec/changes/slice-0-skeleton/)

Предварительные требования

  • Python 3.11 (версия платформы) и 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

Интеграционные тесты (make test-integration, make check-full) поднимают сервис в процессе теста и вступают в queue group tabular-parser на реальной шине: на шине compose не должно работать другого экземпляра tabular-parser (и наоборот: на время теста in-process воркер ответит NOT_IMPLEMENTED на реальные запросы других производителей).

Запуск локально против compose

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. Подробности про compose, смену портов и сброс — ../docs/dev/README.md.

По умолчанию сервис слушает 0.0.0.0:8000 (SERVER__HOST, SERVER__PORT); api/.env.example сужает адрес до 127.0.0.1:8000 (только loopback). Проверка:

curl -s localhost:8000/healthcheck

В ответе — uptime, version и nats: ok — подключён, down — включён (NATS_ENABLED=true), но связи нет, off — выключен (NATS_ENABLED=false, воркер не запущен).

Если 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 с; 0 — без повторов)
DONE_CACHE_SIZE 1000 размер LRU обработанных job_id (дедупликация внутри процесса)
SERVER__HOST 0.0.0.0 адрес привязки HTTP-сервера
SERVER__PORT 8000 порт HTTP-сервера

SERVICE_NAME — имя сервиса в конвертах контракта (service.name); допустимо только tabular-parser, иное значение — ошибка конфигурации при старте. Queue group подписки NATS — константа tabular-parser в src/core/nats.py, настройкой не меняется.

Description
No description provided
Readme 5 MiB
Languages
Python 99.7%
Makefile 0.2%