gendesign/backend/app/core/config.py
lekss361 7c05d0a0d8 feat(objective): full sync pipeline + dynamic admin config
Objective API (api.objctv.ru) интегрирован как новый source of truth для
per-flat данных по новостройкам УрФО. Заменяет промежуточный Anton-SQLite
(legacy bootstrap-ETL остался как fallback в свёрнутом блоке UI).

Schema (data/sql/68_v2 — applied на проде, 6 таблиц):
- objective_lots          (UPSERT по lot_id; 303 677 rows)
- objective_corpus_room_month (long-формат месяц×корпус×room_bucket; 19 738 rows)
- objective_lots_history  (append-only weekly snapshots для elasticity)
- objective_complex_mapping (Objective ComplexName ↔ domrf_kn_objects.obj_id)
- objective_raw_reports   (jsonb страховка на смену схемы API)
- objective_scrape_runs   (журнал прогонов)
+ data/sql/72_objective_sync_config (single-row динамический конфиг)

Backend:
- services/scrapers/objective.py: ObjectiveClient — Bearer-токен (Redis +
  in-memory fallback), retry на 401/429/5xx, Retry-After header support
- services/objective_etl.py: ETL SQLite Антона → PG (legacy)
- services/objective_sync_config.py: read/update single-row config
- workers/tasks/scrape_objective.py:
  * sync_objective_group: 2 рабочих отчёта (corp_sum, lots_pf), inline-парсинг
  * sync_all_groups: wrapper, перебирает группы из БД-конфига с
    inter-group паузой; PATCH-merge explicit args > DB config
- workers/tasks/objective_etl.py: Celery task для legacy bootstrap
- workers/celery_app.py: beat читает cron из БД при старте (fallback на env)
- api/v1/admin_scrape.py: 5 новых endpoints для /objective/*

Frontend (frontend/src/app/admin/scrape/objective/page.tsx):
- PRIMARY blue блок «🌐 Наш sync» с input для override групп
- Collapsible «⚙️ Настройки» с формой (cron + 8 параметров) → PUT в БД
- Coverage-панель с PG counts + строка про SQLite Антона как legacy
- Collapsible «🛠 Bootstrap ETL» — legacy-инструмент

Beat schedule: вторник 06:00 МСК, ~10-15 мин на 4 группы (Свердл.обл +
Челябинск + Тюмень + Пермь = ~700K квартир УрФО). Расписание и параметры
меняются через админку без редеплоя (cron требует restart beat).

Эмпирические находки об API (probe 2026-05-10):
- 13 из 21 проверенных group_name доступны на тарифе (включая «Свердловская
  область», «Челябинск», «Тюмень», «Пермь», но НЕ «Свердловская обл» —
  формат имени строгий)
- ComplexName требует БЕЗ префикса «ЖК» и БЕЗ кавычек
- Поле «Банк» для всех 303k = NULL (тариф не отдаёт), но «Тип обременения»
  работает (36% строк = ипотека) → ipoteka_share возможен, банковская
  атрибуция — нет

Docker:
- bind-mount /opt/gendesign/site-finder:/data/anton-sqlite:ro в worker

GitHub backlog: добавлены #22-25 (recommend_mix v3 — 4 сабтаска по
улучшению алгоритма на основе фидбэка про POI / границы районов /
конкурентов / окно данных / success-driven mix).

Knowledge graph (memory/memory-gendesign.jsonl): обновлены entities
Objective_Integration_May07_2026, Schema_Objective_v2_May07,
Objective_API_Findings_May07, Module_Objective_Client + новый
Session_End_May07_2026.

TODO для прода: прописать OBJECTIVE_API_KEY=<key> в backend/.env +
docker compose restart worker beat.
2026-05-10 19:54:15 +03:00

89 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"]
sentry_dsn: str | None = None
# Release tag для Sentry — обычно git short sha, проставляется
# deploy.yml в backend/.env.runtime (см. workflow). Локально оставляем
# пустым — Sentry припишет 'unknown'.
sentry_release: str | None = None
environment: str = "dev"
# 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:0005: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 schedule (кадастровые кварталы / здания).
# По умолчанию: 20-е число февраля/мая/августа/ноября в 03:30 МСК
# (после квартальных публикаций rosreestr_deals).
scrape_nspd_cron: str = "30 3 20 2,5,8,11 *"
# Регионы для NSPD-sweep (comma-separated rosreestr region codes).
scrape_nspd_default_regions: str = "66"
# Пауза между NSPD-запросами в мс. <600мс — высокий риск WAF 403.
scrape_nspd_rate_ms: int = 600
# 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=<KEY> → JSON с Bearer-токеном
# 2. GET /v2/Report/GetReport?... + Authorization: Bearer <token>
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"
# 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"
settings = Settings()