gendesign/tradein-mvp/backend/scripts/README.md
Light1YT b8f9213cd9 feat(tradein): persistent infra для backfill scripts
3 проблемы фиксятся:
1. tradein-mvp/backend/Dockerfile копировал только COPY app — scripts/
   слетал при каждом deploy, требовал manual docker cp.
2. docker-compose.prod.yml без env_file — .env.runtime не читался
   контейнером, переменные приходили только через host env.
3. Env-var name mismatch: compose YANDEX_GEOCODER_KEY vs script
   YANDEX_GEOCODER_API_KEY — script всегда видел None.

Изменения:
- backend/Dockerfile: COPY scripts ./scripts в builder + runner stage
- docker-compose.prod.yml:
  * env_file: ./backend/.env.runtime (required: false) — pattern из
    main backend (PR #585)
  * убран дубль `YANDEX_GEOCODER_API_KEY: ${...}` из environment:
    block — он бы overrideal env_file пустым значением. Reviewer nit
    applied inline.
- pydantic Settings (app/core/config.py): rename yandex_geocoder_key
  → yandex_geocoder_api_key. Case-insensitive env binding автоматически
  подхватывает `YANDEX_GEOCODER_API_KEY` (no Field alias нужен).
- All 5 call sites в app/services/geocoder.py обновлены.
- app/tasks/geocode_missing.py docstring + test mock обновлены.
- backend/scripts/README.md: canonical `docker exec` commands без
  manual `docker cp` workflow.
- DEPLOY.md + .env.example обновлены с новым именем переменной.

Verified:
- pytest tests/test_backfill_house_coords.py + test_audit_address_mismatch.py
  + test_cadastral_reverse.py → 56 pass
- docker compose -f docker-compose.prod.yml config → valid syntax
- pydantic binding: YANDEX_GEOCODER_API_KEY=foo → settings.yandex_geocoder_api_key=='foo'
- code-reviewer LGTM (nit applied inline)

После merge — на VPS уже создан /opt/gendesign/tradein-mvp/backend/.env.runtime
с YANDEX_GEOCODER_API_KEY (см. PR #591 deploy). deploy.yml triggers
force-recreate из-за нового image hash → env_file pick up автоматически.
2026-05-27 11:59:14 +05:00

5.8 KiB
Raw Blame History

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.


Production usage (canonical)

Scripts ship inside the tradein-backend image (PR F — COPY scripts ./scripts в backend/Dockerfile). На VPS они уже в /app/scripts/ — никаких manual docker cp не нужно.

YANDEX_GEOCODER_API_KEY подтягивается из /opt/gendesign/tradein-mvp/backend/ .env.runtime через env_file: в docker-compose.prod.yml — никакого -e в docker exec не нужно.

# Backfill (forward geocode 4170 houses без coords)
ssh gendesign 'docker exec tradein-backend python -m scripts.backfill_house_coords --batch 2026-05-27_backfill'

# Audit-only (reverse geocode проверка для уже geocoded houses)
ssh gendesign 'docker exec tradein-backend python -m scripts.backfill_house_coords --audit-only --batch 2026-05-27_audit'

# Canary first
ssh gendesign 'docker exec tradein-backend python -m scripts.backfill_house_coords --limit 100 --batch canary_$(date +%F)'

После изменения backend/.env.runtime нужен --force-recreate контейнера (см. .claude/rules/deploy.md):

ssh gendesign 'cd /opt/gendesign/tradein-mvp && docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d --force-recreate --no-deps backend'

Address audit + backfill (issue #582)

End-to-end address quality pipeline. Three scripts, two helpers, two SQL files.

Локальные примеры ниже — для dev-машины с uv run и переменными в shell. На prod используй canonical docker exec команды из секции выше — там YANDEX_GEOCODER_API_KEY уже подгружен из backend/.env.runtime.

audit_address_mismatch.py — Phase 1 baseline (PR #583)

Stratified-sample audit (200 EKB houses) comparing houses.address vs Yandex Geocoder reverse lookup. Writes one row per house into address_mismatch_audit with the snapped point + canonical address + distance.

DATABASE_URL=postgresql+psycopg://... \
YANDEX_GEOCODER_API_KEY=... \
uv run python -m scripts.audit_address_mismatch \
    --batch 2026-05-25_run1 \
    --limit-per-district 25

Mode auto picks API if the key is set, otherwise Playwright (CAPTCHA-aware, 4-7s sleep between calls). API tier free is 25k req/day → 200-row sample takes ~10s with no quota concern.

Report:

psql "$DATABASE_URL" -v batch='2026-05-25_run1' \
    -f scripts/address_audit_report.sql

backfill_house_coords.py — Phase 2-3 (PR for #582)

Two modes (--audit-only flag switches between them):

Backfill (default) — forward-geocode houses.address for the ~4141 rows WHERE lat IS NULL OR lon IS NULL. Only writes back if Yandex returns precision='exact' or 'number' (skips street-only / locality matches). Each processed row gets an address_mismatch_audit entry with status backfill / imprecise / no_match / error.

DATABASE_URL=postgresql+psycopg://... \
YANDEX_GEOCODER_API_KEY=... \
uv run python -m scripts.backfill_house_coords \
    --batch 2026-05-27_backfill

Expected duration (~4141 rows, 50ms between calls, ~250ms RTT per request): 20-25 min. Expected output split (rough baseline from Phase 1 numbers):

Status Approx rows What it means
backfill ~3.3k3.7k UPDATE landed, lat/lon now populated
imprecise ~300500 Match returned but precision too low — needs review
no_match ~100300 Yandex couldn't resolve; address probably mangled
error <50 HTTP errors / timeouts — re-run picks them up

Audit-only — reverse-geocode the ~4452 houses WITH coords, write audit rows with status ok (≤50m) / mismatch (>50m) / no_match / error. Does NOT modify the houses table.

uv run python -m scripts.backfill_house_coords \
    --batch 2026-05-27_audit --audit-only

Combined budget for both phases (~8.6k requests) is well under the 25k/day Geocoder free tier.

Common ops

Canary first — run with --limit 100 and inspect the audit table before letting the full job loose:

uv run python -m scripts.backfill_house_coords \
    --batch canary_$(date +%F) --limit 100
psql "$DATABASE_URL" -c "
  SELECT audit_status, COUNT(*)
    FROM address_mismatch_audit
   WHERE audit_batch = 'canary_$(date +%F)'
   GROUP BY audit_status;
"

Resume after crash / quota hit — same --batch label, the UNIQUE (house_id, audit_batch) index skips finished rows:

uv run python -m scripts.backfill_house_coords --batch 2026-05-27_backfill
# ... interruption ...
uv run python -m scripts.backfill_house_coords --batch 2026-05-27_backfill
# logs: "resuming batch 2026-05-27_backfill: N rows already processed"

Helpers (not entry points)

  • _yandex_reverse.pyforward_via_api(), reverse_via_api(), reverse_via_playwright(), YandexReverseResult dataclass. Both API paths share _parse_api_payload because Yandex's forward/reverse envelopes have the same shape.
  • audit_address_sample.sql — random sample for the Phase 1 audit (used by audit_address_mismatch.py).
  • address_audit_report.sql — psql-driven post-run summary (p50/p75/p95 distance, top-20 outliers, per-district breakdown).