115 lines
4.5 KiB
Python
115 lines
4.5 KiB
Python
"""Celery task: синхронизация макро-показателей ЦБ (ключевая ставка) в
|
||
``macro_indicator`` (#945 PR B, GG-форсайт / Site Finder v2).
|
||
|
||
Тянет историю ключевой ставки через ``fetch_key_rate`` (живой SOAP CBR
|
||
DailyInfoWebServ) и апсертит каждую (дату, ставку) в ``macro_indicator`` как
|
||
ряд ``indicator_type='key_rate'``, ``region='rf'``, ``source='cbr'``.
|
||
|
||
Детерминированно, без LLM. ON CONFLICT делает повторные прогоны идемпотентными
|
||
(re-run обновляет value + updated_at). Расписание — еженедельно (ключевая
|
||
ставка меняется редко), регистрируется в beat_schedule.py.
|
||
|
||
Mirror conventions: ``SessionLocal()`` + try/finally close, ``logger`` (не
|
||
print), SAVEPOINT per-row в цикле upsert (backend.md), ``CAST(:x AS type)`` —
|
||
никогда ``:x::type`` (psycopg v3). На fetch-фейле — логируем и пробрасываем
|
||
(surfaces в Celery/GlitchTip), не глотаем.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from datetime import date
|
||
from decimal import Decimal
|
||
from typing import Any
|
||
|
||
from sqlalchemy import text
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.db import SessionLocal
|
||
from app.services.scrapers.cbr_macro import fetch_key_rate
|
||
from app.workers.celery_app import celery_app
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# psycopg v3: CAST(:x AS type) — НИКОГДА :x::type (SQLAlchemy+psycopg3 роняет
|
||
# синтаксис на ::). Контракт колонок совпадает с macro_indicator (migration 123):
|
||
# (indicator_type, region, obs_date, value, source, frequency, unit, comment,
|
||
# updated_at) PK (indicator_type, region, obs_date).
|
||
UPSERT_KEY_RATE_SQL = text(
|
||
"""
|
||
INSERT INTO macro_indicator (
|
||
indicator_type, region, obs_date, value,
|
||
source, frequency, unit, comment
|
||
) VALUES (
|
||
'key_rate', 'rf', CAST(:d AS date), CAST(:v AS numeric),
|
||
'cbr', 'daily', '%', 'CBR key rate'
|
||
)
|
||
ON CONFLICT (indicator_type, region, obs_date) DO UPDATE SET
|
||
value = EXCLUDED.value,
|
||
updated_at = now()
|
||
"""
|
||
)
|
||
|
||
|
||
def _upsert_key_rate(db: Session, rows: list[tuple[date, Decimal]]) -> int:
|
||
"""Апсертит (date, rate) в macro_indicator. SAVEPOINT per-row, чтобы один
|
||
битый ряд не откатывал всю транзакцию. Возвращает число успешных upsert'ов."""
|
||
upserted = 0
|
||
for d, v in rows:
|
||
try:
|
||
with db.begin_nested():
|
||
db.execute(UPSERT_KEY_RATE_SQL, {"d": d, "v": v})
|
||
upserted += 1
|
||
except Exception as e:
|
||
logger.warning("upsert key_rate %s=%s failed: %s", d, v, e)
|
||
db.commit()
|
||
return upserted
|
||
|
||
|
||
@celery_app.task(
|
||
bind=True,
|
||
name="tasks.cbr_macro_sync.cbr_macro_sync",
|
||
max_retries=2,
|
||
)
|
||
def cbr_macro_sync(
|
||
self: Any,
|
||
from_date: str | None = None,
|
||
to_date: str | None = None,
|
||
) -> dict[str, Any]:
|
||
"""Загрузить историю ключевой ставки ЦБ и апсертить в macro_indicator.
|
||
|
||
Args:
|
||
from_date: ISO-дата начала периода ('YYYY-MM-DD'). None → дефолт
|
||
scraper'а (2019-01-01, полный бэкфилл).
|
||
to_date: ISO-дата конца периода. None → сегодня.
|
||
|
||
Returns:
|
||
Счётчики ``{"fetched": N, "upserted": M, "from_date": ..., "to_date": ...}``.
|
||
|
||
Raises:
|
||
Пробрасывает любую ошибку fetch'а (CBRScraperError и пр.) — чтобы фейл
|
||
был виден в Celery / GlitchTip, а не молча проглочен.
|
||
"""
|
||
fd = date.fromisoformat(from_date) if from_date else None
|
||
td = date.fromisoformat(to_date) if to_date else None
|
||
|
||
db = SessionLocal()
|
||
try:
|
||
rows = fetch_key_rate(from_date=fd, to_date=td)
|
||
fetched = len(rows)
|
||
logger.info("cbr_macro_sync: fetched %d key_rate rows", fetched)
|
||
|
||
upserted = _upsert_key_rate(db, rows)
|
||
logger.info("cbr_macro_sync: upserted %d/%d key_rate rows", upserted, fetched)
|
||
|
||
return {
|
||
"fetched": fetched,
|
||
"upserted": upserted,
|
||
"from_date": fd.isoformat() if fd else None,
|
||
"to_date": td.isoformat() if td else None,
|
||
}
|
||
except Exception as e:
|
||
logger.exception("cbr_macro_sync failed: %s", e)
|
||
raise
|
||
finally:
|
||
db.close()
|