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

7.2 KiB
Raw Blame History

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 остаётся)

  • 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
  • 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. Инструменты

  • 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 зелёные
  • 2.2 Dockerfile (python:3.11-slim; CMD python run_server.py, эмулятор — через command в compose), .env.example, .env.test; проверка: docker build успешен без Nexus

3. Контракт

  • 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. Горячие папки и эмулятор

  • 4.1 src/gateway/hotfolder.py: протокол HotFolder, LocalHotFolder (put/poll/healthy); tests/unit/test_local_hotfolder.py на tmp_path; проверка: put пишет <uuid>_<name>, poll возвращает None до появления ответа и байты после
  • 4.2 src/gateway/smb.py: SmbHotFolder поверх smbclient (UNC \\host\path\Input), те же методы; unit-тест с подменённым smbclient (вызовы open_file, listdir); проверка: тест зелёный, в режиме local модуль не импортируется
  • 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. Воркер и нормализатор-заглушка

  • 5.1 src/normalizer/__init__.py: протокол Normalizer, NormalizeOutcome, NullNormalizer (ошибка ИС → код из ответа; успех → NOT_IMPLEMENTED; не JSON → INVALID_FORMAT); unit-тесты на aep_5.json, aep_8_form4.json, синтетическом ответе с error; проверка: зелёный
  • 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
  • 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