gendesign/backend/app/services/site_finder/ird_overlay_lookup.py
Light1YT 2e428405d2 fix(site-finder): расширить except в parcel_ird_overlaps до DataError (#1095)
parcel_ird_overlaps ловил только (OperationalError, ProgrammingError).
При D9b-wiring в analyze malformed WKT в ST_GeomFromText давал PostGIS
ERROR → SQLAlchemy DataError, который пробивал try/except → analyze
падал вместо graceful-degrade.

- ird_overlay_lookup: + DataError в imports и в except tuple.
- quarter_dump_lookup: симметричное расширение в _get_engineering_*.
- test_ird_overlay_lookup: parametrized test_graceful_when_db_fails
  покрывает 3 класса (Operational/Programming + DataError для WKT).

Closes #1095
2026-06-13 15:02:50 +05:00

90 lines
4.6 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.

"""ИРД-overlay lookup для analyze_parcel (#1067 D9).
Читает ``ird_overlays`` (м.132, наполняется harvest-task #1067 B5) и отдаёт ИРД-ограничения,
пересекающие участок: ЗОУИТ / ПЗЗ-границы / ОКН / ООПТ-леса-вода — с reg_numb_border и (когда
резолвлено, #1067 C8/D9-content) читаемым типом/индексом зоны.
Отличие от ``quarter_dump_lookup`` (nspd_quarter_dumps blob-кеш): тот отдаёт per-quarter дамп
ЗОУИТ/zoning из NSPD QuarterDump; ``ird_overlays`` — выделенный per-feature ИРД-store с
reg_numb_border + content-колонками (для геопортал-резолва ВРИ/типа). Комплементарно: канонический
ИРД-слой «волшебной таблицы».
Graceful: если таблица ``ird_overlays`` ещё не задеплоена (pre-migration) или БД-ошибка — возвращает
пустой результат (analyze не падает). Wiring в analyze — flag-gated (D9b).
"""
from __future__ import annotations
import logging
from typing import Any
from sqlalchemy import text
from sqlalchemy.exc import DataError, OperationalError, ProgrammingError
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
# Пустой результат — sentinel для analyze try/except (НЕ мутировать).
IRD_EMPTY_RESULT: dict[str, Any] = {"ird_overlaps": [], "ird_by_kind": {}}
# ИРД-overlay по участку: ST_Intersects через GIST idx_ird_overlays_geom. geom уже EPSG:4326
# (harvest стор ST_Transform 3857→4326). Участок передаётся как WKT в 4326. CAST psycopg v3.
# content добавлен для геопортальных ЗОУИТ-слоёв (gknspecial_harvest #1063/#1064/#1065/#1066).
_IRD_OVERLAP_SQL = text(
"""
SELECT layer_kind, reg_numb_border, category, subcategory,
zone_index, zone_type_name, content
FROM ird_overlays
WHERE ST_Intersects(geom, ST_GeomFromText(CAST(:parcel_wkt AS text), 4326))
ORDER BY layer_kind, reg_numb_border
"""
)
def parcel_ird_overlaps(db: Session, parcel_wkt: str | None) -> dict[str, Any]:
"""ИРД-ограничения, пересекающие участок.
Args:
db: сессия.
parcel_wkt: геометрия участка в WKT (EPSG:4326). None → пустой результат.
Returns:
{'ird_overlaps': [{layer_kind, reg_numb_border, category, subcategory,
zone_index, zone_type_name, content}], 'ird_by_kind': {layer_kind: count}}.
content — JSONB-dict с ограничениями/реквизитами зоны (геопортальные слои:
gknspecial_harvest #1063/#1064/#1065/#1066); None для НСПД-слоёв до резолва D9.
Пусто при отсутствии участка/таблицы/пересечений (analyze не падает).
"""
if not parcel_wkt:
return {"ird_overlaps": [], "ird_by_kind": {}}
try:
rows = db.execute(_IRD_OVERLAP_SQL, {"parcel_wkt": parcel_wkt}).mappings().all()
except (OperationalError, ProgrammingError, DataError) as exc:
# OperationalError — БД недоступна / connection drop.
# ProgrammingError — ird_overlays ещё не задеплоена (UndefinedTable, pre-migration).
# DataError — malformed WKT в ST_GeomFromText (PostGIS ERROR пробрасывает SQLAlchemy
# как DataError/InternalError). parcel_wkt системно-генерируемый, но при D9b-wiring
# в analyze graceful-degrade — единственная корректная стратегия (analyze не падает).
logger.warning("parcel_ird_overlaps: ird_overlays недоступна, skip: %s", exc)
return {"ird_overlaps": [], "ird_by_kind": {}}
overlaps: list[dict[str, Any]] = []
by_kind: dict[str, int] = {}
for r in rows:
kind = r["layer_kind"]
overlaps.append(
{
"layer_kind": kind,
"reg_numb_border": r["reg_numb_border"],
"category": r["category"],
"subcategory": r["subcategory"],
"zone_index": r["zone_index"],
"zone_type_name": r["zone_type_name"],
"content": r["content"],
}
)
by_kind[kind] = by_kind.get(kind, 0) + 1
return {"ird_overlaps": overlaps, "ird_by_kind": by_kind}
__all__ = ["IRD_EMPTY_RESULT", "parcel_ird_overlaps"]