fix(tradein/houses): персистить house_class из avito «Класс жилья» и yandex parse (#2003) #2359
6 changed files with 286 additions and 1 deletions
|
|
@ -86,6 +86,10 @@ EXPAND_PARAMS_MAP: dict[str, tuple[str, str]] = {
|
|||
# вернёт None (хранится только в raw_characteristics).
|
||||
"Пассажирских лифтов": ("passenger_elevators", "int"),
|
||||
"Грузовых лифтов": ("cargo_elevators", "int"),
|
||||
# Класс жилья (#2003) — ТОЛЬКО developmentPage (новостройки); housePage
|
||||
# (вторичка) этот параметр не отдаёт. Сырое значение "Комфорт"/"Бизнес"/... →
|
||||
# каноничный enum houses.house_class через _normalize_house_class.
|
||||
"Класс жилья": ("house_class", "house_class"),
|
||||
}
|
||||
|
||||
# URL-маркер новостройки: /catalog/novostroyki/... (vs вторичка /catalog/houses/...).
|
||||
|
|
@ -104,6 +108,21 @@ HOUSE_TYPE_MAP: dict[str, str] = {
|
|||
"деревянный": "wood",
|
||||
}
|
||||
|
||||
# Нормализация класса жилья из Avito "Класс жилья" в canonical houses.house_class
|
||||
# (#2003). Avito сам отдаёт только 4 значения (Комфорт/Бизнес/Премиум/Эконом —
|
||||
# live-подтверждено), но карта дополнена "Комфорт+"/"Элит" для согласованности с
|
||||
# вокабуляром yandex_helpers.parse_house_class (тот же столбец пишут оба источника).
|
||||
HOUSE_CLASS_MAP: dict[str, str] = {
|
||||
"эконом": "economy",
|
||||
"комфорт": "comfort",
|
||||
"комфорт+": "comfort_plus",
|
||||
"комфорт плюс": "comfort_plus",
|
||||
"бизнес": "business",
|
||||
"премиум": "premium",
|
||||
"элит": "elite",
|
||||
"элитный": "elite",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dataclasses
|
||||
|
|
@ -122,6 +141,7 @@ class HouseInfo:
|
|||
year_built: int | None = None
|
||||
total_floors: int | None = None
|
||||
house_type: str | None = None # нормализован: monolith/panel/brick/...
|
||||
house_class: str | None = None # нормализован: comfort/business/premium/economy/... (#2003)
|
||||
material_floors: str | None = None
|
||||
hot_water: str | None = None
|
||||
passenger_elevators: int | None = None
|
||||
|
|
@ -244,6 +264,18 @@ def _normalize_house_type(raw: str | None) -> str | None:
|
|||
return HOUSE_TYPE_MAP.get(raw.lower(), "other")
|
||||
|
||||
|
||||
def _normalize_house_class(raw: str | None) -> str | None:
|
||||
"""Нормализует класс жилья: "Комфорт" → "comfort" (#2003).
|
||||
|
||||
В отличие от _normalize_house_type, неизвестное значение → None (НЕ "other"):
|
||||
house_class используется как точный фильтр (`= ANY(...)` в search_query.py) —
|
||||
мусорный bucket-токен там бесполезен и только засоряет список значений.
|
||||
"""
|
||||
if not raw:
|
||||
return None
|
||||
return HOUSE_CLASS_MAP.get(raw.strip().lower())
|
||||
|
||||
|
||||
def _cast_expand_param(value: str, cast_type: str) -> Any:
|
||||
"""Кастует строковое значение expandParams к нужному типу."""
|
||||
if cast_type == "int":
|
||||
|
|
@ -255,6 +287,8 @@ def _cast_expand_param(value: str, cast_type: str) -> Any:
|
|||
return value.strip().lower() == "да"
|
||||
if cast_type == "house_type":
|
||||
return _normalize_house_type(value)
|
||||
if cast_type == "house_class":
|
||||
return _normalize_house_class(value)
|
||||
# str
|
||||
return value
|
||||
|
||||
|
|
@ -374,6 +408,7 @@ def _parse_house_page(widget: dict[str, Any]) -> HouseInfo:
|
|||
year_built=parsed_params.get("year_built"),
|
||||
total_floors=parsed_params.get("total_floors"),
|
||||
house_type=parsed_params.get("house_type"),
|
||||
house_class=parsed_params.get("house_class"),
|
||||
material_floors=parsed_params.get("material_floors"),
|
||||
hot_water=parsed_params.get("hot_water"),
|
||||
passenger_elevators=parsed_params.get("passenger_elevators"),
|
||||
|
|
@ -447,6 +482,7 @@ def _parse_development_page(widget: dict[str, Any]) -> HouseInfo:
|
|||
year_built=parsed_params.get("year_built"), # обычно None — дом строится
|
||||
total_floors=parsed_params.get("total_floors"),
|
||||
house_type=parsed_params.get("house_type"),
|
||||
house_class=parsed_params.get("house_class"),
|
||||
material_floors=parsed_params.get("material_floors"),
|
||||
hot_water=parsed_params.get("hot_water"),
|
||||
passenger_elevators=parsed_params.get("passenger_elevators"),
|
||||
|
|
@ -958,6 +994,9 @@ def _persist_house(db: Session, h: HouseInfo, house_url: str) -> int | None:
|
|||
avito_id_hash) перезаписываются напрямую.
|
||||
- address/lat/lon/year_built — через COALESCE чтобы не затирать данные
|
||||
канонического дома от cian/кадастра.
|
||||
- house_class (#2003) — COALESCE(new, existing): свежее avito-значение
|
||||
(топ-приоритет источник) побеждает, но None не затирает значение от
|
||||
более низкоприоритетного источника (см. HOUSE_FIELD_PRIORITY).
|
||||
3. Не вызывает db.commit() — коммитит вызывающий save_house_catalog_enrichment
|
||||
(advisory lock xact-scoped, должен держаться до commit).
|
||||
|
||||
|
|
@ -995,6 +1034,12 @@ def _persist_house(db: Session, h: HouseInfo, house_url: str) -> int | None:
|
|||
full_address = CAST(:full_address AS text),
|
||||
avito_id_hash = CAST(:avito_id_hash AS text),
|
||||
house_type = CAST(:house_type AS text),
|
||||
-- house_class (#2003): avito_houses_catalog топ-приоритет источник
|
||||
-- (conflict_resolution.HOUSE_FIELD_PRIORITY) — свежее avito-значение
|
||||
-- всегда побеждает, но None (вторичка не отдаёт "Класс жилья") не
|
||||
-- затирает значение, уже проставленное более низкоприоритетным
|
||||
-- источником (напр. yandex_realty_nb).
|
||||
house_class = COALESCE(CAST(:house_class AS text), houses.house_class),
|
||||
total_floors = CAST(:total_floors AS int),
|
||||
passenger_elevators = CAST(:passenger_elevators AS int),
|
||||
cargo_elevators = CAST(:cargo_elevators AS int),
|
||||
|
|
@ -1030,6 +1075,7 @@ def _persist_house(db: Session, h: HouseInfo, house_url: str) -> int | None:
|
|||
"full_address": h.full_address,
|
||||
"avito_id_hash": h.ext_id_hash,
|
||||
"house_type": h.house_type,
|
||||
"house_class": h.house_class,
|
||||
"total_floors": h.total_floors,
|
||||
"passenger_elevators": h.passenger_elevators,
|
||||
"cargo_elevators": h.cargo_elevators,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ Context
|
|||
Через BrowserFetcher (tradein-browser camoufox) — единственный рабочий путь.
|
||||
3. UPSERT в market.yandex_jk_enrichment (ON CONFLICT (ext_id) DO UPDATE).
|
||||
4. UPDATE houses.yandex_jk_id WHERE yandex_jk_slug = slug.
|
||||
5. Если info.house_class распарсен — UPDATE houses.house_class (#2003, fill-only:
|
||||
COALESCE не затирает уже проставленное более приоритетным источником значение;
|
||||
yandex_realty_nb — низший приоритет в HOUSE_FIELD_PRIORITY для house_class).
|
||||
|
||||
Idempotency
|
||||
-----------
|
||||
|
|
@ -135,6 +138,18 @@ _UPDATE_SLUG = """
|
|||
WHERE id = CAST(:hid AS bigint)
|
||||
"""
|
||||
|
||||
# house_class (#2003): yandex_realty_nb — самый низкий приоритет источник для
|
||||
# house_class (conflict_resolution.HOUSE_FIELD_PRIORITY: avito_houses_catalog >
|
||||
# cian > yandex_realty_nb) — COALESCE(houses.house_class, ...) заполняет ТОЛЬКО
|
||||
# пустое значение, никогда не перезаписывает уже проставленное avito/cian.
|
||||
# Вызывается только когда info.house_class IS NOT NULL (см. caller) — лишний
|
||||
# UPDATE с NULL никогда не выполняется.
|
||||
_UPDATE_HOUSE_CLASS = """
|
||||
UPDATE houses
|
||||
SET house_class = COALESCE(houses.house_class, CAST(:house_class AS text))
|
||||
WHERE id = CAST(:hid AS bigint)
|
||||
"""
|
||||
|
||||
_UPSERT_ENRICHMENT = """
|
||||
INSERT INTO market.yandex_jk_enrichment (
|
||||
ext_id, name, developer_name, address,
|
||||
|
|
@ -377,16 +392,26 @@ async def enrich_yandex_newbuilding_sweep(
|
|||
"raw_payload": json.dumps(info.raw_payload or {}, ensure_ascii=False),
|
||||
},
|
||||
)
|
||||
# house_class (#2003): распарсен parse_house_class из description (yandex_newbuilding
|
||||
# parse()), но раньше нигде не персистился (только market.yandex_jk_enrichment,
|
||||
# без колонки house_class) — Bug 1 находка. Пишем в houses в ТОЙ ЖЕ savepoint,
|
||||
# только если распарсили (иначе UPDATE не нужен — houses.house_class не трогаем).
|
||||
if info.house_class is not None:
|
||||
db.execute(
|
||||
text(_UPDATE_HOUSE_CLASS),
|
||||
{"hid": house_id, "house_class": info.house_class},
|
||||
)
|
||||
sp.commit()
|
||||
db.commit()
|
||||
result.rows_inserted += 1
|
||||
result.succeeded += 1
|
||||
logger.info(
|
||||
"enriched house_id=%s ext_id=%s slug=%s name=%r",
|
||||
"enriched house_id=%s ext_id=%s slug=%s name=%r house_class=%s",
|
||||
house_id,
|
||||
info.ext_id,
|
||||
jk_slug,
|
||||
info.name,
|
||||
info.house_class,
|
||||
)
|
||||
except Exception as exc:
|
||||
sp.rollback()
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ class FakeDB:
|
|||
self.enrichment: dict[str, dict] = {}
|
||||
# houses.yandex_jk_slug written during resolve
|
||||
self.slug_updates: dict[int, str] = {}
|
||||
# houses.house_class written after successful enrichment (#2003)
|
||||
self.house_class_updates: dict[int, str] = {}
|
||||
self.commits = 0
|
||||
self.rollbacks = 0
|
||||
self._sp_stack: list[_FakeSavepoint] = []
|
||||
|
|
@ -104,6 +106,11 @@ class FakeDB:
|
|||
self.slug_updates[params["hid"]] = params["slug"]
|
||||
return _FakeResult()
|
||||
|
||||
# UPDATE houses SET house_class (#2003)
|
||||
if "UPDATE houses" in sql and "house_class" in sql:
|
||||
self.house_class_updates[params["hid"]] = params["house_class"]
|
||||
return _FakeResult()
|
||||
|
||||
# Idempotency check: SELECT 1 FROM market.yandex_jk_enrichment
|
||||
if "SELECT 1 FROM market.yandex_jk_enrichment" in sql:
|
||||
ext_id = params.get("ext_id")
|
||||
|
|
@ -214,6 +221,48 @@ async def test_sweep_with_existing_slug_upserts_enrichment():
|
|||
assert result.failed_resolve == 0
|
||||
assert "999" in db.enrichment
|
||||
assert db.enrichment["999"]["name"] == "ЖК Татлин"
|
||||
# house_class не распарсен в этой фикстуре (None) — UPDATE houses.house_class не должен
|
||||
# выполниться вовсе (Bug 1 guard: лишний UPDATE с NULL пропускается).
|
||||
assert 10 not in db.house_class_updates
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sweep_persists_house_class_when_parsed():
|
||||
"""#2003: info.house_class распарсен → UPDATE houses.house_class выполняется.
|
||||
|
||||
Ранее (Bug 1) house_class парсился в YandexNewbuildingInfo, но нигде не персистился —
|
||||
только market.yandex_jk_enrichment (без колонки house_class). Регрессия на фикс.
|
||||
"""
|
||||
db = FakeDB(
|
||||
[
|
||||
{
|
||||
"house_id": 11,
|
||||
"ext_id": "1000",
|
||||
"yandex_jk_slug": "tatlin",
|
||||
"yandex_jk_id": "1000",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
from app.services.scrapers.yandex_newbuilding import YandexNewbuildingInfo
|
||||
|
||||
fake_info = YandexNewbuildingInfo(
|
||||
ext_id="1000",
|
||||
ext_slug="tatlin",
|
||||
source_url="https://realty.yandex.ru/ekaterinburg/kupit/novostrojka/tatlin-1000/",
|
||||
name="ЖК Татлин",
|
||||
house_class="comfort_plus",
|
||||
)
|
||||
|
||||
with patch("app.services.scrapers.yandex_newbuilding.YandexNewbuildingScraper") as mock_scraper:
|
||||
scraper_instance = MagicMock()
|
||||
scraper_instance.fetch_jk = AsyncMock(return_value=fake_info)
|
||||
mock_scraper.return_value = scraper_instance
|
||||
|
||||
result = await enrich_yandex_newbuilding_sweep(db, limit=5, request_delay_sec=0)
|
||||
|
||||
assert result.succeeded == 1
|
||||
assert db.house_class_updates.get(11) == "comfort_plus"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -274,6 +274,54 @@ def test_persist_house_coalesce_fields_in_bind():
|
|||
assert bind["year_built"] == h.year_built
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 7b: #2003 — house_class передаётся в UPDATE bind (COALESCE fill-only)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_persist_house_house_class_in_bind():
|
||||
"""UPDATE передаёт house_class в bind для COALESCE(CAST(:house_class AS text), houses.*)."""
|
||||
db = MagicMock()
|
||||
db.execute.return_value = MagicMock()
|
||||
|
||||
h = _make_house()
|
||||
h.house_class = "comfort"
|
||||
|
||||
with patch(
|
||||
"app.services.scrapers.avito_houses.match_or_create_house",
|
||||
return_value=(1, 1.0, "new"),
|
||||
):
|
||||
_persist_house(db, h, "")
|
||||
|
||||
bind = db.execute.call_args[0][1]
|
||||
assert bind["house_class"] == "comfort"
|
||||
|
||||
sql_obj = db.execute.call_args[0][0]
|
||||
assert "house_class" in str(sql_obj)
|
||||
|
||||
|
||||
def test_persist_house_house_class_none_does_not_null_via_coalesce():
|
||||
"""house_class=None в bind — SQL использует COALESCE(new, existing), не голый overwrite."""
|
||||
db = MagicMock()
|
||||
db.execute.return_value = MagicMock()
|
||||
|
||||
h = _make_house() # house_class остаётся дефолтным None
|
||||
|
||||
with patch(
|
||||
"app.services.scrapers.avito_houses.match_or_create_house",
|
||||
return_value=(1, 1.0, "new"),
|
||||
):
|
||||
_persist_house(db, h, "")
|
||||
|
||||
bind = db.execute.call_args[0][1]
|
||||
assert bind["house_class"] is None
|
||||
|
||||
sql_obj = str(db.execute.call_args[0][0])
|
||||
# SQL должен явно защищать существующее значение через COALESCE, а не
|
||||
# безусловный overwrite (который занулил бы значение от другого источника).
|
||||
assert "COALESCE(CAST(:house_class AS text), houses.house_class)" in sql_obj
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 8: #1820 — ext_id=0 graceful-skip гард
|
||||
# Два id-less URL (ext_id=0, без адреса) → match_or_create_house НЕ вызывается,
|
||||
|
|
|
|||
|
|
@ -551,6 +551,77 @@ def test_house_type_normalization() -> None:
|
|||
assert _normalize_house_type(None) is None
|
||||
|
||||
|
||||
def test_house_class_normalization() -> None:
|
||||
"""Все варианты класса жилья нормализуются корректно (#2003)."""
|
||||
from app.services.scrapers.avito_houses import _normalize_house_class
|
||||
|
||||
assert _normalize_house_class("Комфорт") == "comfort"
|
||||
assert _normalize_house_class("Бизнес") == "business"
|
||||
assert _normalize_house_class("Премиум") == "premium"
|
||||
assert _normalize_house_class("Эконом") == "economy"
|
||||
assert _normalize_house_class("Комфорт+") == "comfort_plus"
|
||||
assert _normalize_house_class("Элит") == "elite"
|
||||
# Регистронезависимость + пробелы по краям
|
||||
assert _normalize_house_class(" комфорт ") == "comfort"
|
||||
# Неизвестное значение → None (НЕ "other" — house_class точный ANY(...) фильтр)
|
||||
assert _normalize_house_class("Неизвестный класс") is None
|
||||
assert _normalize_house_class(None) is None
|
||||
assert _normalize_house_class("") is None
|
||||
|
||||
|
||||
def test_house_class_parsed_from_novostroyka_expand_params() -> None:
|
||||
""" "Класс жилья" из developmentPage expandParams → HouseInfo.house_class (#2003)."""
|
||||
state: dict = {
|
||||
"data": {
|
||||
"data": {
|
||||
"page": {
|
||||
"placeholders": [
|
||||
{
|
||||
"type": "developmentPage",
|
||||
"props": {
|
||||
"developmentPage": {
|
||||
"developmentData": {
|
||||
"avitoId": 42,
|
||||
"coords": {"lat": 56.8, "lng": 60.6},
|
||||
"developer": {},
|
||||
"mapPreview": {},
|
||||
"aboutDevelopment": {
|
||||
"tabs": [
|
||||
{
|
||||
"expandParams": {
|
||||
"items": [
|
||||
{
|
||||
"params": [
|
||||
{
|
||||
"type": "Класс жилья",
|
||||
"value": "Комфорт",
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
e = parse_houses_state(state, "/catalog/novostroyki/ekaterinburg/test/42")
|
||||
assert e.house.house_class == "comfort"
|
||||
|
||||
|
||||
def test_house_class_absent_on_secondary_listing() -> None:
|
||||
"""Вторичка (housePage) не отдаёт "Класс жилья" → house_class остаётся None."""
|
||||
e = parse_houses_state(MINIMAL_STATE, "/catalog/houses/ekaterinburg/test/3171365")
|
||||
assert e.house.house_class is None
|
||||
|
||||
|
||||
def test_strip_price() -> None:
|
||||
"""_strip_price корректно извлекает числа из строк Avito."""
|
||||
from app.services.scrapers.avito_houses import _strip_price
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ EXPAND_PARAMS_MAP: dict[str, tuple[str, str]] = {
|
|||
# вернёт None (хранится только в raw_characteristics).
|
||||
"Пассажирских лифтов": ("passenger_elevators", "int"),
|
||||
"Грузовых лифтов": ("cargo_elevators", "int"),
|
||||
# Класс жилья (#2003) — ТОЛЬКО developmentPage (новостройки); housePage
|
||||
# (вторичка) этот параметр не отдаёт. Сырое значение "Комфорт"/"Бизнес"/... →
|
||||
# каноничный enum houses.house_class через _normalize_house_class.
|
||||
"Класс жилья": ("house_class", "house_class"),
|
||||
}
|
||||
|
||||
# URL-маркер новостройки: /catalog/novostroyki/... (vs вторичка /catalog/houses/...).
|
||||
|
|
@ -104,6 +108,21 @@ HOUSE_TYPE_MAP: dict[str, str] = {
|
|||
"деревянный": "wood",
|
||||
}
|
||||
|
||||
# Нормализация класса жилья из Avito "Класс жилья" в canonical houses.house_class
|
||||
# (#2003). Avito сам отдаёт только 4 значения (Комфорт/Бизнес/Премиум/Эконом —
|
||||
# live-подтверждено), но карта дополнена "Комфорт+"/"Элит" для согласованности с
|
||||
# вокабуляром yandex_helpers.parse_house_class (тот же столбец пишут оба источника).
|
||||
HOUSE_CLASS_MAP: dict[str, str] = {
|
||||
"эконом": "economy",
|
||||
"комфорт": "comfort",
|
||||
"комфорт+": "comfort_plus",
|
||||
"комфорт плюс": "comfort_plus",
|
||||
"бизнес": "business",
|
||||
"премиум": "premium",
|
||||
"элит": "elite",
|
||||
"элитный": "elite",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dataclasses
|
||||
|
|
@ -122,6 +141,7 @@ class HouseInfo:
|
|||
year_built: int | None = None
|
||||
total_floors: int | None = None
|
||||
house_type: str | None = None # нормализован: monolith/panel/brick/...
|
||||
house_class: str | None = None # нормализован: comfort/business/premium/economy/... (#2003)
|
||||
material_floors: str | None = None
|
||||
hot_water: str | None = None
|
||||
passenger_elevators: int | None = None
|
||||
|
|
@ -244,6 +264,18 @@ def _normalize_house_type(raw: str | None) -> str | None:
|
|||
return HOUSE_TYPE_MAP.get(raw.lower(), "other")
|
||||
|
||||
|
||||
def _normalize_house_class(raw: str | None) -> str | None:
|
||||
"""Нормализует класс жилья: "Комфорт" → "comfort" (#2003).
|
||||
|
||||
В отличие от _normalize_house_type, неизвестное значение → None (НЕ "other"):
|
||||
house_class используется как точный фильтр (`= ANY(...)` в search_query.py) —
|
||||
мусорный bucket-токен там бесполезен и только засоряет список значений.
|
||||
"""
|
||||
if not raw:
|
||||
return None
|
||||
return HOUSE_CLASS_MAP.get(raw.strip().lower())
|
||||
|
||||
|
||||
def _cast_expand_param(value: str, cast_type: str) -> Any:
|
||||
"""Кастует строковое значение expandParams к нужному типу."""
|
||||
if cast_type == "int":
|
||||
|
|
@ -255,6 +287,8 @@ def _cast_expand_param(value: str, cast_type: str) -> Any:
|
|||
return value.strip().lower() == "да"
|
||||
if cast_type == "house_type":
|
||||
return _normalize_house_type(value)
|
||||
if cast_type == "house_class":
|
||||
return _normalize_house_class(value)
|
||||
# str
|
||||
return value
|
||||
|
||||
|
|
@ -374,6 +408,7 @@ def _parse_house_page(widget: dict[str, Any]) -> HouseInfo:
|
|||
year_built=parsed_params.get("year_built"),
|
||||
total_floors=parsed_params.get("total_floors"),
|
||||
house_type=parsed_params.get("house_type"),
|
||||
house_class=parsed_params.get("house_class"),
|
||||
material_floors=parsed_params.get("material_floors"),
|
||||
hot_water=parsed_params.get("hot_water"),
|
||||
passenger_elevators=parsed_params.get("passenger_elevators"),
|
||||
|
|
@ -447,6 +482,7 @@ def _parse_development_page(widget: dict[str, Any]) -> HouseInfo:
|
|||
year_built=parsed_params.get("year_built"), # обычно None — дом строится
|
||||
total_floors=parsed_params.get("total_floors"),
|
||||
house_type=parsed_params.get("house_type"),
|
||||
house_class=parsed_params.get("house_class"),
|
||||
material_floors=parsed_params.get("material_floors"),
|
||||
hot_water=parsed_params.get("hot_water"),
|
||||
passenger_elevators=parsed_params.get("passenger_elevators"),
|
||||
|
|
@ -964,6 +1000,9 @@ def _persist_house(
|
|||
avito_id_hash) перезаписываются напрямую.
|
||||
- address/lat/lon/year_built — через COALESCE чтобы не затирать данные
|
||||
канонического дома от cian/кадастра.
|
||||
- house_class (#2003) — COALESCE(new, existing): свежее avito-значение
|
||||
(топ-приоритет источник) побеждает, но None не затирает значение от
|
||||
более низкоприоритетного источника (см. HOUSE_FIELD_PRIORITY).
|
||||
3. Не вызывает db.commit() — коммитит вызывающий save_house_catalog_enrichment
|
||||
(advisory lock xact-scoped, должен держаться до commit).
|
||||
|
||||
|
|
@ -1001,6 +1040,12 @@ def _persist_house(
|
|||
full_address = CAST(:full_address AS text),
|
||||
avito_id_hash = CAST(:avito_id_hash AS text),
|
||||
house_type = CAST(:house_type AS text),
|
||||
-- house_class (#2003): avito_houses_catalog топ-приоритет источник
|
||||
-- (conflict_resolution.HOUSE_FIELD_PRIORITY) — свежее avito-значение
|
||||
-- всегда побеждает, но None (вторичка не отдаёт "Класс жилья") не
|
||||
-- затирает значение, уже проставленное более низкоприоритетным
|
||||
-- источником (напр. yandex_realty_nb).
|
||||
house_class = COALESCE(CAST(:house_class AS text), houses.house_class),
|
||||
total_floors = CAST(:total_floors AS int),
|
||||
passenger_elevators = CAST(:passenger_elevators AS int),
|
||||
cargo_elevators = CAST(:cargo_elevators AS int),
|
||||
|
|
@ -1036,6 +1081,7 @@ def _persist_house(
|
|||
"full_address": h.full_address,
|
||||
"avito_id_hash": h.ext_id_hash,
|
||||
"house_type": h.house_type,
|
||||
"house_class": h.house_class,
|
||||
"total_floors": h.total_floors,
|
||||
"passenger_elevators": h.passenger_elevators,
|
||||
"cargo_elevators": h.cargo_elevators,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue