"""Scheduled task: pull СберИндекс city-level secondary-housing price index (#887). Delegates HTTP fetch + upsert to app.services.sber_index.pull_sber_indices(). Wired into the in-app scheduler as source='sber_index_pull'. Triggered monthly via the scrape_schedules seed (migration 093). Three dashboards fetched per configured city: - real_estate_deals (hedonic sold-price index) - residential_real_estate_prices (repeat-sales hedonic) - dinamika-tsen-obyavlenii (asking-price benchmark) NOTE: estimator time-adjust wire (step 4, #887) is OUT OF SCOPE for this module. That is a client-visible change requiring a backtest gate — see issue #887 step 4 and the follow-up note in app/services/sber_index.py. """ from __future__ import annotations import logging from dataclasses import dataclass, field from sqlalchemy.orm import Session from app.services import scrape_runs as runs_mod from app.services.sber_index import pull_sber_indices logger = logging.getLogger(__name__) __all__ = [ "SberIndexPullResult", "run_sber_index_pull", ] @dataclass class SberIndexPullResult: """Result counters from a scheduled СберИндекс pull run.""" upserted: int = 0 skipped: int = 0 errors: int = 0 duration_sec: float = field(default=0.0) async def run_sber_index_pull( db: Session, *, run_id: int, params: dict, # type: ignore[type-arg] ) -> SberIndexPullResult: """Execute СберИндекс pull with run lifecycle management. Wraps pull_sber_indices(), emitting heartbeat before the fetch and marking the scrape_run as done/failed on completion. Params (from default_params jsonb in scrape_schedules): interval_days: int — informational only (scheduler uses next_run_at arithmetic). """ import time counters: dict[str, int] = { "upserted": 0, "skipped": 0, "errors": 0, } started_at = time.monotonic() try: runs_mod.update_heartbeat(db, run_id, counters) result_counters = await pull_sber_indices(db) counters = result_counters duration_sec = time.monotonic() - started_at runs_mod.mark_done(db, run_id, {**counters, "duration_sec": int(duration_sec)}) logger.info( "sber_index_pull run_id=%d done — upserted=%d skipped=%d errors=%d %.1fs", run_id, counters["upserted"], counters["skipped"], counters["errors"], duration_sec, ) return SberIndexPullResult( upserted=counters["upserted"], skipped=counters["skipped"], errors=counters["errors"], duration_sec=duration_sec, ) except Exception as exc: logger.exception("sber_index_pull run_id=%d failed", run_id) runs_mod.mark_failed(db, run_id, str(exc)[:1000], counters) raise