fix(tradein-imv): adapt to Avito 3-step API contract (2026-05+)
Avito changed IMV flow — /web/1/coords/by_address no longer returns
geoHash. New flow has 3 endpoints:
1. /web/1/coords/by_address -> normalize + lat/lon
2. /js/v2/geo/position -> rich JWT (geoFieldsHash) with addressId/
locationId/metroId/districtId
3. /web/1/realty-imv/get-data -> recommendedPrice/lowerPrice/higherPrice
Old code skipped step 2 -> IMVAddressNotFoundError for every address since
contract change. PR #505 retry-with-cleaned-address never had a chance.
Verified via playwright browser for production estimate
a0a0b820-e8a8-4eee-aa73-0ab3b98ac233 (Zavodskaya 44-a): Avito IMV
recommendedPrice=7,520,000 RUB (range 7,294,400-7,896,000).
This commit is contained in:
parent
45ee0a2945
commit
c897d5793a
1 changed files with 79 additions and 31 deletions
|
|
@ -1,10 +1,11 @@
|
|||
"""avito_imv.py — Avito IMV evaluation API client (2-request flow).
|
||||
"""avito_imv.py — Avito IMV evaluation API client (3-request flow, 2026-05+).
|
||||
|
||||
Stage 2d: geocode + IMV evaluation.
|
||||
|
||||
Flow:
|
||||
1) GET /web/1/coords/by_address?address=<urlenc> → geoHash + geo IDs
|
||||
2) POST /web/1/realty-imv/get-data → price + placementHistory + suggestions
|
||||
Flow (current contract):
|
||||
1) GET /web/1/coords/by_address?address=<urlenc> → normalize + lat/lon
|
||||
2) POST /js/v2/geo/position → rich JWT (geoFieldsHash)
|
||||
3) POST /web/1/realty-imv/get-data → price + placementHistory + suggestions
|
||||
|
||||
Таблицы:
|
||||
avito_imv_evaluations (018_avito_imv_evaluations.sql)
|
||||
|
|
@ -30,6 +31,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
AVITO_BASE = "https://www.avito.ru"
|
||||
COORDS_ENDPOINT = "/web/1/coords/by_address"
|
||||
POSITION_ENDPOINT = "/js/v2/geo/position"
|
||||
IMV_ENDPOINT = "/web/1/realty-imv/get-data"
|
||||
|
||||
# Заголовки имитируют браузер на странице evaluation/realty
|
||||
|
|
@ -271,16 +273,19 @@ async def evaluate_via_imv(
|
|||
*,
|
||||
cffi_session: Any | None = None,
|
||||
) -> IMVEvaluation:
|
||||
"""Avito IMV — 2 HTTP requests:
|
||||
"""Avito IMV — 3 HTTP requests (current contract 2026-05+):
|
||||
|
||||
1) GET /web/1/coords/by_address?address=<urlenc> → geoHash + geo IDs
|
||||
2) POST /web/1/realty-imv/get-data → price + placementHistory + suggestions
|
||||
1) GET /web/1/coords/by_address?address=<urlenc> → normalize + lat/lon
|
||||
2) POST /js/v2/geo/position → rich JWT (geoFieldsHash)
|
||||
3) POST /web/1/realty-imv/get-data → price + placementHistory + suggestions
|
||||
|
||||
Returns IMVEvaluation с уже вычисленным cache_key.
|
||||
|
||||
Raises:
|
||||
IMVAddressNotFoundError если geoHash пустой / не вернулся
|
||||
httpx.HTTPStatusError на 4xx/5xx
|
||||
IMVAddressNotFoundError если point/normalizedAddress пустые (step 1)
|
||||
или geoFieldsHash не вернулся (step 2)
|
||||
IMVAuthError на 401/403
|
||||
IMVTransientError на 5xx / network errors
|
||||
"""
|
||||
# Импортируем здесь чтобы избежать циклических зависимостей при тестах без сети
|
||||
try:
|
||||
|
|
@ -333,40 +338,83 @@ def _raise_for_status_categorized(resp: Any, context: str) -> None:
|
|||
|
||||
|
||||
async def _geocode(session: Any, address: str) -> IMVGeo:
|
||||
"""Request 1: GET /web/1/coords/by_address → IMVGeo."""
|
||||
"""Two-step Avito IMV geocoder (current contract 2026-05+).
|
||||
|
||||
Step A: GET /web/1/coords/by_address → normalize + lat/lon
|
||||
Step B: POST /js/v2/geo/position → rich JWT (geoFieldsHash) with
|
||||
addressId/locationId/metroId/districtId
|
||||
|
||||
JWT from step B is what /realty-imv/get-data expects as `geoHash`.
|
||||
"""
|
||||
# Step A — normalize + coords
|
||||
params = urlencode({"address": address})
|
||||
url = f"{AVITO_BASE}{COORDS_ENDPOINT}?{params}"
|
||||
logger.info("IMV geocode: address=%r", address)
|
||||
url_a = f"{AVITO_BASE}{COORDS_ENDPOINT}?{params}"
|
||||
logger.info("IMV geocode A: address=%r", address)
|
||||
|
||||
try:
|
||||
resp = await session.get(url, headers=_COMMON_HEADERS)
|
||||
resp_a = await session.get(url_a, headers=_COMMON_HEADERS)
|
||||
except Exception as exc:
|
||||
# Timeout / connection error — retryable
|
||||
raise IMVTransientError(f"Avito geocode network error: {exc}") from exc
|
||||
_raise_for_status_categorized(resp, "geocode")
|
||||
data: dict[str, Any] = resp.json()
|
||||
raise IMVTransientError(f"Avito geocode A network error: {exc}") from exc
|
||||
_raise_for_status_categorized(resp_a, "geocode-A")
|
||||
data_a: dict[str, Any] = resp_a.json()
|
||||
|
||||
geo_hash = data.get("geoHash")
|
||||
if not geo_hash:
|
||||
point = data_a.get("point") or {}
|
||||
lat = point.get("latitude")
|
||||
lon = point.get("longitude")
|
||||
normalized = data_a.get("normalizedAddress")
|
||||
|
||||
if not lat or not lon or not normalized:
|
||||
raise IMVAddressNotFoundError(
|
||||
f"Avito /coords/by_address не вернул geoHash для адреса: {address!r}"
|
||||
f"Avito /coords/by_address не вернул point для {address!r}"
|
||||
)
|
||||
|
||||
# Step B — rich JWT
|
||||
url_b = f"{AVITO_BASE}{POSITION_ENDPOINT}"
|
||||
payload_b = {
|
||||
"address": normalized,
|
||||
"addressId": "",
|
||||
"categoryId": 24, # 24 = недвижимость / квартиры
|
||||
"isRadius": False,
|
||||
"latitude": lat,
|
||||
"longitude": lon,
|
||||
}
|
||||
logger.info("IMV geocode B: position for %r", normalized[:60])
|
||||
|
||||
try:
|
||||
resp_b = await session.post(
|
||||
url_b,
|
||||
headers={**_COMMON_HEADERS, "Content-Type": "application/json"},
|
||||
json=payload_b,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise IMVTransientError(f"Avito geocode B network error: {exc}") from exc
|
||||
_raise_for_status_categorized(resp_b, "geocode-B")
|
||||
data_b: dict[str, Any] = resp_b.json()
|
||||
|
||||
jwt = data_b.get("geoFieldsHash")
|
||||
if not jwt:
|
||||
raise IMVAddressNotFoundError(
|
||||
f"Avito /js/v2/geo/position не вернул geoFieldsHash для {normalized!r}: "
|
||||
f"errorText={data_b.get('errorText')!r}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"IMV geocode OK: locationId=%s lat=%s lon=%s",
|
||||
data.get("locationId"),
|
||||
data.get("latitude"),
|
||||
data.get("longitude"),
|
||||
"IMV geocode OK: addressId=%s locationId=%s metroId=%s lat=%s lon=%s",
|
||||
data_b.get("addressId"),
|
||||
data_b.get("locationId"),
|
||||
data_b.get("metroId"),
|
||||
lat,
|
||||
lon,
|
||||
)
|
||||
|
||||
return IMVGeo(
|
||||
geo_hash=geo_hash,
|
||||
lat=data.get("latitude"),
|
||||
lon=data.get("longitude"),
|
||||
avito_address_id=data.get("addressId"),
|
||||
avito_location_id=data.get("locationId"),
|
||||
avito_metro_id=data.get("metroId"),
|
||||
avito_district_id=data.get("districtId"),
|
||||
geo_hash=jwt,
|
||||
lat=lat,
|
||||
lon=lon,
|
||||
avito_address_id=data_b.get("addressId"),
|
||||
avito_location_id=data_b.get("locationId"),
|
||||
avito_metro_id=data_b.get("metroId"),
|
||||
avito_district_id=data_b.get("districtId"),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue