fix(nspd): list_layers parse flat layers[] response, not children-tree (#1077)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-frontend (push) Has been skipped
Deploy / build-backend (push) Successful in 1m26s
Deploy / deploy (push) Successful in 1m9s
Deploy / build-worker (push) Successful in 2m27s

NSPDClient.list_layers() парсил _walk_layer_tree по id/children, но НСПД layers-theme-tree отдаёт слои в плоском layers[] с ключом layerId → list_layers() молча возвращал [] для любой темы. Fix: сначала плоский data["layers"], иначе fallback на _walk_layer_tree; defensive None-guard на missing id. +тест на flat-форму, backward-compat сохранён.

Refs #1067.
Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
This commit is contained in:
lekss361 2026-06-06 17:51:14 +00:00 committed by bot-reviewer
parent d9c2157d02
commit 38d948f078
2 changed files with 66 additions and 6 deletions

View file

@ -613,9 +613,24 @@ class NSPDClient:
# Shape defense: NSPD иногда оборачивает в {"data": [...]} — паттерн уже # Shape defense: NSPD иногда оборачивает в {"data": [...]} — паттерн уже
# ловили в Bug_Nspd_Geo_Str_Object_No_Get_Fixed. Без проверки walker # ловили в Bug_Nspd_Geo_Str_Object_No_Get_Fixed. Без проверки walker
# молча вернул бы [], а caller увидел бы пустой catalog. # молча вернул бы [], а caller увидел бы пустой catalog.
if isinstance(data, dict) and "data" in data and not data.get("children"): if (
isinstance(data, dict)
and "data" in data
and not data.get("children")
and "layers" not in data
):
data = data.get("data") or [] data = data.get("data") or []
if not isinstance(data, list | dict): # Реальная форма layers-theme-tree: {"layers": [...flat...], "tree": ...},
# где каждая запись = {"layerId": N, "title", "layerName", "layerType"}.
# Раньше walker искал id/children и для этой (актуальной) формы молча
# возвращал [] — слои не в children-дереве, а в плоском layers[]
# (см. research/NSPD_IRD_Layers_Automation_Recon_Jun06). Сначала пробуем
# плоский layers[], иначе fallback на nested-tree walker (id/children).
if isinstance(data, dict) and isinstance(data.get("layers"), list):
raw_entries: list[dict[str, Any]] = data["layers"]
elif isinstance(data, list | dict):
raw_entries = _walk_layer_tree(data)
else:
logger.warning( logger.warning(
"list_layers: unexpected NSPD response type %s for theme_id=%s", "list_layers: unexpected NSPD response type %s for theme_id=%s",
type(data).__name__, type(data).__name__,
@ -623,12 +638,14 @@ class NSPDClient:
) )
return [] return []
layers: list[NSPDLayer] = [] layers: list[NSPDLayer] = []
# Tree response: nested {"children": [...], "id": N, "title": "..."} for entry in raw_entries:
# Используем walker по subtree чтобы вытащить все leaf layers # актуальная форма использует layerId; старая nested-tree — id
for entry in _walk_layer_tree(data): raw_id = entry.get("layerId", entry.get("id"))
if raw_id is None:
continue
layers.append( layers.append(
NSPDLayer( NSPDLayer(
layer_id=int(entry["id"]), layer_id=int(raw_id),
title=str(entry.get("title", "")), title=str(entry.get("title", "")),
layer_type=entry.get("layerType") or entry.get("layer_type"), layer_type=entry.get("layerType") or entry.get("layer_type"),
metadata=entry, metadata=entry,

View file

@ -323,6 +323,49 @@ def test_list_layers_handles_garbage_response(monkeypatch: pytest.MonkeyPatch) -
assert NSPDClient().list_layers(theme_id=1) == [] assert NSPDClient().list_layers(theme_id=1) == []
def test_list_layers_parses_flat_layers_response(monkeypatch: pytest.MonkeyPatch) -> None:
"""Актуальная форма NSPD: {"layers": [...flat...], "tree": ...} с ключом layerId.
Регрессия на баг: walker искал id/children, а реальный ответ держит слои в
плоском layers[] (layerId/title/layerName/layerType) list_layers молча
возвращал [] для любой темы (см.
research/NSPD_IRD_Layers_Automation_Recon_Jun06).
"""
fake_resp = {
"layers": [
{
"layerId": 36945,
"title": "Кадастровые округа",
"layerName": "ЕГРН. Округа",
"layerType": "wms",
},
{
"layerId": 875838,
"title": "Территориальные зоны",
"layerName": "ПЗЗ",
"layerType": "wms",
},
{
"layerId": 37577,
"title": "ЗОУИТ ОКН",
"layerName": "ЗОУИТ ОКН",
"layerType": "wms",
},
],
"tree": {"categories": []},
}
monkeypatch.setattr(
"app.services.scrapers.nspd_client._http_get_json",
lambda url, **kw: fake_resp,
)
layers = NSPDClient().list_layers(theme_id=1)
assert sorted(layer.layer_id for layer in layers) == [36945, 37577, 875838]
tz = next(layer for layer in layers if layer.layer_id == 875838)
assert tz.title == "Территориальные зоны"
assert tz.layer_type == "wms"
assert tz.metadata["layerName"] == "ПЗЗ"
# ── _geojson_bbox_3857 tests ────────────────────────────────────────────────── # ── _geojson_bbox_3857 tests ──────────────────────────────────────────────────