refactor(tradein): drop dead schema — sale_type_text column + audit_log (#731)
Verified-safe subset of TradeinDb_Audit_May30 (analyst-pruned). - remove sale_type_text refs from yandex_detail.py (model field + parse + ctor) and conflict_resolution.py priority map (column 100% NULL, code-only) - migration 095: ALTER listings DROP COLUMN sale_type_text; DROP TABLE audit_log (0 rows, 0 refs, no triggers). BEGIN/COMMIT + IF EXISTS, idempotent. Refs #731
This commit is contained in:
parent
2890199d07
commit
e6e5214fa5
5 changed files with 109 additions and 138 deletions
|
|
@ -6,6 +6,7 @@ Source-of-truth dicts read by merge logic in match_or_create_house/listing.
|
||||||
Sources covered: avito (serp/detail/houses_catalog/domoteka/imv),
|
Sources covered: avito (serp/detail/houses_catalog/domoteka/imv),
|
||||||
cian (serp/bti/detail/stats/valuation), yandex (serp/detail/realty_nb/valuation).
|
cian (serp/bti/detail/stats/valuation), yandex (serp/detail/realty_nb/valuation).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
@ -14,102 +15,95 @@ from typing import Any
|
||||||
# HOUSE_FIELD_PRIORITY — vault sec 3.5
|
# HOUSE_FIELD_PRIORITY — vault sec 3.5
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
HOUSE_FIELD_PRIORITY: dict[str, list[str] | str] = {
|
HOUSE_FIELD_PRIORITY: dict[str, list[str] | str] = {
|
||||||
"address": ["cian", "avito", "yandex"],
|
"address": ["cian", "avito", "yandex"],
|
||||||
"lat": ["cian_serp", "avito_houses_catalog", "yandex_realty_nb"],
|
"lat": ["cian_serp", "avito_houses_catalog", "yandex_realty_nb"],
|
||||||
"lon": ["cian_serp", "avito_houses_catalog", "yandex_realty_nb"],
|
"lon": ["cian_serp", "avito_houses_catalog", "yandex_realty_nb"],
|
||||||
"year_built": [
|
"year_built": [
|
||||||
"cian_bti", "cian_serp", "avito_houses_catalog", "yandex_valuation", "yandex_realty_nb",
|
"cian_bti",
|
||||||
|
"cian_serp",
|
||||||
|
"avito_houses_catalog",
|
||||||
|
"yandex_valuation",
|
||||||
|
"yandex_realty_nb",
|
||||||
],
|
],
|
||||||
"house_type": ["cian_bti", "cian_serp", "avito", "yandex_valuation", "yandex_realty_nb"],
|
"house_type": ["cian_bti", "cian_serp", "avito", "yandex_valuation", "yandex_realty_nb"],
|
||||||
"series_name": ["cian_bti"],
|
"series_name": ["cian_bti"],
|
||||||
"passenger_lifts_count": ["cian", "avito"],
|
"passenger_lifts_count": ["cian", "avito"],
|
||||||
"cargo_lifts_count": ["cian", "avito"],
|
"cargo_lifts_count": ["cian", "avito"],
|
||||||
"has_concierge": ["cian", "avito"],
|
"has_concierge": ["cian", "avito"],
|
||||||
"closed_yard": ["cian", "avito"],
|
"closed_yard": ["cian", "avito"],
|
||||||
"parking_type": ["cian"],
|
"parking_type": ["cian"],
|
||||||
"flat_count": ["cian_bti"],
|
"flat_count": ["cian_bti"],
|
||||||
"entrances": ["cian_bti"],
|
"entrances": ["cian_bti"],
|
||||||
"is_emergency": ["cian_bti"],
|
"is_emergency": ["cian_bti"],
|
||||||
"management_company_id": ["cian_valuation"],
|
"management_company_id": ["cian_valuation"],
|
||||||
"house_class": ["avito_houses_catalog", "cian", "yandex_realty_nb"],
|
"house_class": ["avito_houses_catalog", "cian", "yandex_realty_nb"],
|
||||||
"rating_score": ["avito_houses_catalog", "cian", "yandex_realty_nb"],
|
"rating_score": ["avito_houses_catalog", "cian", "yandex_realty_nb"],
|
||||||
"reviews_count": ["avito_houses_catalog", "cian", "yandex_realty_nb"],
|
"reviews_count": ["avito_houses_catalog", "cian", "yandex_realty_nb"],
|
||||||
|
|
||||||
# Yandex unique fields (newbuilding landing only)
|
# Yandex unique fields (newbuilding landing only)
|
||||||
"text_reviews_count": ["yandex_realty_nb"], # 353 text reviews — Yandex strongpoint
|
"text_reviews_count": ["yandex_realty_nb"], # 353 text reviews — Yandex strongpoint
|
||||||
"corpus_count": ["yandex_realty_nb"], # "три башни" → 3
|
"corpus_count": ["yandex_realty_nb"], # "три башни" → 3
|
||||||
"total_area_ha": ["yandex_realty_nb"], # ЖК footprint
|
"total_area_ha": ["yandex_realty_nb"], # ЖК footprint
|
||||||
"commission_year": ["cian_serp", "yandex_realty_nb"],
|
"commission_year": ["cian_serp", "yandex_realty_nb"],
|
||||||
"commission_month": ["yandex_realty_nb"], # raw RU month name
|
"commission_month": ["yandex_realty_nb"], # raw RU month name
|
||||||
"developer_name": ["cian", "yandex_realty_nb"],
|
"developer_name": ["cian", "yandex_realty_nb"],
|
||||||
"has_panorama": ["yandex_valuation"], # Yandex 3D panorama flag
|
"has_panorama": ["yandex_valuation"], # Yandex 3D panorama flag
|
||||||
"yandex_total_listings": ["yandex_valuation"], # "N объектов" в истории
|
"yandex_total_listings": ["yandex_valuation"], # "N объектов" в истории
|
||||||
|
|
||||||
# Yandex Valuation enrichment (existing house attrs)
|
# Yandex Valuation enrichment (existing house attrs)
|
||||||
"has_lift": ["cian_bti", "cian_detail", "yandex_valuation"],
|
"has_lift": ["cian_bti", "cian_detail", "yandex_valuation"],
|
||||||
"ceiling_height": ["cian_detail", "yandex_valuation"],
|
"ceiling_height": ["cian_detail", "yandex_valuation"],
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# LISTING_FIELD_PRIORITY — vault sec 4.5
|
# LISTING_FIELD_PRIORITY — vault sec 4.5
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
LISTING_FIELD_PRIORITY: dict[str, list[str] | str] = {
|
LISTING_FIELD_PRIORITY: dict[str, list[str] | str] = {
|
||||||
"address": ["cian_serp", "avito_detail", "avito_serp"],
|
"address": ["cian_serp", "avito_detail", "avito_serp"],
|
||||||
"lat": ["cian_serp", "avito_detail"],
|
"lat": ["cian_serp", "avito_detail"],
|
||||||
"lon": ["cian_serp", "avito_detail"],
|
"lon": ["cian_serp", "avito_detail"],
|
||||||
"area_m2": ["cian_serp", "avito_detail"],
|
"area_m2": ["cian_serp", "avito_detail"],
|
||||||
"living_area_m2": ["cian_serp"],
|
"living_area_m2": ["cian_serp"],
|
||||||
"kitchen_area_m2": ["cian_serp", "avito_detail"],
|
"kitchen_area_m2": ["cian_serp", "avito_detail"],
|
||||||
"ceiling_height": ["cian_detail"],
|
"ceiling_height": ["cian_detail"],
|
||||||
"floor": ["cian_serp", "avito_detail"],
|
"floor": ["cian_serp", "avito_detail"],
|
||||||
"total_floors": ["cian_serp", "avito_detail"],
|
"total_floors": ["cian_serp", "avito_detail"],
|
||||||
"year_built": ["cian_serp"],
|
"year_built": ["cian_serp"],
|
||||||
"house_type": ["cian_serp", "avito_detail", "yandex_detail"],
|
"house_type": ["cian_serp", "avito_detail", "yandex_detail"],
|
||||||
"repair_state": ["cian_detail", "avito_detail"],
|
"repair_state": ["cian_detail", "avito_detail"],
|
||||||
"has_balcony": ["cian_serp", "avito_detail"],
|
"has_balcony": ["cian_serp", "avito_detail"],
|
||||||
"balconies_count": ["cian_serp"],
|
"balconies_count": ["cian_serp"],
|
||||||
"loggias_count": ["cian_serp"],
|
"loggias_count": ["cian_serp"],
|
||||||
"windows_view_type": ["cian_detail", "avito_detail"],
|
"windows_view_type": ["cian_detail", "avito_detail"],
|
||||||
"separate_wcs_count": ["cian_detail"],
|
"separate_wcs_count": ["cian_detail"],
|
||||||
"combined_wcs_count": ["cian_detail"],
|
"combined_wcs_count": ["cian_detail"],
|
||||||
"room_type": ["cian_detail", "avito_detail"],
|
"room_type": ["cian_detail", "avito_detail"],
|
||||||
"has_furniture": ["cian_serp", "avito_detail"],
|
"has_furniture": ["cian_serp", "avito_detail"],
|
||||||
"phones": ["cian_serp"],
|
"phones": ["cian_serp"],
|
||||||
"description": ["cian_serp", "avito_detail", "yandex_detail"],
|
"description": ["cian_serp", "avito_detail", "yandex_detail"],
|
||||||
"photo_urls": "union",
|
"photo_urls": "union",
|
||||||
|
|
||||||
# Avito Domoteka unique
|
# Avito Domoteka unique
|
||||||
"owners_count": ["avito_domoteka"],
|
"owners_count": ["avito_domoteka"],
|
||||||
"owners_at_least": ["avito_domoteka"],
|
"owners_at_least": ["avito_domoteka"],
|
||||||
"last_owner_change_date": ["avito_domoteka"],
|
"last_owner_change_date": ["avito_domoteka"],
|
||||||
"encumbrances_clean": ["avito_domoteka"],
|
"encumbrances_clean": ["avito_domoteka"],
|
||||||
"registry_match": ["avito_domoteka"],
|
"registry_match": ["avito_domoteka"],
|
||||||
|
|
||||||
# Cian-only
|
# Cian-only
|
||||||
"is_rosreestr_checked": ["cian_serp"],
|
"is_rosreestr_checked": ["cian_serp"],
|
||||||
"is_layout_approved": ["cian_serp"],
|
"is_layout_approved": ["cian_serp"],
|
||||||
"is_commercial_ownership_verified": ["cian_serp"],
|
"is_commercial_ownership_verified": ["cian_serp"],
|
||||||
|
|
||||||
# Cross-validation
|
# Cross-validation
|
||||||
"price_rub": "cross_validate",
|
"price_rub": "cross_validate",
|
||||||
"kadastr_num": "first_non_null",
|
"kadastr_num": "first_non_null",
|
||||||
# NOTE: task description claims price_rub=['cian','avito','yandex']; vault sec 4.5
|
# NOTE: task description claims price_rub=['cian','avito','yandex']; vault sec 4.5
|
||||||
# says 'cross_validate' (flag if diff > 10%). Following vault — change to list if
|
# says 'cross_validate' (flag if diff > 10%). Following vault — change to list if
|
||||||
# cross_validate semantics aren't desired at caller.
|
# cross_validate semantics aren't desired at caller.
|
||||||
|
|
||||||
# Yandex unique (agency block — OfferCardAuthorInfo)
|
# Yandex unique (agency block — OfferCardAuthorInfo)
|
||||||
"agency_name": ["yandex_detail"],
|
"agency_name": ["yandex_detail"],
|
||||||
"agency_founded_year": ["yandex_detail"],
|
"agency_founded_year": ["yandex_detail"],
|
||||||
"agency_objects_count": ["yandex_detail"],
|
"agency_objects_count": ["yandex_detail"],
|
||||||
|
|
||||||
# Yandex parallel views column (NOT existing `views_total` which is Cian's)
|
# Yandex parallel views column (NOT existing `views_total` which is Cian's)
|
||||||
"views_total_yandex": ["yandex_detail"],
|
"views_total_yandex": ["yandex_detail"],
|
||||||
|
|
||||||
# Yandex raw publish-date text (relative form)
|
# Yandex raw publish-date text (relative form)
|
||||||
"publish_date_relative": ["yandex_detail"],
|
"publish_date_relative": ["yandex_detail"],
|
||||||
|
|
||||||
# Yandex raw RU sale-type phrase (vs existing `sale_type` enum)
|
|
||||||
"sale_type_text": ["yandex_detail"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -129,7 +123,7 @@ def _resolve(
|
||||||
for v in candidates.values():
|
for v in candidates.values():
|
||||||
if v is None:
|
if v is None:
|
||||||
continue
|
continue
|
||||||
if isinstance(v, (list, tuple, set)):
|
if isinstance(v, list | tuple | set):
|
||||||
out.extend(v)
|
out.extend(v)
|
||||||
else:
|
else:
|
||||||
out.append(v)
|
out.append(v)
|
||||||
|
|
@ -156,7 +150,7 @@ def _resolve(
|
||||||
return min(non_null) if non_null else None
|
return min(non_null) if non_null else None
|
||||||
if rule == "cross_validate":
|
if rule == "cross_validate":
|
||||||
# Caller decides — return median by default
|
# Caller decides — return median by default
|
||||||
nums = [v for v in candidates.values() if isinstance(v, (int, float))]
|
nums = [v for v in candidates.values() if isinstance(v, int | float)]
|
||||||
if not nums:
|
if not nums:
|
||||||
return None
|
return None
|
||||||
nums.sort()
|
nums.sort()
|
||||||
|
|
@ -186,6 +180,7 @@ def resolve_listing_field(field: str, candidates: dict[str, Any]) -> Any:
|
||||||
# Legacy stub — kept for backward compat with existing __init__.py and tests
|
# Legacy stub — kept for backward compat with existing __init__.py and tests
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def update_canonical_fields(
|
def update_canonical_fields(
|
||||||
db: Any,
|
db: Any,
|
||||||
listing_id: int,
|
listing_id: int,
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,6 @@ class DetailEnrichment(BaseModel):
|
||||||
# Description (full text)
|
# Description (full text)
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
|
|
||||||
# Sale type — raw RU phrase ('свободная продажа' / 'альтернативная')
|
|
||||||
sale_type_text: str | None = None
|
|
||||||
|
|
||||||
# Repair state — enum, inferred from description text (#622).
|
# Repair state — enum, inferred from description text (#622).
|
||||||
# Yandex не отдаёт структурного поля ремонта, поэтому только инференс.
|
# Yandex не отдаёт структурного поля ремонта, поэтому только инференс.
|
||||||
repair_state: str | None = None
|
repair_state: str | None = None
|
||||||
|
|
@ -116,9 +113,7 @@ class YandexDetailScraper(BaseScraper):
|
||||||
self.request_delay_sec = get_scraper_delay(self.name)
|
self.request_delay_sec = get_scraper_delay(self.name)
|
||||||
|
|
||||||
# BaseScraper requires fetch_around — detail isn't geo-based, raise NotImplementedError
|
# BaseScraper requires fetch_around — detail isn't geo-based, raise NotImplementedError
|
||||||
async def fetch_around(
|
async def fetch_around(self, lat: float, lon: float, radius_m: int = 1000) -> list: # type: ignore[override]
|
||||||
self, lat: float, lon: float, radius_m: int = 1000
|
|
||||||
) -> list: # type: ignore[override]
|
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"YandexDetailScraper is offer-id-based; use fetch_detail(offer_url) instead."
|
"YandexDetailScraper is offer-id-based; use fetch_detail(offer_url) instead."
|
||||||
)
|
)
|
||||||
|
|
@ -130,9 +125,7 @@ class YandexDetailScraper(BaseScraper):
|
||||||
logger.exception("yandex detail fetch failed: %s", offer_url)
|
logger.exception("yandex detail fetch failed: %s", offer_url)
|
||||||
return None
|
return None
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
logger.warning(
|
logger.warning("yandex detail returned %d for %s", response.status_code, offer_url)
|
||||||
"yandex detail returned %d for %s", response.status_code, offer_url
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
result = self.parse(response.text, offer_url=offer_url)
|
result = self.parse(response.text, offer_url=offer_url)
|
||||||
await self.sleep_between_requests()
|
await self.sleep_between_requests()
|
||||||
|
|
@ -174,13 +167,6 @@ class YandexDetailScraper(BaseScraper):
|
||||||
summary_node = tree.css_first('[data-test="OfferCardSummary"]')
|
summary_node = tree.css_first('[data-test="OfferCardSummary"]')
|
||||||
summary_text = summary_node.text(strip=True) if summary_node else ""
|
summary_text = summary_node.text(strip=True) if summary_node else ""
|
||||||
|
|
||||||
# Sale type — raw RU phrase
|
|
||||||
sale_type_text: str | None = None
|
|
||||||
for phrase in ("свободная продажа", "альтернативная"):
|
|
||||||
if phrase in summary_text.lower():
|
|
||||||
sale_type_text = phrase
|
|
||||||
break
|
|
||||||
|
|
||||||
# Views + relative publish date from summary text
|
# Views + relative publish date from summary text
|
||||||
views_match = RE_VIEWS.search(summary_text)
|
views_match = RE_VIEWS.search(summary_text)
|
||||||
views_total = int(views_match.group(1)) if views_match else None
|
views_total = int(views_match.group(1)) if views_match else None
|
||||||
|
|
@ -235,9 +221,7 @@ class YandexDetailScraper(BaseScraper):
|
||||||
if nb_match:
|
if nb_match:
|
||||||
nb_id = nb_match.group(1)
|
nb_id = nb_match.group(1)
|
||||||
nb_url = (
|
nb_url = (
|
||||||
nb_href
|
nb_href if nb_href.startswith("http") else f"https://realty.yandex.ru{nb_href}"
|
||||||
if nb_href.startswith("http")
|
|
||||||
else f"https://realty.yandex.ru{nb_href}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- NLP best-effort from description ---
|
# --- NLP best-effort from description ---
|
||||||
|
|
@ -260,7 +244,6 @@ class YandexDetailScraper(BaseScraper):
|
||||||
total_floors=total_floors,
|
total_floors=total_floors,
|
||||||
address=address,
|
address=address,
|
||||||
description=description,
|
description=description,
|
||||||
sale_type_text=sale_type_text,
|
|
||||||
repair_state=repair_state,
|
repair_state=repair_state,
|
||||||
views_total=views_total,
|
views_total=views_total,
|
||||||
publish_date=publish_date,
|
publish_date=publish_date,
|
||||||
|
|
|
||||||
24
tradein-mvp/backend/data/sql/095_dead_schema.sql
Normal file
24
tradein-mvp/backend/data/sql/095_dead_schema.sql
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
-- 095_dead_schema.sql
|
||||||
|
-- Purpose: Удаление мёртвой схемы (верифицированный безопасный подмножество аудита TradeinDb_Audit_May30).
|
||||||
|
--
|
||||||
|
-- Контекст (Issue #731):
|
||||||
|
-- sale_type_text: столбец 100% NULL в listings, code refs удалены в том же PR.
|
||||||
|
-- audit_log: 0 строк, 0 python refs, триггеры не зависят (верифицировано).
|
||||||
|
--
|
||||||
|
-- Idempotency:
|
||||||
|
-- ALTER TABLE ... DROP COLUMN IF EXISTS — безопасен при повторном запуске.
|
||||||
|
-- DROP TABLE IF EXISTS — безопасен при повторном запуске.
|
||||||
|
--
|
||||||
|
-- Явно НЕ удаляем: houses_price_dynamics, agents, sellers, estimate_photos,
|
||||||
|
-- house_reviews, listings_snapshots.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- #731: drop dead schema (verified-safe subset of TradeinDb_Audit_May30)
|
||||||
|
-- sale_type_text: 100% NULL in listings, code refs removed in same PR
|
||||||
|
ALTER TABLE IF EXISTS listings DROP COLUMN IF EXISTS sale_type_text;
|
||||||
|
|
||||||
|
-- audit_log: 0 rows, 0 python refs, no triggers depend on it (verified)
|
||||||
|
DROP TABLE IF EXISTS audit_log;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"""Per-field priority resolution tests."""
|
"""Per-field priority resolution tests."""
|
||||||
|
|
||||||
from app.services.matching.conflict_resolution import (
|
from app.services.matching.conflict_resolution import (
|
||||||
HOUSE_FIELD_PRIORITY,
|
HOUSE_FIELD_PRIORITY,
|
||||||
LISTING_FIELD_PRIORITY,
|
LISTING_FIELD_PRIORITY,
|
||||||
|
|
@ -10,6 +11,7 @@ from app.services.matching.conflict_resolution import (
|
||||||
# Pre-existing tests (preserved)
|
# Pre-existing tests (preserved)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_house_year_built_prefers_cian_bti() -> None:
|
def test_house_year_built_prefers_cian_bti() -> None:
|
||||||
out = resolve_house_field(
|
out = resolve_house_field(
|
||||||
"year_built",
|
"year_built",
|
||||||
|
|
@ -29,7 +31,8 @@ def test_listing_owners_count_avito_domoteka_only() -> None:
|
||||||
|
|
||||||
def test_listing_photo_urls_union() -> None:
|
def test_listing_photo_urls_union() -> None:
|
||||||
out = resolve_listing_field(
|
out = resolve_listing_field(
|
||||||
"photo_urls", {"avito": ["a.jpg", "b.jpg"], "cian": ["b.jpg", "c.jpg"]},
|
"photo_urls",
|
||||||
|
{"avito": ["a.jpg", "b.jpg"], "cian": ["b.jpg", "c.jpg"]},
|
||||||
)
|
)
|
||||||
assert set(out) == {"a.jpg", "b.jpg", "c.jpg"}
|
assert set(out) == {"a.jpg", "b.jpg", "c.jpg"}
|
||||||
|
|
||||||
|
|
@ -51,15 +54,14 @@ def test_listing_priority_dict_has_owners() -> None:
|
||||||
# Yandex house priority tests
|
# Yandex house priority tests
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestYandexHousePriority:
|
class TestYandexHousePriority:
|
||||||
def test_house_lat_yandex_realty_nb_when_cian_missing(self) -> None:
|
def test_house_lat_yandex_realty_nb_when_cian_missing(self) -> None:
|
||||||
out = resolve_house_field("lat", {"yandex_realty_nb": 56.85})
|
out = resolve_house_field("lat", {"yandex_realty_nb": 56.85})
|
||||||
assert out == 56.85
|
assert out == 56.85
|
||||||
|
|
||||||
def test_house_lat_cian_preferred_over_yandex(self) -> None:
|
def test_house_lat_cian_preferred_over_yandex(self) -> None:
|
||||||
out = resolve_house_field(
|
out = resolve_house_field("lat", {"cian_serp": 56.83, "yandex_realty_nb": 56.85})
|
||||||
"lat", {"cian_serp": 56.83, "yandex_realty_nb": 56.85}
|
|
||||||
)
|
|
||||||
assert out == 56.83
|
assert out == 56.83
|
||||||
|
|
||||||
def test_house_year_built_yandex_valuation_picked(self) -> None:
|
def test_house_year_built_yandex_valuation_picked(self) -> None:
|
||||||
|
|
@ -67,9 +69,7 @@ class TestYandexHousePriority:
|
||||||
assert out == 1981
|
assert out == 1981
|
||||||
|
|
||||||
def test_house_year_built_cian_bti_preferred(self) -> None:
|
def test_house_year_built_cian_bti_preferred(self) -> None:
|
||||||
out = resolve_house_field(
|
out = resolve_house_field("year_built", {"cian_bti": 1980, "yandex_valuation": 1981})
|
||||||
"year_built", {"cian_bti": 1980, "yandex_valuation": 1981}
|
|
||||||
)
|
|
||||||
assert out == 1980
|
assert out == 1980
|
||||||
|
|
||||||
def test_house_text_reviews_count_yandex_only(self) -> None:
|
def test_house_text_reviews_count_yandex_only(self) -> None:
|
||||||
|
|
@ -81,9 +81,7 @@ class TestYandexHousePriority:
|
||||||
assert out == 3
|
assert out == 3
|
||||||
|
|
||||||
def test_house_commission_year_cian_serp_preferred(self) -> None:
|
def test_house_commission_year_cian_serp_preferred(self) -> None:
|
||||||
out = resolve_house_field(
|
out = resolve_house_field("commission_year", {"cian_serp": 2022, "yandex_realty_nb": 2023})
|
||||||
"commission_year", {"cian_serp": 2022, "yandex_realty_nb": 2023}
|
|
||||||
)
|
|
||||||
assert out == 2022
|
assert out == 2022
|
||||||
|
|
||||||
def test_house_commission_month_yandex_only(self) -> None:
|
def test_house_commission_month_yandex_only(self) -> None:
|
||||||
|
|
@ -98,15 +96,11 @@ class TestYandexHousePriority:
|
||||||
assert out == "PRINZIP"
|
assert out == "PRINZIP"
|
||||||
|
|
||||||
def test_house_has_lift_cian_bti_preferred_over_yandex_valuation(self) -> None:
|
def test_house_has_lift_cian_bti_preferred_over_yandex_valuation(self) -> None:
|
||||||
out = resolve_house_field(
|
out = resolve_house_field("has_lift", {"cian_bti": True, "yandex_valuation": True})
|
||||||
"has_lift", {"cian_bti": True, "yandex_valuation": True}
|
|
||||||
)
|
|
||||||
assert out is True
|
assert out is True
|
||||||
|
|
||||||
def test_house_ceiling_height_cian_detail_preferred(self) -> None:
|
def test_house_ceiling_height_cian_detail_preferred(self) -> None:
|
||||||
out = resolve_house_field(
|
out = resolve_house_field("ceiling_height", {"cian_detail": 2.7, "yandex_valuation": 2.5})
|
||||||
"ceiling_height", {"cian_detail": 2.7, "yandex_valuation": 2.5}
|
|
||||||
)
|
|
||||||
assert out == 2.7
|
assert out == 2.7
|
||||||
|
|
||||||
def test_house_has_panorama_yandex_valuation_only(self) -> None:
|
def test_house_has_panorama_yandex_valuation_only(self) -> None:
|
||||||
|
|
@ -141,6 +135,7 @@ class TestYandexHousePriority:
|
||||||
# Yandex listing priority tests
|
# Yandex listing priority tests
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestYandexListingPriority:
|
class TestYandexListingPriority:
|
||||||
def test_listing_description_cian_preferred_over_yandex_detail(self) -> None:
|
def test_listing_description_cian_preferred_over_yandex_detail(self) -> None:
|
||||||
out = resolve_listing_field(
|
out = resolve_listing_field(
|
||||||
|
|
@ -154,9 +149,7 @@ class TestYandexListingPriority:
|
||||||
assert out == "brick"
|
assert out == "brick"
|
||||||
|
|
||||||
def test_listing_agency_name_yandex_only(self) -> None:
|
def test_listing_agency_name_yandex_only(self) -> None:
|
||||||
out = resolve_listing_field(
|
out = resolve_listing_field("agency_name", {"yandex_detail": "Агентство «Диал»"})
|
||||||
"agency_name", {"yandex_detail": "Агентство «Диал»"}
|
|
||||||
)
|
|
||||||
assert out == "Агентство «Диал»"
|
assert out == "Агентство «Диал»"
|
||||||
|
|
||||||
def test_listing_agency_founded_year_yandex_only(self) -> None:
|
def test_listing_agency_founded_year_yandex_only(self) -> None:
|
||||||
|
|
@ -172,17 +165,9 @@ class TestYandexListingPriority:
|
||||||
assert out == 874
|
assert out == 874
|
||||||
|
|
||||||
def test_listing_publish_date_relative_yandex_only(self) -> None:
|
def test_listing_publish_date_relative_yandex_only(self) -> None:
|
||||||
out = resolve_listing_field(
|
out = resolve_listing_field("publish_date_relative", {"yandex_detail": "3 дня назад"})
|
||||||
"publish_date_relative", {"yandex_detail": "3 дня назад"}
|
|
||||||
)
|
|
||||||
assert out == "3 дня назад"
|
assert out == "3 дня назад"
|
||||||
|
|
||||||
def test_listing_sale_type_text_yandex_only(self) -> None:
|
|
||||||
out = resolve_listing_field(
|
|
||||||
"sale_type_text", {"yandex_detail": "Прямая продажа"}
|
|
||||||
)
|
|
||||||
assert out == "Прямая продажа"
|
|
||||||
|
|
||||||
def test_listing_agency_name_none_when_no_yandex(self) -> None:
|
def test_listing_agency_name_none_when_no_yandex(self) -> None:
|
||||||
out = resolve_listing_field("agency_name", {"cian_serp": None})
|
out = resolve_listing_field("agency_name", {"cian_serp": None})
|
||||||
assert out is None
|
assert out is None
|
||||||
|
|
@ -209,7 +194,6 @@ class TestYandexListingPriority:
|
||||||
"agency_objects_count",
|
"agency_objects_count",
|
||||||
"views_total_yandex",
|
"views_total_yandex",
|
||||||
"publish_date_relative",
|
"publish_date_relative",
|
||||||
"sale_type_text",
|
|
||||||
]
|
]
|
||||||
for key in yandex_keys:
|
for key in yandex_keys:
|
||||||
assert key in LISTING_FIELD_PRIORITY, f"{key!r} missing from LISTING_FIELD_PRIORITY"
|
assert key in LISTING_FIELD_PRIORITY, f"{key!r} missing from LISTING_FIELD_PRIORITY"
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,7 @@ def _make_html(
|
||||||
) -> str:
|
) -> str:
|
||||||
ld_block = ""
|
ld_block = ""
|
||||||
if product_ld is not None:
|
if product_ld is not None:
|
||||||
ld_block = (
|
ld_block = f'<script type="application/ld+json">{json.dumps(product_ld)}</script>'
|
||||||
f'<script type="application/ld+json">{json.dumps(product_ld)}</script>'
|
|
||||||
)
|
|
||||||
|
|
||||||
nb_link = ""
|
nb_link = ""
|
||||||
if nb_href:
|
if nb_href:
|
||||||
|
|
@ -128,11 +126,6 @@ class TestFullDetailFixture:
|
||||||
assert result.views_total == 342
|
assert result.views_total == 342
|
||||||
assert result.publish_date == date(2026, 4, 10)
|
assert result.publish_date == date(2026, 4, 10)
|
||||||
|
|
||||||
def test_sale_type_free(self) -> None:
|
|
||||||
result = SCRAPER.parse(FULL_HTML, offer_url=_BASE_OFFER_URL)
|
|
||||||
assert result is not None
|
|
||||||
assert result.sale_type_text == "свободная продажа"
|
|
||||||
|
|
||||||
def test_price_per_m2_from_summary(self) -> None:
|
def test_price_per_m2_from_summary(self) -> None:
|
||||||
result = SCRAPER.parse(FULL_HTML, offer_url=_BASE_OFFER_URL)
|
result = SCRAPER.parse(FULL_HTML, offer_url=_BASE_OFFER_URL)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
@ -196,7 +189,6 @@ class TestStudio:
|
||||||
assert result.area_m2 == pytest.approx(28.0)
|
assert result.area_m2 == pytest.approx(28.0)
|
||||||
assert result.floor == 2
|
assert result.floor == 2
|
||||||
assert result.total_floors == 9
|
assert result.total_floors == 9
|
||||||
assert result.sale_type_text == "альтернативная"
|
|
||||||
|
|
||||||
|
|
||||||
class TestNoAgencySection:
|
class TestNoAgencySection:
|
||||||
|
|
@ -221,10 +213,7 @@ class TestAgencyBlock:
|
||||||
<h2>АН Городской риелтор</h2>
|
<h2>АН Городской риелтор</h2>
|
||||||
<p>Год основания 1998. Продали 150 объектов.</p>
|
<p>Год основания 1998. Продали 150 объектов.</p>
|
||||||
</div>"""
|
</div>"""
|
||||||
summary = (
|
summary = "Иван Петров АН Городской риелтор • 50 просмотров" " • опубликовано 3 марта 2025"
|
||||||
"Иван Петров АН Городской риелтор • 50 просмотров"
|
|
||||||
" • опубликовано 3 марта 2025"
|
|
||||||
)
|
|
||||||
html = _make_html(author_block=author_html, summary_text=summary)
|
html = _make_html(author_block=author_html, summary_text=summary)
|
||||||
result = SCRAPER.parse(html, offer_url=_BASE_OFFER_URL)
|
result = SCRAPER.parse(html, offer_url=_BASE_OFFER_URL)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
|
|
@ -291,9 +280,7 @@ class TestMetroStations:
|
||||||
|
|
||||||
class TestRelativeDate:
|
class TestRelativeDate:
|
||||||
def test_parse_relative_date_yesterday(self) -> None:
|
def test_parse_relative_date_yesterday(self) -> None:
|
||||||
html = _make_html(
|
html = _make_html(summary_text="Россия, ЕКБ, ул. Тест • 100 просмотров • вчера")
|
||||||
summary_text="Россия, ЕКБ, ул. Тест • 100 просмотров • вчера"
|
|
||||||
)
|
|
||||||
result = SCRAPER.parse(html, offer_url=_BASE_OFFER_URL)
|
result = SCRAPER.parse(html, offer_url=_BASE_OFFER_URL)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result.publish_date_relative == "вчера"
|
assert result.publish_date_relative == "вчера"
|
||||||
|
|
@ -313,9 +300,7 @@ class TestRelativeDate:
|
||||||
|
|
||||||
class TestPublishDate:
|
class TestPublishDate:
|
||||||
def test_publish_date_ru_format(self) -> None:
|
def test_publish_date_ru_format(self) -> None:
|
||||||
html = _make_html(
|
html = _make_html(summary_text="Россия, ЕКБ • 200 просмотров • опубликовано 5 января 2026")
|
||||||
summary_text="Россия, ЕКБ • 200 просмотров • опубликовано 5 января 2026"
|
|
||||||
)
|
|
||||||
result = SCRAPER.parse(html, offer_url=_BASE_OFFER_URL)
|
result = SCRAPER.parse(html, offer_url=_BASE_OFFER_URL)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue