Periodic harvest готовых НСПД ИРД-слоёв (ЗОУИТ 37577-581, ПЗЗ-границы 875838, ОКН 875840, ООПТ/леса/вода 875845/866/832/835) по кварталам ЕКБ (66:41:* из cad_parcels) → grid-walk → upsert ird_overlays (м.132) по (source_layer_id, geom_data_id). Геометрия 3857→4326. content_* НЕ трогаем (резолвер D9). SAVEPOINT per-feature; сбой квартала/слоя изолирован. Beat: ird-harvest-weekly (Mon 05:00 МСК). Refs #1067 (B5). Тесты: 3 passed.
133 lines
4.6 KiB
Python
133 lines
4.6 KiB
Python
"""Тесты harvest_ird_overlays (#1067 B5) — НСПД ИРД-harvest → ird_overlays.
|
||
|
||
Сеть/БД не дёргаются: мокаем NSPDClient + SessionLocal, проверяем grid-walk loop,
|
||
skip битых фич и upsert-параметры.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from contextlib import contextmanager
|
||
from typing import Any
|
||
|
||
from app.workers.tasks import ird_harvest
|
||
|
||
|
||
class _Feat:
|
||
def __init__(self, geometry: dict[str, Any] | None, properties: dict[str, Any]) -> None:
|
||
self.geometry = geometry
|
||
self.properties = properties
|
||
|
||
|
||
class _SearchRes:
|
||
def __init__(self, feat: _Feat | None) -> None:
|
||
self.first = feat
|
||
|
||
|
||
class _FakeClient:
|
||
"""NSPDClient-двойник: квартал → полигон (3857), grid-walk → 1 фича на слой."""
|
||
|
||
def __init__(self, *a: Any, **kw: Any) -> None:
|
||
self.grid_calls: list[int] = []
|
||
|
||
def search_by_cad(self, cad: str, thematic_id: int = 1) -> _SearchRes:
|
||
ring = [[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]]
|
||
geom = {"type": "Polygon", "coordinates": [ring]}
|
||
return _SearchRes(_Feat(geom, {"cad": cad}))
|
||
|
||
def get_features_in_bbox_grid(self, layer_id: int, bbox: Any, **kw: Any) -> list[_Feat]:
|
||
self.grid_calls.append(layer_id)
|
||
return [
|
||
_Feat(
|
||
{"type": "Polygon", "coordinates": [[[1, 1], [2, 1], [2, 2], [1, 1]]]},
|
||
{
|
||
"geom_data_id": 1000 + layer_id,
|
||
"reg_numb_border": "66:41-6.1",
|
||
"category": 36940,
|
||
"subcategory": 5,
|
||
},
|
||
)
|
||
]
|
||
|
||
|
||
class _FakeDB:
|
||
"""Session-двойник: пишет execute-вызовы, begin_nested как no-op CM."""
|
||
|
||
def __init__(self) -> None:
|
||
self.executed: list[tuple[Any, dict[str, Any] | None]] = []
|
||
self.committed = False
|
||
self.closed = False
|
||
|
||
@contextmanager
|
||
def begin_nested(self) -> Any:
|
||
yield
|
||
|
||
def execute(self, sql: Any, params: dict[str, Any] | None = None) -> Any:
|
||
self.executed.append((sql, params))
|
||
return None
|
||
|
||
def commit(self) -> None:
|
||
self.committed = True
|
||
|
||
def close(self) -> None:
|
||
self.closed = True
|
||
|
||
|
||
def _patch(monkeypatch: Any, db: _FakeDB) -> _FakeClient:
|
||
client = _FakeClient()
|
||
monkeypatch.setattr(ird_harvest, "NSPDClient", lambda *a, **kw: client)
|
||
monkeypatch.setattr(ird_harvest, "SessionLocal", lambda: db)
|
||
return client
|
||
|
||
|
||
def test_harvest_explicit_quarters_upserts_all_layers(monkeypatch: Any) -> None:
|
||
"""1 квартал × N ИРД-слоёв → upsert по фиче на слой; commit + close."""
|
||
db = _FakeDB()
|
||
client = _patch(monkeypatch, db)
|
||
n_layers = len(ird_harvest.IRD_HARVEST_LAYER_KEYS)
|
||
|
||
result = ird_harvest.harvest_ird_overlays(quarters=["66:41:0204016"])
|
||
|
||
assert result == {"quarters": 1, "features": n_layers}
|
||
assert len(client.grid_calls) == n_layers # grid-walk по каждому слою
|
||
assert len(db.executed) == n_layers # upsert по фиче на слой
|
||
assert db.committed is True
|
||
assert db.closed is True
|
||
# параметры upsert: geom как JSON-строка, layer_kind из ключей
|
||
_, params = db.executed[0]
|
||
assert params is not None
|
||
assert params["layer_kind"] in ird_harvest.IRD_HARVEST_LAYER_KEYS
|
||
assert isinstance(params["geojson"], str)
|
||
assert params["reg_numb_border"] == "66:41-6.1"
|
||
|
||
|
||
def test_harvest_skips_empty_quarter(monkeypatch: Any) -> None:
|
||
"""Квартал без геометрии в НСПД → 0 кварталов/фич, без падения."""
|
||
db = _FakeDB()
|
||
client = _patch(monkeypatch, db)
|
||
monkeypatch.setattr(client, "search_by_cad", lambda cad, thematic_id=1: _SearchRes(None))
|
||
|
||
result = ird_harvest.harvest_ird_overlays(quarters=["66:41:9999999"])
|
||
|
||
assert result == {"quarters": 0, "features": 0}
|
||
assert db.executed == []
|
||
assert db.committed is True
|
||
|
||
|
||
def test_upsert_feature_skips_missing_geom_data_id(monkeypatch: Any) -> None:
|
||
"""Фича без geom_data_id / geometry → не пишется (defensive)."""
|
||
db = _FakeDB()
|
||
no_id = _Feat({"type": "Polygon", "coordinates": []}, {"reg_numb_border": "x"})
|
||
no_geom = _Feat(None, {"geom_data_id": 5})
|
||
assert (
|
||
ird_harvest._upsert_feature(
|
||
db, layer_id=37577, layer_kind="zouit_okn", feature=no_id, fetched_at="t"
|
||
)
|
||
is False
|
||
)
|
||
assert (
|
||
ird_harvest._upsert_feature(
|
||
db, layer_id=37577, layer_kind="zouit_okn", feature=no_geom, fetched_at="t"
|
||
)
|
||
is False
|
||
)
|
||
assert db.executed == []
|