|
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m56s
Deploy / build-worker (push) Successful in 4m16s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m49s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / deploy (push) Successful in 2m25s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
|
||
|---|---|---|
| .. | ||
| backfill_external_valuations_house_id.py | ||
| backfill_houses_dadata.py | ||
| backfill_listing_sources.py | ||
| backtest_estimator.py | ||
| domclick_local_runner.py | ||
| geocode_deals_from_houses.py | ||
| geocode_deals_nominatim.py | ||
| ingest_domclick_jsonl.py | ||
| README.md | ||
tradein-mvp/backend/scripts/
Ops scripts that touch the production database directly. Run via python -m scripts.<name> from the backend/ working directory after uv sync.
All scripts are idempotent / resumable where they write — re-running the same
--batch label skips already-processed rows (UNIQUE constraints in target
tables). Failures inside a per-row loop never roll back the outer transaction;
each row is wrapped in a SAVEPOINT (db.begin_nested()) per .claude/rules/backend.md.
Address audit + backfill (issue #582) — REMOVED (#2593)
audit_address_mismatch.py, backfill_house_coords.py, _yandex_reverse.py
и их SQL-хелперы (audit_address_sample.sql, address_audit_report.sql)
удалены — весь pipeline опирался на Yandex Geocoder API, который выпилен
из проекта (#2593, части 1-3). houses.address→lat/lon geocoding теперь
идёт через app/services/geocoder.py (кадастр/геопортал ЕКБ-тиры + Nominatim
fallback, единственный живой внешний провайдер) на обычном write-path
(/api/v1/trade-in/estimate, listing ingest). Разовый forward-backfill
недостающих houses координат — scripts/geocode_deals_nominatim.py
(живой, работает с rosreestr_deals, не с houses — читай его docstring
перед использованием на других таблицах). Таблица address_mismatch_audit
осталась в схеме (используется house_dedup_merge.py при слиянии дублей
домов, независимо от Yandex-аудита).
Matching backfill (PR J)
backfill_listing_sources.py — retroactive matching for ~18k listings
PR I (commit 7e24ccb) hooked the matching service into save_listings() so
every new scrape now writes a listing_sources row + resolves a canonical
houses row. This script does the same work retroactively for all
existing listings — listing_sources only had rows from new scrapes
post-PR I.
What it does per row:
match_or_create_house()(Tier 0-3) — useslistings.house_source/house_ext_idwhen present (Avito Houses Catalog, Cian newbuilding), else falls back to address/lat/lon/cadastrals.upsert_listing_source()withmethod='backfill',confidence=0.9(vs real-timesource_link1.0 — distinguishes the two in audits).UPDATE listings.house_id_fkwhen the row didn't already have one.
# Canary
DATABASE_URL=postgresql+psycopg://... \
uv run python -m scripts.backfill_listing_sources \
--limit 100 --dry-run
# Real run, one source at a time (staged rollout)
uv run python -m scripts.backfill_listing_sources --source avito
# Full run
uv run python -m scripts.backfill_listing_sources --batch-size 500
Idempotent / resumable — the source query is
WHERE NOT EXISTS (SELECT 1 FROM listing_sources ls WHERE ls.ext_source = listings.source AND ls.ext_id = COALESCE(listings.source_id, listings.dedup_hash)). Re-runs only pick up rows still missing from
listing_sources. upsert_listing_source adds a second layer of safety via
ON CONFLICT (ext_source, ext_id) DO UPDATE.
No network calls — pure in-DB matching (Yandex Geocoder is blocked on
prod, and match_or_create_house does not call it anyway).
Per-row SAVEPOINT (db.begin_nested()) per .claude/rules/backend.md —
one bad row never aborts the surrounding batch.
Expected output (PR J initial run):
| Source | Rows | Expected matched | Notes |
|---|---|---|---|
| avito | 9302 | 9000+ | Many carry house_source/house_ext_id |
| cian | 5158 | 5000+ | Most carry house_source/house_ext_id |
| yandex | 3704 | 3700+ | No source_id → uses dedup_hash as ext_id |
| n1 | 264 | 264 | All have address/coords |
Expected duration: rough estimate ~5-15 minutes on prod for ~18k rows
(advisory-lock + 1-3 DB roundtrips per listing for Tier 0-3, ~500 commit
checkpoints at default batch size). Run with --limit 100 first to
calibrate, then let the full job loose.
Final summary in the log includes per-source coverage % so you can verify the run landed:
backfill done (dry_run=False): processed=18428 matched=18428
house_resolved=18200 house_failed=228 skipped=0 errors=0
avito processed=9302 matched=9302 house_resolved=9290 ...
cian processed=5158 matched=5158 house_resolved=5100 ...
yandex processed=3704 matched=3704 house_resolved=3540 ...
n1 processed=264 matched=264 house_resolved=270 ...
final listing_sources coverage:
avito 9302 / 9302 (100.0%)
cian 5158 / 5158 (100.0%)
...
Estimator backtest (issue #648)
backtest_estimator.py — asking→sold accuracy harness
STRICTLY READ-ONLY (SELECT-only; no INSERT/UPDATE/DDL/commit). Measures the
estimator's asking-median + Tukey-IQR core against rosreestr ДКП sold prices.
For a sample of ДКП deals it predicts the asking median from nearby active
listings (reusing the estimator's own _filter_outliers / _percentile), then
reports per-deal signed/abs error % aggregated overall + per-rooms (студия / 1к /
2к / 3к / 4+), plus a city-wide deal-vs-asking headline spread.
DATABASE_URL=postgresql+psycopg://... \
python -m scripts.backtest_estimator --sample 300 --since 2025-06-01
# machine-readable:
python -m scripts.backtest_estimator --json
Stage 1 correction block. On top of the raw [ASKING] metrics the harness
emits a second [CORRECTED] block: from the SAME matched sample it derives a
per-rooms asking→sold ratio ratio[bucket] = median(sold_ppm2) / median(pred_ask_ppm2) (global fallback for buckets with < MIN_BUCKET = 20
matched deals), then re-scores pred_sold = pred_ask * ratio[bucket] through the
same metric math. This DEMONSTRATES that a per-rooms factor removes the
systematic +29.6% asking→sold bias — it changes nothing in prod.
Honesty: by default the ratio is IN-SAMPLE (derived AND evaluated on the same deals), so the corrected bias is near-zero by construction. That proves the MECHANISM, not out-of-sample accuracy. Pass
--holdout-splitto fit on even-id deals and evaluate on the odd-id half (deterministic, no RNG) for an honest number. The production ratio (Stage 2) is fit over a SEPARATE window and A/B'd on held-out data.
# honest out-of-sample corrected number (even-id fit / odd-id eval):
python -m scripts.backtest_estimator --sample 600 --holdout-split
Caveats (also printed): CURRENT listings vs PAST deals (not point-in-time —
needs listing_source_snapshots #570); asking-median + IQR core only; ДКП =
registered price. Pure metric/ratio helpers are unit-tested in
tests/test_backtest_estimator.py (no DB).
domclick_local_runner.py — DomClick EKB вторичка с домашнего IP (offline)
SELF-CONTAINED (без app.* импортов, stdlib + playwright.async_api).
Запускается ОПЕРАТОРОМ на его машине с домашнего/резидентного IP — DomClick
фронтит QRATOR, который банит datacenter/mobile-proxy, но пропускает домашний.
Пишет JSONL (НЕ в БД); заливка в trade-in БД — отдельным шагом ingest_domclick_jsonl.py.
Транспорт — оба слоя через браузер (page.goto, verified live 2026-06-27):
- enumerate — фронтовый SERP
ekaterinburg.domclick.ru/search?...(SSR). Прямой BFFbff-search-web/api/offers/v1режет QRATOR-403 уже с домашнего IP, а фронтовый SERP грузится чисто. id офферов — из DOM (a[href*="/card/"], ждём черезwait_for_selector— карточки догидрируются ~6-12с), total — из<title>. Пагинацияoffset(page-size 20); при total>2000 — price-bisection бакета (границы из title). - detail —
page.goto(card)→ сырой HTML (__SSR_STATE__: renovation, living/kitchen, priceHistory, egrnData owners/collateral, sale_type, views, wall/floor) + offer-card v3 XHRprice_prediction(AVM заполненный) иsold_similar(--with-sold-similar) — они с домашнего IP отдают 200 (в отличие от прод-прокси). Layer-A (price/area/rooms/floor/total_floors/lat/lon/address) тоже из card-SSR.
Режим окна
QRATOR ловит СТАРЫЙ headless (launch(headless=True)) → 403 | Домклик. NEW-headless
И headed → 200 + полный __SSR_STATE__. Дефолт = new-headless (окно скрыто);
--headed = видимое окно (debug).
Resume / enum-cache (рестарт не теряет позицию)
- detail-resume: при старте читает JSONL и пропускает id с терминальным статусом (ok/blocked/parse_fail).
--enum-cache PATH: enumerate (~6400 id, ~1.5-2ч) пишет полный список id в файл с sentinel завершённости; следующий запуск грузит кэш и ПРОПУСКАЕТ enumerate → сразу detail. Композится с detail-resume → рестарт продолжает строго с текущей позиции, без пересбора и без перекачки готовых.
Темп (НЕ спалить домашний IP)
- пауза между карточками
--min-delay(12с) /--max-delay(30с); - render-wait
--render-min(6с) /--render-max(10с) — settle поверхwait_for_selector; - «человеческий перерыв» каждые 25-40 карточек; на 403 — ретрай, потом
status="blocked".
Дефолт ≈ 20-34с/карта; для быстрее — --min-delay 5 --max-delay 12 --render-min 3 --render-max 5 (~16-20с/карта; card-path чистый, запас есть). Полный свод ЕКБ (~6300) —
несколько ночей; держи ПК от сна (иначе wall-clock простаивает, данные не теряются).
cd tradein-mvp/backend
# боевой прогон с кэшем id + sold_similar (рекомендуется):
python scripts/domclick_local_runner.py --out domclick_ekb.jsonl \
--enum-cache enum_cache.jsonl --with-sold-similar
# быстрый smoke (маленькие задержки ТОЛЬКО для теста):
python scripts/domclick_local_runner.py --limit 2 --min-delay 3 --max-delay 5 --out dc_test.jsonl
# только перечислить (без detail):
python scripts/domclick_local_runner.py --enumerate-only --out enum.jsonl
Нужен python с playwright (python -c "import playwright").
ingest_domclick_jsonl.py — JSONL раннера → trade-in БД
Запускается ВНУТРИ tradein-контейнера (импортит app). Читает JSONL раннера, на каждую
ok-запись: ScrapedLot → save_listings() (upsert + house-match хук в SAVEPOINT,
fault-tolerant — нематчнутый листинг всё равно вставляется) → save_detail_enrichment()
(detail-колонки COALESCE + offer_price_history). Идемпотентно (ON CONFLICT), --limit,
--dry-run. Запуск: python -m scripts.ingest_domclick_jsonl --jsonl <path> (PYTHONPATH=/app).