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 ──────────────────────────────────────────────────