feat(tradein): persistent infra для backfill scripts (PR F) #592
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"]
|
CORS_ORIGINS=["http://localhost:8080","http://localhost:3000"]
|
||||||
ENVIRONMENT=dev
|
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) ===
|
# === Frontend (build-time) ===
|
||||||
# Backend URL для Next.js rewrites (browser обращается к /api/* — Next.js проксирует на BACKEND_URL/api/*).
|
# Backend URL для Next.js rewrites (browser обращается к /api/* — Next.js проксирует на BACKEND_URL/api/*).
|
||||||
# В docker compose это уже выставлено в http://backend:8000 — менять не нужно.
|
# В docker compose это уже выставлено в http://backend:8000 — менять не нужно.
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,23 @@ import /opt/gendesign/tradein-mvp/deploy/Caddyfile.tradein-fragment
|
||||||
|
|
||||||
## Что нужно положить в `.env.runtime` (на сервере, НЕ в git)
|
## Что нужно положить в `.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
|
```bash
|
||||||
# /opt/gendesign/tradein-mvp/.env.runtime
|
# /opt/gendesign/tradein-mvp/.env.runtime
|
||||||
TRADEIN_POSTGRES_USER=tradein
|
TRADEIN_POSTGRES_USER=tradein
|
||||||
TRADEIN_POSTGRES_PASSWORD=<сгенерировать openssl rand -hex 32>
|
TRADEIN_POSTGRES_PASSWORD=<сгенерировать openssl rand -hex 32>
|
||||||
TRADEIN_CONTACT_EMAIL=tradein@gendsgn.ru
|
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).
|
# Encryption key for Cian session cookies (pgp_sym_encrypt / Stage 9 Calculator).
|
||||||
# Empty = Valuation Calculator scraper disabled + /api/v1/cookies/upload returns 503.
|
# 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>
|
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
|
### Обновление `COOKIE_ENCRYPTION_KEY` на существующем VPS
|
||||||
|
|
||||||
|
|
@ -120,15 +153,20 @@ nano Caddyfile
|
||||||
# вставить ПЕРЕД последним handle { ... }:
|
# вставить ПЕРЕД последним handle { ... }:
|
||||||
# import /opt/gendesign/tradein-mvp/deploy/Caddyfile.tradein-fragment
|
# 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
|
cat > tradein-mvp/.env.runtime <<EOF
|
||||||
TRADEIN_POSTGRES_USER=tradein
|
TRADEIN_POSTGRES_USER=tradein
|
||||||
TRADEIN_POSTGRES_PASSWORD=$(openssl rand -hex 32)
|
TRADEIN_POSTGRES_PASSWORD=$(openssl rand -hex 32)
|
||||||
TRADEIN_CONTACT_EMAIL=tradein@gendsgn.ru
|
TRADEIN_CONTACT_EMAIL=tradein@gendsgn.ru
|
||||||
YANDEX_GEOCODER_KEY=
|
YANDEX_GEOCODER_API_KEY=
|
||||||
EOF
|
EOF
|
||||||
chmod 600 tradein-mvp/.env.runtime
|
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 сам)
|
# Запустить stack первый раз вручную (после этого GitHub Actions сам)
|
||||||
docker network inspect gendesign_shared >/dev/null 2>&1 || docker network create gendesign_shared
|
docker network inspect gendesign_shared >/dev/null 2>&1 || docker network create gendesign_shared
|
||||||
cd tradein-mvp
|
cd tradein-mvp
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
uv sync --no-dev
|
uv sync --no-dev
|
||||||
|
|
||||||
COPY app ./app
|
COPY app ./app
|
||||||
|
COPY scripts ./scripts
|
||||||
|
|
||||||
|
|
||||||
# ---- runner ----
|
# ---- runner ----
|
||||||
|
|
@ -48,6 +49,7 @@ WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder --chown=app:app /app/.venv /app/.venv
|
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/app /app/app
|
||||||
|
COPY --from=builder --chown=app:app /app/scripts /app/scripts
|
||||||
|
|
||||||
USER app
|
USER app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ class Settings(BaseSettings):
|
||||||
cors_origins: list[str] = ["http://localhost", "http://localhost:3000", "http://localhost:8080"]
|
cors_origins: list[str] = ["http://localhost", "http://localhost:3000", "http://localhost:8080"]
|
||||||
environment: str = "dev"
|
environment: str = "dev"
|
||||||
|
|
||||||
# Geocoder
|
# Geocoder. Env var name `YANDEX_GEOCODER_API_KEY` — consistent с scripts/
|
||||||
yandex_geocoder_key: str | None = None # 25K req/day free после регистрации
|
# 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)
|
yandex_suggest_key: str | None = None # для frontend autocomplete (proxy через backend)
|
||||||
# для User-Agent в Nominatim (Nominatim Usage Policy)
|
# для User-Agent в Nominatim (Nominatim Usage Policy)
|
||||||
contact_email: str = "erginrajpopxbe@outlook.com"
|
contact_email: str = "erginrajpopxbe@outlook.com"
|
||||||
|
|
|
||||||
|
|
@ -545,9 +545,9 @@ async def suggest(
|
||||||
if cad_results:
|
if cad_results:
|
||||||
return cad_results
|
return cad_results
|
||||||
|
|
||||||
if settings.yandex_geocoder_key:
|
if settings.yandex_geocoder_api_key:
|
||||||
try:
|
try:
|
||||||
results = await _yandex_suggest(query, settings.yandex_geocoder_key, limit)
|
results = await _yandex_suggest(query, settings.yandex_geocoder_api_key, limit)
|
||||||
if results:
|
if results:
|
||||||
return results
|
return results
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -600,9 +600,9 @@ async def geocode(address: str, db: Session) -> GeocodeResult | None:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# 3. Yandex (если есть key) с typo-fallback
|
# 3. Yandex (если есть key) с typo-fallback
|
||||||
if settings.yandex_geocoder_key:
|
if settings.yandex_geocoder_api_key:
|
||||||
try:
|
try:
|
||||||
result = await _yandex_lookup(address, settings.yandex_geocoder_key)
|
result = await _yandex_lookup(address, settings.yandex_geocoder_api_key)
|
||||||
# Если результат вне ЕКБ — пробуем typo-варианты
|
# Если результат вне ЕКБ — пробуем typo-варианты
|
||||||
in_ekb = (
|
in_ekb = (
|
||||||
result is not None
|
result is not None
|
||||||
|
|
@ -616,7 +616,7 @@ async def geocode(address: str, db: Session) -> GeocodeResult | None:
|
||||||
# Tier 2: typo-variants
|
# Tier 2: typo-variants
|
||||||
for variant in _typo_variants(address, limit=4):
|
for variant in _typo_variants(address, limit=4):
|
||||||
try:
|
try:
|
||||||
result = await _yandex_lookup(variant, settings.yandex_geocoder_key)
|
result = await _yandex_lookup(variant, settings.yandex_geocoder_api_key)
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
if result is None:
|
if result is None:
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
- Future: cron job (Celery beat если bootstrap'нут / OS cron)
|
- Future: cron job (Celery beat если bootstrap'нут / OS cron)
|
||||||
|
|
||||||
Pattern: dedup по address (1 unique address → 1 geocode call → UPDATE all listings).
|
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):
|
Отличие от /admin/geocode-missing (per-ID):
|
||||||
- Этот модуль группирует по address → меньше API calls (dedup).
|
- Этот модуль группирует по 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)
|
## Address audit + backfill (issue #582)
|
||||||
|
|
||||||
End-to-end address quality pipeline. Three scripts, two helpers, two SQL files.
|
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)
|
### `audit_address_mismatch.py` — Phase 1 baseline (PR #583)
|
||||||
|
|
||||||
Stratified-sample audit (200 EKB houses) comparing `houses.address` vs
|
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],
|
return_value=[yandex_result],
|
||||||
) as mock_yandex,
|
) 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)
|
results = await suggest("Малышева", db=db, limit=8)
|
||||||
|
|
||||||
assert len(results) == 1
|
assert len(results) == 1
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,22 @@ services:
|
||||||
backend:
|
backend:
|
||||||
image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest}
|
image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest}
|
||||||
container_name: tradein-backend
|
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:
|
environment:
|
||||||
DATABASE_URL: "postgresql+psycopg://${TRADEIN_POSTGRES_USER:-tradein}:${TRADEIN_POSTGRES_PASSWORD}@postgres:5432/tradein"
|
DATABASE_URL: "postgresql+psycopg://${TRADEIN_POSTGRES_USER:-tradein}:${TRADEIN_POSTGRES_PASSWORD}@postgres:5432/tradein"
|
||||||
PUBLIC_URL: "https://gendsgn.ru/trade-in"
|
PUBLIC_URL: "https://gendsgn.ru/trade-in"
|
||||||
CORS_ORIGINS: '["https://gendsgn.ru"]'
|
CORS_ORIGINS: '["https://gendsgn.ru"]'
|
||||||
ENVIRONMENT: "production"
|
ENVIRONMENT: "production"
|
||||||
CONTACT_EMAIL: "${TRADEIN_CONTACT_EMAIL:-tradein@gendsgn.ru}"
|
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).
|
# Защита /api/v1/admin/* — задаётся в .env.runtime. Пусто = открыто (dev).
|
||||||
ADMIN_TOKEN: "${ADMIN_TOKEN:-}"
|
ADMIN_TOKEN: "${ADMIN_TOKEN:-}"
|
||||||
# GlitchTip DSN — мониторинг ошибок (#396). Пусто = выключено.
|
# GlitchTip DSN — мониторинг ошибок (#396). Пусто = выключено.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue