fix(nspd): list_layers parse flat layers[] response, not children-tree
All checks were successful
CI / changes (push) Successful in 7s
CI / changes (pull_request) Successful in 5s
CI / backend-tests (push) Successful in 5m46s
CI / backend-tests (pull_request) Successful in 5m45s

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

Refs #1067 (ИРД — discovery слоёв НСПД).
This commit is contained in:
bot-backend 2026-06-06 20:44:50 +03:00
parent d9c2157d02
commit a609bcf741
2 changed files with 66 additions and 6 deletions

View file

@ -613,9 +613,24 @@ class NSPDClient:
# Shape defense: NSPD иногда оборачивает в {"data": [...]} — паттерн уже
# ловили в Bug_Nspd_Geo_Str_Object_No_Get_Fixed. Без проверки walker
# молча вернул бы [], а 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 []
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(
"list_layers: unexpected NSPD response type %s for theme_id=%s",
type(data).__name__,
@ -623,12 +638,14 @@ class NSPDClient:
)
return []
layers: list[NSPDLayer] = []
# Tree response: nested {"children": [...], "id": N, "title": "..."}
# Используем walker по subtree чтобы вытащить все leaf layers
for entry in _walk_layer_tree(data):
for entry in raw_entries:
# актуальная форма использует layerId; старая nested-tree — id
raw_id = entry.get("layerId", entry.get("id"))
if raw_id is None:
continue
layers.append(
NSPDLayer(
layer_id=int(entry["id"]),
layer_id=int(raw_id),
title=str(entry.get("title", "")),
layer_type=entry.get("layerType") or entry.get("layer_type"),
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) == []
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 ──────────────────────────────────────────────────