Andrey Epifancev 09f0bb32e2 Срез 0 [5.3]: /healthcheck.nats и .hotfolder, воркер и NATS в lifespan, отметка задачи
src/core/health.py: HealthProvider — nats ok/down/disabled по is_connected; hotfolder —
healthy() в потоке под wait_for (2 с) с кэшем 5 с, зависшая проба не запускается
повторно (T8.4). Сбой регистрации SMB-сессии виден естественно: SmbHotFolder.healthy()
обращается к шаре с теми же учётными данными (T2.3).

app.py: в lifespan S3Storage, NatsGateway, Worker(NullNormalizer); подключение к NATS
и подписка — фоновой задачей (connect ждёт сервер), её сбой — logger.exception и
nats=down без падения HTTP; остановка отменяет подключение, останавливает воркер,
закрывает NATS и S3. Объекты lifespan — в app.state.

test_app.py: тесты lifespan переведены на NATS_ENABLED=false и tmp-корень (иначе фоновое
подключение шло бы в compose NATS), проверка полей healthcheck расширена nats/hotfolder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 03:50:22 +04:00

72 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Tasks: Срез 0 — скелет scan-gateway
Репозиторий — scan-gateway; команды — из `api/`. Зависимости: 3.1 ждёт контракт v1.0 (`docs` 1.1–1.4); интеграционные
тесты 6.x — compose (`docs` 2.1, 5.3). Каждая задача ≤ половины дня. CI нет — проверки локальные.
## 1. Вырезать фронт и БД из шаблона (SMB остаётся)
- [x] 1.1 Удалить `web/`, `setup/.npmrc`, npm-шаги `setup/setup.sh`, `api/alembic/`, `api/alembic.ini`, `api/src/db/`,
`api/src/models/`, `api/src/repositories/`; убрать `Dockerfile` из `.gitignore`; проверка: `git status` — только удаления и `.gitignore`
- [x] 1.2 `src/app.py`: убрать mount SPA/`os.makedirs("web")`, CORS, импорт `raisa_fastapi_protected_api`; оставить SMB-сессию
(только при `SCAN_HOTFOLDER_MODE=smb`); `src/core/config.py`: убрать `DBSettings`; добавить `ENV_FILE`,
`SERVICE_NAME="scan-gateway"`, `NATS_ENABLED=true`, `PUBLISH_RETRIES=3`, `DONE_CACHE_SIZE=1000`,
`SCAN_HOTFOLDER_MODE="local"`, `SCAN_HOTFOLDER_PATH`, `SCAN_INPUT_DIR="Input"`, `SCAN_OUTPUT_DIR="Output"`,
`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 и эмулятора),
`CLAUDE.md` — актуализировать «Соглашения шаблона» по факту; проверка: упомянутые файлы существуют
## 2. Инструменты
- [x] 2.1 `api/pyproject.toml` (ruff, mypy), `requirements-dev.txt` = `-r requirements.txt` + ruff, mypy, pytest,
pytest-asyncio, pytest-env, httpx, jsonschema; `Makefile`: `venv` (python3.11, без `raisa_*`), `lint`, `test`,
`test-integration`, `check`, `check-full`, `up`, `emulator`; проверка: `make venv && make lint` зелёные
- [x] 2.2 `Dockerfile` (`python:3.11-slim`; `CMD python run_server.py`, эмулятор — через `command` в compose),
`.env.example`, `.env.test`; проверка: `docker build` успешен без Nexus
## 3. Контракт
- [x] 3.1 `api/contracts/` — копии схем и `examples/` из `../docs/contracts`; `src/contract/` — pydantic-модели (конверт со
статусами `processing | done | needs_check | error`); `tests/unit/test_contract.py` — идентичность (skip без docs),
примеры, round-trip; проверка: тесты зелёные
## 4. Горячие папки и эмулятор
- [x] 4.1 `src/gateway/hotfolder.py`: протокол `HotFolder`, `LocalHotFolder` (put/poll/healthy); `tests/unit/test_local_hotfolder.py`
на `tmp_path`; проверка: put пишет `<uuid>_<name>`, poll возвращает `None` до появления ответа и байты после
- [x] 4.2 `src/gateway/smb.py`: `SmbHotFolder` поверх `smbclient` (UNC `\\host\path\Input`), те же методы; unit-тест с
подменённым `smbclient` (вызовы `open_file`, `listdir`); проверка: тест зелёный, в режиме `local` модуль не импортируется
- [x] 4.3 `src/scan_emulator/__init__.py` + `__main__.py`: функция `process_once(root, fixtures, delay)` и цикл; поиск
фикстуры по имени/префиксу, подстановка `file_uid`, ошибка `INVALID_DOCUMENT`, переименование в `.done`;
`tests/unit/test_emulator.py` — сценарии spec «Точное имя», «Префикс», «Нет пары» на `tmp_path` с копиями
`../fixtures/ocr/*.json` (маркер `fixtures`, skip без фикстур); проверка: зелёный
## 5. Воркер и нормализатор-заглушка
- [x] 5.1 `src/normalizer/__init__.py`: протокол `Normalizer`, `NormalizeOutcome`, `NullNormalizer` (ошибка ИС → код из
ответа; успех → `NOT_IMPLEMENTED`; не JSON → `INVALID_FORMAT`); unit-тесты на `aep_5.json`, `aep_8_form4.json`,
синтетическом ответе с `error`; проверка: зелёный
- [x] 5.2 `src/core/clients.py` (`get_bytes`, `put_json`, `put_bytes`), `src/core/nats.py` (подключение с/без nkeys,
автопереподключение, `subscribe_requests(queue="scan-gateway", cb)`, `publish` с `Nats-Msg-Id` и повторами);
проверка: unit с фейками, интеграционный тест подключения к compose-NATS без JetStream
- [x] 5.3 `src/core/worker.py`: `on_request(msg)`: валидация → LRU `job_id` → ack `processing` → очередь; семафор
`SCAN_MAX_INFLIGHT`; `handle(job)`: S3 get → `hotfolder.put` → опрос до дедлайна → `raw/{job_id}.json` → `Normalizer` →
результат/конверт → publish; таблица ошибок D-7; `/healthcheck.nats`, `.hotfolder`; проверка: unit-тесты `handle` с
фейками и `LocalHotFolder` + `process_once` эмулятора — сценарии spec (ack раньше файла, повторный запрос → один файл,
успех → NOT_IMPLEMENTED с `raw_s3_key`, INVALID_DOCUMENT, TIMEOUT с укороченными таймаутами, RECOGNITION_FAILED при
отсутствии каталога, SCHEMA_MISMATCH без ack, FILE_READ_ERROR)
- [ ] 5.4 Тест параллелизма: пять заданий при `SCAN_MAX_INFLIGHT = 4` — пять ack сразу, четыре файла во входящем каталоге,
пятый после освобождения слота (фейковые S3/NATS, `LocalHotFolder`); проверка: зелёный
## 6. Интеграция, проверки, завершение
- [ ] 6.1 `tests/integration/test_roundtrip.py` (маркер `integration`): compose с `scan-emulator` — положить `aep_5.pdf` в
MinIO `uploads/…`, опубликовать `request-image.json`, собрать `recognition.result`: `processing`, затем
`NOT_IMPLEMENTED` с `raw_s3_key`; проверить объект `raw/<job_id>.json` в MinIO; второй запрос с `unknown.pdf` →
`INVALID_DOCUMENT`; проверка: зелёный
- [ ] 6.2 `make check-full`: `make check` + `pytest -m integration` + `docker build`; проверка: зелёный против compose docs
на ветке `feat/1-slice-0-skeleton`
- [ ] 6.3 Прогон DoD: `make up` → `make check-full` → ручная проверка из `docs/dev/README.md`; `openspec validate
slice-0-skeleton --strict`; вывод `make check-full` — в описание PR