feat(#263 sub-A): cad_* overlay schemas + cadastre territorial_zones (ПЗЗ) #265

Merged
lekss361 merged 2 commits from feat/263-sub-a-cad-nspd-overlay-schemas-and-pzz into main 2026-05-17 07:50:09 +00:00
3 changed files with 31 additions and 8 deletions
Showing only changes of commit 8e377059a4 - Show all commits

View file

@ -19,6 +19,7 @@ Resumable: phase_state в cadastre_jobs показывает прогресс.
from __future__ import annotations
import hashlib
import json
import logging
from collections.abc import Callable
@ -1210,10 +1211,16 @@ def _save_territorial_zones(db: Session, quarter_cad: str, features: list[dict])
geom = f.get("geometry")
geom_geojson: str | None = json.dumps(geom) if geom else None
# zone_id — NSPD feature id или синтетический fallback
zone_id = str(
props.get("id") or props.get("zone_id") or f.get("id") or f"{quarter_cad}_{inserted}"
)
# zone_id — NSPD feature id или стабильный fallback на основе md5 от properties.
# md5 гарантирует идемпотентность между runs (счётчик inserted сбрасывается).
_raw_id = props.get("id") or props.get("zone_id") or f.get("id")
if _raw_id:
zone_id = str(_raw_id)
else:
_props_hash = hashlib.md5(
json.dumps(props, sort_keys=True).encode("utf-8")
).hexdigest()[:12]
zone_id = f"{quarter_cad}_{_props_hash}"
zone_code = (
props.get("zone_code") or props.get("zone_index") or props.get("reg_numb_border")
)
@ -1221,6 +1228,9 @@ def _save_territorial_zones(db: Session, quarter_cad: str, features: list[dict])
permitted_use = props.get("permitted_use") or props.get("vri")
try:
# begin_nested() требует активной outer-транзакции для SAVEPOINT.
# SQLAlchemy Session (autobegin=True) автоматически начинает tx при первом
# db.execute() в этом loop — outer tx гарантирована.
with db.begin_nested():
db.execute(
text("""
@ -1235,7 +1245,10 @@ def _save_territorial_zones(db: Session, quarter_cad: str, features: list[dict])
CAST(:permitted_use AS text),
CAST(:raw_props AS jsonb),
CASE WHEN CAST(:geom AS text) IS NOT NULL
THEN ST_GeogFromGeoJSON(CAST(:geom AS text))
THEN ST_Transform(
ST_SetSRID(ST_GeomFromGeoJSON(CAST(:geom AS text)), 3857),
4326
)::geography
ELSE NULL
END
)

View file

@ -102,7 +102,7 @@ class TestSaveTerritorialZones:
assert call_kwargs["geom"] is None
def test_feature_without_zone_id_uses_fallback(self) -> None:
"""Feature без id → синтетический fallback zone_id не вызывает краш."""
"""Feature без id → md5-based fallback zone_id (stable между runs)."""
db = _make_db_mock()
features = [
{
@ -116,8 +116,18 @@ class TestSaveTerritorialZones:
assert result == 1
call_kwargs = db.execute.call_args[0][1]
# fallback zone_id содержит quarter_cad
assert "66:41:0204016" in call_kwargs["zone_id"]
zone_id: str = call_kwargs["zone_id"]
# fallback zone_id содержит quarter_cad и стабильный hash (12 hex chars)
assert zone_id.startswith("66:41:0204016_")
suffix = zone_id.split("_", 3)[-1]
assert len(suffix) == 12
assert all(c in "0123456789abcdef" for c in suffix)
# Второй вызов с теми же данными → тот же zone_id (идемпотентность)
db2 = _make_db_mock()
_save_territorial_zones(db2, "66:41:0204016", features)
call_kwargs2 = db2.execute.call_args[0][1]
assert call_kwargs2["zone_id"] == zone_id
def test_zone_id_from_props_id(self) -> None:
"""Если feature.id=None, но props['id'] есть — используется props['id']."""