Московская область считается по своему ряду Сбериндекса (#3507)
All checks were successful
Deploy Trade-In / test (push) Successful in 3m51s
Deploy Trade-In / build-backend (push) Successful in 1m21s
Deploy Trade-In / deploy (push) Successful in 1m35s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 1m40s
Deploy Trade-In / changes (push) Successful in 15s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped

This commit is contained in:
lekss361 2026-09-12 14:50:01 +00:00
parent a8ba2d5d3c
commit 5d46e59bdc
4 changed files with 59 additions and 20 deletions

View file

@ -328,6 +328,13 @@ SBER_TIME_ADJUST_REGION = "Свердловская область" # ряд р
_SBER_REGION_SERIES: dict[int, str] = {
66: SBER_TIME_ADJUST_REGION,
77: "Москва",
# Московская область — свой ряд, а не фолбэк на «Россию». Загрузчик научился
# тянуть REF_AREA=50 (services/sber_index), и 12.09.2026 ряд приехал на прод:
# real_estate_deals/«Вторичный», 116 месяцев 2017-01..2026-08 — та же глубина,
# что у Москвы и обл.66. До этой строки область считалась по общероссийскому
# ряду, то есть коридор ошибался на расхождение области со страной; теперь
# ошибка только внутрирегиональная.
50: "Московская область",
}
# Ряд для региона ВНЕ карты. Выбран общероссийский, а не отказ от поправки (1.0):

View file

@ -233,9 +233,11 @@ REGIONS: dict[int, Region] = {
),
# Тиров обогащения у области пока НЕТ ни одного: IMV/квартальный
# индекс/кадастр/POI не заведены (проверено — frozenset() пуст
# намеренно, не заглушка). Ряд Сбериндекса по области загружен в
# источник (#3498), но в карте _SBER_REGION_SERIES эстиматора кода 50
# ЕЩЁ НЕТ — тир пока недоступен, добавляется отдельным решением.
# намеренно, не заглушка). Ряд Сбериндекса по области с 12.09.2026 в
# карте _SBER_REGION_SERIES эстиматора есть, но тиром он от этого не
# становится: у Москвы набор тиров тоже пуст, а свой ряд она читает —
# поправка по времени идёт мимо enrichment_tiers (TIER_SBER_INDEX нигде
# за пределами этого реестра не спрашивают).
enrichment_tiers=frozenset(),
# Источники по области несут настоящий city (Химки, Балашиха — не
# муниципальный округ/поселение, в отличие от Москвы) — перезаписывать

View file

@ -539,16 +539,24 @@ async def test_pull_sber_indices_asking_benchmark_logged(caplog: pytest.LogCaptu
)
def test_moscow_oblast_ref_area_is_loaded_but_not_yet_used_by_estimator() -> None:
"""Область грузится заранее: ряд наполняется месяцами, задним числом не берётся.
def test_moscow_oblast_ref_area_feeds_the_estimator_under_the_same_name() -> None:
"""Имя ряда у загрузчика и у эстиматора обязано совпадать буква в букву.
Одновременно проверяем, что заведение ряда НЕ включает область в оценку и НЕ
поднимает тревогу монитора свежести: обе величины считаются от карты
эстиматора (_SBER_REGION_SERIES), а не от карты загрузчика.
Загрузчик завёл REF_AREA=50 раньше (#3498), когда карта эстиматора кода 50
ещё не знала и область считалась по «России». 12.09.2026 ряд приехал на прод
(real_estate_deals/«Вторичный», 116 месяцев 2017-01..2026-08), и код 50 попал
в `_SBER_REGION_SERIES`. С этого момента расхождение имён перестаёт быть
безобидным: эстиматор молча читал бы пустой ряд, а монитор свежести требовал
бы ряд, которого загрузчик не пишет. Тест сцепляет обе стороны по имени.
"""
from app.services.estimator import SBER_MONITORED_REGIONS, SBER_REQUIRED_REGIONS
from app.services.estimator import (
SBER_MONITORED_REGIONS,
SBER_REQUIRED_REGIONS,
sber_region_series_name,
)
from app.services.sber_index import SBER_REF_AREAS
assert SBER_REF_AREAS["50"] == "Московская область"
assert "Московская область" not in SBER_REQUIRED_REGIONS
assert "Московская область" not in SBER_MONITORED_REGIONS
assert sber_region_series_name(50) == SBER_REF_AREAS["50"]
assert "Московская область" in SBER_REQUIRED_REGIONS
assert "Московская область" in SBER_MONITORED_REGIONS

View file

@ -88,6 +88,21 @@ def test_series_names_agree_with_region_registry() -> None:
assert regions_mod.REGIONS[code].name == name
def test_resolver_maps_50_to_moscow_oblast_not_russia() -> None:
"""Область получает СВОЙ ряд, а не общероссийский фолбэк.
Загрузчик научился тянуть REF_AREA=50 (#3498), и 12.09.2026 ряд приехал на
прод: real_estate_deals/«Вторичный», 116 месяцев 2017-01..2026-08 та же
глубина, что у Москвы и обл.66. Пока кода 50 в карте не было, коридор
области считался по «России», то есть ошибался на всё расхождение области
со страной. Тест красный на прежней карте ПО ЗНАЧЕНИЮ: там вернулась бы
«Россия».
"""
assert est.sber_region_series_name(50) == "Московская область"
assert est.sber_region_series_name(50) != est.SBER_TIME_ADJUST_FALLBACK_REGION
assert "Московская область" in est.SBER_REQUIRED_REGIONS
def test_monitored_regions_cover_every_series_the_estimator_can_read() -> None:
assert set(est.SBER_MONITORED_REGIONS) == set(est._SBER_REGION_SERIES.values()) | {
est.SBER_TIME_ADJUST_FALLBACK_REGION
@ -275,8 +290,15 @@ def test_monitor_verdict_for_sverdlovsk_unchanged(monkeypatch: pytest.MonkeyPatc
def _without_moscow() -> _MonDB:
"""Источник переименовал «Москва» → ряда с прежним именем больше нет."""
return _MonDB({"Свердловская область": _JUN_2026, "Россия": _JUN_2026})
"""Источник переименовал «Москва» → ряда с прежним именем больше нет.
Остальные обязательные ряды на месте иначе тесты ниже считали бы пропажу
Москвы вместе с чужими пропажами. Строятся от карты эстиматора, чтобы новый
регион в ней не ронял эти тесты по счётчику (ровно так они и покраснели,
когда в карту приехала область).
"""
present = {r: _JUN_2026 for r in est.SBER_MONITORED_REGIONS if r != "Москва"}
return _MonDB(present)
def test_missing_moscow_does_not_suppress_the_sverdlovsk_verdict(
@ -352,7 +374,7 @@ def test_missing_fallback_series_does_not_fail_the_monitor(
) -> None:
"""«Россия» — фолбэк: сегодня по нему не считается ни одна сделка → не сбой."""
calls = _patch_runs(monkeypatch)
db = _MonDB({"Свердловская область": _JUN_2026, "Москва": _JUN_2026})
db = _MonDB({r: _JUN_2026 for r in est.SBER_REQUIRED_REGIONS})
out = mon.check_sber_freshness(db, run_id=4, params={}, now=_NOW) # type: ignore[arg-type]
assert calls["failed"] is None
assert out["regions_missing"] == 1
@ -457,15 +479,17 @@ def test_duplicate_series_name_does_not_pin_regions_missing(
def test_early_exit_fills_the_new_counters(monkeypatch: pytest.MonkeyPatch) -> None:
"""КРАСНЫЙ ДО ПРАВКИ: в раннем выходе новые ключи отдавали нули.
Пропажа ДВУХ обязательных рядов была неотличима от пропажи одного свердловского,
а найденный ряд не попадал в age_days_max вовсе.
Пропажа ВСЕХ обязательных рядов была неотличима от пропажи одного
свердловского, а найденный ряд не попадал в age_days_max вовсе. Счётчик
сверяем с длиной карты, а не с литералом: новый регион в ней не повод
этому тесту краснеть.
"""
calls = _patch_runs(monkeypatch)
db = _MonDB({est.SBER_TIME_ADJUST_FALLBACK_REGION: _JUN_2026})
out = mon.check_sber_freshness(db, run_id=11, params={}, now=_NOW) # type: ignore[arg-type]
assert calls["done"] is None
assert calls["failed"] is not None
assert out["regions_missing"] == 2
assert out["regions_missing"] == len(est.SBER_REQUIRED_REGIONS)
assert out["alert_regions_missing"] == 1 # ДО ПРАВКИ: 0
assert out["age_days_max"] == 72 # ДО ПРАВКИ: 0 — найденный ряд был не виден
# прежняя семантика раннего выхода не тронута
@ -492,9 +516,7 @@ class _MonDBDriverFault(_MonDB):
def execute(self, stmt: Any, params: dict[str, Any] | None = None) -> _Result:
if self._deactivated:
raise PendingRollbackError(
"Can't reconnect until invalid transaction is rolled back"
)
raise PendingRollbackError("Can't reconnect until invalid transaction is rolled back")
params = params or {}
if params.get("city") == self._broken:
self._deactivated = True