Normalize Russian vehicle data

This commit is contained in:
qananasikq
2026-08-18 15:59:00 +03:00
parent a19a2009ed
commit f3329f76a5
2 changed files with 267 additions and 64 deletions
+145 -56
View File
@@ -14,43 +14,46 @@ from .models import MobileDeListing
PARSER_ID_ALPHABET = ascii_letters + digits PARSER_ID_ALPHABET = ascii_letters + digits
PARSER_ID_RE = re.compile(r"^car-[A-Za-z0-9]{22}$") PARSER_ID_RE = re.compile(r"^car-[A-Za-z0-9]{22}$")
_BODY_MAP = { _RU_BODY_MAP = {
"кабриолет": "OPEN", "кабриолет": "OPEN",
"родстер": "OPEN", "родстер": "OPEN",
"седан": "SEDAN",
"внедорожник": "SUV",
"пикап": "PICKUP",
"универсал": "STATION_WAGON",
"минивэн": "MINIVAN",
"микроавтобус": "MINIVAN",
"фургон": "MINIVAN",
"купе": "COUPE",
"хэтчбек": "HATCHBACK",
"малолитражный": "HATCHBACK",
}
_LEGACY_BODY_MAP = {
"cabrio": "OPEN", "cabrio": "OPEN",
"cabriolet": "OPEN", "cabriolet": "OPEN",
"roadster": "OPEN", "roadster": "OPEN",
"седан": "SEDAN",
"limousine": "SEDAN", "limousine": "SEDAN",
"saloon": "SEDAN", "saloon": "SEDAN",
"sedan": "SEDAN", "sedan": "SEDAN",
"внедорожник": "SUV",
"suv": "SUV", "suv": "SUV",
"offroad": "SUV", "offroad": "SUV",
"gelandewagen": "SUV", "gelandewagen": "SUV",
"geländewagen": "SUV", "geländewagen": "SUV",
"пикап": "PICKUP",
"pickup": "PICKUP", "pickup": "PICKUP",
"pick-up": "PICKUP", "pick-up": "PICKUP",
"универсал": "STATION_WAGON",
"kombi": "STATION_WAGON", "kombi": "STATION_WAGON",
"estatecar": "STATION_WAGON", "estatecar": "STATION_WAGON",
"touring": "STATION_WAGON", "touring": "STATION_WAGON",
"estate": "STATION_WAGON", "estate": "STATION_WAGON",
"минивэн": "MINIVAN",
"микроавтобус": "MINIVAN",
"фургон": "MINIVAN",
"van": "MINIVAN", "van": "MINIVAN",
"kleinbus": "MINIVAN", "kleinbus": "MINIVAN",
"bus": "MINIVAN", "bus": "MINIVAN",
"active tourer": "MINIVAN", "active tourer": "MINIVAN",
"gran tourer": "MINIVAN", "gran tourer": "MINIVAN",
"купе": "COUPE",
"coupe": "COUPE", "coupe": "COUPE",
"sportscar": "COUPE", "sportscar": "COUPE",
"sports car": "COUPE", "sports car": "COUPE",
"хэтчбек": "HATCHBACK",
"малолитражный": "HATCHBACK",
"hatchback": "HATCHBACK", "hatchback": "HATCHBACK",
"kleinwagen": "HATCHBACK", "kleinwagen": "HATCHBACK",
"smallcar": "HATCHBACK", "smallcar": "HATCHBACK",
@@ -59,21 +62,46 @@ _BODY_MAP = {
"compactcar": "HATCHBACK", "compactcar": "HATCHBACK",
} }
_GEARBOX_MAP = { _RU_GEARBOX_MAP = {
"автоматическая": "AT", "автоматическая": "AT",
"автоматическая коробка": "AT", "автоматическая коробка": "AT",
"automatik": "AT", "механика": "MT",
"automatic": "AT",
"механическая": "MT", "механическая": "MT",
"механическая коробка": "MT", "механическая коробка": "MT",
"manual": "MT",
"вариатор": "CVT", "вариатор": "CVT",
}
_LEGACY_GEARBOX_MAP = {
"automatik": "AT",
"automatic": "AT",
"manual": "MT",
"cvt": "CVT", "cvt": "CVT",
} }
_COLOR_MAP = { _RU_COLOR_MAP = {
"черный": "black", "черный": "black",
"чёрный": "black", "чёрный": "black",
"белый": "white",
"серебристый": "silver",
"серебряный": "silver",
"серый": "gray",
"красный": "red",
"синий": "blue",
"голубой": "blue",
"зеленый": "green",
"зелёный": "green",
"коричневый": "brown",
"бежевый": "beige",
"желтый": "yellow",
"жёлтый": "yellow",
"оранжевый": "orange",
"золотистый": "gold",
"золотой": "gold",
"бронзовый": "bronze",
"фиолетовый": "purple",
}
_LEGACY_COLOR_MAP = {
"schwarz": "black", "schwarz": "black",
"saphirschwarz": "black", "saphirschwarz": "black",
"carbonschwarz": "black", "carbonschwarz": "black",
@@ -81,19 +109,15 @@ _COLOR_MAP = {
"jet black": "black", "jet black": "black",
"jetblack": "black", "jetblack": "black",
"black": "black", "black": "black",
"белый": "white",
"weiss": "white", "weiss": "white",
"weiß": "white", "weiß": "white",
"alpinweiss": "white", "alpinweiss": "white",
"alpine white": "white", "alpine white": "white",
"mineralweiss": "white", "mineralweiss": "white",
"white": "white", "white": "white",
"серебристый": "silver",
"серебряный": "silver",
"silber": "silver", "silber": "silver",
"argent": "silver", "argent": "silver",
"silver": "silver", "silver": "silver",
"серый": "gray",
"grau": "gray", "grau": "gray",
"grey": "gray", "grey": "gray",
"anthrazit": "gray", "anthrazit": "gray",
@@ -106,49 +130,50 @@ _COLOR_MAP = {
"sophistograu": "gray", "sophistograu": "gray",
"skyscraper grau": "gray", "skyscraper grau": "gray",
"gray": "gray", "gray": "gray",
"красный": "red",
"rot": "red", "rot": "red",
"burgundy": "red", "burgundy": "red",
"bordeaux": "red", "bordeaux": "red",
"maroon": "red", "maroon": "red",
"red": "red", "red": "red",
"синий": "blue",
"голубой": "blue",
"blau": "blue", "blau": "blue",
"turquoise": "blue", "turquoise": "blue",
"cyan": "blue", "cyan": "blue",
"blue": "blue", "blue": "blue",
"зеленый": "green",
"зелёный": "green",
"grün": "green", "grün": "green",
"gruen": "green", "gruen": "green",
"green": "green", "green": "green",
"коричневый": "brown",
"braun": "brown", "braun": "brown",
"brown": "brown", "brown": "brown",
"бежевый": "beige",
"beige": "beige", "beige": "beige",
"champagner": "beige", "champagner": "beige",
"champagne": "beige", "champagne": "beige",
"creme": "beige", "creme": "beige",
"cream": "beige", "cream": "beige",
"ivory": "beige", "ivory": "beige",
"желтый": "yellow",
"жёлтый": "yellow",
"gelb": "yellow", "gelb": "yellow",
"yellow": "yellow", "yellow": "yellow",
"оранжевый": "orange",
"orange": "orange", "orange": "orange",
"золотой": "gold",
"gold": "gold", "gold": "gold",
"бронзовый": "bronze",
"bronze": "bronze", "bronze": "bronze",
"фиолетовый": "purple",
"violett": "purple", "violett": "purple",
"lila": "purple", "lila": "purple",
"purple": "purple", "purple": "purple",
} }
_RU_DRIVE_MARKERS = {
"FWD": ("передний привод", "привод на передние колеса", "передние колеса"),
"RWD": ("задний привод", "привод на задние колеса", "задние колеса"),
"4WD": ("полный привод", "привод на четыре колеса", "все колеса"),
"2WD": ("привод на два колеса", "два ведущих колеса"),
}
_LEGACY_DRIVE_MARKERS = {
"FWD": ("front", "fwd", "frontantrieb", "vorderrad", "antrieb vorne", "front-wheel", "front wheel"),
"RWD": ("rear", "rwd", "heckantrieb", "hinterrad", "antrieb hinten", "rear-wheel", "rear wheel"),
"4WD": ("awd", "4wd", "4x4", "quattro", "xdrive", "4matic", "4motion", "allrad", "all-wheel", "all wheel", "four-wheel", "four wheel"),
"2WD": ("sdrive", "2wd", "two-wheel drive", "two wheel drive", "zweiradantrieb"),
}
_IMAGE_FIELD_HINTS = ["image", "images", "media", "gallery", "photo", "pic", "picture", "url", "src", "uri", "ref"] _IMAGE_FIELD_HINTS = ["image", "images", "media", "gallery", "photo", "pic", "picture", "url", "src", "uri", "ref"]
_MOBILEDE_IMAGE_HOST = "img.classistatic.de" _MOBILEDE_IMAGE_HOST = "img.classistatic.de"
_MOBILEDE_IMAGE_PATH_PREFIX = "/api/v1/mo-prod/images/" _MOBILEDE_IMAGE_PATH_PREFIX = "/api/v1/mo-prod/images/"
@@ -161,6 +186,7 @@ _BODY_VALUE_KEYS = {"category", "bodytype", "vehiclecategory", "body"}
_ENGINE_VOLUME_VALUE_KEYS = {"cubiccapacity", "enginevolume", "displacement", "enginedisplacement"} _ENGINE_VOLUME_VALUE_KEYS = {"cubiccapacity", "enginevolume", "displacement", "enginedisplacement"}
_DRIVE_VALUE_KEYS = {"wheeldrive", "drivetrain", "drive"} _DRIVE_VALUE_KEYS = {"wheeldrive", "drivetrain", "drive"}
_GEARBOX_VALUE_KEYS = {"transmission", "gearbox", "transmissiontype"} _GEARBOX_VALUE_KEYS = {"transmission", "gearbox", "transmissiontype"}
_STEERING_VALUE_KEYS = {"steeringwheel", "steeringposition", "steering", "righthanddrive"}
class MobileDeMapper: class MobileDeMapper:
@@ -183,12 +209,20 @@ class MobileDeMapper:
nested_engine = self._find_first_value(raw, _ENGINE_VOLUME_VALUE_KEYS) nested_engine = self._find_first_value(raw, _ENGINE_VOLUME_VALUE_KEYS)
nested_drive = self._find_first_value(raw, _DRIVE_VALUE_KEYS) nested_drive = self._find_first_value(raw, _DRIVE_VALUE_KEYS)
nested_gearbox = self._find_first_value(raw, _GEARBOX_VALUE_KEYS) nested_gearbox = self._find_first_value(raw, _GEARBOX_VALUE_KEYS)
nested_steering = self._find_first_value(raw, _STEERING_VALUE_KEYS)
attr_country = self._mapping_value(attr, "cn", "countryCode", "country") attr_country = self._mapping_value(attr, "cn", "countryCode", "country")
attr_color = self._mapping_value(attr, "ecol", "color", "exteriorColor", "manufacturerColorName", "paint") attr_color = self._mapping_value(attr, "ecol", "color", "exteriorColor", "manufacturerColorName", "paint")
attr_body = self._mapping_value(attr, "c", "category", "bodyType", "body") attr_body = self._mapping_value(attr, "c", "category", "bodyType", "body")
attr_engine = self._mapping_value(attr, "cc", "cubicCapacity", "engineVolume", "displacement", "engineDisplacement") attr_engine = self._mapping_value(attr, "cc", "cubicCapacity", "engineVolume", "displacement", "engineDisplacement")
attr_drive = self._mapping_value(attr, "wd", "wheelDrive", "drivetrain", "drive", "driveType", "antriebsart") attr_drive = self._mapping_value(attr, "wd", "wheelDrive", "drivetrain", "drive", "driveType", "antriebsart")
attr_gearbox = self._mapping_value(attr, "tr", "transmission", "gearbox", "transmissionType") attr_gearbox = self._mapping_value(attr, "tr", "transmission", "gearbox", "transmissionType")
attr_steering = self._mapping_value(attr, "steeringWheel", "steeringPosition", "steering", "rightHandDrive")
raw_features = raw.get("features") if isinstance(raw.get("features"), list) else []
steering_text = " ".join(
str(part)
for part in [attr_steering, nested_steering, *raw_features]
if part is not None and str(part).strip()
)
drive_text = " ".join( drive_text = " ".join(
part part
for part in [ for part in [
@@ -257,7 +291,10 @@ class MobileDeMapper:
), ),
drive=self._normalize_drive(drive_text), drive=self._normalize_drive(drive_text),
gearbox=self._normalize_gearbox(listing.transmission or attr_gearbox or nested_gearbox), gearbox=self._normalize_gearbox(listing.transmission or attr_gearbox or nested_gearbox),
steering_wheel="LEFT", steering_wheel=self._normalize_steering(
steering_text,
right_hand_drive=self._first_present(raw.get("rightHandDrive"), attr.get("rightHandDrive")),
),
body_type=self._normalize_body_from_candidates( body_type=self._normalize_body_from_candidates(
attr_body, attr_body,
raw.get("category"), raw.get("category"),
@@ -281,7 +318,7 @@ class MobileDeMapper:
), ),
selling_type="STOCK", selling_type="STOCK",
one_owner=self._is_one_owner(attr.get("pvo") or raw.get("numPreviousOwners")), one_owner=self._is_one_owner(attr.get("pvo") or raw.get("numPreviousOwners")),
new_car=self._bool_value(raw.get("isNew")) or self._bool_value(raw.get("isConditionNew")), new_car=self._bool_value(raw.get("isConditionNew")),
is_hidden=False, is_hidden=False,
origin="MOBILEDE", origin="MOBILEDE",
origin_url=listing.url, origin_url=listing.url,
@@ -333,23 +370,28 @@ class MobileDeMapper:
self._find_first_value(detail, _COLOR_VALUE_KEYS), self._find_first_value(detail, _COLOR_VALUE_KEYS),
) )
) )
drive_text = " ".join( features = detail.get("features") if isinstance(detail.get("features"), list) else []
feature_text = " ".join(str(feature) for feature in features if isinstance(feature, str))
explicit_drive_text = " ".join(
part part
for part in [ for part in [
short_title,
subtitle,
self._mapping_value(attrs, "wheelDrive", "drivetrain", "drive", "driveType", "antriebsart"), self._mapping_value(attrs, "wheelDrive", "drivetrain", "drive", "driveType", "antriebsart"),
detail.get("wheelDrive"), detail.get("wheelDrive"),
detail.get("drivetrain"), detail.get("drivetrain"),
detail.get("drive"), detail.get("drive"),
detail.get("driveType"), detail.get("driveType"),
self._find_first_value(detail, _DRIVE_VALUE_KEYS), self._find_first_value(detail, _DRIVE_VALUE_KEYS),
feature_text,
] ]
if isinstance(part, str) and part.strip() if isinstance(part, str) and part.strip()
) )
drive = self._normalize_drive(explicit_drive_text) or self._normalize_drive(f"{short_title} {subtitle}")
damage_text = self._text(attrs.get("damageCondition")).lower() damage_text = self._text(attrs.get("damageCondition")).lower()
is_damaged = self._is_damaged_value(detail.get("hasDamage"), damage_text) is_damaged = self._is_damaged_value(
self._first_present(detail.get("isDamageCase"), detail.get("hasDamage")),
damage_text,
)
owners_text = self._text( owners_text = self._text(
self._mapping_value(attrs, "numPreviousOwners", "numberOfPreviousOwners") self._mapping_value(attrs, "numPreviousOwners", "numberOfPreviousOwners")
@@ -367,9 +409,25 @@ class MobileDeMapper:
country=self._normalize_country(contact.get("countryCode") or contact.get("country") or "DE"), country=self._normalize_country(contact.get("countryCode") or contact.get("country") or "DE"),
is_sold=False, is_sold=False,
color=color, color=color,
drive=self._normalize_drive(drive_text), drive=drive,
gearbox=gearbox, gearbox=gearbox,
steering_wheel="LEFT", steering_wheel=self._normalize_steering(
" ".join(
str(part)
for part in [
self._mapping_value(attrs, "steeringWheel", "steeringPosition", "steering"),
detail.get("steeringWheel"),
detail.get("steeringPosition"),
detail.get("steering"),
feature_text,
]
if part is not None and str(part).strip()
),
right_hand_drive=self._first_present(
detail.get("rightHandDrive"),
self._mapping_value(attrs, "rightHandDrive"),
),
),
body_type=body_type, body_type=body_type,
engine_volume=self._engine_volume_from_candidates( engine_volume=self._engine_volume_from_candidates(
self._mapping_value(attrs, "cubicCapacity", "cc", "engineVolume", "displacement", "engineDisplacement"), self._mapping_value(attrs, "cubicCapacity", "cc", "engineVolume", "displacement", "engineDisplacement"),
@@ -384,16 +442,28 @@ class MobileDeMapper:
), ),
selling_type="STOCK", selling_type="STOCK",
one_owner=one_owner, one_owner=one_owner,
new_car=self._bool_value(detail.get("isNew")) or self._bool_value(detail.get("isConditionNew")), new_car=self._bool_value(detail.get("isConditionNew")),
is_hidden=False, is_hidden=False,
origin="MOBILEDE", origin="MOBILEDE",
origin_url=MobileDeClient.build_detail_url(listing_id), origin_url=MobileDeClient.build_detail_url(listing_id),
origin_id=origin_id, origin_id=origin_id,
is_damaged=is_damaged, is_damaged=is_damaged,
evaluation=self._rating_text(detail.get("priceRating") or detail.get("rating")), evaluation=self._rating_text(detail.get("priceRating") or detail.get("rating")),
non_smoking=False, non_smoking=self._contains_marker(feature_text, "машина некурящего водителя"),
rental=False, rental=self._contains_marker(
repair_history=False, feature_text,
"прокатный автомобиль",
"арендный автомобиль",
"rental vehicle",
"mietwagen",
),
repair_history=self._contains_marker(
damage_text,
"устраненные повреждения",
"устранённые повреждения",
"repaired damage",
"reparierter unfallschaden",
),
slug=self._slugify(" ".join(part for part in [title or f"{brand} {model}", str(year) if year is not None else ""] if part)), slug=self._slugify(" ".join(part for part in [title or f"{brand} {model}", str(year) if year is not None else ""] if part)),
last_seen_at=datetime.now(timezone.utc), last_seen_at=datetime.now(timezone.utc),
details_confirmed=True, details_confirmed=True,
@@ -504,7 +574,8 @@ class MobileDeMapper:
@staticmethod @staticmethod
def _normalize_gearbox(value: Any) -> str | None: def _normalize_gearbox(value: Any) -> str | None:
text = "" if value is None else str(value).lower() text = "" if value is None else str(value).lower()
for marker, mapped in _GEARBOX_MAP.items(): for value_map in (_RU_GEARBOX_MAP, _LEGACY_GEARBOX_MAP):
for marker, mapped in value_map.items():
if marker in text: if marker in text:
return mapped return mapped
return None return None
@@ -512,14 +583,23 @@ class MobileDeMapper:
@staticmethod @staticmethod
def _normalize_drive(value: Any) -> str | None: def _normalize_drive(value: Any) -> str | None:
text = "" if value is None else str(value).lower() text = "" if value is None else str(value).lower()
if any(marker in text for marker in ("передний привод", "передние колеса", "front", "fwd", "frontantrieb", "vorderrad", "antrieb vorne", "front-wheel", "front wheel")): for marker_map in (_RU_DRIVE_MARKERS, _LEGACY_DRIVE_MARKERS):
return "FWD" for mapped, markers in marker_map.items():
if any(marker in text for marker in ("задний привод", "задние колеса", "rear", "rwd", "heckantrieb", "hinterrad", "antrieb hinten", "rear-wheel", "rear wheel")): if any(marker in text for marker in markers):
return "RWD" return mapped
if any(marker in text for marker in ("полный привод", "все колеса", "awd", "4wd", "4x4", "quattro", "xdrive", "4matic", "4motion", "allrad", "all-wheel", "all wheel", "four-wheel", "four wheel")):
return "4WD"
return None return None
@classmethod
def _normalize_steering(cls, value: Any, *, right_hand_drive: Any = None) -> str:
if right_hand_drive is not None:
return "RIGHT" if cls._bool_value(right_hand_drive) else "LEFT"
text = "" if value is None else str(value).lower()
if any(marker in text for marker in ("правый руль", "праворуль", "right-hand drive", "right hand drive", "rhd", "rechtslenker", "lenkrad rechts")):
return "RIGHT"
if any(marker in text for marker in ("левый руль", "леворуль", "left-hand drive", "left hand drive", "lhd", "linkslenker", "lenkrad links")):
return "LEFT"
return "LEFT"
@staticmethod @staticmethod
def _normalize_country(value: Any) -> str: def _normalize_country(value: Any) -> str:
text = "" if value is None else str(value).strip().upper() text = "" if value is None else str(value).strip().upper()
@@ -542,9 +622,10 @@ class MobileDeMapper:
@staticmethod @staticmethod
def _normalize_body(value: Any) -> str: def _normalize_body(value: Any) -> str:
text = "" if value is None else str(value).lower() text = "" if value is None else str(value).lower()
if re.search(r"\bbmw\s+x(?:[1-7]|m)\b", text): if re.search(r"\bbmw\s+(?:x(?:[1-7]|m)|ix(?:[1-3])?|xm)\b", text):
return "SUV" return "SUV"
for marker, mapped in _BODY_MAP.items(): for value_map in (_RU_BODY_MAP, _LEGACY_BODY_MAP):
for marker, mapped in value_map.items():
if marker in {"van", "bus"}: if marker in {"van", "bus"}:
if re.search(rf"\b{re.escape(marker)}\b", text): if re.search(rf"\b{re.escape(marker)}\b", text):
return mapped return mapped
@@ -552,6 +633,11 @@ class MobileDeMapper:
return mapped return mapped
return "OTHER" return "OTHER"
@staticmethod
def _contains_marker(value: Any, *markers: str) -> bool:
text = "" if value is None else str(value).lower()
return any(marker.lower() in text for marker in markers)
@classmethod @classmethod
def _normalize_body_from_candidates(cls, *values: Any) -> str: def _normalize_body_from_candidates(cls, *values: Any) -> str:
for value in values: for value in values:
@@ -625,10 +711,13 @@ class MobileDeMapper:
text = "" if value is None else str(value).lower().strip() text = "" if value is None else str(value).lower().strip()
text = text.replace("ä", "a").replace("ö", "o").replace("ü", "u").replace("ß", "ss") text = text.replace("ä", "a").replace("ö", "o").replace("ü", "u").replace("ß", "ss")
text = re.sub(r"[_\-/]+", " ", text) text = re.sub(r"[_\-/]+", " ", text)
for marker, mapped in _COLOR_MAP.items(): if text == "wei":
return "white"
for value_map in (_RU_COLOR_MAP, _LEGACY_COLOR_MAP):
for marker, mapped in value_map.items():
if marker in text: if marker in text:
return mapped return mapped
return text[:50] if text else "other" return "other"
@staticmethod @staticmethod
def _is_one_owner(value: Any) -> bool: def _is_one_owner(value: Any) -> bool:
+114
View File
@@ -434,6 +434,17 @@ class TestMobileDeMapper(unittest.TestCase):
self.assertEqual(record.drive, "FWD") self.assertEqual(record.drive, "FWD")
def test_bmw_sdrive_maps_to_two_wheel_drive(self) -> None:
record = self.mapper.detail_to_car_record(
"1014",
{
"shortTitle": "BMW X1",
"subTitle": "sDrive18i Advantage",
},
)
self.assertEqual(record.drive, "2WD")
def test_russian_search_values_map_to_english_contract(self) -> None: def test_russian_search_values_map_to_english_contract(self) -> None:
record = self.mapper.listing_to_car_record( record = self.mapper.listing_to_car_record(
MobileDeListing( MobileDeListing(
@@ -469,6 +480,64 @@ class TestMobileDeMapper(unittest.TestCase):
self.assertFalse(record.is_damaged) self.assertFalse(record.is_damaged)
self.assertFalse(record.new_car) self.assertFalse(record.new_car)
def test_russian_gold_color_maps_to_english_contract(self) -> None:
record = self.mapper.listing_to_car_record(
MobileDeListing(
id="2002",
url="https://suchen.mobile.de/fahrzeuge/details.html?id=2002",
title="BMW X2",
raw={"attr": {"ecol": "Золотистый Металлик Матовый"}},
)
)
self.assertEqual(record.color, "gold")
def test_truncated_white_color_maps_to_english_contract(self) -> None:
record = self.mapper.listing_to_car_record(
MobileDeListing(
id="2003",
url="https://suchen.mobile.de/fahrzeuge/details.html?id=2003",
title="BMW 320",
raw={"attr": {"ecol": "Wei"}},
)
)
self.assertEqual(record.color, "white")
def test_unknown_color_maps_to_other(self) -> None:
record = self.mapper.listing_to_car_record(
MobileDeListing(
id="2004",
url="https://suchen.mobile.de/fahrzeuge/details.html?id=2004",
title="BMW 320",
raw={"attr": {"ecol": "Неизвестное покрытие"}},
)
)
self.assertEqual(record.color, "other")
def test_right_hand_drive_maps_from_russian_feature(self) -> None:
record = self.mapper.detail_to_car_record(
"2005",
{
"shortTitle": "BMW 320",
"features": ["Правый руль"],
},
)
self.assertEqual(record.steering_wheel, "RIGHT")
def test_right_hand_drive_maps_from_explicit_flag(self) -> None:
record = self.mapper.detail_to_car_record(
"2006",
{
"shortTitle": "BMW 320",
"rightHandDrive": True,
},
)
self.assertEqual(record.steering_wheel, "RIGHT")
def test_russian_detail_values_and_exact_gallery_map_to_english_contract(self) -> None: def test_russian_detail_values_and_exact_gallery_map_to_english_contract(self) -> None:
first = "img.classistatic.de/api/v1/mo-prod/images/a1/a1111111-1111-4111-8111-111111111111" first = "img.classistatic.de/api/v1/mo-prod/images/a1/a1111111-1111-4111-8111-111111111111"
second = "img.classistatic.de/api/v1/mo-prod/images/b2/b2222222-2222-4222-8222-222222222222" second = "img.classistatic.de/api/v1/mo-prod/images/b2/b2222222-2222-4222-8222-222222222222"
@@ -482,6 +551,11 @@ class TestMobileDeMapper(unittest.TestCase):
"model": {"localized": "Macan"}, "model": {"localized": "Macan"},
"price": {"grs": {"amount": 59900, "currency": "EUR"}}, "price": {"grs": {"amount": 59900, "currency": "EUR"}},
"isConditionNew": "false", "isConditionNew": "false",
"isDamageCase": True,
"features": [
"Привод на четыре колеса",
"Машина некурящего водителя",
],
"attributes": [ "attributes": [
{"tag": "firstRegistration", "value": "04/2022"}, {"tag": "firstRegistration", "value": "04/2022"},
{"tag": "mileage", "value": "149 300 км"}, {"tag": "mileage", "value": "149 300 км"},
@@ -503,8 +577,12 @@ class TestMobileDeMapper(unittest.TestCase):
self.assertEqual(record.gearbox, "AT") self.assertEqual(record.gearbox, "AT")
self.assertEqual(record.body_type, "SUV") self.assertEqual(record.body_type, "SUV")
self.assertEqual(record.engine_volume, 2894) self.assertEqual(record.engine_volume, 2894)
self.assertEqual(record.drive, "4WD")
self.assertTrue(record.one_owner) self.assertTrue(record.one_owner)
self.assertTrue(record.is_damaged) self.assertTrue(record.is_damaged)
self.assertTrue(record.non_smoking)
self.assertTrue(record.repair_history)
self.assertFalse(record.rental)
self.assertFalse(record.new_car) self.assertFalse(record.new_car)
self.assertEqual(len(record.images), 2) self.assertEqual(len(record.images), 2)
self.assertEqual( self.assertEqual(
@@ -515,6 +593,42 @@ class TestMobileDeMapper(unittest.TestCase):
], ],
) )
def test_detail_is_new_ad_does_not_mean_new_car(self) -> None:
record = self.mapper.detail_to_car_record(
"2003",
{
"shortTitle": "BMW X5",
"make": {"localized": "BMW"},
"model": {"localized": "X5"},
"isNew": True,
"isConditionNew": False,
"attributes": [
{"tag": "firstRegistration", "value": "05/2023"},
{"tag": "mileage", "value": "108 398 км"},
{"tag": "damageCondition", "value": "Подержанный"},
],
},
)
self.assertFalse(record.new_car)
def test_russian_mechanics_and_bmw_ix_body_are_mapped(self) -> None:
record = self.mapper.detail_to_car_record(
"2004",
{
"shortTitle": "BMW iX3",
"make": {"localized": "BMW"},
"model": {"localized": "iX3"},
"attributes": [
{"tag": "transmission", "value": "Механика"},
{"tag": "category", "value": "Иное"},
],
},
)
self.assertEqual(record.gearbox, "MT")
self.assertEqual(record.body_type, "SUV")
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()