gendesign/backend/app/services/site_finder/ird_analyze.py
lekss361 7b40e7e480
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m33s
Deploy / build-worker (push) Successful in 2m49s
Deploy / deploy (push) Successful in 1m11s
feat(sf): wire ИРД-слой в analyze за флагом enable_ird_analyze (#1101)
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>
2026-06-06 19:50:57 +00:00

114 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Сборка ИРД-блока для 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"]