feat(tradein): persistent infra для backfill scripts (PR F) (#592)
This commit is contained in:
parent
3c506222e6
commit
1ce9e10012
9 changed files with 105 additions and 14 deletions
|
|
@ -6,6 +6,12 @@ DATABASE_URL=postgresql+psycopg://tradein:tradein@postgres:5432/tradein
|
|||
CORS_ORIGINS=["http://localhost:8080","http://localhost:3000"]
|
||||
ENVIRONMENT=dev
|
||||
|
||||
# Yandex Geocoder API key (25k req/day free tier).
|
||||
# Required for backfill scripts (scripts/backfill_house_coords.py + audit_address_mismatch.py).
|
||||
# Empty = Nominatim fallback для backend геокодинга; backfill scripts требуют этот ключ
|
||||
# и упадут с SystemExit без него.
|
||||
YANDEX_GEOCODER_API_KEY=
|
||||
|
||||
# === Frontend (build-time) ===
|
||||
# Backend URL для Next.js rewrites (browser обращается к /api/* — Next.js проксирует на BACKEND_URL/api/*).
|
||||
# В docker compose это уже выставлено в http://backend:8000 — менять не нужно.
|
||||
|
|
|
|||
|
|
@ -50,12 +50,23 @@ import /opt/gendesign/tradein-mvp/deploy/Caddyfile.tradein-fragment
|
|||
|
||||
## Что нужно положить в `.env.runtime` (на сервере, НЕ в git)
|
||||
|
||||
Два файла:
|
||||
|
||||
1. `/opt/gendesign/tradein-mvp/.env.runtime` — переменные для docker compose
|
||||
substitution (`${TRADEIN_POSTGRES_PASSWORD}` и т.п. в compose-файле). Читается
|
||||
shell-скриптом deploy через `source .env.runtime` перед `compose up`.
|
||||
2. `/opt/gendesign/tradein-mvp/backend/.env.runtime` — переменные внутри
|
||||
контейнера `tradein-backend` (читаются через `env_file:` в compose). Сюда
|
||||
попадают `YANDEX_GEOCODER_API_KEY`, `ADMIN_TOKEN`, `COOKIE_ENCRYPTION_KEY` —
|
||||
всё, что нужно scripts/backfill_house_coords.py и application code внутри
|
||||
контейнера.
|
||||
|
||||
```bash
|
||||
# /opt/gendesign/tradein-mvp/.env.runtime
|
||||
TRADEIN_POSTGRES_USER=tradein
|
||||
TRADEIN_POSTGRES_PASSWORD=<сгенерировать openssl rand -hex 32>
|
||||
TRADEIN_CONTACT_EMAIL=tradein@gendsgn.ru
|
||||
YANDEX_GEOCODER_KEY= # пусто пока, Nominatim fallback работает
|
||||
YANDEX_GEOCODER_API_KEY= # пусто пока, Nominatim fallback работает
|
||||
|
||||
# Encryption key for Cian session cookies (pgp_sym_encrypt / Stage 9 Calculator).
|
||||
# Empty = Valuation Calculator scraper disabled + /api/v1/cookies/upload returns 503.
|
||||
|
|
@ -64,7 +75,29 @@ YANDEX_GEOCODER_KEY= # пусто пока, Nominatim fallback работает
|
|||
COOKIE_ENCRYPTION_KEY=<64-char hex>
|
||||
```
|
||||
|
||||
Файл создаётся вручную при первом деплое.
|
||||
```bash
|
||||
# /opt/gendesign/tradein-mvp/backend/.env.runtime — те же ключи которые
|
||||
# читаются ВНУТРИ container'а (scripts/backfill_house_coords.py, app/*).
|
||||
# Может быть симлинком на ../.env.runtime если переменные совпадают:
|
||||
# ln -s ../.env.runtime /opt/gendesign/tradein-mvp/backend/.env.runtime
|
||||
YANDEX_GEOCODER_API_KEY=<key или пусто>
|
||||
ADMIN_TOKEN=<token или пусто>
|
||||
COOKIE_ENCRYPTION_KEY=<64-char hex>
|
||||
GENDESIGN_FDW_PASSWORD=<password или пусто>
|
||||
GLITCHTIP_DSN=<dsn или пусто>
|
||||
```
|
||||
|
||||
Оба файла создаются вручную при первом деплое.
|
||||
|
||||
### После изменения `backend/.env.runtime`
|
||||
|
||||
```bash
|
||||
# .env.runtime читается на старте container — `compose restart` НЕ перечитывает.
|
||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml \
|
||||
up -d --force-recreate --no-deps backend
|
||||
```
|
||||
|
||||
См. `.claude/rules/deploy.md` — main backend pattern идентичный.
|
||||
|
||||
### Обновление `COOKIE_ENCRYPTION_KEY` на существующем VPS
|
||||
|
||||
|
|
@ -120,15 +153,20 @@ nano Caddyfile
|
|||
# вставить ПЕРЕД последним handle { ... }:
|
||||
# import /opt/gendesign/tradein-mvp/deploy/Caddyfile.tradein-fragment
|
||||
|
||||
# Создать .env.runtime
|
||||
# Создать .env.runtime — два файла:
|
||||
# 1. tradein-mvp/.env.runtime — для compose substitution на host'е
|
||||
cat > tradein-mvp/.env.runtime <<EOF
|
||||
TRADEIN_POSTGRES_USER=tradein
|
||||
TRADEIN_POSTGRES_PASSWORD=$(openssl rand -hex 32)
|
||||
TRADEIN_CONTACT_EMAIL=tradein@gendsgn.ru
|
||||
YANDEX_GEOCODER_KEY=
|
||||
YANDEX_GEOCODER_API_KEY=
|
||||
EOF
|
||||
chmod 600 tradein-mvp/.env.runtime
|
||||
|
||||
# 2. tradein-mvp/backend/.env.runtime — для env_file внутри container'а.
|
||||
# Симлинк работает если переменные одни и те же; иначе создать отдельный файл.
|
||||
ln -sf ../.env.runtime tradein-mvp/backend/.env.runtime
|
||||
|
||||
# Запустить stack первый раз вручную (после этого GitHub Actions сам)
|
||||
docker network inspect gendesign_shared >/dev/null 2>&1 || docker network create gendesign_shared
|
||||
cd tradein-mvp
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
|||
uv sync --no-dev
|
||||
|
||||
COPY app ./app
|
||||
COPY scripts ./scripts
|
||||
|
||||
|
||||
# ---- runner ----
|
||||
|
|
@ -48,6 +49,7 @@ WORKDIR /app
|
|||
|
||||
COPY --from=builder --chown=app:app /app/.venv /app/.venv
|
||||
COPY --from=builder --chown=app:app /app/app /app/app
|
||||
COPY --from=builder --chown=app:app /app/scripts /app/scripts
|
||||
|
||||
USER app
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ class Settings(BaseSettings):
|
|||
cors_origins: list[str] = ["http://localhost", "http://localhost:3000", "http://localhost:8080"]
|
||||
environment: str = "dev"
|
||||
|
||||
# Geocoder
|
||||
yandex_geocoder_key: str | None = None # 25K req/day free после регистрации
|
||||
# Geocoder. Env var name `YANDEX_GEOCODER_API_KEY` — consistent с scripts/
|
||||
# backfill_house_coords.py + audit_address_mismatch.py + main backend
|
||||
# OpenRouteService_API_KEY pattern. Renamed from YANDEX_GEOCODER_KEY (PR F).
|
||||
yandex_geocoder_api_key: str | None = None # 25K req/day free после регистрации
|
||||
yandex_suggest_key: str | None = None # для frontend autocomplete (proxy через backend)
|
||||
# для User-Agent в Nominatim (Nominatim Usage Policy)
|
||||
contact_email: str = "erginrajpopxbe@outlook.com"
|
||||
|
|
|
|||
|
|
@ -545,9 +545,9 @@ async def suggest(
|
|||
if cad_results:
|
||||
return cad_results
|
||||
|
||||
if settings.yandex_geocoder_key:
|
||||
if settings.yandex_geocoder_api_key:
|
||||
try:
|
||||
results = await _yandex_suggest(query, settings.yandex_geocoder_key, limit)
|
||||
results = await _yandex_suggest(query, settings.yandex_geocoder_api_key, limit)
|
||||
if results:
|
||||
return results
|
||||
except Exception:
|
||||
|
|
@ -600,9 +600,9 @@ async def geocode(address: str, db: Session) -> GeocodeResult | None:
|
|||
return result
|
||||
|
||||
# 3. Yandex (если есть key) с typo-fallback
|
||||
if settings.yandex_geocoder_key:
|
||||
if settings.yandex_geocoder_api_key:
|
||||
try:
|
||||
result = await _yandex_lookup(address, settings.yandex_geocoder_key)
|
||||
result = await _yandex_lookup(address, settings.yandex_geocoder_api_key)
|
||||
# Если результат вне ЕКБ — пробуем typo-варианты
|
||||
in_ekb = (
|
||||
result is not None
|
||||
|
|
@ -616,7 +616,7 @@ async def geocode(address: str, db: Session) -> GeocodeResult | None:
|
|||
# Tier 2: typo-variants
|
||||
for variant in _typo_variants(address, limit=4):
|
||||
try:
|
||||
result = await _yandex_lookup(variant, settings.yandex_geocoder_key)
|
||||
result = await _yandex_lookup(variant, settings.yandex_geocoder_api_key)
|
||||
except Exception:
|
||||
continue
|
||||
if result is None:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
- Future: cron job (Celery beat если bootstrap'нут / OS cron)
|
||||
|
||||
Pattern: dedup по address (1 unique address → 1 geocode call → UPDATE all listings).
|
||||
Rate limit: Nominatim 1 req/sec. Yandex 25K/day если YANDEX_GEOCODER_KEY set.
|
||||
Rate limit: Nominatim 1 req/sec. Yandex 25K/day если YANDEX_GEOCODER_API_KEY set.
|
||||
|
||||
Отличие от /admin/geocode-missing (per-ID):
|
||||
- Этот модуль группирует по address → меньше API calls (dedup).
|
||||
|
|
|
|||
|
|
@ -10,10 +10,44 @@ each row is wrapped in a SAVEPOINT (`db.begin_nested()`) per `.claude/rules/back
|
|||
|
||||
---
|
||||
|
||||
## 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` не нужно.
|
||||
|
||||
```bash
|
||||
# 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`):
|
||||
|
||||
```bash
|
||||
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
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ async def test_suggest_falls_back_to_yandex_when_cadastral_empty() -> None:
|
|||
return_value=[yandex_result],
|
||||
) as mock_yandex,
|
||||
):
|
||||
mock_settings.yandex_geocoder_key = "fake-key"
|
||||
mock_settings.yandex_geocoder_api_key = "fake-key"
|
||||
results = await suggest("Малышева", db=db, limit=8)
|
||||
|
||||
assert len(results) == 1
|
||||
|
|
|
|||
|
|
@ -36,13 +36,22 @@ services:
|
|||
backend:
|
||||
image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest}
|
||||
container_name: tradein-backend
|
||||
# .env.runtime пишется вручную на VPS (см. DEPLOY.md / scripts/README.md).
|
||||
# Контейнер читает YANDEX_GEOCODER_API_KEY, COOKIE_ENCRYPTION_KEY и пр. из
|
||||
# этого файла — без него backfill scripts падают с "key required".
|
||||
# required: false — compose не падает если файла нет (первый деплой).
|
||||
env_file:
|
||||
- path: ./backend/.env.runtime
|
||||
required: false
|
||||
environment:
|
||||
DATABASE_URL: "postgresql+psycopg://${TRADEIN_POSTGRES_USER:-tradein}:${TRADEIN_POSTGRES_PASSWORD}@postgres:5432/tradein"
|
||||
PUBLIC_URL: "https://gendsgn.ru/trade-in"
|
||||
CORS_ORIGINS: '["https://gendsgn.ru"]'
|
||||
ENVIRONMENT: "production"
|
||||
CONTACT_EMAIL: "${TRADEIN_CONTACT_EMAIL:-tradein@gendsgn.ru}"
|
||||
YANDEX_GEOCODER_KEY: "${YANDEX_GEOCODER_KEY:-}"
|
||||
# Yandex Geocoder API key — читается из backend/.env.runtime (env_file).
|
||||
# Раньше дублировалось здесь как ${YANDEX_GEOCODER_API_KEY:-} — убрано
|
||||
# т.к. environment: overrides env_file и при пустой host-env стирает значение.
|
||||
# Защита /api/v1/admin/* — задаётся в .env.runtime. Пусто = открыто (dev).
|
||||
ADMIN_TOKEN: "${ADMIN_TOKEN:-}"
|
||||
# GlitchTip DSN — мониторинг ошибок (#396). Пусто = выключено.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue