diff --git a/CLAUDE.md b/CLAUDE.md index a16d0cf..2b639ae 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,29 +12,38 @@ - Нормализация по фактическим ответам `../fixtures/ocr/aep_*.json`, а не по ТЗ (OQ-13, OQ-14): confidence → 0–100, поля организации/даты — по тому, что реально приходит. - Один PDF может содержать ББ и ОФР → два документа в результате. -- Dev-режим: эмулятор ИС Сканирование, возвращающий `aep_N.json` по имени файла (`SCAN_EMULATOR=1`). +- Dev-режим: эмулятор ИС Сканирование, возвращающий `aep_N.json` по имени файла: `make emulator` (то же, что + `python -m src.scan_emulator` из `api/`); в контейнере — тот же образ с `command: python -m src.scan_emulator`. +- Описание сервиса, протокол горячих папок, env, запуск и коды ошибок — `README.md`; требования и решения Среза 0 — + `openspec/changes/slice-0-skeleton/` (`design.md`). ## Соглашения шаблона Тот же шаблон команды, что у `arm-core` («Шаблон FastAPI», `RSHB/DKB_ARMKA@ba172f8`); общие соглашения — -`../docs/context/03-architecture.md`, «Соглашения шаблона команды». Сейчас репозиторий — полная копия каркаса `arm-core`; -change `slice-0-skeleton` приводит его к сервису **без фронта и без БД**, но с SMB. Все команды — из `api/`. +`../docs/context/03-architecture.md`, «Соглашения шаблона команды». Репозиторий приведён к сервису **без фронта и без +БД**, но с SMB. Команды `make` — из корня репозитория (рецепты сами заходят в `api/`), `pytest` — из `api/`. -**Оставлено от шаблона:** `api/run_server.py` (точка входа платформы, `SERVER__HOST/PORT`), `api/src/app.py` (lifespan с -регистрацией SMB-сессии, `GET /healthcheck`, gzip/proxy middleware, обработчики ошибок), `core/config.py` (pydantic-settings, -`OPENBAO__MINIO__*`, `OPENBAO__NATS__*`, `OPENBAO__SMB__*`, `DEBUG`), `core/clients.py` (S3), `core/nats_callback.py`, -`core/logger.py`, `core/exception_handlers.py`, `common/exceptions.py`, `pytest.ini`, `requirements*.txt` (включая -`smbprotocol`), `raisa_config.yml`, `setup/setup.sh` + `pip.conf` (без npm-шага). +**Оставлено от шаблона:** `api/run_server.py` (точка входа платформы, `SERVER__HOST/PORT`), `api/src/app.py` (lifespan: +SMB-сессия, S3, NATS, воркер; `GET /healthcheck`; gzip/proxy middleware), `core/config.py` (pydantic-settings, +`OPENBAO__MINIO__*`, `OPENBAO__NATS__*`, `OPENBAO__SMB__*`, `DEBUG`), `core/logger.py`, `core/exception_handlers.py`, +`common/exceptions.py`, `api/pytest.ini`, `api/requirements*.txt` (включая `smbprotocol`), `raisa_config.yml`, +`setup/setup.sh` + `setup/pip.conf` (без npm-шага). -**Вырезано (Срез 0):** `web/` целиком и `setup/.npmrc`; из `app.py` — mount SPA (`SPAStaticFiles`, `os.makedirs("web")`), -CORS, импорт `raisa_fastapi_protected_api`; `db/`, `models/`, `repositories/`, `alembic/`, `alembic.ini`; из настроек — -`OPENBAO.DATABASE`; из `requirements.txt` — `asyncpg`, `SQLAlchemy`, `alembic`, `argon2-cffi`, `raisa_fastapi_protected_api`; -шаблонный `README.md` заменён описанием сервиса. +**Вырезано (Срез 0):** `web/`, `setup/.npmrc`; из `app.py` — mount SPA, CORS, `raisa_fastapi_protected_api`; `db/`, +`models/`, `repositories/`, `alembic/`; `OPENBAO.DATABASE`; из `requirements.txt` — `asyncpg`, `SQLAlchemy`, `alembic`, +`raisa_fastapi_protected_api`; шаблонные `core/nats_callback.py` и `README.md` заменены своими. -**Добавлено:** `api/src/gateway/` (горячие папки: положить `UUID_имя.pdf` во «Входящие», опрашивать «Исходящие/UUID/UUID.json»; -режим `local` — обычный каталог для dev, `smb` — как в шаблоне через `smbclient`; референс поллинга — -`../reference-old-arm/api/src/core/PDFWorker`), `api/src/normalizer/` (ответ Content AI → `statementDocument`; Срез 3), -`api/src/contract/` (pydantic-модели `recognition-result` v1.0; идентичность с `../docs/contracts` проверяется в `make check`), -`api/src/core/worker.py` (подписка queue group `recognition.image.request` → S3 → горячая папка → ожидание ≤ `reply_deadline` -→ S3 `raw/` и `results/` → `recognition.result`), `api/src/scan_emulator/` (эмулятор ИС Сканирование для compose: следит за -«Входящими», кладёт `../fixtures/ocr/aep_N.json` по имени файла в «Исходящие»; запускается тем же образом, `SCAN_EMULATOR=1`), -`Dockerfile`, `api/pyproject.toml` (ruff, mypy). Проверки: `pytest`, `ruff check . && ruff format --check .`, `mypy src`. +**Добавлено:** +- `api/src/gateway/` — горячие папки: протокол `HotFolder` (`hotfolder.py`: `LocalHotFolder`, `inbox_name`), `smb.py` + (`SmbHotFolder`, импортируется лениво), фабрика `create_hotfolder`; референс поллинга — `../reference-old-arm/api/src/core/PDFWorker`. +- `api/src/core/` — `worker.py` (задание: проверка → ack → S3 → горячая папка → опрос → `raw/` → нормализатор → + результат; ошибки по таблице D-7), `nats.py` (core NATS без JetStream, queue group `scan-gateway`, публикация с + повторами), `clients.py` (`S3Storage`), `health.py` (`nats`/`hotfolder` в `/healthcheck`). +- `api/src/normalizer/` — `Normalizer`, `NullNormalizer` (Срез 0: ошибка ИС → её код, успех → `NOT_IMPLEMENTED`); + настоящая нормализация — Срез 3. +- `api/src/contract/` + `api/contracts/` — pydantic-модели и байт-в-байт копии схем `../docs/contracts`; идентичность + проверяет `api/tests/unit/test_contract.py` (входит в `make check`). +- `api/src/scan_emulator/` — эмулятор ИС Сканирование (`SCAN_FIXTURES_DIR`, `SCAN_EMULATOR_DELAY`, + `SCAN_EMULATOR_POLL_SECONDS`); в compose контейнера пока нет — `make emulator`. +- `Makefile` (`venv`, `lint`, `test`, `test-integration`, `check`, `check-full`, `up`, `emulator`), `api/pyproject.toml` + (ruff, mypy), `Dockerfile` + `.dockerignore` (один образ: шлюз и эмулятор), `api/.env.example`, `api/.env.test`. +- Тесты: `api/tests/unit/`, `api/tests/integration/` (маркеры `integration`, `fixtures`); перед PR — `make check-full`. diff --git a/README.md b/README.md index 6b8cbb1..4627515 100644 --- a/README.md +++ b/README.md @@ -1,163 +1,224 @@ -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) [![linting: pylint](https://img.shields.io/badge/linting-pylint-yellowgreen)](https://github.com/pylint-dev/pylint) -__________________________________________________________________________________________________________ -# dkb_armka, v 0.0.1 -____ +# scan-gateway -| | Исполнитель | Заказчик | -|-----------|:----------------------------------------:|:----------------------------------------------:| -| Сотрудник | raykov-mse | raykov-mse | -| Отдел | aurora | aurora | -| Контакты | raykovmse@rshb.ru | raykovmse@rshb.ru | ------ -# Шаблон FastAPI +Адаптер внешней ИС Сканирование (Content AI) для АРМ КА. Принимает из NATS запросы на распознавание изображений +(PDF/PNG/JPG), передаёт файл в ИС через «горячие папки», забирает ответ, сохраняет сырой ответ в S3, нормализует его +и публикует результат в `recognition.result`. Контракт обмена — `../docs/contracts/` (v1.0), архитектура — +`../docs/context/03-architecture.md`. -## Описание +Границы сервиса (DECISION-4): -Данное шаблон предоставляет проект, в котором приложение состоит из API и UI. При использовании данного типа проекта, Вы можете разрабатывать, как отдельные API сервисы, так и WEB приложения с интерфейсом. Для построения WEB интерфейса используется React. Далее описаны основные иснтрументы предустановленные данным шаблоном: +- нет базы данных: единственный писатель в БД — `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/`. -Для удобной настройки проекта под разработку. Перейдите в каталог setup и выполните слудеющие команды: +## Поток обработки + +``` +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. горячая папка: /_ + 6. опрос //.json каждые SCAN_POLL_SECONDS, до дедлайна + 7. сырой ответ ──► 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__*` | платформа | + +- Входящий файл: `/_`. `original_name` приходит из запроса и не доверяется: берётся + последний компонент пути, символы, недопустимые в Windows/SMB (`: < > " | ? *` и управляющие), заменяются на `_`, + завершающие точки и пробелы отбрасываются; кириллица и пробелы сохраняются. Если имени не осталось — + `RECOGNITION_FAILED`. +- Запись атомарна: сначала `.<имя>.part` в том же каталоге, затем переименование. Каталог `Input` создаётся при + необходимости (без родителей), корень горячей папки сам не создаётся: нет корня — `RECOGNITION_FAILED`. +- Ответ ИС: `//.json` (читается только этот файл). `job_id` проверяется на + безопасность пути. Ответ, который ещё пишется (не JSON), перечитывается на следующем опросе. +- Таймаут ожидания: `min(reply_deadline, момент записи файла + SCAN_TIMEOUT_SECONDS)`; опрос каждые `SCAN_POLL_SECONDS`. + Ошибка чтения «Исходящих» не прерывает ожидание. Если срок вышел, пока задание ждало слот, файл в ИС не отправляется. +- Повторной отправки в ИС нет (OQ-15): после `TIMEOUT` входящий файл остаётся в каталоге, поздний ответ ИС теряется + (сырой файл остаётся на шаре). +- Эмулятор ИС в `local`-режиме: для файла `_<имя>` ждёт `SCAN_EMULATOR_DELAY`, пишет ответ из фикстуры + (`<имя без расширения>.json`, затем первый по алфавиту `<имя без расширения>*.json`; `file_uid` подставляется), потом + переименовывает входящий файл в `….done`. Нет фикстуры — ответ-ошибка `INVALID_DOCUMENT`. Dot-файлы и `.done` + игнорируются. + +## Переменные окружения + +Файл окружения — `ENV_FILE` (по умолчанию `.env`, путь от текущего каталога, то есть от `api/`); переменные окружения +приоритетнее файла. Шаблон — `api/.env.example`. Настройки читаются при старте, не при импорте. + +| Переменная | По умолчанию | Назначение | +|---|---|---| +| `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_POLL_SECONDS` | `5` | период опроса «Исходящих», секунды | +| `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` | `Input`, `Output` | подкаталоги корня (как у шлюза) | +| `SCAN_FIXTURES_DIR` | `../../fixtures/ocr` | каталог эталонных ответов ИС (`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`: код пробрасывается, если он входит в `ErrorCode` (например, `INVALID_DOCUMENT`) | да | +| `NOT_IMPLEMENTED` | ответ ИС без ошибки (нормализатор Среза 0 — заглушка) | да | +| `UNKNOWN_ERROR` | сырой ответ или результат не записан в S3; нормализатор упал; неизвестный код ИС; непредвиденная ошибка | да | + +## Контракт 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 = :`. +- Ответы — subject `recognition.result`: ack `status=processing`, `Nats-Msg-Id = ack::`; итог + `status = done | needs_check | error`, `Nats-Msg-Id = result::`. `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 -cd setup -chmod +x setup.sh -./setup.sh +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///aep_5.pdf` (имя файла должно совпадать с фикстурой эмулятора: `aep_5.*` -> `aep_5.json`, + `aep_8.*` -> `aep_8_form4.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: :1`. +5. Ожидать: ack `processing`, через `SCAN_EMULATOR_DELAY` — конверт `error`. Для `aep_5`/`aep_8` это `INVALID_DOCUMENT` + с сообщением ИС (обе реальные фикстуры — ответы-ошибки); для неизвестного имени — `INVALID_DOCUMENT` эмулятора. + В бакете появится `raw/.json`, `raw_s3_key` указан в конверте. +6. Путь «успех -> `NOT_IMPLEMENTED`» требует успешной фикстуры: добавьте в `SCAN_FIXTURES_DIR` синтетический + `<имя>.json` с `error: null` и непустым `data_ocr` (так делает `test_roundtrip.py`, фикстура `ok_doc.json`). + +### Проверки ```bash -complete setup web application -To start api, cd to api directory, and use venv, start run_server.py -To start api, cd to web directory, and use command npm run start! -Happy Hacking! :) +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 check-full # check + test-integration; перед 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. +Сборка Docker-образа в `make check-full` пока не входит. -### Backend +## `/healthcheck` -Исходный код API находится в каталоге api. После клонирования репозитория требуется создать виртуальное окружение и установить зависимости, это достигается следующими командами: +`GET /healthcheck` всегда отвечает 200; состояние — в полях: -```bash -cd api -python -m venv ./.venv -cp ./../setup/pip.conf ./.venv/pip.conf -source ./.venv/bin/python -pip install -r requirements.txt -``` +| Поле | Значения | +|---|---| +| `uptime` | время старта процесса | +| `version` | `VERSION` | +| `nats` | `ok` — соединение установлено; `down` — подключение идёт, переподключение, сбой; `disabled` — `NATS_ENABLED=false` | +| `hotfolder` | `ok` — `HotFolder.healthy()` (корень существует и доступен); `down` — нет, проверка не уложилась в 2 с или упала | -Запуск приложения осуществляется через файл: run_server.py +Подключение к NATS идёт в фоне: HTTP отвечает сразу (`nats = down`), подписка появляется после подключения. +Результат проверки горячей папки кэшируется на 5 с. -В качестве фреймворка ля api используется FastAPI. (Как понятно из названия шаблона) Подробнее ознакомится с возможностями Fast API вы можете в официальной документации: [Fast API документация](https://fastapi.tiangolo.com/) +## Docker-образ -Вы можете дополнительно устанавливать библиотеки необходимые для разработки. Настоятельно не рекомендуется изменять базовые предустановки из файла requirements.txt +Один образ (`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`. -Базовый шаблон предоставляет минимальный функционал по работе с данными. Далее описаны основные моменты влияющие на работу приложения. - -#### Основная структура проекта - -```bash -api -├── requirements.txt -├── run_server.py -├── src -│ ├── app.py -│ ├── __init__.py -│ ├── settings -│ │ ├── __init__.py -│ │ ├── models.py -│ │ └── settings.py -└── web -``` - -Вы можете придерживатся любой структуры проекта, главное, чтобы у Вас присутствовал файл run_server.py. Который запускает web сервер uvicorn или gunicorn. Также обязательно Ваш сервер должен подниматся на определенном хосте и порте. Требуемый хост и порт передаются внутрь через переменные окружения SERVER_HOST, SERVER_PORT - -```python -uvicorn.run( - app, - host=settings.server.HOST, - port=settings.server.PORT, - lifespan="on" - ) -``` - -#### Отказ от ui (react) - -Если внутри вашего приложения не требуется добавлять визуальный интерфейс, а Вам требуется исключительно разработать API, то в таком случае не стоит удалять директорию web находящуюся на уровне с директорией api. ТРебуется внести небольшие правки в конфигурацию приложения, в частности в файл: /api/src/app.py, удалив следующее содержимое. В таком случае Fast API сервис не будет обрабатывать статические файлы, от web приложения. - -```python -#Подключение собранного web приложения -app.mount("/", StaticFiles(directory="web", html=True), name="web") -``` - -### Frontend - -Исходный код UI находится в каталоге web. После клонирования репозитория требуется установить зависимости, это достигается следующими командами: - -```bash -cd web -npm install -``` - -Запуск приложения осуществляется через файл команду: - -```bash -npm run start -``` - -В качестве фреймворка для ui используется React. Подробнее ознакомится с возможностями React вы можете в официальной документации: [React документация](https://react.dev/reference/react) - -Вы можете дополнительно устанавливать библиотеки необходимые для разработки. Настоятельно не рекомендуется изменять базовые предустановки из файла package.json - -Базовый шаблон предоставляет минимальный интерфейс по работе с данными. Далее описаны основные моменты влияющие на работу web приложения. - -#### Основная структура проекта - -```bash -web -├── biome.json -├── .npmrc -├── dist -├── package.json -├── postcss.config.cjs -├── public -│ └── index.html -├── README.md -├── rsbuild.config.ts -├── src -│ ├── App.tsx -│ ├── config.ts -│ ├── env.d.ts -│ ├── index.css -│ └── index.tsx -├── tailwind.config.js -└── tsconfig.json -``` - -Вы можете придерживатся любой структуры проекта, внутри src. Но на верхнем уровне находятся файлы конфигурации и сборки их требуется изменять только в крайнем случае. Для сборки приложения используется [RSBuild](https://rsbuild.dev/) - -Для создания UI настоятельно рекомендуется использовать предустановленную библиотеку компонентови стилей: - -- tailwindcss -- daisy-ui -- react-daisy-ui -- raisa-ui - -!!! Важно, если внутри web приложения Вы хотите обращатся к API данного проекта, используйте следующую конструкцию: - -```javascript - -fetch(`${Config.API_URL}/api`, { - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - method: 'GET' -}) - -``` - -Речь идет об вызываемом эндпоинте ${Config.API_URL}/api, Обязательно используйте конструкцию данного вида т.к. при сборке приложения статический сайт обращается относительно api и вместо -запросов вида http://localhost:8000/api должно быть так: /api +## Ограничения Среза 0 +- `NullNormalizer` (`api/src/normalizer/`): ошибка ИС пробрасывается с её кодом, любой успешный ответ даёт + `NOT_IMPLEMENTED`. Настоящая нормализация `data_ocr` в `statementDocument` — Срез 3. +- Реальные фикстуры `aep_5.json` и `aep_8_form4.json` — ответы-ошибки ИС (`INVALID_DOCUMENT`, пустой `data_ocr`): + успешных образцов нет, это открытый вопрос Среза 3. +- Режим `smb` покрыт unit-тестами с подменённым `smbclient`; на реальной шаре ИС Сканирование не проверялся. Фактические + имена каталогов и путь шары на стенде неизвестны — они настраиваются, а не зашиты в код. +- Повторной отправки в ИС нет (OQ-15); падение шлюза после ack теряет задание до `reply_deadline` ядра. +- Контейнер `scan-emulator` в compose отсутствует, сборка образа в `make check-full` не входит. diff --git a/docs/superpowers/plans/2026-10-03-slice-0-skeleton.md b/docs/superpowers/plans/2026-10-03-slice-0-skeleton.md index 98bfe80..8914be3 100644 --- a/docs/superpowers/plans/2026-10-03-slice-0-skeleton.md +++ b/docs/superpowers/plans/2026-10-03-slice-0-skeleton.md @@ -28,7 +28,7 @@ ### Task 3: [1.3] `README.md` — описание сервиса (назначение, протокол горячих папок, env, запуск -- [ ] `README.md` — описание сервиса (назначение, протокол горячих папок, env, запуск против compose и эмулятора), `CLAUDE.md` — актуализировать «Соглашения шаблона» по факту; проверка: упомянутые файлы существуют +- [x] `README.md` — описание сервиса (назначение, протокол горячих папок, env, запуск против compose и эмулятора), `CLAUDE.md` — актуализировать «Соглашения шаблона» по факту; проверка: упомянутые файлы существуют ### Task 4: [2.1] `api/pyproject.toml` (ruff, mypy), `requirements-dev.txt` = `-r requirements.txt diff --git a/openspec/changes/slice-0-skeleton/tasks.md b/openspec/changes/slice-0-skeleton/tasks.md index 8fc894f..8f3a072 100644 --- a/openspec/changes/slice-0-skeleton/tasks.md +++ b/openspec/changes/slice-0-skeleton/tasks.md @@ -14,7 +14,7 @@ `SCAN_POLL_SECONDS=5`, `SCAN_TIMEOUT_SECONDS=300`, `SCAN_MAX_INFLIGHT=4`; `requirements.txt` без `asyncpg`, `SQLAlchemy`, `alembic`, `argon2-cffi`, `raisa_fastapi_protected_api`; проверка: чистый venv (python3.11), `python run_server.py` с `.env.example`, `/healthcheck` 200 -- [ ] 1.3 `README.md` — описание сервиса (назначение, протокол горячих папок, env, запуск против compose и эмулятора), +- [x] 1.3 `README.md` — описание сервиса (назначение, протокол горячих папок, env, запуск против compose и эмулятора), `CLAUDE.md` — актуализировать «Соглашения шаблона» по факту; проверка: упомянутые файлы существуют ## 2. Инструменты