From 38d948f07857aba7e96d0f004bb00b7ac2159287 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Sat, 6 Jun 2026 17:51:14 +0000 Subject: [PATCH] fix(nspd): list_layers parse flat layers[] response, not children-tree (#1077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-committed-by: lekss361 --- backend/app/services/scrapers/nspd_client.py | 29 ++++++++++--- backend/tests/test_nspd_client.py | 43 ++++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/backend/app/services/scrapers/nspd_client.py b/backend/app/services/scrapers/nspd_client.py index 670b66e2..0475ce6e 100644 --- a/backend/app/services/scrapers/nspd_client.py +++ b/backend/app/services/scrapers/nspd_client.py @@ -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, diff --git a/backend/tests/test_nspd_client.py b/backend/tests/test_nspd_client.py index efc9f796..d3606aab 100644 --- a/backend/tests/test_nspd_client.py +++ b/backend/tests/test_nspd_client.py @@ -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 ──────────────────────────────────────────────────