gendesign/backend/tests/scrapers/test_cbr_macro.py
Light1YT ea42d29a4a feat(site-finder): live CBR key-rate scraper → macro_indicator (#945 PR B)
Site Finder v2 / GG-форсайт. Deterministic, no LLM. Fills the key_rate series
(empty after PR A) into macro_indicator from the live CBR source.

- services/scrapers/cbr_macro.py: fetch_key_rate via CBR SOAP DailyInfoWebServ
  KeyRateXML(fromDate, ToDate) (POST soap+xml; ToDate capital-T + dateTime
  suffix required — flat GET returns empty). Pure parse_key_rate_xml (TZ-safe
  date, empty/malformed → []). Live-verified: 85 rows, current rate 14.5%.
- workers/tasks/cbr_macro_sync.py: upsert (key_rate, rf, daily, %) into
  macro_indicator ON CONFLICT DO UPDATE; SAVEPOINT per row; fetch-fail → raise.
- beat: cbr-macro-sync-weekly (Mon 05:30 МСК, Europe/Moscow tz).
- 15 tests (pure parse + mocked upsert). No migration (table from #963 PR A).
2026-06-03 00:05:25 +05:00

131 lines
5.2 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.

"""Тесты pure-парсера CBR KeyRate (offline, без живой сети).
Покрывают: парсинг (date, value), обработку tz-суффикса, сортировку,
пустой / битый / без-KR XML → []. Фикстуры — из реального формата
KeyRateXML-ответа DailyInfoWebServ (verified live 2026-06-02).
"""
from __future__ import annotations
from datetime import date
from app.services.scrapers.cbr_macro import parse_key_rate_xml
# Реальный shape KeyRateXML (без diffgram). 3 строки, descending по дате,
# tz-суффикс +03:00, точка как десятичный разделитель.
FIXTURE_KEYRATE_XML = (
'<?xml version="1.0" encoding="utf-8"?>'
'<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">'
"<soap:Body>"
'<KeyRateXMLResponse xmlns="http://web.cbr.ru/">'
"<KeyRateXMLResult>"
'<KeyRate xmlns="">'
"<KR><DT>2023-09-15T00:00:00+03:00</DT><Rate>12.00</Rate></KR>"
"<KR><DT>2023-08-15T00:00:00+03:00</DT><Rate>12.00</Rate></KR>"
"<KR><DT>2023-08-14T00:00:00+03:00</DT><Rate>8.50</Rate></KR>"
"</KeyRate>"
"</KeyRateXMLResult>"
"</KeyRateXMLResponse>"
"</soap:Body>"
"</soap:Envelope>"
)
# Diffgram-вариант (метод KeyRate) — парсер должен его тоже понимать (находит KR
# где угодно), несмотря на schema-блок и namespace на KR.
FIXTURE_KEYRATE_DIFFGRAM = (
'<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">'
"<soap:Body><KeyRateResponse xmlns=\"http://web.cbr.ru/\"><KeyRateResult>"
'<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"'
' xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">'
'<KeyRate xmlns="">'
'<KR diffgr:id="KR1" msdata:rowOrder="0">'
"<DT>2023-09-29T00:00:00+03:00</DT><Rate>13.00</Rate></KR>"
'<KR diffgr:id="KR2" msdata:rowOrder="1">'
"<DT>2023-09-15T00:00:00+03:00</DT><Rate>12.00</Rate></KR>"
"</KeyRate></diffgr:diffgram></KeyRateResult></KeyRateResponse>"
"</soap:Body></soap:Envelope>"
)
def test_parse_basic_rows() -> None:
"""3 KR-строки → 3 кортежа (date, float), отсортированы по возрастанию даты."""
rows = parse_key_rate_xml(FIXTURE_KEYRATE_XML)
assert rows == [
(date(2023, 8, 14), 8.5),
(date(2023, 8, 15), 12.0),
(date(2023, 9, 15), 12.0),
]
def test_parse_returns_sorted_ascending() -> None:
"""Вход descending — выход обязан быть ascending по дате."""
rows = parse_key_rate_xml(FIXTURE_KEYRATE_XML)
dates = [d for d, _ in rows]
assert dates == sorted(dates)
def test_parse_strips_timezone_suffix() -> None:
"""DT с tz-суффиксом '+03:00' парсится в чистую дату (без сдвига)."""
rows = parse_key_rate_xml(FIXTURE_KEYRATE_XML)
assert (date(2023, 8, 15), 12.0) in rows
# значение float, не строка
assert all(isinstance(v, float) for _, v in rows)
def test_parse_value_types() -> None:
"""Дробная ставка 8.50 → 8.5 (float)."""
rows = dict(parse_key_rate_xml(FIXTURE_KEYRATE_XML))
assert rows[date(2023, 8, 14)] == 8.5
def test_parse_diffgram_variant() -> None:
"""Diffgram-обёртка (метод KeyRate) тоже парсится — KR находится в любом
namespace / вложенности."""
rows = parse_key_rate_xml(FIXTURE_KEYRATE_DIFFGRAM)
assert rows == [
(date(2023, 9, 15), 12.0),
(date(2023, 9, 29), 13.0),
]
def test_parse_empty_string() -> None:
assert parse_key_rate_xml("") == []
assert parse_key_rate_xml(" ") == []
def test_parse_empty_response_no_kr() -> None:
"""Валидный, но пустой KeyRateXMLResponse (период без данных) → []."""
xml = (
'<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">'
'<soap:Body><KeyRateXMLResponse xmlns="http://web.cbr.ru/" />'
"</soap:Body></soap:Envelope>"
)
assert parse_key_rate_xml(xml) == []
def test_parse_malformed_xml() -> None:
"""Битый XML → [] (не бросает)."""
assert parse_key_rate_xml("<KeyRate><KR><DT>2023") == []
def test_parse_skips_kr_with_missing_fields() -> None:
"""KR без DT или без Rate пропускается; валидные остаются."""
xml = (
"<KeyRate>"
"<KR><DT>2023-08-15T00:00:00+03:00</DT><Rate>12.00</Rate></KR>"
"<KR><DT>2023-08-16T00:00:00+03:00</DT></KR>" # нет Rate
"<KR><Rate>11.00</Rate></KR>" # нет DT
"</KeyRate>"
)
assert parse_key_rate_xml(xml) == [(date(2023, 8, 15), 12.0)]
def test_parse_dedups_by_date_last_wins() -> None:
"""Дубли по дате схлопываются — последний выигрывает."""
xml = (
"<KeyRate>"
"<KR><DT>2023-08-15T00:00:00+03:00</DT><Rate>12.00</Rate></KR>"
"<KR><DT>2023-08-15T00:00:00+03:00</DT><Rate>13.00</Rate></KR>"
"</KeyRate>"
)
assert parse_key_rate_xml(xml) == [(date(2023, 8, 15), 13.0)]