D9b capstone — поле `ird` в ответе analyze за флагом enable_ird_analyze (default OFF): parcel_ird_overlaps (м.132, incl opportunity) + функц.зона/КРТ (геопортал WFS) + ПЗЗ-регламент (C8b cache-first). Логика в self-contained ird_analyze.py; parcels.py +13 строк flag-gated, defense-in-depth ×3 graceful. Schema additive (ird Optional). Замыкает ИРД-эпик #1067 (#1078/#1090/#1092/#1099/#1100/#1058/#1060). 4 теста. Refs #1067. Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local>
114 lines
5.7 KiB
Python
114 lines
5.7 KiB
Python
"""Сборка ИРД-блока для 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"]
|