feat(scrapers): NSPD client foundation (#94) — search + WMS + layers #98
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#98
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/nspd-client-foundation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Foundation для G1 #28, G2 #29, G3 #30, E1 #51, I3 #44 и поддержки on-demand #93.
Полное закрытие #94 разбито на несколько PR (sequential — per memory rule):
cadastre_fetch.py(заменит rosreestr2coord)Что внутри
backend/app/services/scrapers/nspd_client.pysearch_by_cad(cad, thematic_id)/api/geoportal/v2/search/geoportal?query=...get_feature_info(layer_id, lon, lat)/api/aeggis/v4/{layer}/wms?REQUEST=GetFeatureInfoget_features_in_bbox(layer_id, bbox_3857)list_layers(theme_id)/api/geoportal/v1/layers-theme-tree?themeId=Typed responses (frozen dataclasses):
NSPDFeature— GeoJSON Feature с typed feature_id/geometry/propertiesNSPDSearchResult— обёртка над features + raw + helpers (.first, .is_empty)NSPDLayer— layer metadata из layers-theme-treeLAYERS catalog — 32 layer_id с семантическими именами по 6 TIER per #94:
territorial_zones=875838(G1 ПЗЗ),parcels=36048,buildings=36049,engineering_structures=36328,red_lines=879243zouit_okn=37577,zouit_engineering=37578,zouit_natural=37580,zouit_protected=37579,zouit_other=37581Reuse existing infra:
fetch_geoportalиз nspd_lite (WAF-compatible urllib + rate limit)NspdLiteWafError(403/429) → caller делает backoffCoordinate transforms:
lonlat_to_3857()— WGS84 → Web Mercatorbbox_around_point_m()— буфер вокруг точки в метрахTests:
backend/tests/test_nspd_client.py14 unit tests, no network calls:
uv run pytest tests/test_nspd_client.py -v→ 14 passed in 0.08s.Также включено
Micro-fix из PR #95 review (cosmetic, non-blocking): inline comment "до 25s" → "до 15s" чтобы matched
_INLINE_FETCH_WAIT_S(был уже снижен).Acceptance (per #94)
nspd_client.pyс 4 методами + типизацияsearch_by_cad(...)контракт parsed correctly (test)Test plan
ruff check + format— cleanpytest tests/test_nspd_client.py— 14/14 passedWhy foundation-first split
Per sequential PR rule (memory: feedback_sequential_prs.md): один задача → один PR. Полная реализация #94 = client + schema + Celery + admin + интеграция = ~3-4 дня. Делать всё в одном PR — review impossible, conflict-prone. Этот PR = только client (foundation), следующие PR строят на нём.
⚠️ Auto-review: minor (non-blocking)
backend/app/services/scrapers/nspd_client.py::235—def bbox_around_point_m(lon: float, lat: float, buffer_m: int) -> tuple[float, float, float, float]:= 101 символ. Pre-commit ruff завалит commit. Разбить сигнатуру на несколько строк.:201-202—import json, timeвнутри тела_http_get_jsonнестандартно для кодовой базы (нет аналогов вnspd_lite.py). Перенести на top-level.:133—ssl._create_unverified_context()дублирует_SSL_CTXизnspd_lite.py:55. Переиспользовать:from app.services.scrapers.nspd_lite import _SSL_CTX. Так workaround не дублируется, изменения SSL-конфига применяются глобально.list_layers:394—_walk_layer_tree(data)получает response напрямую без проверки shape. Если NSPD вернёт dict с обёрткой{"data": [...]}, walker молча вернёт[]. Добавитьdata = data if isinstance(data, list) else data.get("data", [])или хотя бы залогировать неожиданный тип. (Паттерн ловили вBug_Nspd_Geo_Str_Object_No_Get_Fixed.)search_by_cad:260без валидации форматаcad_num— спецсимволы/пробелы корректно URL-encode'нутся (нет injection), но молча вернут пустой результат. Задокументировать ожидаемый формат в docstring.backend/tests/test_nspd_client.py:get_feature_info,get_features_in_bbox,list_layers—_http_get_jsonне покрыт ни одним тестом. URL-сборка с bbox в EPSG:3857 не верифицируется. Foundation PR — желательно добавить по одному монкипатчу на_http_get_jsonper method.backend/app/api/v1/parcels.py:1060:_INLINE_FETCH_WAIT_S, которой нет в файле (grep 0 совпадений). Либо переименовано, либо в другом модуле. Убрать или поправить.Positive:
nspd_lite.py(не повторит баг93f1379с 406).DEFAULT_RATE_MS=600— WAF-safety соблюдена.NspdLiteWafErrorдля 403/429 — корректный контракт (backoff — ответственность caller).urlencode/typed params — нет injection.frozen=True, slots=Truedataclasses — immutable, memory-efficient.__all__явный экспорт._walk_layer_treeпокрыт 3 тестами (flat/nested/empty).Все 6 findings применены в `6ce7322`:
nspd_client.py:
test_nspd_client.py: +5 mock тестов (19/19 passed):
parcels.py: docstring stale '~25с' → '~15с'.
Bonus: ruff UP038 `isinstance(data, (list, dict))` → `isinstance(data, list | dict)` (PY3.10+ union).
`ruff` + 19/19 tests passed. Ждём re-review.
✅ Auto-review passed (LGTM) — все 6 findings корректно применены, 5 новых тестов non-vacuous:
Fixes verified:
bbox_around_point_msignature split на 3 строки — line ≤100 OK.import json/import time→ top-level (lines 63, 66)._SSL_CTXimported fromnspd_lite(line 74), single source of truth.list_layersshape defense (lines 419-427):isinstance(data, dict) and "data" in data and not data.get("children")для unwrap +not isinstance(data, list | dict)для garbage. Guard на "children" предотвращает unwrap легитимного tree-root.search_by_caddocstring:NN:NN:NNNNNN[:NN[:NN]]+ примеры 3/4/5-segment + ссылка наvalidate_cad_format.parcels.py~25с→~15св docstring и inline comment, + добавлено rationale про threadpool safety.Tests verified non-vacuous (5 new):
test_get_feature_info_parses_response— URL contains/api/aeggis/v4/875838/wms,REQUEST=GetFeatureInfo,STYLES=,CRS=EPSG%3A3857.test_get_features_in_bbox_url_builds_correctly—BBOXpropagation,WIDTH=4096,I=J=2048(center pixel).test_list_layers_walks_tree_response— nested tree → 2 leaves with metadata preserved.test_list_layers_handles_data_wrapper—{"data": [...], "meta": ...}exercise shape-defense.test_list_layers_handles_garbage_response— string input →[](early-return path).Bonus: ruff UP038 fix consistent с PY3.12 target.
Merging.