"""Нормализация repair_state → каноничный enum для listings.repair_state. Целевой enum: needs_repair / standard / good / excellent. Используется во всех инgest-путях: avito_detail, cian SERP, cian_detail. Источники raw-значений: - Avito HTML: «Ремонт: евро» → avito_detail._REPAIR_MAP → «euro» → good - Cian SERP JSON: offer.decoration → «euro» / «cosmetic» / «without» / ... - Cian Detail JSON: offer.repairType → «cosmetic» / «design» / «no» / «euro» / ... Маппинг основан на _IMV_REPAIR_MAP из estimator.py (обратная форма): needs_repair→«required», standard→«cosmetic», good→«euro», excellent→«designer». Связь с estimator._REPAIR_COEF: needs_repair=0.94 / standard=1.00 / good=1.05 / excellent=1.10 """ from __future__ import annotations import logging logger = logging.getLogger(__name__) # Все известные raw-значения с источником в комментарии _RAW_TO_ENUM: dict[str, str] = { # ── needs_repair ──────────────────────────────────────────────────────────── "required": "needs_repair", # Avito: «требуется» / «без ремонта» (через _REPAIR_MAP) "without": "needs_repair", # Cian: без ремонта "no": "needs_repair", # Cian: нет ремонта (синоним without) "rough": "needs_repair", # Cian/общий: черновая отделка # ── standard ──────────────────────────────────────────────────────────────── "cosmetic": "standard", # Avito/Cian: косметический # ── good ──────────────────────────────────────────────────────────────────── "euro": "good", # Avito/Cian: евро "fine": "good", # Cian: хорошая отделка (предчистовая / whitebox) # ── excellent ─────────────────────────────────────────────────────────────── "designer": "excellent", # Avito: дизайнерский "design": "excellent", # Cian: дизайнерский (camelCase вариант) } # Допустимые значения целевого enum — для pass-through защиты _VALID_ENUM: frozenset[str] = frozenset({"needs_repair", "standard", "good", "excellent"}) def normalize_repair_state(raw: str | None) -> str | None: """Преобразовать raw repair-значение в каноничный enum. Если raw уже является корректным enum-значением — вернуть as-is (идемпотентно). Если raw не распознан — вернуть None + залогировать WARNING один раз. Args: raw: сырое значение из парсера (e.g. «euro», «without», «design») или None. Returns: Одно из needs_repair / standard / good / excellent, либо None. """ if raw is None: return None # Pass-through: уже нормализованный enum (idempotency — нужно при ре-обработке) if raw in _VALID_ENUM: return raw result = _RAW_TO_ENUM.get(raw) if result is None: logger.warning("repair_state_normalizer: unknown raw value %r — stored as NULL", raw) return result