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).
131 lines
5.2 KiB
Python
131 lines
5.2 KiB
Python
"""Тесты 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)]
|