"""Read-only достаёт персистентный §22-форсайт участка для чата (#957, Step 1). Тонкая обёртка над `latest_run_for(..., schema_version="1.0")` — ровно тот же seam, что читает `GET /{cad_num}/forecast` (parcels.py): последний ран ИМЕННО §22-схемы ('1.0', НЕ analyze-1.0 inline-dict). `run.result` — это `SiteFinderReport.as_dict()` (стабильный 8-секционный контракт), `run.id` — run_id для grounding. Q2-РЕШЕНИЕ = READ-ONLY: рана нет → возвращаем (None, None). Эндпоинт по этому отдаёт report_status='pending' + детерминированный RU-ответ «отчёт ещё не готов, запустите анализ участка». НИЧЕГО не считаем, НЕ enqueue'им, НЕ пере-вычисляем (чат не триггерит форсайт — это делает POST /analyze). Сбой БД пробрасываем НЕ из этого модуля — гасит/логирует его вызывающий эндпоинт (как get_parcel_forecast → pending). """ from __future__ import annotations import logging from typing import Any from sqlalchemy.orm import Session from app.services.analysis_runs.repository import latest_run_for logger = logging.getLogger(__name__) # Версия схемы §22-форсайта (зеркало _FORECAST_SCHEMA_VERSION в parcels.py). Читаем # именно её, а не analyze-1.0: view вернул бы §22-ран поверх analyze, но фильтр по # схеме явный и устойчивый. _FORECAST_SCHEMA_VERSION = "1.0" def get_report_for_chat( db: Session, cad_num: str, ) -> tuple[dict[str, Any] | None, int | None]: """Достать последний §22-отчёт участка как (report_dict, run_id) или (None, None). READ-ONLY (Q2): рана нужной схемы нет → (None, None), БЕЗ enqueue/пере-расчёта. `run.result` — `SiteFinderReport.as_dict()` (8 секций). Если result почему-то не dict (мусор/частичная запись) — логируем и отдаём (None, None) как «нет отчёта», чтобы чат не упал (graceful). """ run = latest_run_for(db, cad_num, schema_version=_FORECAST_SCHEMA_VERSION) if run is None: return None, None result = run.result if not isinstance(result, dict): # Ран есть, но result не похож на отчёт (старая/битая запись) — не валим чат, # трактуем как «отчёта ещё нет» (pending). Логируем для диагностики. logger.warning( "chat: run %s for cad=%s has non-dict result (%s) — treating as pending", run.id, cad_num, type(result).__name__, ) return None, None return result, run.id