diff --git a/backend/app/services/scrapers/rosstat_emiss.py b/backend/app/services/scrapers/rosstat_emiss.py index 181f4a8b..14b633bf 100644 --- a/backend/app/services/scrapers/rosstat_emiss.py +++ b/backend/app/services/scrapers/rosstat_emiss.py @@ -298,9 +298,7 @@ def _parse_value(raw: str) -> Decimal | None: raw = (raw or "").strip() if not raw or raw in {"-", "—", "…", "..."}: return None - cleaned = ( - raw.replace(" ", "").replace(" ", "").replace(" ", "").replace(",", ".") - ) + cleaned = raw.replace(" ", "").replace(" ", "").replace(" ", "").replace(",", ".") try: return Decimal(cleaned) except (InvalidOperation, ValueError): @@ -399,9 +397,7 @@ def parse_dataset_rows( # ── HTTP fetch (thin) ─────────────────────────────────────────────────────────── -def _http_get( - client: httpx.Client, url: str, *, retries: int = DEFAULT_RETRIES -) -> bytes: +def _http_get(client: httpx.Client, url: str, *, retries: int = DEFAULT_RETRIES) -> bytes: """GET с ретраями (сеть / 5xx) → байты тела. Module-level для monkeypatch в тестах. Raises: @@ -622,9 +618,7 @@ _MONTH_NAME_TO_NUM: dict[str, int] = { "декабрь": 12, } # Годовой под-период (некоторые индикаторы): «значение показателя за год», «год». -_YEAR_PERIODS: frozenset[str] = frozenset( - {"год", "значение показателя за год", "за год"} -) +_YEAR_PERIODS: frozenset[str] = frozenset({"год", "значение показателя за год", "за год"}) @dataclass(frozen=True) @@ -704,6 +698,25 @@ def _emiss_period_to_month(period: str) -> int | None: return None +def _emiss_period_granularity(period: str) -> str: + """PERIOD-строку fedstat → гранулярность ('year' | 'quarter' | 'month'). + + Используется как третья компонента ключа дедупликации в parse_emiss_sdmx, чтобы + годовой ряд ('год') и квартальный Q1 ('I квартал') не перезаписывали друг друга при + одинаковых (region, obs_date=YYYY-01-01) (#1606). + Возвращает 'unknown' для нераспознанных под-периодов (на практике такие ряды + пропускаются ещё до этого вызова через _emiss_period_to_month → None). + """ + p = (period or "").strip().lower() + if p in _YEAR_PERIODS: + return "year" + if p in _QUARTER_START_MONTH: + return "quarter" + if p in _MONTH_NAME_TO_NUM: + return "month" + return "unknown" + + def _series_concepts(series: ET.Element) -> tuple[dict[str, str], dict[str, str]]: """Извлечь (SeriesKey, Attributes) concept→value словари из . @@ -736,7 +749,10 @@ def parse_emiss_sdmx(raw: bytes | str, spec: EmissIndicatorSpec) -> list[EmissRo не среди spec.targets — ряд пропускается. PERIOD (Attributes) → стартовый месяц под-периода; год — из ' "" '' '' "" - '2024' + "2024" '' "" ) @@ -179,7 +230,7 @@ def test_comma_decimal_value_parsed() -> None: '' '' "" - '2024' + "2024" '' "" ) @@ -199,13 +250,13 @@ def test_dedup_last_wins() -> None: '' '' "" - '2024' + "2024" '' "" '' '' "" - '2024' + "2024" '' "" )