"""Сборка ИРД-блока для analyze_parcel (#1067 D9b — analyze-wiring). Единая точка, собирающая «слой ИРД/future-supply» участка для ответа analyze (GG-форсайт). Объединяет ТРИ источника, каждый — graceful (сбой одного не валит блок и тем более analyze): 1. ``parcel_ird_overlaps`` (DB, м.132 ird_overlays) — ограничения/обременения + opportunity-ЗУ, пересекающие участок (ЗОУИТ / ПЗЗ-границы / ОКН / ООПТ + opportunity_* из #1086). Дёшево (GIST ST_Intersects), всегда безопасно. 2. ЕКБ-геопортал WFS по centroid (network, опц.): функц.зона генплана (#1058) + КРТ (#1060). 3. ПЗЗ-регламент зоны (#1067 C8b): zone_regulation_cache по индексу зоны (cache-first через ``get_or_fetch_zone_regulation`` — WFS index дёшев, urbanCard только на cache-miss). Вызывается ТОЛЬКО за feature-flag ``settings.enable_ird_analyze`` (default off): источники 2-3 — live-зависимость от внешнего геопортала (reverse-eng, без SLA) в hot-пути analyze. Флаг off → parcels.py даже не зовёт этот модуль. Сетевой timeout укорочен (geoportal_timeout) чтобы не держать sync-handler. Логика вынесена из parcels.py (HOT-файл) сюда — в parcels.py остаётся 1 import + gated-блок. """ from __future__ import annotations import logging from typing import Any from sqlalchemy.orm import Session from app.services.scrapers.ekb_geoportal_client import EKBGeoportalClient from app.services.site_finder.ird_overlay_lookup import parcel_ird_overlaps from app.services.site_finder.zone_regulation import get_or_fetch_zone_regulation logger = logging.getLogger(__name__) # Короткий timeout для geoportal-вызовов в hot-пути analyze (default клиента — 20s, слишком долго). _GEOPORTAL_TIMEOUT_S = 8 def _functional_zone_at( client: EKBGeoportalClient, lon: float, lat: float ) -> dict[str, Any] | None: """Функц.зона генплана (#1058) в точке — properties первой WFS-фичи; None при сбое/пусто.""" try: feats = client.features_at_point("functional_zone", lon, lat) except Exception as exc: # внешний геопортал, graceful logger.warning("ird_analyze: functional_zone WFS failed: %s", exc) return None return feats[0].properties if feats else None def _krt_at(client: EKBGeoportalClient, lon: float, lat: float) -> list[dict[str, Any]]: """КРТ-объекты (#1060) в точке — properties всех WFS-фич. [] при отсутствии/сбое.""" try: feats = client.features_at_point("krt", lon, lat) except Exception as exc: # внешний геопортал, graceful logger.warning("ird_analyze: krt WFS failed: %s", exc) return [] return [f.properties for f in feats] def _zone_regulation_at( db: Session, client: EKBGeoportalClient, lon: float, lat: float ) -> dict[str, Any] | None: """ПЗЗ-регламент зоны (#1067 C8b) в точке — cache-first. None при отсутствии/сбое.""" try: return get_or_fetch_zone_regulation(db, lon, lat, client=client) except Exception as exc: # внешний геопортал/БД, graceful logger.warning("ird_analyze: zone_regulation failed: %s", exc) return None def build_ird_analyze_block( db: Session, geom_wkt: str | None, lon: float | None, lat: float | None, *, client: EKBGeoportalClient | None = None, ) -> dict[str, Any]: """Собрать ИРД-блок участка для analyze (#1067 D9b). Args: db: сессия. geom_wkt: геометрия участка WKT (EPSG:4326) для DB-overlay ``ird_overlays``. lon/lat: centroid участка (EPSG:4326) для geoportal-резолва функц.зоны/КРТ/регламента. client: переиспользуемый EKBGeoportalClient (для тестов/одного TLS-пула). По умолчанию создаётся с укороченным timeout. Returns: dict: ``ird_overlaps``/``ird_by_kind`` (DB-overlay, incl opportunity_*), ``opportunity_overlaps`` (подмножество layer_kind='opportunity_*'), ``functional_zone`` (генплан, dict|None), ``krt`` (list), ``zone_regulation`` (dict|None). Любой источник деградирует независимо — блок всегда well-formed. """ overlay = parcel_ird_overlaps(db, geom_wkt) overlaps: list[dict[str, Any]] = overlay.get("ird_overlaps", []) opportunity = [o for o in overlaps if str(o.get("layer_kind", "")).startswith("opportunity_")] block: dict[str, Any] = { "ird_overlaps": overlaps, "ird_by_kind": overlay.get("ird_by_kind", {}), "opportunity_overlaps": opportunity, "functional_zone": None, "krt": [], "zone_regulation": None, } # Geoportal-источники — только при валидном centroid (network, опц.). if lon is not None and lat is not None: client = client or EKBGeoportalClient(timeout=_GEOPORTAL_TIMEOUT_S) block["functional_zone"] = _functional_zone_at(client, lon, lat) block["krt"] = _krt_at(client, lon, lat) block["zone_regulation"] = _zone_regulation_at(db, client, lon, lat) return block __all__ = ["build_ird_analyze_block"]