Перейти к основному содержимому

Data API (gwptd-collector)

HTTP/JSON граница, через которую mp.hyp.ru (PHP) читает собранные данные вместо прямого доступа к БД (вводная Михаила). Контракт: ../docs/DESIGN-2026-06-08.md §3 + ../docs/DATA-API-REPORTS.md.

Статус: автономный read-only сервис S3 NEXT. Он читает локальные gwptd_intake / gwptd_kernel / gwptd_monitoring; сервисный Bearer token ограничивает data:read, status:read и control:read.

Запуск (dev)

cd data_api
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
# env: V3_MYSQL_HOST/PORT/USER/PASSWORD (как у коллектора, :3406). Локально auth выключен, если DATA_API_TOKENS пуст.
uvicorn main:app --reload --port 8080
# Swagger: http://localhost:8080/docs ; health: /health

Что реализовано (живые срезы, по одному из каждого семейства)

EndpointИсточник
GET /api/v1/status/freshnessgwptd_intake.collection_run (последний run на endpoint + last_success)
GET /api/v1/status/collection-runs (+ /{run_id})gwptd_intake.collection_run (строгие фильтры, bounded limit)
GET /api/v1/status/parser-errorsgwptd_intake.parser_error × collection_run; raw payload не выдаётся
GET /api/v1/status/reconciliation (+ /{check_id})локальный gwptd_monitoring.cutover_reconciliation_log
GET /api/v1/facts/stocks-dailygwptd_kernel.fact_stocks_daily (keyset-пагинация по snapshot_date,id)
GET /api/v1/control/endpoints (+ /{code})gwptd_monitoring.api_endpoint_registry (SELECT *, устойчив к pre/post-ALTER)
GET /api/v1/control/field-mapsDB-проекция api_field_map; при её отсутствии — валидированный declared-каталог из 79 ProductSpec с явным provenance
GET /healthSELECT 1

Оставшийся каркас (stub → 501)

/facts/{orders,sales,prices-daily,finance,storage-daily,promotions-daily,analytics-daily}, /dims/*, неизвестные /reports/*, /compat/mp-*-daily и control-plane мутации. Служебные status/field-map чтения больше не являются заглушками.

Классификация ошибок источника (errors.py)

db.fetch_* классифицирует ошибки MySQL по errno (план spec-driven §4.13 R2, quick-win):

КлассКогдаНаружу
OptionalSourceMissingнет таблицы/колонки/гранта на optional-зеркале s2_ref/s20_ref или dark gwptd_core_compatловится except MIRROR_ERRORS → прежний fallback/degraded; allowlisted core-reader повышает ошибку обратно до mandatory
MandatorySourceErrorте же errno на обязательной схеме (kernel и др.); 1045 креды500 SOURCE_ERROR + лог
QuerySourceErrorпрочие ошибки SQL (1064 syntax, …)500 QUERY_ERROR + лог
SourceUnavailableсоединение/timeout (CR 2xxx, 1040, 1205, 3024)503 SOURCE_UNAVAILABLE, data_status=unavailable

Плюс глобальный handler на неожиданные исключения → 500 INTERNAL (envelope, не голый traceback). Итог: сломанный SQL и упавшее соединение больше НЕ маскируются под пустой деградированный 200 (DNR-007). data_status расширен значениями degraded/unavailable (зеркально в app/Support/DataStatus.php).

Согласованное чтение одного ответа (P1B-5, dark)

DATA_API_READ_COHERENCE_MODE управляет request-scoped чтением:

  • off (default) сохраняет прежнее отдельное соединение на каждый db.fetch_*;
  • observe лениво открывает один read-only REPEATABLE READ ... WITH CONSISTENT SNAPSHOT, но до первого business-query может откатиться на прежние чтения, если MySQL не дал запустить snapshot; после начала чтения ошибка cleanup уже фатальна и успешный ответ не выпускается;
  • enforce требует snapshot и завершает запрос ошибкой, если его невозможно открыть или корректно закрыть.

Snapshot применяется ко всем синхронным /api/v1/*-роутам через SnapshotAPIRoute. Auth/validation выполняются раньше и не открывают БД. Соединение открывается, используется, откатывается и закрывается в одном AnyIO worker thread; async/generator/streaming/background endpoints этим route-class запрещены. Режим остаётся off до отдельного P1B activation gate.

Перед активацией enforce нужен отдельный runtime-attestation: каждая реально читаемая таблица во всех kernel/intake/monitoring и временных mirror-схемах должна быть InnoDB. Этот patch такой live-инвентарь не подменяет и не считает P1B-5 активированным.

Переход product reader на canonical core

DATA_API_PRODUCT_READ_SOURCE управляет только обогащением карточкой товара; исторические факты продолжают использовать свой неизменный kernel.model_id:

  • kernel — дефолт, поведение и форма ответа не меняются;
  • shadow_observe — ответ остаётся kernel, а в лог попадают только агрегатные counts покрытия/расхождения с gwptd_core_compat, без артикулов и названий;
  • core_allowlist — core-view используется только маршрутами, перечисленными в DATA_API_CORE_PRODUCT_ROUTE_ALLOWLIST. Пустой allowlist оставляет все маршруты на kernel.

Первый поддержанный маршрут — reports.onec-stock: переключается только join бренда, тогда как остаток, склад и дата среза остаются в 1С intake. Отсутствие dark-схемы в observe-режиме логируется и не меняет ответ; в allowlist-режиме это честная обязательная ошибка без молчаливого fallback.

Файлы

  • main.py — app, роутеры, error-envelope, health, глобальные хендлеры таксономии.
  • config.py — БД (env V3_MYSQL_*), схемы, лимиты, токены/скоупы.
  • db.py — read-only pymysql (DictCursor), в стиле collector/db/connection.py; классифицирует ошибки через errors.py и переиспользует request snapshot.
  • read_coherence.py — synchronous SnapshotAPIRoute, lazy RR snapshot и same-thread cleanup.
  • errors.py — таксономия ошибок источника (см. выше).
  • envelope.py — data/error-обёртки + cursor (base64 JSON).
  • auth.py — Bearer + scope (data:read/status:read/control:read/control:write/run:write).
  • routers/ — status, facts, dims, control, reports, compat.

Прод-заметки

  • БД-юзер для data/status-линий должен быть read-only grant (отдельно от collector-writer). На S3 Next это gwptd_next_reader; phase 80 добавляет ему один точный SELECT для dark core compatibility seam и fail-closed проверяет, что у него нет других прямых raw/stage/core/core-compat прав.
  • Кэш/ETag/kernel_snapshot_at для report-эндпоинтов — см. DATA-API-REPORTS.md (Phase 3 продолжение).
  • Auth обязателен в проде: задать DATA_API_TOKENS.