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
90 lines
4.6 KiB
Python
90 lines
4.6 KiB
Python
"""ИРД-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"]
|