import os import warnings from pydantic import model_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") database_url: str = "postgresql+psycopg://gendesign:gendesign@localhost:5432/gendesign" redis_url: str = "redis://localhost:6379/0" cors_origins: list[str] = ["http://localhost:3000"] # GlitchTip error tracking (Sentry-compatible self-hosted). # Формат DSN: https://@errors.gendsgn.ru/ # Пустая строка / None = SDK не инициализируется (no-op). glitchtip_dsn: str | None = None glitchtip_traces_sample_rate: float = 0.05 environment: str = "dev" # Test-mode flag (env TESTING=1). СТРОГО default False — в проде RBAC-гейт # (app/main.py rbac_guard) активен. True только в pytest (tests/conftest.py), # где запросы идут по app мимо Caddy и не несут X-Authenticated-User. testing: bool = False @model_validator(mode="after") def _promote_legacy_sentry_dsn(self) -> "Settings": """Backward-compat: legacy SENTRY_DSN → glitchtip_dsn ТОЛЬКО для self-hosted. Старый VPS .env.runtime мог содержать SENTRY_DSN=https://...@sentry.io/... (legacy SaaS Sentry). Промоутить его НЕЛЬЗЯ — события пойдут в чужой проект. Принимаем только URLs указывающие на наш errors.gendsgn.ru host. """ if not self.glitchtip_dsn: legacy = os.getenv("SENTRY_DSN") if legacy: if "errors.gendsgn.ru" in legacy: warnings.warn( "SENTRY_DSN is set but ignored by pydantic; rename to GLITCHTIP_DSN. " "Auto-promoting for backward compat.", DeprecationWarning, stacklevel=2, ) self.glitchtip_dsn = legacy else: warnings.warn( f"SENTRY_DSN points to non-GlitchTip host " f"({legacy.split('@', 1)[-1][:40]}...) — ignoring. " "Set GLITCHTIP_DSN explicitly.", UserWarning, stacklevel=2, ) return self # External APIs (Stage 2) rosreestr_pkk_base_url: str = "https://pkk.rosreestr.ru/api/features/1" overpass_url: str = "https://overpass-api.de/api/interpreter" # Scraper schedule (наш.дом.рф kn-API). # Crontab format: "minute hour day_of_month month day_of_week" (Celery crontab). # Default: каждый понедельник в окне 04:00–05:00 по МСК. scrape_kn_cron: str = "15 4 * * mon" # Random delay window in seconds added on top of scheduled start so the worker # does not hit DOM.РФ at exactly the same minute every cycle. 0 = disabled. scrape_kn_jitter_seconds: int = 1800 # Default region(s) for scheduled sweeps. Comma-separated. scrape_kn_default_regions: str = "66" # Path to a pre-captured Playwright storage_state.json (committed in repo, # used by worker to skip cold-start WAF challenge). scrape_kn_state_path: str = "data/playwright_state.json" # Token to authorize ad-hoc /api/v1/admin/scrape/* trigger calls. # Empty string = endpoint disabled. scrape_admin_token: str = "" # NSPD-scraper (Playwright) УДАЛЁН 2026-05-11. Сменён на bulk geo-fetcher # через rosreestr2coord — запускается вручную через /admin/scrape/geo. # Settings оставлены deprecated на случай отката (можно удалить позже). scrape_nspd_cron: str = "30 3 20 2,5,8,11 *" # DEPRECATED scrape_nspd_default_regions: str = "66" # DEPRECATED scrape_nspd_rate_ms: int = 600 # DEPRECATED — see nspd_lite_rate_ms # Objective.ru API (api.objctv.ru) — платная аналитика первички/вторички. # Лежит наряду с DOM.РФ kn + rosreestr CSV как 3-й источник истины с # самой богатой моделью (per-flat per-day, escrow/банк-долг, ЕГРН-ИНН). # Получить ключ: личный закрытый ApiKey от Объектива (тариф per-территория). # Tank-схема: # 1. GET /Users/User/GetToken?apiKey= → JSON с Bearer-токеном # 2. GET /v2/Report/GetReport?... + Authorization: Bearer objective_api_key: str | None = None objective_base_url: str = "https://api.objctv.ru" # Группа = «настройка территории» внутри Объектива. Чаще всего совпадает с # городом/областью. Доступные группы определяются тарифом ключа. objective_default_group: str = "Екатеринбург" # Список групп для еженедельного auto-sync (csv). Эмпирически проверено # 2026-05-10 что эти группы доступны на нашем тарифе: # - "Свердловская область" → 1626 corp_sum/3д (= ЕКБ + спутники + регион) # - "Челябинск" → 355 # - "Тюмень" → 985 # - "Пермь" → 423 # Стратегия Variant B (включить наш sync параллельно Антоновому ETL): # Антон тянет только Екб (303k lots). Наш sync даёт +30k Свердл.обл # за пределами Екб + ~700k квартир УрФО (Челябинск, Тюмень, Пермь). # Итог в PG: ~1М квартир уральского + западносибирского рынка. # Между группами task делает паузу _OBJECTIVE_INTER_GROUP_DELAY = 30с # чтобы не упереться в rate-limit Объектива. objective_sync_groups: str = "Свердловская область,Челябинск,Тюмень,Пермь" # Токен Bearer короткоживущий (предположительно 30 мин — уточнить). # Кешируем в Redis с этим TTL (с запасом 5 мин). objective_token_ttl_seconds: int = 25 * 60 # Расписание sync — еженедельно по вторникам в 06:00 МСК. # ПОЧЕМУ ВТОРНИК (а не понедельник как раньше): Антон тянет в понедельник, # к утру вторника его SQLite уже обновлён — наш sync захватывает свежий # snapshot за тот же отчётный период. objective_sync_cron: str = "0 6 * * tue" # NSPD lite (urllib через WAF) — feature toggle для переключения с # Playwright-based nspd_kn.py на минималистичный nspd_lite.py. # 2026-05-11: эмпирически проверено что urllib проходит WAF nspd.gov.ru # (TLS-fingerprint stdlib не блокируется). Старый Playwright-путь # остаётся как fallback на случай возврата WAF-проблем. use_nspd_lite: bool = True # Default rate-limit для nspd_lite fetcher (мс между запросами). nspd_lite_rate_ms: int = 600 # ETL Антоновского /sf/api/* SQLite → нашу PG (objective_* таблицы). # На проде монтируется bind-mount-ом docker-compose: # /opt/gendesign/site-finder/analysis.db -> /data/anton-sqlite/analysis.db # Триггерится вручную через POST /api/v1/admin/scrape/objective. # Для локальной разработки — указать абсолютный путь к скачанному snapshot: # OBJECTIVE_ANTON_SQLITE_PATH=C:/Users/user/source/repos/gendesign/sf_anton_snapshot.db objective_anton_sqlite_path: str = "/data/anton-sqlite/analysis.db" # OpenRouteService API (https://openrouteservice.org/dev/#/signup). # Free tier: 2000 запросов/день. Используется в /parcels/{cad}/isochrones. # Если не задан — endpoint вернёт 503 с инструкцией по регистрации. openrouteservice_api_key: str = "" # ИРД-слой в analyze (#1067 D9b «GG-форсайт»): поле `ird` в ответе analyze — # parcel_ird_overlaps (м.132, incl opportunity) + функц.зона/КРТ (геопортал WFS) + # ПЗЗ-регламент зоны (C8b). Default OFF: источники 2-3 — live-зависимость от внешнего # ЕКБ-геопортала (reverse-eng, без SLA) в hot-пути analyze. Включать после прогона # harvest на проде (B6) и заполнения zone_regulation_cache. enable_ird_analyze: bool = False settings = Settings()