feat(tradein/cian): valuation backfill + runner script для prod #539
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#539
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/cian-valuation-backfill"
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?
What
Extends the backfill task from PR #535 to also populate price predictions (
external_valuationsvia Cian Calculator), плюс shell runner который drains backlog в prod одной командой.User ask: «скрипт для наполнения новой таблицы в проде всех исторических и предикта стоимости».
Backend extension
tradein-mvp/backend/app/tasks/cian_history_backfill.py— new block 3if do_valuations:iterates Cian listings WITH full geo (address, area_m2, rooms, floor, total_floors) ANDNOT EXISTSinexternal_valuationsдля них (filterexpires_at > NOW()). For each:await estimate_via_cian_valuation(db, ..., use_cache=False, listing_id=l.id)— fresh fetch, auto-кэш вexternal_valuationsс canonicallisting_idFK (mig 044).CianBackfillResult+ соответствующее вCianBackfillResp(valuations_total/processed/succeeded/failed).do_valuations: bool = False— default False для backward compat.Runner script
tradein-mvp/scripts/cian_backfill_all.sh—set -euo pipefail. Loops POST endpoint покаlistings_total + valuations_total == 0. Env:TRADEIN_API_URL,BATCH_SIZE(max 200 per endpoint cap),MAX_ITERS,DO_LISTINGS,DO_VALUATIONS,DO_HOUSES(default false — нетcian_zhk_urlcol).--dry-runдля пробы,--helpдля usage. Per-item commit в task → cancel-safe (Ctrl+C не оставит half-state).How to use в prod
Rate limit:
scraper_settingsciandelay (default 5s). При 100 listings × 5s × 2 phases = ~17 мин/batch. Endpoint timeout safe —batch_sizecapped 1..200 (PR #535).Test plan
--dry-runпротив local backend → expect listings_total/valuations_total non-zero countsoffer_price_history(где не было) +external_valuationsсsource=cian_valuation,listing_idnon-NULLFollow-ups
cian_zhk_urlколонки (mig 020 имеет толькоcian_internal_house_id bigint). Migration 065 + populate во время SERP scrape — отдельный PR.Follows up Cian history+valuation activation 2026-05-24 (4-PR sequence #523/#525/#530/#535).
Deep Code Review — PR #539
Summary
da38cd0Scope
Extends PR #535 backfill with block 3 (
if do_valuations) iterating Cian listings missing validexternal_valuationsrows (mig 044 listing_id FK), callingestimate_via_cian_valuation(..., use_cache=False, listing_id=l.id). New 4 counters, new endpoint flagdo_valuations: bool = False(backward-compat default). Addsscripts/cian_backfill_all.shdrain runner with env-knob config,--dry-run,--help,MAX_ITERS=1000safety.🟠 P1 — Idempotency hole on cache_key collision (recommended follow-up, not blocking)
external_valuationsuniqueness isUNIQUE (source, cache_key)(mig 026).cache_key = sha256(address + total_area + rooms + floor + repair + deal_type)— not keyed by listing_id. In_save_to_cache(cian_valuation.py:418) the conflict path doeslisting_id = COALESCE(EXCLUDED.listing_id, external_valuations.listing_id)— preserves the first listing_id.When two cian listings share identical normalized params (re-listings, agency duplicates, multi-flat same-building strings before per-flat refinement, etc.), only one ever gets
ev.listing_id = l.id. The new NOT EXISTS filter:never matches the second listing → it re-fetches Cian on every drain run despite a valid cached row existing under the same
cache_key. The runner's stop condition (lt + vt == 0) may then never reach 0 —MAX_ITERS=1000is the safety net but it's silent quota burn.Suggested follow-up (one of):
COALESCEforlisting_idin_save_to_cacheON CONFLICT — overwrite with latest. Loses house-level linkage if upstream changes, but ensures each backfill round writes back its listing_id.cache_keyin the SELECT (digest function) and filter on it directly.Practical impact: low if address dedup upstream is tight; not regressing existing behavior (block 3 is new + opt-in).
🟡 P2 — Background mode parity
Peer endpoint
POST /scrape/geocode-missing-listings(admin.py:370) supportsbackground: bool+BackgroundTasks+ ownSessionLocal().cian-backfill-historydoesn't (inherited from #535). Withbatch_size=200 × 5s × 2 phases ≈ 33 min, synchronous run will 504 through Caddy/proxy. Runner mitigates by looping smaller batches (default 100 → ~17 min/iter), but still over typical 120s proxy timeouts.Not blocking — already flagged on #535 by reviewer memory; runner provides workaround. Worth a 10-line follow-up to add
background=truelike the geocode peer.🟡 P2 —
use_cache=Falsewastes intra-batch cache hitsIf the same address appears twice in a single 100-row batch (rare but possible), the second iteration re-fetches Cian instead of hitting the just-written cache row. Trade-off acknowledged in code comment (
# force fresh fetch — populate cache). Acceptable for backfill semantics where the user wants fresh data.🟢 P3 — Cosmetic
do_houses=trueenv override (non-default) would print the same no-op log every loop iteration. Defaultfalse, so usually moot.cval.sale_price_rubtruthy check treats 0 as failure (impossible value in practice, fine).--dry-runparsing runs before--helpcheck;--dry-run --helpsets DRY then exits via help. Harmless.Positive
estimate_via_cian_valuationsignature matches (use_cache: bool,listing_id: int | None, returnsCianValuationResult | None, graceful — no exceptions propagate from cookies/fetch failures)._save_to_cacheuses correct CAST for psycopg v3 (CAST(:lid AS bigint),CAST(:ta AS numeric)).valuations_succeededvsvaluations_failedfromcval is not None and cval.sale_price_rub.do_valuations=Falsedefault → no behavioral change for existing #535 callers/cron.set -euo pipefail,curl -fsS(fail on HTTP error),MAX_ITERS=1000safety cap, env-knob config with sane defaults.expires_at > NOW()in NOT EXISTS respects 24h TTL — re-runs after a day will refresh stale rows.Query(50, ge=1, le=200)cap honored; runner can't bypass.text()).match_or_create_house(#527/#531/#534) — separate code path.Tests
No new tests added for block 3. Consistent with #535 pattern (no backfill tests there either).
tests/test_cian_valuation.pycovers underlying API. Manual--dry-run+ small batch verification documented in PR body. Acceptable.Risk / blast radius
Ready to merge.