Merge pull request 'fix(tradein/cian): читать bti из offerData — BTI-персист в houses писал ноль строк (#2435)' (#2668) from feat/2435-cian-house-enrichment into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m50s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 1m55s
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m50s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 1m55s
This commit is contained in:
commit
ef8609d725
2 changed files with 66 additions and 4 deletions
|
|
@ -21,9 +21,13 @@ import os
|
|||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
||||
|
||||
from contextlib import contextmanager
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from scraper_kit.providers.cian.detail import DetailEnrichment, save_detail_enrichment
|
||||
from scraper_kit.providers.cian.detail import (
|
||||
DetailEnrichment,
|
||||
fetch_detail,
|
||||
save_detail_enrichment,
|
||||
)
|
||||
from scraper_kit.providers.cian.valuation import CianValuationResult, _save_to_cache
|
||||
|
||||
# ── Part 1: bti_data → houses (detail.py) ────────────────────────────────────
|
||||
|
|
@ -190,6 +194,58 @@ def test_bti_missing_listing_address_skips_resolve():
|
|||
db.commit.assert_called_once()
|
||||
|
||||
|
||||
# ── Part 1b: bti_data реально извлекается с живой страницы (регрессия) ───────
|
||||
#
|
||||
# Тесты выше кормят `bti_data` напрямую в `save_detail_enrichment`, поэтому они
|
||||
# оставались зелёными, пока `fetch_detail` на живых страницах НИКОГДА его не
|
||||
# заполнял: bti читался только как соседний с `defaultState` ключ контейнера
|
||||
# frontend-offer-card, а Cian отдаёт его ВНУТРИ defaultState — `offerData.bti`.
|
||||
# Итог на проде: 628 detail-обогащённых Cian-листингов, 0 домов с BTI-полями.
|
||||
# Здесь прогоняем настоящий сохранённый HTML через настоящий `fetch_detail`.
|
||||
|
||||
|
||||
def _detail_fixture_html() -> str:
|
||||
path = os.path.join(os.path.dirname(__file__), "fixtures", "cian_flat_330982715.html")
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
|
||||
async def test_fetch_detail_extracts_bti_from_offer_data_on_real_page():
|
||||
"""Реальная страница Cian: bti лежит в offerData → bti_data должен заполниться."""
|
||||
fetcher = MagicMock()
|
||||
fetcher.fetch = AsyncMock(return_value=_detail_fixture_html())
|
||||
|
||||
result = await fetch_detail("https://ekb.cian.ru/sale/flat/330982715/", browser_fetcher=fetcher)
|
||||
|
||||
assert result is not None
|
||||
assert result.bti_data, "bti_data пуст — BTI-персист (#2435) в проде остаётся no-op"
|
||||
# Поля, которые пишет _persist_cian_bti_house (см. Part 1).
|
||||
assert result.bti_data["entrances"] == 3
|
||||
assert result.bti_data["flatCount"] == 24
|
||||
assert result.bti_data["isEmergency"] is False
|
||||
assert result.bti_data["houseHeatSupplyType"] == "central"
|
||||
assert result.bti_data["houseGasSupplyType"] == "central"
|
||||
assert result.bti_data["houseOverlapType"] == "concrete"
|
||||
|
||||
|
||||
async def test_fetch_detail_bti_legacy_sibling_location_still_read():
|
||||
"""Фоллбэк: если Cian вернёт bti соседним ключом контейнера — читаем и оттуда."""
|
||||
html = (
|
||||
"<script>window._cianConfig['frontend-offer-card'] = "
|
||||
"(window._cianConfig['frontend-offer-card'] || []).concat(["
|
||||
'{"key":"defaultState","value":{"offerData":{"offer":{"cianId":777}}}},'
|
||||
'{"key":"bti","value":{"houseData":{"seriesName":"1-464","flatCount":58}}}'
|
||||
"]);</script>"
|
||||
)
|
||||
fetcher = MagicMock()
|
||||
fetcher.fetch = AsyncMock(return_value=html)
|
||||
|
||||
result = await fetch_detail("https://ekb.cian.ru/sale/flat/777/", browser_fetcher=fetcher)
|
||||
|
||||
assert result is not None
|
||||
assert result.bti_data == {"seriesName": "1-464", "flatCount": 58}
|
||||
|
||||
|
||||
# ── Part 2: valuation house_info/managementCompany → houses (valuation.py) ──
|
||||
|
||||
_HOUSE_INFO_SAMPLE = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ Different from SERP (cian.py):
|
|||
- URL: https://ekb.cian.ru/sale/flat/<offer_id>/ or https://www.cian.ru/sale/flat/<offer_id>/
|
||||
- MFE: 'frontend-offer-card'
|
||||
- State KEY: 'defaultState' (NOT 'initialState' — SERP uses initialState)
|
||||
- Sister containers in same _cianConfig: bti, priceChanges, stats, agent, newObject
|
||||
- Sister data (bti, priceChanges, stats, agent, newObject) живёт ВНУТРИ defaultState
|
||||
(`offerData.*`), а не отдельными ключами _cianConfig рядом с defaultState
|
||||
|
||||
Parses ~88 offer fields + sister data → DetailEnrichment dataclass.
|
||||
Stage 5 of CianScraper v1.
|
||||
|
|
@ -176,7 +177,12 @@ async def fetch_detail(
|
|||
all_states = extract_all_states(html)
|
||||
offer_card_states = all_states.get("frontend-offer-card", {})
|
||||
|
||||
bti_state = offer_card_states.get("bti")
|
||||
# BTI лежит ВНУТРИ defaultState (`offerData.bti`), а не отдельным ключом рядом с
|
||||
# `defaultState` в контейнере frontend-offer-card. Пока читали только второе место,
|
||||
# `bti_data` всегда был None и #2435 писал в houses ноль строк (прод: 0 домов с
|
||||
# series_name/entrances/... при 628 detail-обогащённых листингах). Соседний ключ
|
||||
# оставлен фоллбэком — на случай, если Cian вернёт старую раскладку.
|
||||
bti_state = offer_data.get("bti") or offer_card_states.get("bti")
|
||||
if bti_state:
|
||||
result.bti_data = bti_state.get("houseData")
|
||||
result.raw_sister_states["bti"] = bti_state
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue