api/tests/synthetic: детерминированный генератор s01…s13 (openpyxl, метки времени книги и zip зафиксированы), минимальный писатель BIFF12 для s13.xlsb (ruling R-9, читается pyxlsb), тесты сценариев спеки osv-parsing на файлах из fixtures/osv/synthetic/ (таблица «файл → ожидания», s13 равен s02 кроме предупреждения xlsb), тест побайтовой воспроизводимости генерации. synthetic_files() учитывает только файлы Excel (README.md каталога не файл набора). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
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 рассчитаны на это.
Как обрабатывается запрос
arm-coreпубликует запрос в NATS, subjectrecognition.tabular.request(схемаrecognition-request), исходный файл лежит в S3 под ключомuploads/{file_id}/{job_id}/<имя>.- Сервис подписан на этот subject в queue group
tabular-parser— несколько экземпляров делят нагрузку между собой. - Запрос валидируется по схеме. Если
job_idещё не обработан этим процессом (process-local LRU), сервис до чтения файла публикует вrecognition.resultконвертstatus = processing(ack) с заголовкомNats-Msg-Id = ack:<job_id>:<attempt>. Повторный запрос с тем жеjob_idполучает повторный ack и второй раз не обрабатывается. - Файл читается из S3 (
s3.bucket/s3.key) и передаётся парсеру («байты + тип файла → документы, метрики или ошибка»). - Результат (схема
recognition-result) записывается в S3 под ключомresults/{file_id}/{job_id}/result.json. - В
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 восстанавливается автоматически (бесконечное автопереподключение).
Запуск на файле
Разбор одного файла без NATS и S3 — из каталога api/:
cd api
ENV_FILE=.env.test .venv/bin/python -m src.parser.cli <файл> [--file-type XLS|XLSX|XLSB]
Тип файла по умолчанию определяется по расширению (.xls, .xlsx, .xlsb, регистр не важен); --file-type его
переопределяет, а для неизвестного расширения обязателен. ENV_FILE=.env.test нужен, потому что импорт пакета src
читает настройки сервиса (OPENBAO__…); для разбора они не используются, подойдёт и api/.env (см. выше).
В stdout печатается JSON файла результата контракта v1.1 — тот же, что воркер кладёт в S3 под
results/{file_id}/{job_id}/result.json (job_id и file_id — нулевые заглушки). Формат вывода: UTF-8 без
экранирования кириллицы, отступ 2 пробела, порядок полей — как в модели контракта, суммы — строки. duration_ms —
фактическая, поэтому меняется от запуска к запуску. При status = error печатается {"status": "error", "error": {...}},
а код и сообщение ошибки дублируются в stderr.
| Код возврата | Значение |
|---|---|
0 |
done |
1 |
needs_check — нарушены контрольные суммы (§1.4) |
2 |
error — разбор вернул ошибку (error.code, error.message) |
3 |
файл не найден / не читается / ошибка использования (нет аргумента, неизвестное расширение без --file-type, недопустимое значение --file-type); у argparse код 2 по умолчанию, здесь он занят статусом error |
Пример (начало вывода на ОСВ_66 за 2024.xls из fixtures):
{
"schema_version": "1.1",
"job_id": "00000000-0000-0000-0000-000000000000",
"file_id": "00000000-0000-0000-0000-000000000000",
"service": {
"name": "tabular-parser",
"version": "1.0.0"
},
"status": "done",
"metrics": {
"duration_ms": 2,
"rows": 9,
"warnings": [
"sheet=Лист_1 hierarchy=outline"
]
},
"documents": [
{
"document_type": "OSV",
"locator": "sheet:Лист_1",
"header": {
"organization_name": "АО \"Русский бисквит\"",
"organization_inn": null,
"account_code_raw": "66",
"period_text": "2024 г.",
"period_year": 2024,
"period_quarter": "Q4",
"is_cumulative": true,
"data_type": "BU",
"currency": "RUB",
"header_variant": 1
},
Переменные окружения
Настройки читает 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, настройкой
не меняется.