gendesign/backend/app/services/site_finder/riasurt_lookup.py
bot-backend 01360e9c3d
Some checks failed
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
fix(site_finder): SAVEPOINT-isolate gate-lookup db.execute swallows (#2464 Wave 2) (#2468)
2026-07-08 06:46:03 +00:00

155 lines
6.2 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.

"""РИАСУРТ Свердл gate-lookup для analyze_parcel (#108, multi-city scaling).
Читает ``riasurt_sverdl`` (наполняется harvest-task riasurt_sverdl_harvest.py) и отдаёт зоны
РИАСУРТ Свердл, пересекающие участок: тер.зона / функц.зона / красные линии / СЗЗ / ЗСО /
затопление / КРТ. Только для участков агломерации ЕКБ (ОКРАИНЫ — Берёзовский, В.Пышма,
Среднеуральск, Арамиль, Сысерть), НЕ для ЕКБ-сити (тот не покрыт РИАСУРТ Свердл).
Gate-форма (``response["gate"]["riasurt"]``) собирается из topic-бакетов:
tier / func_zone / red_lines / szz / szo / flood_zone / krt + raw-список overlaps.
Graceful: нет таблицы (pre-migration) / пустой WKT / БД-ошибка → пустой gate (analyze не падает).
Зеркалит стиль ``ird_overlay_lookup.py``.
"""
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__)
# Кадастровый район ЕКБ-сити — 66:41. Агломерация (окраины) — прочие районы 66:xx
# (Берёзовский 66:35, В.Пышма/Среднеуральск 66:62, Арамиль/Сысерть 66:25, ...).
_EKB_CITY_PREFIX = "66:41"
# Пустой gate-результат — sentinel (НЕ мутировать).
RIASURT_EMPTY_GATE: dict[str, Any] = {
"applicable": False,
"tier": None,
"func_zone": None,
"red_lines": False,
"szz": False,
"szo": False,
"flood_zone": False,
"krt": False,
"overlaps": [],
}
# Пересечения участка с зонами РИАСУРТ. geom в БД — EPSG:3857; участок передаём WKT в 4326 и
# трансформируем в 3857 для ST_Intersects по GIST idx_riasurt_sverdl_geom. CAST psycopg v3.
_RIASURT_OVERLAP_SQL = text(
"""
SELECT source_layer_id, layer_topic, mo_name, obshnz, description
FROM riasurt_sverdl
WHERE ST_Intersects(
geom,
ST_Transform(ST_GeomFromText(CAST(:parcel_wkt AS text), 4326), 3857)
)
ORDER BY layer_topic, source_layer_id
"""
)
def is_in_aglomeration_but_not_ekb(cad: str | None) -> bool:
"""True если участок в агломерации ЕКБ, но НЕ в самом ЕКБ-сити (66:41).
РИАСУРТ Свердл покрывает только окраины агломерации (Берёзовский, В.Пышма,
Среднеуральск, Арамиль, Сысерть) — кадастровые районы 66:xx, кроме 66:41.
ЕКБ-сити (66:41) и участки вне Свердл (не 66:*) → False.
Args:
cad: кадастровый номер участка ('66:35:...'). None/пусто → False.
Returns:
bool.
"""
if not cad:
return False
cad = cad.strip()
if not cad.startswith("66:"):
return False
return not cad.startswith(_EKB_CITY_PREFIX + ":") and cad != _EKB_CITY_PREFIX
def parcel_riasurt_gate(
db: Session,
parcel_wkt: str | None,
cad: str | None,
) -> dict[str, Any]:
"""Gate-блок РИАСУРТ Свердл для участка агломерации ЕКБ.
Если участок не в агломерации (или в ЕКБ-сити) → RIASURT_EMPTY_GATE (applicable=False).
Иначе ST_Intersects по riasurt_sverdl и сборка topic-бакетов.
Args:
db: сессия.
parcel_wkt: геометрия участка WKT (EPSG:4326). None → пустой gate.
cad: кадастровый номер (для is_in_aglomeration_but_not_ekb).
Returns:
dict с полями tier / func_zone / red_lines / szz / szo / flood_zone / krt + overlaps.
Пусто при отсутствии участка / таблицы / пересечений / вне агломерации.
"""
if not parcel_wkt or not is_in_aglomeration_but_not_ekb(cad):
return dict(RIASURT_EMPTY_GATE)
try:
with db.begin_nested():
rows = db.execute(_RIASURT_OVERLAP_SQL, {"parcel_wkt": parcel_wkt}).mappings().all()
except (OperationalError, ProgrammingError, DataError) as exc:
logger.warning("parcel_riasurt_gate: таблица/запрос недоступны, skip: %s", exc)
return dict(RIASURT_EMPTY_GATE)
except Exception as exc:
logger.warning("parcel_riasurt_gate: неожиданная ошибка, skip: %s", exc)
return dict(RIASURT_EMPTY_GATE)
gate: dict[str, Any] = {
"applicable": True,
"tier": None, # description зоны topic='territorial_zone'
"func_zone": None, # description зоны topic='functional_zone'
"red_lines": False,
"szz": False,
"szo": False,
"flood_zone": False,
"krt": False,
"overlaps": [],
}
for r in rows:
topic = r["layer_topic"]
gate["overlaps"].append(
{
"source_layer_id": r["source_layer_id"],
"layer_topic": topic,
"mo_name": r["mo_name"],
"obshnz": r["obshnz"],
"description": r["description"],
}
)
if topic == "territorial_zone" and gate["tier"] is None:
gate["tier"] = r["description"] or r["obshnz"]
elif topic == "functional_zone" and gate["func_zone"] is None:
gate["func_zone"] = r["description"] or r["obshnz"]
elif topic == "red_lines":
gate["red_lines"] = True
elif topic == "szz":
gate["szz"] = True
elif topic == "szo":
gate["szo"] = True
elif topic == "flood_zone":
gate["flood_zone"] = True
elif topic == "krt":
gate["krt"] = True
return gate
__all__ = [
"RIASURT_EMPTY_GATE",
"is_in_aglomeration_but_not_ekb",
"parcel_riasurt_gate",
]