gendesign/backend/app/services/site_finder/okn_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

103 lines
4.1 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.

"""ОКН-lookup для analyze_parcel (#1066, #1159).
Читает ``okn_objects`` (м.141) и отдаёт объекты культурного наследия
в заданном радиусе от участка — [{source_id, category, distance_m, name,
address}].
ST_DWithin по geography (метрическое расстояние в метрах) через GIST-индекс
idx_okn_objects_geom. Участок передаётся как WKT в EPSG:4326.
``name``/``address`` извлекаются из ``raw_props`` (обогащаются воркером через
detail-карточку ЕГРКН). Если detail недоступен — возвращается None для этих
полей, остальные поля сохраняются. Без detail-фикса (#1159 acceptance 2)
analyze-таблица показывала «ОКН рядом» без указания КАКОГО — критично для ИРД.
Graceful: если таблица ``okn_objects`` ещё не задеплоена (pre-migration)
или БД-ошибка — возвращает пустой список (analyze не падает).
"""
from __future__ import annotations
import logging
from typing import Any
from sqlalchemy import text
from sqlalchemy.exc import OperationalError, ProgrammingError
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
# Пространственный запрос: ST_DWithin по geography даёт расстояние в метрах.
# CAST psycopg v3 (никогда :param::type — vault Pattern_CAST_AS_Type).
# raw_props->>'name'/'address' — JSON-string accessor (NULL если ключа нет
# или raw_props=NULL). Безопасно для строк без detail-обогащения.
_OKN_DWITHIN_SQL = text("""
SELECT
source_id,
category,
ROUND(
ST_Distance(
geom::geography,
ST_GeomFromText(CAST(:parcel_wkt AS text), 4326)::geography
)
)::integer AS distance_m,
raw_props->>'name' AS name,
raw_props->>'address' AS address
FROM okn_objects
WHERE ST_DWithin(
geom::geography,
ST_GeomFromText(CAST(:parcel_wkt AS text), 4326)::geography,
CAST(:radius_m AS double precision)
)
ORDER BY distance_m ASC
""")
def parcel_okn_objects(
db: Session,
parcel_wkt: str | None,
radius_m: float = 100.0,
) -> list[dict[str, Any]]:
"""ОКН-объекты в радиусе radius_m от участка.
Args:
db: SQLAlchemy-сессия.
parcel_wkt: геометрия участка в WKT (EPSG:4326). None → пустой список.
radius_m: радиус поиска в метрах (по умолчанию 100 м).
Returns:
Список dict [{source_id, category, distance_m, name, address}],
отсортированный по расстоянию ASC. ``name``/``address`` могут быть
None если detail-карточка не догружена. Пустой список при
отсутствии участка / таблицы / объектов в радиусе (analyze не падает).
"""
if not parcel_wkt:
return []
try:
with db.begin_nested():
rows = db.execute(
_OKN_DWITHIN_SQL,
{"parcel_wkt": parcel_wkt, "radius_m": radius_m},
).fetchall()
except (OperationalError, ProgrammingError) as exc:
logger.warning("parcel_okn_objects: okn_objects недоступна, skip: %s", exc)
return []
except Exception as exc:
logger.warning("parcel_okn_objects: ошибка запроса okn_objects: %s", exc)
return []
result: list[dict[str, Any]] = []
for row in rows:
result.append(
{
"source_id": row[0],
"category": row[1],
"distance_m": row[2],
"name": row[3],
"address": row[4],
}
)
return result
__all__ = ["parcel_okn_objects"]