- README: верхняя граница задержки settle, шаг 5 «Полного круга»; критерии ошибочного файла (корень не объект) - CLAUDE.md: по одному json на каждый распознанный документ; критерии ошибочного файла - design.md: D-5 — оговорка про нестроковый error.code отдельным предложением; №79 — источник указания, SQL уборки в порядке FK - open-questions-proposal.md п.20: замены ../docs/dev/README.md (185–187, 224–226, 252–253) и комментарий docker-compose.yml:184 - план: Task 6 — пометка про замену down -v (R-27) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
284 lines
29 KiB
Markdown
284 lines
29 KiB
Markdown
# scan-gateway
|
||
|
||
Адаптер внешней ИС Сканирование (Content AI) для АРМ КА. Принимает из NATS запросы на распознавание изображений
|
||
(PDF/PNG/JPG), передаёт файл в ИС через «горячие папки», забирает ответ, сохраняет сырой ответ в S3, нормализует его
|
||
и публикует результат в `recognition.result`. Контракт обмена — `../docs/contracts/` (v1.0), архитектура —
|
||
`../docs/context/03-architecture.md`.
|
||
|
||
Границы сервиса (DECISION-4):
|
||
|
||
- нет базы данных: единственный писатель в БД — `arm-core`; шлюз видит только NATS, S3 и горячие папки;
|
||
- нет фронта; HTTP — только `GET /healthcheck`;
|
||
- сырой ответ ИС лежит в S3 (`raw/{job_id}.json`) и в БД не регистрируется;
|
||
- саму ИС Сканирование мы не пишем: для разработки есть эмулятор (`api/src/scan_emulator/`).
|
||
|
||
Состояние: Срез 0 (скелет). Сквозной путь работает, нормализатор — заглушка (см. «Ограничения Среза 0»).
|
||
Правила для агента и соглашения репозитория — `CLAUDE.md`; требования и решения — `openspec/changes/slice-0-skeleton/`.
|
||
|
||
## Поток обработки
|
||
|
||
```
|
||
recognition.image.request (NATS, queue group scan-gateway)
|
||
1. разбор и проверка запроса ──нет/не по схеме──► error SCHEMA_MISMATCH (без ack)
|
||
2. ack: recognition.result, status=processing (до чтения S3 и до ожидания слота)
|
||
3. слот (SCAN_MAX_INFLIGHT одновременных заданий)
|
||
4. файл из S3 (s3.bucket/s3.key)
|
||
5. горячая папка: <Input>/<job_id>_<original_name> (через <Tmp>/.<имя>.part и rename)
|
||
6. опрос <Output>/<job_id>/ каждые SCAN_POLL_SECONDS до дедлайна; набор *.json готов, когда
|
||
не менялся SCAN_SETTLE_SECONDS
|
||
7. склейка всех *.json каталога ──► S3 raw/{job_id}.json (до нормализации)
|
||
8. нормализатор ──► (done/needs_check) S3 results/{file_id}/{job_id}/result.json
|
||
9. recognition.result: итоговый конверт (done | needs_check | error), в нём raw_s3_key
|
||
```
|
||
|
||
Каждое задание заканчивается опубликованным конвертом: сбой превращается в `error` с кодом (таблица ниже), а не в
|
||
молчание. Если не удалось даже опубликовать (после повторов) — запись в лог уровня error, ядро закроет задание по
|
||
своему `reply_deadline`. Падение шлюза после ack теряет задание до `reply_deadline` (без JetStream иначе нельзя).
|
||
|
||
Повторный запрос с `job_id`, который уже в работе или в LRU обработанных (`DONE_CACHE_SIZE`), получает повторный
|
||
ack (с `attempt` повторного запроса); файл в ИС второй раз не отправляется.
|
||
|
||
## Протокол горячих папок
|
||
|
||
Реализации интерфейса `HotFolder` (`api/src/gateway/hotfolder.py`: `put`, `poll`, `healthy`):
|
||
|
||
| Режим | Реализация | Для чего |
|
||
|---|---|---|
|
||
| `local` (по умолчанию) | `LocalHotFolder`, обычный каталог `SCAN_HOTFOLDER_PATH` | разработка, тесты, эмулятор |
|
||
| `smb` | `SmbHotFolder` (`api/src/gateway/smb.py`, `smbprotocol`), шара из `OPENBAO__SMB__*` | платформа |
|
||
|
||
- Входящий файл: `<SCAN_INPUT_DIR>/<job_id>_<original_name>`. `original_name` приходит из запроса и не доверяется: берётся
|
||
последний компонент пути, символы, недопустимые в Windows/SMB (`: < > " | ? *` и управляющие), заменяются на `_`,
|
||
завершающие точки и пробелы отбрасываются; кириллица и пробелы сохраняются; длинное имя укорачивается так, чтобы
|
||
временный `.<имя>.part` умещался в 240 байт UTF-8 (расширение сохраняется). Если имени не осталось —
|
||
`RECOGNITION_FAILED`.
|
||
- Запись атомарна: сначала временный `.<имя>.part` в служебном каталоге `<SCAN_TMP_DIR>` (по умолчанию `Tmp`), затем
|
||
переименование в `Input`. `Tmp` лежит рядом с `Input` и `Output`, на той же ФС/шаре, чтобы `rename` оставался
|
||
атомарным, а ИС не забрала недописанный файл из `Input`. Каталоги `Input` и `Tmp` создаются при необходимости (без
|
||
родителей), корень горячей папки сам не создаётся: нет корня — `RECOGNITION_FAILED`.
|
||
- Ответ ИС — каталог `<SCAN_OUTPUT_DIR>/<job_id>/`: ИС кладёт туда **все** распознанные документы задания одновременно,
|
||
по одному `*.json` на файл (`Output/<job_id>/*.json`). Шлюз читает **все** `*.json` каталога и склеивает их в один
|
||
ответ; прочие файлы каталога игнорируются. `job_id` проверяется на безопасность пути. В одном json может быть
|
||
несколько таблиц разных форм (баланс, ОФР); таблицы в Срезе 0 не интерпретируются (Срез 3).
|
||
- Готовность набора — по стабилизации (settle): в каталоге есть хотя бы один `*.json`, а имена и размеры `*.json` не
|
||
менялись `SCAN_SETTLE_SECONDS` (по умолчанию 2 с). Отсчёт идёт от первого опроса, который увидел окончательный
|
||
набор (а не от времени изменения файлов), поэтому ответ приходит не раньше чем через `SCAN_SETTLE_SECONDS` после
|
||
такого опроса и не позднее `SCAN_SETTLE_SECONDS + SCAN_POLL_SECONDS` после первого опроса, увидевшего набор; `0` — готов любой непустой набор. Появление или изменение файла сбрасывает отсчёт; набор, который так
|
||
и не стабилизировался до дедлайна, даёт `TIMEOUT`.
|
||
- Таймаут ожидания: `min(reply_deadline, момент записи файла + SCAN_TIMEOUT_SECONDS)`; опрос каждые `SCAN_POLL_SECONDS`
|
||
(ожидание settle входит в этот срок).
|
||
Ошибка чтения «Исходящих» не прерывает ожидание. Если срок вышел, пока задание ждало слот, файл в ИС не отправляется.
|
||
- Повторной отправки в ИС нет (OQ-15): после `TIMEOUT` входящий файл остаётся в каталоге, поздний ответ ИС теряется
|
||
(сырой файл остаётся на шаре).
|
||
- Эмулятор ИС в `local`-режиме: для файла `<uuid>_<имя>` ждёт `SCAN_EMULATOR_DELAY` и кладёт в `Output/<uuid>/` весь
|
||
набор фикстур: `<имя>.json` -> `<uuid>.json` и `<имя>_<суффикс>.json` -> `<uuid>_<суффикс>.json` (`<имя>` — имя
|
||
входящего файла без расширения; именно `<имя>_*`, а не префикс `<имя>*`: `aep_3` не забирает `aep_30*`; в каждом
|
||
файле `file_uid` = `<uuid>`). Набор собирается во временном каталоге `Tmp` и появляется в `Output/<uuid>/`
|
||
атомарно целиком (rename каталога). Затем входящий файл переименовывается в `….done`. Нет фикстуры — один
|
||
ответ-ошибка `INVALID_DOCUMENT`. Dot-файлы и `.done` игнорируются.
|
||
|
||
## Склеенный ответ ИС (`raw/{job_id}.json`)
|
||
|
||
В S3 до нормализации сохраняется **склейка** всех `*.json` каталога ответа (контракт v1.0 не меняется: `raw_s3_key` —
|
||
по-прежнему одна строка `raw/{job_id}.json`):
|
||
|
||
```json
|
||
{
|
||
"job_id": "<job_id>",
|
||
"files": [
|
||
{"name": "<job_id>.json", "response": {"file_uid": "...", "error": {"code": "", "message": ""}, "data_ocr": {}}, "parse_error": null},
|
||
{"name": "<job_id>_ofr.json", "response": {"file_uid": "...", "error": {"code": "INVALID_DOCUMENT", "message": "..."}}, "parse_error": null}
|
||
]
|
||
}
|
||
```
|
||
|
||
`files` — все `*.json` каталога по возрастанию имени. Файл, который не разобрался как JSON, не роняет задание:
|
||
`response = null`, `parse_error` — текст ошибки. Нормализатору передаётся ровно этот объект.
|
||
|
||
Формат файла ответа Content AI: `{"file_uid", "error": {"code", "message"}, "data_ocr": {...}}`. **Успех — это
|
||
`error.code == ""`** (`error` при этом не `null`: `{"code": "", "message": ""}`), поэтому проверять `error != null`
|
||
нельзя. Ошибка — непустой `error.code` (`INVALID_DOCUMENT`, ...).
|
||
|
||
## Переменные окружения
|
||
|
||
Файл окружения — `ENV_FILE` (по умолчанию `.env`, путь от текущего каталога, то есть от `api/`); переменные окружения
|
||
приоритетнее файла. Шаблон — `api/.env.example`. Настройки шлюза читаются при импорте `src.app`: модуль создаёт `app`
|
||
(это происходит при запуске `run_server.py` или `uvicorn src.app:app`). Импорт пакета `src` приложение не создаёт, поэтому
|
||
эмулятор (`python -m src.scan_emulator`) настройки шлюза не читает: у него свои переменные (`SCAN_HOTFOLDER_PATH`,
|
||
`SCAN_FIXTURES_DIR`, `SCAN_EMULATOR_*`) из окружения и `ENV_FILE`.
|
||
|
||
| Переменная | По умолчанию | Назначение |
|
||
|---|---|---|
|
||
| `OPENBAO__MINIO__MINIO_URL` | — (обязательна) | адрес MinIO `host:port`, без схемы |
|
||
| `OPENBAO__MINIO__MINIO_ADMIN_LOGIN` | — (обязательна) | логин MinIO |
|
||
| `OPENBAO__MINIO__MINIO_ADMIN_PASSWORD` | — (обязательна) | пароль MinIO |
|
||
| `OPENBAO__MINIO__MINIO_BUCKET` | — (обязательна) | единственный бакет сервиса; запрос с другим `s3.bucket` -> `FILE_READ_ERROR` |
|
||
| `OPENBAO__MINIO__MINIO_SECURE` | `true` | https; для MinIO из compose — `false` |
|
||
| `OPENBAO__NATS__NATS_URL` | — (обязательна) | адрес NATS |
|
||
| `OPENBAO__NATS__NATS_SEED`, `OPENBAO__NATS__NATS_JWT` | пусто | nkeys/JWT платформы; оба пусты — без учётных данных; задан один — ошибка конфигурации |
|
||
| `OPENBAO__SMB__SMB_HOST`, `OPENBAO__SMB__SMB_PATH`, `OPENBAO__SMB__SMB_LOGIN`, `OPENBAO__SMB__SMB_PASSWORD` | пусто | шара горячих папок; обязательны только при `SCAN_HOTFOLDER_MODE=smb`; `SMB_PATH` — `share/dir` или полный UNC |
|
||
| `NATS_ENABLED` | `true` | `false` — воркер не запускается, подключения к NATS нет (`/healthcheck.nats = disabled`) |
|
||
| `PUBLISH_RETRIES` | `3` | повторы публикации ack/результата после первой попытки (всего до 4) |
|
||
| `DONE_CACHE_SIZE` | `1000` | размер LRU обработанных `job_id` (дедупликация) |
|
||
| `SCAN_HOTFOLDER_MODE` | `local` | `local` или `smb` |
|
||
| `SCAN_HOTFOLDER_PATH` | пусто | корень горячей папки в режиме `local` (обязателен, каталог должен существовать) |
|
||
| `SCAN_INPUT_DIR` | `Input` | «Входящие» относительно корня |
|
||
| `SCAN_OUTPUT_DIR` | `Output` | «Исходящие» относительно корня |
|
||
| `SCAN_TMP_DIR` | `Tmp` | служебный каталог относительно корня (на той же ФС/шаре, что `Input`/`Output`): временный `.part` записи входящего файла |
|
||
| `SCAN_POLL_SECONDS` | `5` | период опроса «Исходящих», секунды |
|
||
| `SCAN_SETTLE_SECONDS` | `2` | набор `*.json` в `Output/<job_id>/` готов, когда имена и размеры не менялись столько секунд, считая от первого опроса, увидевшего окончательный набор (>= 0; `0` — сразу) |
|
||
| `SCAN_TIMEOUT_SECONDS` | `300` | потолок ожидания ответа ИС, секунды (дополнительно к `reply_deadline`) |
|
||
| `SCAN_MAX_INFLIGHT` | `4` | одновременно обрабатываемых заданий |
|
||
| `SERVICE_NAME` | `scan-gateway` | `service.name` в конверте |
|
||
| `VERSION` | `1.0.0` | версия сервиса (`/healthcheck`, `service.version`) |
|
||
| `ROOT_PATH` | `/` | префикс FastAPI за прокси |
|
||
| `SERVER__HOST`, `SERVER__PORT` | `0.0.0.0`, `8000` | адрес HTTP-сервера `run_server.py` |
|
||
| `DEBUG` | `false` | флаг шаблона платформы; поведение шлюза не меняет |
|
||
| `ENV_FILE` | `.env` | какой файл окружения читать |
|
||
|
||
Эмулятор (`python -m src.scan_emulator`) читает свои переменные и не использует настройки шлюза (в его контейнере нет
|
||
MinIO и NATS); те же значения подхватываются из `ENV_FILE`:
|
||
|
||
| Переменная | По умолчанию | Назначение |
|
||
|---|---|---|
|
||
| `SCAN_HOTFOLDER_PATH` | — (обязательна) | корень горячей папки; должен существовать, иначе код выхода 2 |
|
||
| `SCAN_INPUT_DIR`, `SCAN_OUTPUT_DIR`, `SCAN_TMP_DIR` | `Input`, `Output`, `Tmp` | подкаталоги корня (как у шлюза); в `Tmp` эмулятор собирает набор ответа перед rename в `Output/<uuid>/` |
|
||
| `SCAN_FIXTURES_DIR` | `../../fixtures/ocr` | каталог эталонных ответов ИС (`aep_N.json`, `aep_N_<суффикс>.json`); путь от `api/` |
|
||
| `SCAN_EMULATOR_DELAY` | `2` | задержка перед выдачей ответа, секунды (>= 0) |
|
||
| `SCAN_EMULATOR_POLL_SECONDS` | `1` | период просмотра «Входящих», секунды (> 0) |
|
||
|
||
## Коды ошибок
|
||
|
||
Итоговый конверт `error` содержит `error.code` и `error.message`. Нормализатор может вернуть любой код `ErrorCode`
|
||
контракта, остальные коды ставит воркер (`api/src/core/worker.py`).
|
||
|
||
| Код | Когда | Ack |
|
||
|---|---|---|
|
||
| `SCHEMA_MISMATCH` | тело не JSON / не по схеме / MAJOR версии ≠ 1 / `pipeline` ≠ `image`. Конверт публикуется, только если в теле есть корректные `job_id` и `file_id`, иначе запрос пишется в лог и отбрасывается | нет |
|
||
| `FILE_READ_ERROR` | объект S3 не прочитан; `s3.bucket` не совпадает с бакетом сервиса | да |
|
||
| `RECOGNITION_FAILED` | не записан файл во «Входящие» (горячая папка недоступна); недопустимое `original_name`; «Исходящие» не читались ни разу до дедлайна | да |
|
||
| `TIMEOUT` | ИС не ответила до `min(reply_deadline, старт + SCAN_TIMEOUT_SECONDS)`; `reply_deadline` истёк до отправки файла | да |
|
||
| `INVALID_FORMAT` | все файлы ответа ошибочны, и у первого ошибочного файла не разобрался JSON (сырой ответ сохранён, `raw_s3_key` указан) или корень ответа не объект; склейка нарушена или пуста | да |
|
||
| код ошибки ИС | **все** файлы ответа ошибочны (`error.code` непустой): код первого ошибочного файла по имени пробрасывается, если он входит в `ErrorCode` (например, `INVALID_DOCUMENT`); каждый ошибочный файл — строка `metrics.warnings` | да |
|
||
| `NOT_IMPLEMENTED` | есть хотя бы один успешный файл (`error.code == ""`); нормализатор Среза 0 — заглушка. Ошибочные файлы набора — в `metrics.warnings` | да |
|
||
| `UNKNOWN_ERROR` | сырой ответ или результат не записан в S3; нормализатор упал; неизвестный код ИС (в том числе `error.code` не строка: число, список, `true`); непредвиденная ошибка | да |
|
||
|
||
Файл ответа считается ошибочным, если его `error.code` непустой, файл не разобрался как JSON (`parse_error`) либо корень
|
||
ответа не объект (`INVALID_FORMAT`).
|
||
`metrics.warnings` итогового конверта содержит по строке `<имя файла>: <код>: <сообщение>` на каждый ошибочный файл (и при
|
||
`NOT_IMPLEMENTED`, и когда ошибочны все файлы); если ошибочных файлов нет, поле `metrics` не передаётся.
|
||
|
||
## Контракт NATS (кратко)
|
||
|
||
Схемы и примеры — `../docs/contracts/`; их байт-в-байт копии лежат в `api/contracts/` (идентичность проверяет unit-тест
|
||
`api/tests/unit/test_contract.py`, входит в `make check`), pydantic-модели — `api/src/contract/`.
|
||
|
||
- Запрос: subject `recognition.image.request`, подписка в queue group `scan-gateway`, `Nats-Msg-Id = <job_id>:<attempt>`.
|
||
- Ответы — subject `recognition.result`: ack `status=processing`, `Nats-Msg-Id = ack:<job_id>:<attempt>`; итог
|
||
`status = done | needs_check | error`, `Nats-Msg-Id = result:<job_id>:<attempt>`. `service.name = "scan-gateway"`.
|
||
- Ключи S3: исходник — из запроса (`s3.key`), сырой ответ (склейка файлов ИС) — `raw/{job_id}.json`, результат —
|
||
`results/{file_id}/{job_id}/result.json`. Бакет один.
|
||
- NATS без JetStream: переподключение и повторная подписка — на `nats-py`, ack и повторы публикации — на уровне
|
||
приложения (`api/src/core/nats.py`).
|
||
|
||
## Запуск
|
||
|
||
Требуются Docker с compose v2, `uv`, Python 3.11. Все команды `make` — из корня репозитория; backend-команды
|
||
рецепты выполняют в `api/`. Рядом в workspace должны лежать `../docs` (compose, контракт) и `../fixtures`.
|
||
|
||
### Разработка
|
||
|
||
```bash
|
||
make up # NATS, MinIO, PostgreSQL: compose-проект arm-ka из ../docs/dev
|
||
make venv # api/.venv (Python 3.11), зависимости без raisa_*
|
||
cp api/.env.example api/.env # значения совпадают с ../docs/dev (NATS 4222, MinIO 9000, бакет arm-ka)
|
||
mkdir -p api/.hotfolder # корень горячей папки: сам не создаётся
|
||
cd api && .venv/bin/python run_server.py # сервис на :8000
|
||
make emulator # в другом терминале: эмулятор ИС Сканирование
|
||
curl -s localhost:8000/healthcheck
|
||
```
|
||
|
||
Целей `down`/`stop` в `Makefile` нет намеренно: compose-проект `arm-ka` общий для сервисов workspace.
|
||
Контейнера `scan-emulator` в compose пока нет (его добавит репозиторий `docs` позже), поэтому эмулятор запускается
|
||
локально командой `make emulator`.
|
||
|
||
### Полный круг против compose и эмулятора
|
||
|
||
Исполняемый пример — `api/tests/integration/test_roundtrip.py` (поднимает приложение и эмулятор в процессе теста,
|
||
публикует запрос, проверяет ack, итоговый конверт, объекты S3). Вручную те же шаги:
|
||
|
||
1. Выполнить «Разработку» выше: сервис и эмулятор работают, `/healthcheck` отдаёт `nats = ok`, `hotfolder = ok`.
|
||
2. Положить файл в MinIO (консоль http://localhost:9001 или `mc`) в бакет `arm-ka`, например
|
||
`uploads/<file_id>/<job_id>/aep_3.pdf` (имя файла должно совпадать с фикстурами эмулятора: `aep_3.*` -> набор
|
||
`aep_3.json` + `aep_3_ofr.json`, `aep_6.*` -> `aep_6.json`, `aep_5.*` -> `aep_5.json`).
|
||
3. Подписаться на `recognition.result` любым клиентом NATS.
|
||
4. Опубликовать в `recognition.image.request` тело по образцу `api/contracts/examples/request-image.json`
|
||
(свои `job_id` и `file_id` — UUID, `original_name`, `s3.key`, `reply_deadline` в будущем, `attempt` = 1) с заголовком
|
||
`Nats-Msg-Id: <job_id>:1`.
|
||
5. Ожидать: ack `processing`, через `SCAN_EMULATOR_DELAY` (+ до `SCAN_SETTLE_SECONDS + SCAN_POLL_SECONDS` после первого опроса, увидевшего набор) —
|
||
конверт `error`:
|
||
- `aep_3` — `NOT_IMPLEMENTED` и `metrics.warnings` с одной строкой `<job_id>_ofr.json: INVALID_DOCUMENT: ...`
|
||
(второй файл набора — ответ-ошибка);
|
||
- `aep_6` — `NOT_IMPLEMENTED` без `metrics`;
|
||
- `aep_5` (и `aep_8`) — `INVALID_DOCUMENT` с сообщением ИС (ответы-ошибки);
|
||
- неизвестное имя — `INVALID_DOCUMENT` эмулятора.
|
||
В бакете появится `raw/<job_id>.json` (склейка всех файлов набора), `raw_s3_key` указан в конверте.
|
||
|
||
### Проверки
|
||
|
||
```bash
|
||
make check # lint (ruff check, ruff format --check, mypy src) + test (pytest -m "not integration")
|
||
make test-integration # pytest -m integration: нужны NATS и MinIO (make up)
|
||
make docker-build # docker build -t scan-gateway:dev . (образ без Nexus)
|
||
make check-full # check + test-integration + docker-build; перед PR
|
||
```
|
||
|
||
Тесты запускаются из `api/`: `.env.test` подключается через `ENV_FILE=.env.test` в `api/pytest.ini` и разрешается
|
||
относительно текущего каталога (рецепты `make` это делают сами). Маркеры: `integration` — нужна инфраструктура,
|
||
исключается из `make test`; `fixtures` — нужны реальные данные из `../fixtures` (нет репозитория — тесты пропускаются).
|
||
Тесты: `api/tests/unit/`, `api/tests/integration/`. Интеграционные тесты делят NATS с другими сервисами workspace:
|
||
если в нём работает другой экземпляр `scan-gateway`, он может забрать запрос теста из queue group.
|
||
Шаги `make check-full` идут последовательно и прерываются на первой ошибке; тесты в образе не запускаются.
|
||
|
||
## `/healthcheck`
|
||
|
||
`GET /healthcheck` всегда отвечает 200; состояние — в полях:
|
||
|
||
| Поле | Значения |
|
||
|---|---|
|
||
| `uptime` | время старта процесса |
|
||
| `version` | `VERSION` |
|
||
| `nats` | `ok` — соединение установлено; `down` — подключение идёт, переподключение, сбой; `disabled` — `NATS_ENABLED=false` |
|
||
| `hotfolder` | `ok` — `HotFolder.healthy()` (корень существует и доступен); `down` — нет, проверка не уложилась в 2 с или упала |
|
||
|
||
Подключение к NATS идёт в фоне: HTTP отвечает сразу (`nats = down`), подписка появляется после подключения.
|
||
Результат проверки горячей папки кэшируется на 5 с.
|
||
|
||
## Docker-образ
|
||
|
||
Один образ (`Dockerfile`, `python:3.11-slim`, контекст сборки — корень репозитория, `docker build -t scan-gateway .`)
|
||
для шлюза и для эмулятора. По умолчанию запускается шлюз (`python run_server.py`, порт 8000); эмулятор — тем же образом
|
||
с `command: python -m src.scan_emulator`. Пакеты `raisa_*` в образ не ставятся. `api/.env` в образ не попадает (см.
|
||
`.dockerignore`): окружение приходит из compose; шлюзу и эмулятору нужен общий том с горячей папкой по пути
|
||
`SCAN_HOTFOLDER_PATH`, эмулятору — каталог фикстур по пути `SCAN_FIXTURES_DIR`.
|
||
|
||
## Ограничения Среза 0
|
||
|
||
- `NullNormalizer` (`api/src/normalizer/`): если ошибочны все файлы ответа, код первого из них пробрасывается; если
|
||
есть успешный файл (`error.code == ""`) — `NOT_IMPLEMENTED`; ошибочные файлы — в `metrics.warnings`. Таблицы
|
||
`data_ocr` (в одном json их может быть несколько, разных форм: баланс, ОФР) не интерпретируются; настоящая
|
||
нормализация в `statementDocument` — Срез 3.
|
||
- Реальные фикстуры: `aep_3.json` (баланс и ОФР), `aep_6.json` — успешные ответы, `aep_3_ofr.json`, `aep_5.json`,
|
||
`aep_8_form4.json` — ответы-ошибки (`INVALID_DOCUMENT`; `data_ocr` — объект с пустыми полями и `table_structure: []`).
|
||
- Режим `smb` покрыт unit-тестами с подменённым `smbclient`; на реальной шаре ИС Сканирование не проверялся. Фактические
|
||
имена каталогов и путь шары на стенде неизвестны — они настраиваются, а не зашиты в код.
|
||
- Повторной отправки в ИС нет (OQ-15); падение шлюза после ack теряет задание до `reply_deadline` ядра.
|
||
- Зависший вызов SMB (`poll`/`put`, до ~60 с по умолчанию `smbclient`) дедлайном воркера не прерывается: вызов идёт
|
||
в потоке без собственного таймаута, поэтому `TIMEOUT` может прийти позже дедлайна на это время.
|
||
- Недописанный набор ответа ИС: защита — settle (`SCAN_SETTLE_SECONDS`), а не разбор содержимого. Если ИС дописывает
|
||
файл дольше settle, будет принят неполный набор; файл, не разобравшийся как JSON, после склейки не перечитывается:
|
||
`response = null`, `parse_error` в `raw/`, ошибка этого файла (`INVALID_FORMAT`) — в `metrics.warnings`.
|
||
- Эмулятор: повреждённая фикстура (не JSON, не UTF-8 или корень не объект) не останавливает его — на её месте в наборе
|
||
он кладёт ответ `INVALID_DOCUMENT` с сообщением о повреждённой фикстуре и пишет ошибку в лог.
|
||
- Контейнер `scan-emulator` в compose отсутствует.
|