198 lines
6.9 KiB
Python
198 lines
6.9 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from types import SimpleNamespace
|
|
|
|
from mobilede_scraper.browser.fast_client import FastListingVehicle
|
|
from mobilede_scraper.core.config import Settings
|
|
from mobilede_scraper.core.runtime_config import RuntimeConfig, RuntimeFiltersConfig
|
|
from mobilede_scraper.fast_sync import FastSyncEngine
|
|
from mobilede_scraper.parsing.fast_mapper import FastCarMapper
|
|
|
|
|
|
def _listing_vehicle(inventory_id: str, *, status: str = "RS") -> FastListingVehicle:
|
|
return FastListingVehicle(
|
|
inventory_id=inventory_id,
|
|
tenant="US",
|
|
auction_id="1_1",
|
|
auction_date="2026-04-08T08:30:00+00:00",
|
|
inventory_status=status,
|
|
currency="USD",
|
|
timed_auction_closed=False,
|
|
timed_auction_indicator=False,
|
|
prebid_indicator=True,
|
|
buynow_indicator=False,
|
|
)
|
|
|
|
|
|
def _detail_payload(inventory_id: str, *, make: str = "ACURA", model: str = "RSX", bid: int = 500) -> dict:
|
|
return {
|
|
"inventoryView": {
|
|
"attributes": {
|
|
"Id": inventory_id,
|
|
"Year": "2002",
|
|
"Make": make,
|
|
"Model": model,
|
|
"Series": "BASE",
|
|
"Currency": "USD",
|
|
"ODOValue": "219187",
|
|
"ExteriorColor": "GRAY",
|
|
"DriveLineTypeDesc": "FWD",
|
|
"Transmission": "Automatic",
|
|
"BodyStyleName": "COUPE",
|
|
"EngineSize": "2.0L I-4",
|
|
"VehicleGrade": "50",
|
|
"PrimaryDamageDesc": "NORMAL WEAR & TEAR",
|
|
},
|
|
"imageDimensions": {
|
|
"keys": {"$values": [{"k": f"{inventory_id}~I1", "w": 1600, "h": 1200, "i": 0}]}
|
|
},
|
|
},
|
|
"auctionInformation": {
|
|
"prebidInformation": {"decimalHighBidAmount": str(bid), "highBidAmount": f"${bid}"},
|
|
"biddingInformation": {"buyNowPrice": "$0"},
|
|
},
|
|
}
|
|
|
|
|
|
def _detail_payload_with_fallbacks(inventory_id: str) -> dict:
|
|
return {
|
|
"inventoryView": {
|
|
"attributes": {
|
|
"Id": inventory_id,
|
|
"FirstRegistration": "2011-03",
|
|
"Make": "BMW",
|
|
"Model": "120",
|
|
"Variant": "Cabrio",
|
|
"Currency": "EUR",
|
|
"Kilometerstand": "95 000",
|
|
"Color": "weiss",
|
|
"DriveType": "Front wheel drive",
|
|
"Gearbox": "Automatik",
|
|
"VehicleClass": "Cabrio",
|
|
"EngineInformation": "2,0 l",
|
|
"VehicleGrade": "A",
|
|
},
|
|
"imageDimensions": {
|
|
"keys": {"$values": [{"k": f"{inventory_id}~I1", "w": 1600, "h": 1200, "i": 0}]}
|
|
},
|
|
},
|
|
"auctionInformation": {
|
|
"biddingInformation": {"buyNowPrice": "8.899 €"},
|
|
},
|
|
}
|
|
|
|
|
|
class FakeFastClient:
|
|
def __init__(self, listing: list[FastListingVehicle], details: dict[str, dict]) -> None:
|
|
self.listing = listing
|
|
self.details = details
|
|
self.detail_calls = 0
|
|
self.persist_calls = 0
|
|
self._settings = SimpleNamespace(
|
|
scraping_profile=SimpleNamespace(
|
|
verbose_progress_logs=False,
|
|
request_jitter_max_s=0.0,
|
|
)
|
|
)
|
|
|
|
def iter_listing_vehicles(self, *, listing_start_url=None, make=None, max_pages=None): # noqa: ANN001, ANN202
|
|
del listing_start_url, make, max_pages
|
|
return iter(self.listing)
|
|
|
|
def fetch_vehicle_detail_payload(self, inventory_id: str) -> dict:
|
|
self.detail_calls += 1
|
|
return self.details[inventory_id]
|
|
|
|
def persist_session_state(self) -> None:
|
|
self.persist_calls += 1
|
|
|
|
|
|
@dataclass
|
|
class FakePersistence:
|
|
inserted: int = 0
|
|
images: int = 0
|
|
|
|
def get_existing_urls_and_ids(self, origin_urls, origin_ids): # noqa: ANN001, ANN202
|
|
del origin_urls, origin_ids
|
|
return set(), set()
|
|
|
|
def upsert_cars_batch(self, records): # noqa: ANN001, ANN202
|
|
self.inserted += len(records)
|
|
self.images += sum(len(record.images) for record in records)
|
|
return {"inserted": len(records), "updated": 0, "images_upserted": sum(len(record.images) for record in records)}
|
|
|
|
def mark_sold_not_in_listing_by_urls(self, active_origin_urls, lane="MOBILEDE"): # noqa: ANN001, ANN202
|
|
del active_origin_urls, lane
|
|
return 0
|
|
|
|
|
|
def test_fast_sync_engine_collects_details_and_bulk_upserts() -> None:
|
|
listing = [_listing_vehicle("45078011~US"), _listing_vehicle("45268167~US")]
|
|
details = {
|
|
"45078011~US": _detail_payload("45078011~US", make="ACURA", model="RSX", bid=500),
|
|
"45268167~US": _detail_payload("45268167~US", make="BMW", model="X5", bid=900),
|
|
}
|
|
client = FakeFastClient(listing, details)
|
|
persistence = FakePersistence()
|
|
engine = FastSyncEngine(
|
|
client=client, # type: ignore[arg-type]
|
|
mapper=FastCarMapper(),
|
|
persistence=persistence, # type: ignore[arg-type]
|
|
batch_size=10,
|
|
fetch_concurrency=2,
|
|
)
|
|
|
|
result = engine.sync_listing(runtime_config=RuntimeConfig(), only_new=False)
|
|
|
|
assert result["status"] == "success"
|
|
assert result["cars_upserted"] == 2
|
|
assert result["images_upserted"] == 2
|
|
assert result["listing"]["mode"] == "fast_hidden_payload"
|
|
assert client.detail_calls == 2
|
|
assert client.persist_calls == 1
|
|
|
|
|
|
def test_fast_sync_engine_applies_runtime_filters_before_db() -> None:
|
|
listing = [_listing_vehicle("45078011~US"), _listing_vehicle("45268167~US")]
|
|
details = {
|
|
"45078011~US": _detail_payload("45078011~US", make="ACURA", model="RSX", bid=500),
|
|
"45268167~US": _detail_payload("45268167~US", make="BMW", model="X5", bid=900),
|
|
}
|
|
runtime = RuntimeConfig(filters=RuntimeFiltersConfig.from_dict({"brands": ["BMW"]}))
|
|
persistence = FakePersistence()
|
|
engine = FastSyncEngine(
|
|
client=FakeFastClient(listing, details), # type: ignore[arg-type]
|
|
mapper=FastCarMapper(),
|
|
persistence=persistence, # type: ignore[arg-type]
|
|
batch_size=10,
|
|
fetch_concurrency=2,
|
|
)
|
|
|
|
result = engine.sync_listing(runtime_config=runtime, only_new=False)
|
|
|
|
assert result["cars_upserted"] == 1
|
|
assert result["cars_filtered"] == 1
|
|
assert persistence.inserted == 1
|
|
|
|
|
|
def test_fast_sync_engine_maps_fallback_mobilede_fields() -> None:
|
|
listing = [_listing_vehicle("449252166")]
|
|
details = {
|
|
"449252166": _detail_payload_with_fallbacks("449252166"),
|
|
}
|
|
persistence = FakePersistence()
|
|
engine = FastSyncEngine(
|
|
client=FakeFastClient(listing, details), # type: ignore[arg-type]
|
|
mapper=FastCarMapper(),
|
|
persistence=persistence, # type: ignore[arg-type]
|
|
batch_size=10,
|
|
fetch_concurrency=1,
|
|
)
|
|
|
|
result = engine.sync_listing(runtime_config=RuntimeConfig(), only_new=False)
|
|
|
|
assert result["cars_upserted"] == 1
|
|
assert persistence.inserted == 1
|
|
assert persistence.images == 1
|