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"
''
""
)