Объединяю несколько связанных изменений вокруг NSPD geo bulk-fetcher:
Adapt to rosreestr2coord v5 API:
- nspd_lite.fetch_via_rosreestr2coord: drop `delay` kwarg from Area()
(removed upstream in v5); keep it in our function signature for
backward-compat, comment why.
- nspd_geo worker: add explicit time.sleep(rate_ms/1000) after lib-branch
fetch — in v4 the library throttled internally via delay, in v5
rate limiting is the caller's job. Без этого получали Area.__init__()
unexpected kwarg `delay` на каждом target.
Drop use_rosreestr2coord switch:
- Removed urllib-vs-lib choice everywhere. We always use community
rosreestr2coord library — авторы регулярно обновляют WAF-tricks,
наш urllib-fetcher (fetch_geoportal) уже неактуален.
- admin_scrape.py: Pydantic schema, INSERT, SELECT, API response
cleaned of `use_rosreestr2coord`.
- nspd_geo.enqueue_geo_job: param dropped, INSERT shrunk.
- worker process loop: dropped `if use_lib:` branch + import of
fetch_geoportal.
- frontend/geo/page.tsx: removed checkbox + GeoJob.use_rosreestr2coord
field + POST body field.
DB column drop:
- data/sql/78_drop_use_rosreestr2coord.sql (NEW):
DROP COLUMN nspd_geo_jobs.use_rosreestr2coord + CREATE OR REPLACE
VIEW v_scrape_runs_unified (которая depended on the column).
- data/sql/77_nspd_geo_jobs.sql: cleaned historical DDL for fresh setups.
- Migration applied to prod (in-conversation via postgres MCP).
Frontend polish:
- Thematic ID changed from free-form number input to labeled select
(1=parcel / 2=quarter / 4=admin / 5=building / 7=zone / 15=complex).
- Auto-sync thematic_id from Job kind on change (override possible).
- ScrapeLogsPanel: extended union type with "nspd_geo" + fixed
/admin/scrape/geo to pass scraperType="nspd_geo" (was "nspd",
filtering empty legacy nspd_scrape_log table; real logs live in
nspd_geo_log via v_scrape_log_unified).
Verified: ruff ✓, tsc --noEmit ✓, migration ran (BEGIN..COMMIT clean).
Deploy order safe: prod column уже удалена → новый backend код, который
не INSERT'ит use_rosreestr2coord, совпадёт со схемой после deploy.
ОТКРЫТИЕ: stdlib urllib проходит WAF nspd.gov.ru (TLS-fingerprint stdlib
отличается от mainstream HTTP-clients). Это позволяет полностью убрать
Playwright + Chromium для NSPD-задач.
* nspd_lite.py: urllib + ssl._create_unverified_context(). 4 публичных
fetcher'а (geoportal/quarter/parcel/building) + fetch_via_rosreestr2coord
fallback на community-lib
* schema 77: nspd_geo_jobs (журнал) + nspd_geo_targets (cad-номера со
статусом pending/done/failed). Resume-state в БД.
* tasks/nspd_geo.py: Celery task process_nspd_geo_job(job_id). Heartbeat
каждые 5 items, WAF backoff 30s × 2^N (max 8 → paused). UPSERT в
cad_quarters_geom / cad_buildings → идемпотентно.
* worker_ready hook: stale jobs (>10min без heartbeat) автоматически
re-enqueue после redeploy. Никаких потерь прогресса.
* 4 admin endpoint + UI /admin/scrape/geo с формой запуска (manual_list /
rosreestr_pending для авто-наполнения из ДДУ-кварталов), progress-bars
и cancel/resume кнопками per job.
* settings: use_nspd_lite=True, nspd_lite_rate_ms=600.
* dep: rosreestr2coord>=4.0.0 (community lib для fallback).
TODO следующих шагов:
- smoke-test на проде через SSH (validation RU-IP не банит urllib)
- feature toggle USE_NSPD_LITE в scrape_nspd.py для переключения с старого
Playwright-пути
- docker lean image (-300 MB после убирания Chromium)
- расширение SCRAPE_NSPD_DEFAULT_REGIONS на 66,74,72,59 (Челябинск/Тюмень/Пермь)
Objective API (api.objctv.ru) интегрирован как новый source of truth для
per-flat данных по новостройкам УрФО. Заменяет промежуточный Anton-SQLite
(legacy bootstrap-ETL остался как fallback в свёрнутом блоке UI).
Schema (data/sql/68_v2 — applied на проде, 6 таблиц):
- objective_lots (UPSERT по lot_id; 303 677 rows)
- objective_corpus_room_month (long-формат месяц×корпус×room_bucket; 19 738 rows)
- objective_lots_history (append-only weekly snapshots для elasticity)
- objective_complex_mapping (Objective ComplexName ↔ domrf_kn_objects.obj_id)
- objective_raw_reports (jsonb страховка на смену схемы API)
- objective_scrape_runs (журнал прогонов)
+ data/sql/72_objective_sync_config (single-row динамический конфиг)
Backend:
- services/scrapers/objective.py: ObjectiveClient — Bearer-токен (Redis +
in-memory fallback), retry на 401/429/5xx, Retry-After header support
- services/objective_etl.py: ETL SQLite Антона → PG (legacy)
- services/objective_sync_config.py: read/update single-row config
- workers/tasks/scrape_objective.py:
* sync_objective_group: 2 рабочих отчёта (corp_sum, lots_pf), inline-парсинг
* sync_all_groups: wrapper, перебирает группы из БД-конфига с
inter-group паузой; PATCH-merge explicit args > DB config
- workers/tasks/objective_etl.py: Celery task для legacy bootstrap
- workers/celery_app.py: beat читает cron из БД при старте (fallback на env)
- api/v1/admin_scrape.py: 5 новых endpoints для /objective/*
Frontend (frontend/src/app/admin/scrape/objective/page.tsx):
- PRIMARY blue блок «🌐 Наш sync» с input для override групп
- Collapsible «⚙️ Настройки» с формой (cron + 8 параметров) → PUT в БД
- Coverage-панель с PG counts + строка про SQLite Антона как legacy
- Collapsible «🛠 Bootstrap ETL» — legacy-инструмент
Beat schedule: вторник 06:00 МСК, ~10-15 мин на 4 группы (Свердл.обл +
Челябинск + Тюмень + Пермь = ~700K квартир УрФО). Расписание и параметры
меняются через админку без редеплоя (cron требует restart beat).
Эмпирические находки об API (probe 2026-05-10):
- 13 из 21 проверенных group_name доступны на тарифе (включая «Свердловская
область», «Челябинск», «Тюмень», «Пермь», но НЕ «Свердловская обл» —
формат имени строгий)
- ComplexName требует БЕЗ префикса «ЖК» и БЕЗ кавычек
- Поле «Банк» для всех 303k = NULL (тариф не отдаёт), но «Тип обременения»
работает (36% строк = ипотека) → ipoteka_share возможен, банковская
атрибуция — нет
Docker:
- bind-mount /opt/gendesign/site-finder:/data/anton-sqlite:ro в worker
GitHub backlog: добавлены #22-25 (recommend_mix v3 — 4 сабтаска по
улучшению алгоритма на основе фидбэка про POI / границы районов /
конкурентов / окно данных / success-driven mix).
Knowledge graph (memory/memory-gendesign.jsonl): обновлены entities
Objective_Integration_May07_2026, Schema_Objective_v2_May07,
Objective_API_Findings_May07, Module_Objective_Client + новый
Session_End_May07_2026.
TODO для прода: прописать OBJECTIVE_API_KEY=<key> в backend/.env +
docker compose restart worker beat.
В nspd_kn.py:get_pending_cads и admin_scrape.py:nspd_coverage regex-фильтры
quarter_cad_number !~ '^00:00:' и !~ ':0000000$' падали с psycopg.errors.
DataError — двоеточие в text() трактуется как ':param'. Заменил на NOT LIKE
с bindparams (быстрее regex и без issue с парсингом).
Эффект на проде:
- /api/v1/admin/scrape/nspd/coverage возвращал 500 → теперь 200
- Кнопка «Запустить sweep» больше не дизейбленa (pending=964 теперь видно)
- Сам scraper тоже бы валился на этом же баге при beat-запуске
B1: дефолт months_window 12→24, расширение fallback 24→27 (max архив).
2024Q1+Q2 теперь реально влияют на recommend_mix (raw 784→3815 сделок
для Академического, ×4.9).
B2: per-bucket elasticity через MODE по семантике (is_studio+rooms),
а не PERCENTILE_CONT(0.5) по total_area. ЖК с mix studios+3к больше не
попадают в bucket "1-к" как среднее. Для Академического: 1-к получает
208 точек регрессии (vs 118), slope -1.17.
B3: share_window_months и elasticity_window_months в API scope —
прозрачность для UI tooltip про рассинхрон окон (24mo rosreestr vs
36mo sale_graph).
B4: cad_buildings.floors при INSERT больше НЕ приводится к INT —
NSPD отдаёт диапазоны "1-2", "2-3" которые теряются как NULL. Хранится
как TEXT, аналитика делает CAST через regex.
R1: data/sql/66_indexes_recommend.sql — 4 индекса для горячего пути
recommend_mix (sale_graph composite, kn_objects partial, kn_flats partial,
cad_buildings partial). Применены в проде через MCP.
R3: ON CONFLICT в insert_buildings теперь обновляет ВСЕ изменяемые
поля (purpose, address, floors, geom, registration_date) — раньше при
re-scrape апдейтились только cost/area/year, остальное игнорилось.
- 56_fetch_ekb_districts.py: Overpass query (admin_level=9 в bbox ЕКБ),
shapely-ассемблинг рваных OSM-веев в MultiPolygon, кэш в data/raw/.
- 56_schema_ekb_districts_geom.sql: CREATE TABLE с GIST.
- 57_kn_objects_district.sql: ALTER + backfill UPDATE через ST_Contains.
3032 ЖК Свердл проставили district_name (208 в Академическом, 158 в Чкаловском,
и т.д.), 454 за bbox ЕКБ остались NULL.
- domrf_kn.py: _backfill_district_names() после Phase A — каждый sweep
автоматически проставляет district_name свежезаписанным ЖК.
- analytics_queries.py: 4 места addr ILIKE → district_name = :dn
(_velocity_baseline, _elasticity_coef, _active_competitors_count, comparable).
- analytics_queries.py: graceful obj_class-degrade — если в районе все
obj_class=NULL (kn-API quirk), фильтр по target_class опускается на уровне
geo-запросов с warning. class_multiplier на ценах продолжает работать.
Smoke (Академический, Comfort, 50K м²): velocity_source=sale_graph (104 ЖК /
1872 точек), competitors=208 в районе, headline «3400 млн ₽ · 112 мес ·
ликвидность 21/100», 5 comparable ЖК района — все реалистичные.