Some checks failed
CI / changes (push) Successful in 7s
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (push) Successful in 1m45s
CI / openapi-codegen-check (pull_request) Successful in 1m43s
CI / backend-tests (push) Failing after 8m36s
CI / backend-tests (pull_request) Failing after 8m36s
Имплементация фиксов 2-го аудита backend/app/** (после merge #1543). Воркер на файл, точечные правки. Верификация: py_compile 58/58 .py. Полностью исправлено (82). Оставлены открытыми (13): partial/needs-cross-file/needs-leha — #1569, #1590, #1593, #1606, #1609, #1617, #1633, #1635, #1637, #1638, #1640, #1642, #1650. Closes #1560 Closes #1561 Closes #1562 Closes #1563 Closes #1564 Closes #1565 Closes #1566 Closes #1567 Closes #1570 Closes #1571 Closes #1572 Closes #1573 Closes #1574 Closes #1576 Closes #1577 Closes #1578 Closes #1579 Closes #1580 Closes #1581 Closes #1582 Closes #1583 Closes #1584 Closes #1585 Closes #1586 Closes #1587 Closes #1588 Closes #1589 Closes #1591 Closes #1592 Closes #1594 Closes #1595 Closes #1596 Closes #1597 Closes #1598 Closes #1599 Closes #1600 Closes #1601 Closes #1602 Closes #1603 Closes #1604 Closes #1605 Closes #1607 Closes #1608 Closes #1610 Closes #1611 Closes #1612 Closes #1613 Closes #1614 Closes #1615 Closes #1616 Closes #1618 Closes #1619 Closes #1620 Closes #1621 Closes #1622 Closes #1623 Closes #1624 Closes #1625 Closes #1626 Closes #1627 Closes #1628 Closes #1629 Closes #1630 Closes #1631 Closes #1632 Closes #1634 Closes #1636 Closes #1639 Closes #1641 Closes #1643 Closes #1644 Closes #1645 Closes #1646 Closes #1647 Closes #1648 Closes #1649 Closes #1651 Closes #1652 Closes #1653 Closes #1654 Closes #1655 Closes #1656
123 lines
5.8 KiB
Python
123 lines
5.8 KiB
Python
"""ТЭП ППТ/ПМТ-lookup для analyze_parcel (#1136 follow-up #1133, GG-форсайт #1067).
|
||
|
||
Читает ``ekb_ppt_tep`` (м.138, наполняется ``ekb_ppt_tep_sync`` Celery-воркером #1085) и
|
||
отдаёт ТЭП-контент пояснительных записок (zone_balance / tep / phasing) для проектов
|
||
планировки/межевания, пересекающих участок.
|
||
|
||
Связка ТЭП ↔ геометрия (best-effort, как описано в м.138):
|
||
``ekb_ppt_tep.doc_ref`` ↔ ``planning_projects.source_key`` (текстовое равенство)
|
||
ИЛИ ``planning_projects.doc_full_name`` (LIKE).
|
||
|
||
Прямой FK не ставим: doc_ref пишется парсером по PDF-источнику (slug/seed), а в WFS
|
||
source_key — целое и не совпадает форматом. Сопоставление — две дешёвые ветки:
|
||
1) ``doc_ref = CAST(source_key AS text)`` (прямое равенство);
|
||
2) ``doc_full_name ILIKE '%' || doc_ref || '%'`` (fallback для slug-ов вида
|
||
ppt2018_22823: длинное WFS-описание содержит короткий slug парсера; LIKE-метасимволы
|
||
в doc_ref экранируются ESCAPE '\').
|
||
|
||
Дедуп: best-effort OR-JOIN может зацепить несколько строк ``ekb_ppt_tep`` на один
|
||
``planning_projects`` (точная ветка + ILIKE-fallback). ``DISTINCT ON (source_key)``
|
||
держит контракт «одна строка = один ППТ/ПМТ-документ», отдавая приоритет точной ветке.
|
||
|
||
Граф вызова: build_ird_analyze_block → parcel_ppt_tep → JOIN planning_projects ∩ ekb_ppt_tep.
|
||
Graceful: нет таблицы / пустой WKT / БД-ошибка → []. Зеркалит стиль ``planning_lookup.py``.
|
||
"""
|
||
|
||
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__)
|
||
|
||
# ТЭП-контент для проектов планировки, пересекающих участок.
|
||
# Join: planning_projects (геометрия) ⋈ ekb_ppt_tep (контент) по doc_ref↔source_key/doc_full_name.
|
||
# ST_Intersects через GIST idx_planning_projects_geom (4326 ↔ WKT 4326).
|
||
# CAST psycopg v3 — никогда :param::type (vault Pattern_CAST_AS_Type).
|
||
_PPT_TEP_OVERLAP_SQL = text(
|
||
r"""
|
||
SELECT
|
||
project_type,
|
||
doc_status_name,
|
||
full_name,
|
||
doc_full_name,
|
||
source_key,
|
||
doc_ref,
|
||
zone_balance,
|
||
tep,
|
||
phasing,
|
||
source_pdf,
|
||
fetched_at
|
||
FROM (
|
||
-- DISTINCT ON (source_key): один ППТ/ПМТ-документ = одна строка (контракт docstring).
|
||
-- Точная ветка (doc_ref = source_key) приоритетна над slug-fallback (ILIKE).
|
||
SELECT DISTINCT ON (pp.source_key)
|
||
pp.project_type,
|
||
pp.doc_status_name,
|
||
pp.full_name,
|
||
pp.doc_full_name,
|
||
pp.source_key,
|
||
pp.dmd_actual_year,
|
||
t.doc_ref,
|
||
t.zone_balance,
|
||
t.tep,
|
||
t.phasing,
|
||
t.source_pdf,
|
||
t.fetched_at
|
||
FROM planning_projects pp
|
||
JOIN ekb_ppt_tep t ON (
|
||
t.doc_ref = CAST(pp.source_key AS text)
|
||
OR (
|
||
pp.doc_full_name IS NOT NULL
|
||
AND pp.doc_full_name ILIKE
|
||
'%' || replace(replace(replace(t.doc_ref, '\', '\\'), '%', '\%'), '_', '\_')
|
||
|| '%' ESCAPE '\'
|
||
)
|
||
)
|
||
WHERE ST_Intersects(pp.geom, ST_GeomFromText(CAST(:parcel_wkt AS text), 4326))
|
||
ORDER BY pp.source_key, (t.doc_ref = CAST(pp.source_key AS text)) DESC
|
||
) deduped
|
||
ORDER BY dmd_actual_year DESC NULLS LAST, project_type
|
||
"""
|
||
)
|
||
|
||
|
||
def parcel_ppt_tep(db: Session, parcel_wkt: str | None) -> list[dict[str, Any]]:
|
||
"""ТЭП-контент ППТ/ПМТ для участка (Табл.11/12/13 пояснительных записок).
|
||
|
||
Args:
|
||
db: SQLAlchemy-сессия.
|
||
parcel_wkt: геометрия участка в WKT (EPSG:4326). None / пусто → [].
|
||
|
||
Returns:
|
||
list[{project_type, doc_status_name, full_name, doc_full_name, source_key,
|
||
doc_ref, zone_balance, tep, phasing, source_pdf, fetched_at}].
|
||
Каждая запись — один ППТ/ПМТ-документ, чья геометрия пересекает участок И
|
||
для которого есть ТЭП-контент в ``ekb_ppt_tep``.
|
||
|
||
Пустой список при:
|
||
- parcel_wkt пуст / None;
|
||
- таблица ``ekb_ppt_tep`` или ``planning_projects`` ещё не задеплоена;
|
||
- нет пересечений по геометрии;
|
||
- нет матча doc_ref ↔ source_key/doc_full_name (best-effort, graceful);
|
||
- БД-ошибка (analyze не падает).
|
||
"""
|
||
if not parcel_wkt:
|
||
return []
|
||
try:
|
||
rows = db.execute(_PPT_TEP_OVERLAP_SQL, {"parcel_wkt": parcel_wkt}).mappings().all()
|
||
except (OperationalError, ProgrammingError) as exc:
|
||
# Таблица ещё не задеплоена / БД-ошибка — graceful degrade.
|
||
logger.warning("parcel_ppt_tep: ekb_ppt_tep / planning_projects недоступна, skip: %s", exc)
|
||
return []
|
||
except Exception as exc:
|
||
logger.warning("parcel_ppt_tep: неожиданная ошибка, skip: %s", exc)
|
||
return []
|
||
return [dict(r) for r in rows]
|
||
|
||
|
||
__all__ = ["parcel_ppt_tep"]
|