admin scrape-эндпоинты фетчили произвольный *_url без host-проверки (SSRF defense-in-depth поверх RBAC). _assert_allowed_url(url): абсолютный URL с не-allowlist хостом → 400; относительный путь → ok (хост подставляется источником). Guard в 5 хендлерах (avito_house/avito_detail/yandex_detail/ cian_detail/cian_newbuilding). scrape_allowed_hosts в config (сверено со scrapers/*.py — добавлены ekb.cian.ru, ekaterinburg.n1.ru). ADMIN_TOKEN (0 чтений в коде) убран из docker-compose.prod.yml + DEPLOY.md. main.py RBAC не тронут. 11 тестов pass, ruff clean.
128 lines
8.9 KiB
Python
128 lines
8.9 KiB
Python
"""Минимальный settings для standalone trade-in MVP."""
|
||
|
||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||
|
||
|
||
class Settings(BaseSettings):
|
||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
||
|
||
# required — задаётся через env DATABASE_URL. Нет дефолта: fail-fast при старте
|
||
# если переменная не задана (C-3 security audit).
|
||
database_url: str
|
||
cors_origins: list[str] = ["http://localhost", "http://localhost:3000", "http://localhost:8080"]
|
||
environment: str = "dev"
|
||
|
||
# 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"
|
||
|
||
# Public URL — для QR-кода в PDF, shareable links, etc.
|
||
public_url: str = "http://127.0.0.1:8080"
|
||
|
||
# GlitchTip DSN — мониторинг ошибок (Sentry-совместимый). #396.
|
||
# Пусто = мониторинг выключен (dev). В prod — env GLITCHTIP_DSN из .env.runtime.
|
||
glitchtip_dsn: str | None = None
|
||
|
||
# Ключ шифрования для pgp_sym_encrypt (Cian session cookies).
|
||
# Задаётся через env COOKIE_ENCRYPTION_KEY. Пусто = шифрование не работает.
|
||
cookie_encryption_key: str = ""
|
||
|
||
# Redis URL для hot-cache (Phase 3.2). Задаётся через env REDIS_URL.
|
||
redis_url: str = "redis://localhost:6379/0"
|
||
|
||
# Rate-limit публичного /api/* (per-IP sliding window). ENV: RATE_LIMIT,
|
||
# RATE_LIMIT_WINDOW_S. Не более rate_limit запросов за rate_limit_window_s
|
||
# секунд с одного IP. Аутентифицированный трафик (X-Authenticated-User от
|
||
# Caddy basic_auth) не лимитируется — см. ratelimit.py (#655).
|
||
rate_limit: int = 300
|
||
rate_limit_window_s: float = 60.0
|
||
|
||
# Password for tradein_fdw_reader role — used by backend startup to create/refresh
|
||
# USER MAPPING for postgres_fdw → gendesign DB (gendesign_remote server).
|
||
# Пусто = USER MAPPING не создаётся, gendesign_cad_buildings не работает (dev).
|
||
gendesign_fdw_password: str | None = None
|
||
|
||
# DaData /clean/address — обогащение target-адреса канонической формой,
|
||
# kadastr_num, ФИАС, координатами, метро. Используется в estimator для
|
||
# on-demand enrichment (PR Q1). Demo tier: 100 req/день. Если хотя бы один
|
||
# не задан — service возвращает None gracefully, estimator продолжает.
|
||
# ENV: DADATA_API_TOKEN, DADATA_API_SECRET.
|
||
dadata_api_token: str | None = None
|
||
dadata_api_secret: str | None = None
|
||
|
||
# ── #651: IMV / Yandex blend (killer accuracy fix) ──────────────────────
|
||
# Радиусная медиана ₽/м² системно недооценивает премиум/видовые квартиры
|
||
# (нет class/segment/IMV-коррекции → premium ~2x underestimate, case 50М vs
|
||
# факт ~100М). Если внешний якорь (Avito IMV recommended_price из
|
||
# house_imv_evaluations, либо Yandex sale) выше нашей медианы более чем в
|
||
# `threshold` раз — подмешиваем якорь к медиане с весом `weight` и
|
||
# расширяем верх диапазона. ОДНОНАПРАВЛЕННО: только повышаем (баг — занижение).
|
||
# Полностью за флагами — безопасно выкатить до демо; при отсутствии IMV/Yandex
|
||
# no-op (медиана не меняется).
|
||
estimate_imv_blend_enabled: bool = True
|
||
estimate_imv_blend_weight: float = 0.5 # вес якоря в blend: median*(1-w)+A*w
|
||
estimate_imv_blend_threshold: float = 1.15 # якорь должен быть > медианы ×1.15
|
||
|
||
# ── #651/#652 v2: same-building anchor (validated, 55 golden cases) ──────────
|
||
# Радиусная медиана размывает премию дома/ЖК → премиум ~2.5x недооценка,
|
||
# комфорт −15-25%. v2 берёт PRIMARY якорь из комплов ТОГО ЖЕ ДОМА (Tier A),
|
||
# similarity-weighted по площади/комнатам, premium-uplift к ~p70 для топ-юнита
|
||
# дома, asking→sold haircut (banded по ppm²), hard guardrail (est ≥ min-comp×0.95)
|
||
# и tighter FSD-диапазон. Полностью за флагом — OFF ⇒ точно текущее поведение.
|
||
# Спек+KPI: vault inbox 2026-05-30 tradein-valuation-algorithm-v2.
|
||
estimate_same_building_anchor_enabled: bool = True
|
||
estimate_sb_min_comps: int = 2 # стоп на первом тире с ≥ N активных комплов
|
||
estimate_sb_area_sigma: float = 0.18 # σ log-нормального area-веса (Gaussian)
|
||
estimate_sb_rooms_match_boost: float = 1.6 # ×вес если rooms компла == target
|
||
# #680-WB within-building heterogeneity refine: floor-similarity Gaussian по
|
||
# ОТНОСИТЕЛЬНОЙ вертикальной позиции (floor/total_floors). Прижимает якорь к
|
||
# комплам с похожим этажом — мелкокомнатный/нижний юнит во флагман-доме больше
|
||
# не наследует цену видового топ-этажа. 0.0 → выключено (точно старое поведение).
|
||
# Откалибровано на 55 golden (offline): σ_f=0.25 даёт лучший medAPE без потери
|
||
# покрытия; Хохрякова 3к/153 overshoot 64%→1.5%, флагман 4к 17.5%→5.4%.
|
||
estimate_sb_floor_sigma: float = 0.25
|
||
estimate_sb_guardrail_tol: float = 0.05 # hard floor: est ≥ min(comp ppm²)×(1−tol)
|
||
asking_to_sold_haircut: float = 0.05 # дефолтная asking→sold скидка (banded по ppm²)
|
||
estimate_fsd_k: float = 1.65 # множитель FSD → полуширина диапазона
|
||
|
||
# ── Estimate enrichment time-budgets (#654) ──────────────────────────────
|
||
# POST /estimate делает несколько ПОСЛЕДОВАТЕЛЬНЫХ блокирующих сетевых
|
||
# вызовов (geocode → Overpass → Yandex valuation → IMV → Cian). Yandex
|
||
# valuation (внутренний httpx timeout 30s) НЕ gated на наличие floor и
|
||
# выполняется на каждой оценке — главный подозреваемый на gateway-таймаут
|
||
# (Caddy 502/504). Эти budget'ы оборачивают самые медленные ungated-вызовы
|
||
# в asyncio.wait_for(): при превышении источник деградирует в None (тот же
|
||
# graceful-путь что и сетевая ошибка), а НЕ роняет весь /estimate в 5xx.
|
||
# Держать суммарный budget ниже Caddy read/write timeout (см.
|
||
# deploy/Caddyfile.tradein-fragment). ENV: ESTIMATE_YANDEX_VALUATION_TIMEOUT_S,
|
||
# ESTIMATE_GEOCODE_BUDGET_S, ESTIMATE_HOUSE_META_TIMEOUT_S.
|
||
estimate_yandex_valuation_timeout_s: float = 8.0
|
||
estimate_geocode_budget_s: float = 12.0
|
||
estimate_house_meta_timeout_s: float = 8.0
|
||
|
||
# Лимит успешных оценок trade-in за календарный месяц на аккаунт (#658).
|
||
# Конфигурируется через env ESTIMATE_QUOTA_LIMIT. Default 15.
|
||
estimate_quota_limit: int = 15
|
||
|
||
# SSRF-защита для admin scrape endpoints (#756).
|
||
# Список хостов которым разрешено передавать абсолютные URL в параметрах *_url.
|
||
# Относительные пути (без netloc) проходят без проверки — хост подставляется
|
||
# фиксированным в самом scraper'е. Задаётся через env SCRAPE_ALLOWED_HOSTS
|
||
# (comma-separated). По умолчанию — все хосты используемые scrapers/*.py.
|
||
scrape_allowed_hosts: set[str] = {
|
||
"www.avito.ru",
|
||
"avito.ru",
|
||
"www.cian.ru",
|
||
"cian.ru",
|
||
"ekb.cian.ru",
|
||
"realty.yandex.ru",
|
||
"ekaterinburg.n1.ru",
|
||
"n1.ru",
|
||
}
|
||
|
||
|
||
settings = Settings()
|