gendesign/tradein-mvp/backend/app/services/scrapers/repair_state_normalizer.py
Light1YT d842811390
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 49s
Deploy Trade-In / deploy (push) Successful in 36s
refactor(tradein): normalize repair_state raw→enum at ingest (#621) (#629)
2026-05-28 15:18:11 +00:00

68 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Нормализация 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