Добавлен tomtom
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
"""TomTom address suggestion adapter package."""
|
||||
|
||||
from app.adapters.address_suggestions.tomtom.client import (
|
||||
TomTomAddressSuggestionProvider,
|
||||
TomTomClientError,
|
||||
TomTomRequestError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"TomTomAddressSuggestionProvider",
|
||||
"TomTomClientError",
|
||||
"TomTomRequestError",
|
||||
]
|
||||
@@ -0,0 +1,155 @@
|
||||
"""TomTom address suggestion adapter."""
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
import httpx
|
||||
|
||||
from app.adapters.address_suggestions.base import (
|
||||
AddressSuggestionClientError,
|
||||
AddressSuggestionProvider,
|
||||
AddressSuggestionRequestError,
|
||||
)
|
||||
from app.config import TomTomAddressSuggestionsConfig
|
||||
from app.schemas.request import AddressSuggestRequest
|
||||
from app.schemas.response import AddressSuggestion
|
||||
|
||||
_TOMTOM_IDX_SET = "PAD,Addr,Str,EPP"
|
||||
|
||||
|
||||
class TomTomClientError(AddressSuggestionClientError):
|
||||
"""Raised when TomTom Search requests fail."""
|
||||
|
||||
|
||||
class TomTomRequestError(TomTomClientError, AddressSuggestionRequestError):
|
||||
"""Raised when TomTom rejects request parameters."""
|
||||
|
||||
|
||||
class TomTomAddressSuggestionProvider(AddressSuggestionProvider):
|
||||
name = "tomtom"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
http_client: httpx.AsyncClient,
|
||||
*,
|
||||
url: str,
|
||||
api_key: str,
|
||||
timeout_seconds: float = 10.0,
|
||||
) -> None:
|
||||
self._http_client = http_client
|
||||
self._url = url.rstrip("/")
|
||||
self._api_key = api_key
|
||||
self._timeout_seconds = timeout_seconds
|
||||
|
||||
@classmethod
|
||||
def from_config(
|
||||
cls,
|
||||
*,
|
||||
http_client: httpx.AsyncClient,
|
||||
config: TomTomAddressSuggestionsConfig,
|
||||
) -> "TomTomAddressSuggestionProvider":
|
||||
return cls(
|
||||
http_client=http_client,
|
||||
url=config.url,
|
||||
api_key=config.api_key,
|
||||
timeout_seconds=config.timeout_seconds,
|
||||
)
|
||||
|
||||
async def suggest(self, request: AddressSuggestRequest) -> list[AddressSuggestion]:
|
||||
request_url = self._build_request_url(request)
|
||||
params = self._build_query_params(request)
|
||||
try:
|
||||
response = await self._http_client.get(
|
||||
request_url,
|
||||
params=params,
|
||||
timeout=self._timeout_seconds,
|
||||
)
|
||||
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
||||
raise TomTomClientError("TomTom Search request failed.") from exc
|
||||
|
||||
if response.status_code == 400:
|
||||
raise TomTomRequestError(
|
||||
"TomTom Search request was rejected with status 400."
|
||||
)
|
||||
|
||||
if response.status_code >= 500 or response.status_code in {403, 429}:
|
||||
raise TomTomClientError(
|
||||
"TomTom Search request failed with status "
|
||||
f"{response.status_code}."
|
||||
)
|
||||
|
||||
if 400 <= response.status_code < 500:
|
||||
raise TomTomClientError(
|
||||
"TomTom Search request failed with status "
|
||||
f"{response.status_code}."
|
||||
)
|
||||
|
||||
try:
|
||||
response.raise_for_status()
|
||||
raw_payload = response.json()
|
||||
except (httpx.HTTPError, TypeError, ValueError) as exc:
|
||||
raise TomTomClientError("TomTom Search returned invalid payload.") from exc
|
||||
|
||||
try:
|
||||
return _map_tomtom_response(raw_payload)
|
||||
except ValueError as exc:
|
||||
raise TomTomClientError(
|
||||
"TomTom Search response payload is invalid."
|
||||
) from exc
|
||||
|
||||
def _build_request_url(self, request: AddressSuggestRequest) -> str:
|
||||
search_query = f"{request.city.strip()} {request.query.strip()}".strip()
|
||||
return f"{self._url}/{quote(search_query, safe='')}.json"
|
||||
|
||||
def _build_query_params(self, request: AddressSuggestRequest) -> dict[str, Any]:
|
||||
params: dict[str, Any] = {
|
||||
"key": self._api_key,
|
||||
"countrySet": request.country_code.strip().upper(),
|
||||
"typeahead": "true",
|
||||
"idxSet": _TOMTOM_IDX_SET,
|
||||
}
|
||||
if request.limit is not None:
|
||||
params["limit"] = request.limit
|
||||
return params
|
||||
|
||||
|
||||
def _map_tomtom_response(payload: object) -> list[AddressSuggestion]:
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("TomTom response must be a JSON object.")
|
||||
|
||||
raw_results = payload.get("results")
|
||||
if raw_results is None:
|
||||
return []
|
||||
if not isinstance(raw_results, list):
|
||||
raise ValueError("TomTom response must include results list.")
|
||||
|
||||
return [_map_tomtom_result(item) for item in raw_results]
|
||||
|
||||
|
||||
def _map_tomtom_result(payload: object) -> AddressSuggestion:
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("TomTom result entry must be an object.")
|
||||
|
||||
raw_address = payload.get("address")
|
||||
if not isinstance(raw_address, dict):
|
||||
raise ValueError("TomTom result address must be an object.")
|
||||
|
||||
address = _coerce_optional_text(raw_address.get("freeformAddress"))
|
||||
if address is None:
|
||||
raise ValueError("TomTom result has no valid freeformAddress.")
|
||||
|
||||
return AddressSuggestion(
|
||||
address=address,
|
||||
street=_coerce_optional_text(raw_address.get("streetName")),
|
||||
house=_coerce_optional_text(raw_address.get("streetNumber")),
|
||||
flat=None,
|
||||
postal_code=_coerce_optional_text(raw_address.get("postalCode")),
|
||||
)
|
||||
|
||||
|
||||
def _coerce_optional_text(value: object) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("TomTom address field must be a string.")
|
||||
return value.strip() or None
|
||||
@@ -60,6 +60,12 @@ class YandexGeosuggestAddressSuggestionsConfig(BaseModel):
|
||||
timeout_seconds: float = Field(default=10.0, gt=0)
|
||||
|
||||
|
||||
class TomTomAddressSuggestionsConfig(BaseModel):
|
||||
url: str = "https://api.tomtom.com/search/2/search"
|
||||
api_key: str = ""
|
||||
timeout_seconds: float = Field(default=10.0, gt=0)
|
||||
|
||||
|
||||
class AddressSuggestionsConfig(BaseModel):
|
||||
country_to_provider: dict[str, str] = Field(default_factory=dict)
|
||||
dadata: DadataAddressSuggestionsConfig = Field(
|
||||
@@ -68,6 +74,9 @@ class AddressSuggestionsConfig(BaseModel):
|
||||
yandex_geosuggest: YandexGeosuggestAddressSuggestionsConfig = Field(
|
||||
default_factory=YandexGeosuggestAddressSuggestionsConfig
|
||||
)
|
||||
tomtom: TomTomAddressSuggestionsConfig = Field(
|
||||
default_factory=TomTomAddressSuggestionsConfig
|
||||
)
|
||||
|
||||
@field_validator("country_to_provider")
|
||||
@classmethod
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
|
||||
from app.adapters.address_suggestions.dadata import DadataAddressSuggestionProvider
|
||||
from app.adapters.address_suggestions.tomtom import TomTomAddressSuggestionProvider
|
||||
from app.adapters.address_suggestions.yandex_geosuggest import (
|
||||
YandexGeosuggestAddressSuggestionProvider,
|
||||
)
|
||||
@@ -45,13 +46,21 @@ def _build_aggregator_service(settings: Settings) -> AggregatorService:
|
||||
http_client=http_client,
|
||||
config=settings.address_suggestions.yandex_geosuggest,
|
||||
)
|
||||
tomtom_provider = TomTomAddressSuggestionProvider.from_config(
|
||||
http_client=http_client,
|
||||
config=settings.address_suggestions.tomtom,
|
||||
)
|
||||
providers = (cdek_provider,)
|
||||
cache = PriceCache.from_repository_config(settings.repository)
|
||||
service = AggregatorService(
|
||||
providers=providers,
|
||||
cache=cache,
|
||||
order_adapter=cdek_provider,
|
||||
address_suggestion_providers=(dadata_provider, yandex_geosuggest_provider),
|
||||
address_suggestion_providers=(
|
||||
dadata_provider,
|
||||
yandex_geosuggest_provider,
|
||||
tomtom_provider,
|
||||
),
|
||||
address_suggestion_country_to_provider=(
|
||||
settings.address_suggestions.country_to_provider
|
||||
),
|
||||
|
||||
@@ -35,6 +35,36 @@ address_suggestions:
|
||||
TJ: "yandex_geosuggest"
|
||||
TM: "yandex_geosuggest"
|
||||
UZ: "yandex_geosuggest"
|
||||
AL: "tomtom"
|
||||
AT: "tomtom"
|
||||
BE: "tomtom"
|
||||
BG: "tomtom"
|
||||
CH: "tomtom"
|
||||
CZ: "tomtom"
|
||||
DE: "tomtom"
|
||||
DK: "tomtom"
|
||||
EE: "tomtom"
|
||||
ES: "tomtom"
|
||||
FI: "tomtom"
|
||||
FR: "tomtom"
|
||||
GB: "tomtom"
|
||||
GR: "tomtom"
|
||||
HR: "tomtom"
|
||||
HU: "tomtom"
|
||||
IE: "tomtom"
|
||||
IT: "tomtom"
|
||||
LT: "tomtom"
|
||||
LV: "tomtom"
|
||||
NL: "tomtom"
|
||||
"NO": "tomtom"
|
||||
PL: "tomtom"
|
||||
PT: "tomtom"
|
||||
RO: "tomtom"
|
||||
RS: "tomtom"
|
||||
SE: "tomtom"
|
||||
SI: "tomtom"
|
||||
SK: "tomtom"
|
||||
UA: "tomtom"
|
||||
dadata:
|
||||
url: "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address"
|
||||
api_key: "test-dadata-api-key"
|
||||
@@ -43,6 +73,10 @@ address_suggestions:
|
||||
url: "https://suggest-maps.yandex.ru/v1/suggest"
|
||||
api_key: "test-yandex-geosuggest-api-key"
|
||||
timeout_seconds: 6.5
|
||||
tomtom:
|
||||
url: "https://api.tomtom.com/search/2/search"
|
||||
api_key: "test-tomtom-api-key"
|
||||
timeout_seconds: 5.5
|
||||
|
||||
observability:
|
||||
enabled: false
|
||||
|
||||
+3
-3
@@ -1,7 +1,6 @@
|
||||
# Spec Tasks Index
|
||||
|
||||
> ⚠️ This file is generated. Do not edit manually.
|
||||
> Generated at (UTC): `2026-03-29T15:00:18+00:00`
|
||||
|
||||
## Tasks
|
||||
|
||||
@@ -31,9 +30,10 @@
|
||||
| 021 | DONE | 2026-03-25 | Add address suggestion adapter and country provider mapping | `spec/tasks/021_add_address_suggestion_adapter_and_country_mapping.md` |
|
||||
| 022 | DONE | 2026-03-25 | Add address suggestion endpoint | `spec/tasks/022_add_address_suggestion_endpoint.md` |
|
||||
| 023 | DONE | 2026-03-29 | Add Yandex Geosuggest address suggestion adapter and CIS routing | `spec/tasks/023_add_yandex_geosuggest_address_suggestion_adapter.md` |
|
||||
| 024 | DONE | 2026-03-29 | Add TomTom address suggestion adapter and Europe routing | `spec/tasks/024_add_tomtom_address_suggestion_adapter.md` |
|
||||
|
||||
## Summary
|
||||
|
||||
- Total: **24**
|
||||
- Total: **25**
|
||||
- TODO: **1**
|
||||
- DONE: **23**
|
||||
- DONE: **24**
|
||||
|
||||
+6
-1
@@ -22,6 +22,7 @@
|
||||
- Выбирать сервис подсказок адреса по `country_code` через маппинг стран в конфиге
|
||||
- Для `RU`, `BY` и `KZ`, сопоставленных с provider id `dadata`, использовать `dadata.ru`
|
||||
- Для `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM` и `UZ`, сопоставленных с provider id `yandex_geosuggest`, использовать Yandex Geosuggest
|
||||
- Для европейских стран, сопоставленных с provider id `tomtom`, использовать TomTom Search API Fuzzy Search
|
||||
- Конфигурация и wiring должны допускать отдельные address suggestion providers для других регионов
|
||||
- Опрашивать всех зарегистрированных провайдеров параллельно
|
||||
- Возвращать унифицированный список тарифов, отсортированных по цене
|
||||
@@ -106,10 +107,12 @@
|
||||
```
|
||||
- `dadata/client.py` — HTTP-клиент `dadata.ru` для address suggestions
|
||||
- `yandex_geosuggest/client.py` — HTTP-клиент Yandex Geosuggest `GET https://suggest-maps.yandex.ru/v1/suggest`
|
||||
- `tomtom/client.py` — HTTP-клиент TomTom Search API Fuzzy Search `GET https://api.tomtom.com/search/2/search/{query}.json`
|
||||
- provider-specific модули address suggestion adapters инкапсулируют внешние API-контракты, auth, serialization и error handling
|
||||
- В конфиге adapter layer хранится маппинг `country_code -> provider_id` для выбора address suggestion provider
|
||||
- Address suggestion adapters возвращают только унифицированные internal models без утечки provider-specific payload в Service
|
||||
- Для Yandex Geosuggest adapter возвращает `postal_code=None`, так как контракт Geosuggest не используется как источник почтового индекса
|
||||
- Для TomTom Search adapter использует `request.city` и `request.query` для формирования search query, передаёт `countrySet=request.country_code`, `typeahead=true`, `idxSet=PAD,Addr,Str,EPP`, а `postal_code` берёт из `address.postalCode`, если оно присутствует
|
||||
|
||||
---
|
||||
|
||||
@@ -228,7 +231,9 @@ app/
|
||||
│ │ ├── base.py # Интерфейс Adapter
|
||||
│ │ ├── dadata/
|
||||
│ │ └── client.py
|
||||
│ │ └── yandex_geosuggest/
|
||||
│ │ ├── yandex_geosuggest/
|
||||
│ │ └── client.py
|
||||
│ │ └── tomtom/
|
||||
│ │ └── client.py
|
||||
│ └── delivery_providers/
|
||||
│ ├── base.py # Интерфейс Adapter
|
||||
|
||||
@@ -20,7 +20,7 @@ created: 2026-03-14
|
||||
- `from_location.address` и `to_location.address` считаются уже выбранными точными строками адреса; в рамках этой задачи запрещено добавлять address suggestion routing, внешние address lookup вызовы и нормализацию адреса.
|
||||
- Контракт входного запроса должен соответствовать разделу `Регистрация заказа (тип "доставка", до двери)` из `http-client.http`.
|
||||
- В scope задачи входят только значения `type=2` и `tariff_code=535`; не расширять поддержку на другие типы заказа и тарифы.
|
||||
- Scope задачи не включает `POST /api/v1/delivery/address/suggest`, конфигурацию address suggestion providers, кеширование, агрегацию тарифов, расчёт стоимости, новые провайдеры и расширение order flow за пределы CDEK.
|
||||
- Scope задачи не включает `POST /api/v1/delivery/suggest-address`, конфигурацию address suggestion providers, кеширование, агрегацию тарифов, расчёт стоимости, новые провайдеры и расширение order flow за пределы CDEK.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
@@ -9,7 +9,7 @@ created: 2026-03-25
|
||||
Перед реализацией order flow клиенту нужен отдельный endpoint, который возвращает подсказки адреса и позволяет выбрать точное значение для `from_location.address` и `to_location.address`. Выбор provider должен происходить по `country_code` через конфигурационный маппинг стран.
|
||||
|
||||
## Goal
|
||||
Добавить `POST /api/v1/delivery/address/suggest` в существующий controller и существующий service с request/response schemas, routing на address suggestion provider по `country_code` и детерминированным HTTP error mapping.
|
||||
Добавить `POST /api/v1/delivery/suggest-address` в существующий controller и существующий service с request/response schemas, routing на address suggestion provider по `country_code` и детерминированным HTTP error mapping.
|
||||
|
||||
## Constraints
|
||||
- Controller отвечает только за DTO validation, routing и mapping service exceptions в HTTP responses.
|
||||
@@ -33,7 +33,7 @@ created: 2026-03-25
|
||||
## Definition of Done
|
||||
- [ ] Добавлены address suggestion request/response schemas.
|
||||
- [ ] Реализован метод `AggregatorService.suggest_addresses()` для provider routing и orchestration.
|
||||
- [ ] Реализован endpoint `POST /api/v1/delivery/address-address` в существующем controller.
|
||||
- [ ] Реализован endpoint `POST /api/v1/delivery/suggest-address` в существующем controller.
|
||||
- [ ] Добавлены service и controller/API tests для address suggestion flow.
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
id: 024
|
||||
title: Add TomTom address suggestion adapter and Europe routing
|
||||
status: DONE
|
||||
created: 2026-03-29
|
||||
---
|
||||
|
||||
## Context
|
||||
Текущий flow подсказок адреса поддерживает `dadata` для `RU`, `BY`, `KZ` и `yandex_geosuggest` для части стран СНГ. Следующий этап требует добавить третий provider `TomTom` для европейских стран, сохранив текущий публичный API и routing через YAML-конфиг.
|
||||
|
||||
## Goal
|
||||
Добавить новый adapter `tomtom` для address suggestions на базе TomTom Search API Fuzzy Search, настроить routing европейских стран на provider id `tomtom` через конфиг и покрыть это config, adapter и service tests без изменения публичного API endpoint.
|
||||
|
||||
## Constraints
|
||||
- Scope задачи ограничен flow подсказок адреса: YAML-конфиг, wiring зависимостей, service routing, новый adapter и тесты.
|
||||
- Controller path и публичный контракт `POST /api/v1/delivery/suggest-address` не изменять.
|
||||
- Service остаётся orchestration layer: выбирает provider по injected config mapping и вызывает ровно один adapter без provider-specific HTTP-логики.
|
||||
- Внешний IO должен оставаться только внутри нового adapter `app/adapters/address_suggestions/tomtom/`.
|
||||
- Новый adapter должен следовать официальному HTTP-контракту TomTom Search API Fuzzy Search: `GET https://api.tomtom.com/search/2/search/{query}.json`.
|
||||
- TomTom adapter должен формировать `query` из `request.city` и `request.query`, передавать `countrySet=request.country_code`, `typeahead=true`, а `request.limit` при наличии маппить в query-параметр `limit`.
|
||||
- Для исключения POI из address suggestion flow adapter должен отправлять address-oriented `idxSet=PAD,Addr,Str,EPP`.
|
||||
- Routing европейских стран должен определяться только YAML-маппингом `country_code -> provider_id`; запрещено добавлять в Service hardcoded классификацию Европы.
|
||||
- Unified mapping наружу должен возвращать только `AddressSuggestion` без утечки provider-specific payload.
|
||||
- Для TomTom Search поля должны маппиться детерминированно: `address.freeformAddress -> address`, `address.streetName -> street`, `address.streetNumber -> house`, `address.postalCode -> postal_code`, `flat=None`.
|
||||
- Ошибки TomTom `400` должны маппиться в deterministic provider request error; `403`, `429`, transport errors и `5xx` должны маппиться в adapter client error/unavailable path.
|
||||
- Scope задачи не включает изменение order flow, price flow, новых endpoint'ов, расширение internal model `AddressSuggestion` и изменение контрактов существующих adapters `dadata` и `yandex_geosuggest`.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- В конфиге address suggestion providers добавлен provider id `tomtom` с параметрами, необходимыми для вызова TomTom Search API Fuzzy Search.
|
||||
- Country mapping в конфиге маршрутизирует на `tomtom` европейские страны, явно перечисленные в YAML, при этом routing для `RU`, `BY`, `KZ` на `dadata` и для `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` на `yandex_geosuggest` сохраняется без изменений.
|
||||
- Реализован adapter `app/adapters/address_suggestions/tomtom/client.py`, который отправляет запрос в TomTom Search API Fuzzy Search по официальному контракту и возвращает `list[AddressSuggestion]`.
|
||||
- Adapter объединяет `request.city` и `request.query` в search query, передаёт `countrySet=request.country_code`, `typeahead=true`, `idxSet=PAD,Addr,Str,EPP` и при наличии `request.limit` маппит его в `limit`.
|
||||
- Поля `address`, `street`, `house` и `postal_code` детерминированно извлекаются из ответа TomTom (`freeformAddress`, `streetName`, `streetNumber`, `postalCode`), а `flat` возвращается как `None`.
|
||||
- `AggregatorService.suggest_addresses()` по `country_code` выбирает `tomtom` для европейских стран, явно сопоставленных в YAML-конфиге, и сохраняет существующий routing на `dadata` и `yandex_geosuggest` для уже поддержанных стран.
|
||||
- При ответе TomTom с `400` service flow возвращает deterministic bad-request path, а при `403`, `429`, `5xx` и transport failure — deterministic unavailable path.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлена конфигурация `tomtom` и обновлён country mapping для европейских стран.
|
||||
- [ ] Реализован новый TomTom adapter без утечки HTTP-деталей в Service.
|
||||
- [ ] Обновлён wiring address suggestion providers без изменения публичного endpoint контракта.
|
||||
- [ ] Добавлены tests для config, adapter mapping/error handling и service routing на `tomtom` с сохранением существующего routing на `dadata` и `yandex_geosuggest`.
|
||||
- [ ] Пройдены все команды из раздела Commands.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/config/test_config_sections.py` для проверки секции `tomtom` и country mapping, в котором европейские страны маршрутизируются на `tomtom`, а существующие маппинги `RU`, `BY`, `KZ` -> `dadata` и `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` -> `yandex_geosuggest` сохраняются.
|
||||
- Добавить `tests/adapters/address_suggestions/tomtom/test_client.py` для success case, mapping `limit`, формирования search query из `city` и `query`, передачи `typeahead=true` и `idxSet=PAD,Addr,Str,EPP`, возврата `flat=None`, извлечения `address`/`street`/`house`/`postal_code` и error scenarios `400`, `403`, `429`, `5xx`.
|
||||
- Обновить `tests/services/test_address_suggestions.py` для проверки routing на `tomtom` по европейским странам из YAML-конфига и сохранения routing на `dadata` и `yandex_geosuggest` для уже поддержанных стран.
|
||||
- При необходимости обновить `tests/controllers/v1/test_address_suggestions.py` только для совместимости существующего endpoint с новым provider routing без изменения публичного API.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest tests/config/test_config_sections.py -q`
|
||||
- `poetry run pytest tests/adapters/address_suggestions/tomtom/test_client.py -q`
|
||||
- `poetry run pytest tests/services/test_address_suggestions.py -q`
|
||||
- `poetry run pytest tests/controllers/v1/test_address_suggestions.py -q`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
@@ -0,0 +1,258 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.adapters.address_suggestions.tomtom.client import (
|
||||
TomTomAddressSuggestionProvider,
|
||||
TomTomClientError,
|
||||
TomTomRequestError,
|
||||
)
|
||||
from app.config import TomTomAddressSuggestionsConfig
|
||||
from app.schemas.request import AddressSuggestRequest
|
||||
from app.schemas.response import AddressSuggestion
|
||||
|
||||
|
||||
class SequenceHTTPClient:
|
||||
def __init__(self, results: list[Any]) -> None:
|
||||
self._results = results
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
|
||||
def _next_result(self) -> Any:
|
||||
return self._results[len(self.calls) - 1]
|
||||
|
||||
async def get(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
params: dict[str, Any] | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> httpx.Response:
|
||||
self.calls.append(
|
||||
{
|
||||
"method": "GET",
|
||||
"url": url,
|
||||
"params": params,
|
||||
"timeout": timeout,
|
||||
}
|
||||
)
|
||||
result = self._next_result()
|
||||
if isinstance(result, Exception):
|
||||
raise result
|
||||
return result
|
||||
|
||||
|
||||
def _make_request(**overrides: object) -> AddressSuggestRequest:
|
||||
payload: dict[str, object] = {
|
||||
"country_code": "DE",
|
||||
"city": "Berlin",
|
||||
"query": "Alexanderplatz 1",
|
||||
"limit": 5,
|
||||
}
|
||||
payload.update(overrides)
|
||||
return AddressSuggestRequest(**payload)
|
||||
|
||||
|
||||
def test_tomtom_provider_maps_response_to_unified_model() -> None:
|
||||
response = httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"results": [
|
||||
{
|
||||
"address": {
|
||||
"freeformAddress": "Alexanderplatz 1, 10178 Berlin",
|
||||
"streetName": "Alexanderplatz",
|
||||
"streetNumber": "1",
|
||||
"postalCode": "10178",
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://api.tomtom.test/search/2/search/Berlin%20Alexanderplatz%201.json",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = TomTomAddressSuggestionProvider(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
url="https://api.tomtom.test/search/2/search",
|
||||
api_key="test-api-key",
|
||||
timeout_seconds=5.5,
|
||||
)
|
||||
|
||||
result = asyncio.run(provider.suggest(_make_request()))
|
||||
|
||||
assert result == [
|
||||
AddressSuggestion(
|
||||
address="Alexanderplatz 1, 10178 Berlin",
|
||||
street="Alexanderplatz",
|
||||
house="1",
|
||||
flat=None,
|
||||
postal_code="10178",
|
||||
)
|
||||
]
|
||||
assert http_client.calls == [
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "https://api.tomtom.test/search/2/search/Berlin%20Alexanderplatz%201.json",
|
||||
"params": {
|
||||
"key": "test-api-key",
|
||||
"countrySet": "DE",
|
||||
"typeahead": "true",
|
||||
"idxSet": "PAD,Addr,Str,EPP",
|
||||
"limit": 5,
|
||||
},
|
||||
"timeout": 5.5,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_tomtom_provider_uses_config_factory_and_omits_limit_when_missing() -> None:
|
||||
response = httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"results": [
|
||||
{
|
||||
"address": {
|
||||
"freeformAddress": "Dam Square, 1012 JS Amsterdam",
|
||||
"streetName": "Dam Square",
|
||||
"postalCode": "1012 JS",
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://api.tomtom.test/search/2/search/Amsterdam%20Dam%20Square.json",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = TomTomAddressSuggestionProvider.from_config(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
config=TomTomAddressSuggestionsConfig(
|
||||
url="https://api.tomtom.test/search/2/search",
|
||||
api_key="config-api-key",
|
||||
timeout_seconds=4.25,
|
||||
),
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
provider.suggest(
|
||||
_make_request(
|
||||
country_code="NL",
|
||||
city="Amsterdam",
|
||||
query="Dam Square",
|
||||
limit=None,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert result == [
|
||||
AddressSuggestion(
|
||||
address="Dam Square, 1012 JS Amsterdam",
|
||||
street="Dam Square",
|
||||
house=None,
|
||||
flat=None,
|
||||
postal_code="1012 JS",
|
||||
)
|
||||
]
|
||||
assert http_client.calls == [
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "https://api.tomtom.test/search/2/search/Amsterdam%20Dam%20Square.json",
|
||||
"params": {
|
||||
"key": "config-api-key",
|
||||
"countrySet": "NL",
|
||||
"typeahead": "true",
|
||||
"idxSet": "PAD,Addr,Str,EPP",
|
||||
},
|
||||
"timeout": 4.25,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_tomtom_provider_maps_400_to_request_error() -> None:
|
||||
response = httpx.Response(
|
||||
400,
|
||||
json={"errorText": "bad request"},
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://api.tomtom.test/search/2/search/Berlin%20Alexanderplatz%201.json",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = TomTomAddressSuggestionProvider(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
url="https://api.tomtom.test/search/2/search",
|
||||
api_key="test-api-key",
|
||||
)
|
||||
|
||||
with pytest.raises(TomTomRequestError, match="status 400"):
|
||||
asyncio.run(provider.suggest(_make_request()))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status_code", [403, 429, 503])
|
||||
def test_tomtom_provider_maps_unavailable_statuses_to_client_error(
|
||||
status_code: int,
|
||||
) -> None:
|
||||
response = httpx.Response(
|
||||
status_code,
|
||||
json={"errorText": "provider error"},
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://api.tomtom.test/search/2/search/Berlin%20Alexanderplatz%201.json",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = TomTomAddressSuggestionProvider(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
url="https://api.tomtom.test/search/2/search",
|
||||
api_key="test-api-key",
|
||||
)
|
||||
|
||||
with pytest.raises(TomTomClientError, match=f"status {status_code}"):
|
||||
asyncio.run(provider.suggest(_make_request()))
|
||||
|
||||
|
||||
def test_tomtom_provider_maps_transport_error_to_client_error() -> None:
|
||||
transport_error = httpx.ReadTimeout(
|
||||
"read timeout",
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://api.tomtom.test/search/2/search/Berlin%20Alexanderplatz%201.json",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([transport_error])
|
||||
provider = TomTomAddressSuggestionProvider(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
url="https://api.tomtom.test/search/2/search",
|
||||
api_key="test-api-key",
|
||||
)
|
||||
|
||||
with pytest.raises(TomTomClientError, match="request failed"):
|
||||
asyncio.run(provider.suggest(_make_request()))
|
||||
|
||||
|
||||
def test_tomtom_provider_raises_on_invalid_payload_shape() -> None:
|
||||
response = httpx.Response(
|
||||
200,
|
||||
json={"results": [{"address": {"freeformAddress": 1}}]},
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://api.tomtom.test/search/2/search/Berlin%20Alexanderplatz%201.json",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = TomTomAddressSuggestionProvider(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
url="https://api.tomtom.test/search/2/search",
|
||||
api_key="test-api-key",
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
TomTomClientError,
|
||||
match="response payload is invalid",
|
||||
):
|
||||
asyncio.run(provider.suggest(_make_request()))
|
||||
@@ -19,6 +19,7 @@ address_suggestions:
|
||||
country_to_provider:
|
||||
RU: "dadata"
|
||||
AM: "yandex_geosuggest"
|
||||
DE: "tomtom"
|
||||
dadata:
|
||||
url: "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address"
|
||||
api_key: "default-dadata-key"
|
||||
@@ -26,6 +27,10 @@ address_suggestions:
|
||||
url: "https://suggest-maps.yandex.ru/v1/suggest"
|
||||
api_key: "default-yandex-key"
|
||||
timeout_seconds: 4.0
|
||||
tomtom:
|
||||
url: "https://api.tomtom.com/search/2/search"
|
||||
api_key: "default-tomtom-key"
|
||||
timeout_seconds: 4.25
|
||||
|
||||
observability:
|
||||
enabled: false
|
||||
|
||||
@@ -19,6 +19,7 @@ address_suggestions:
|
||||
country_to_provider:
|
||||
RU: "dadata"
|
||||
AM: "yandex_geosuggest"
|
||||
DE: "tomtom"
|
||||
dadata:
|
||||
url: "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address"
|
||||
api_key: "override-dadata-key"
|
||||
@@ -27,6 +28,10 @@ address_suggestions:
|
||||
url: "https://suggest-maps.yandex.ru/v1/suggest"
|
||||
api_key: "override-yandex-key"
|
||||
timeout_seconds: 4.5
|
||||
tomtom:
|
||||
url: "https://api.tomtom.com/search/2/search"
|
||||
api_key: "override-tomtom-key"
|
||||
timeout_seconds: 5.25
|
||||
|
||||
observability:
|
||||
enabled: true
|
||||
|
||||
@@ -41,6 +41,51 @@ MISSING_OBSERVABILITY_ENDPOINT_CONFIG_FILE = (
|
||||
/ "fixtures"
|
||||
/ "config.missing_observability_endpoint.yaml"
|
||||
)
|
||||
_DADATA_COUNTRIES = ("RU", "BY", "KZ")
|
||||
_YANDEX_COUNTRIES = ("AM", "AZ", "KG", "MD", "TJ", "TM", "UZ")
|
||||
_TOMTOM_COUNTRIES = (
|
||||
"AL",
|
||||
"AT",
|
||||
"BE",
|
||||
"BG",
|
||||
"CH",
|
||||
"CZ",
|
||||
"DE",
|
||||
"DK",
|
||||
"EE",
|
||||
"ES",
|
||||
"FI",
|
||||
"FR",
|
||||
"GB",
|
||||
"GR",
|
||||
"HR",
|
||||
"HU",
|
||||
"IE",
|
||||
"IT",
|
||||
"LT",
|
||||
"LV",
|
||||
"NL",
|
||||
"NO",
|
||||
"PL",
|
||||
"PT",
|
||||
"RO",
|
||||
"RS",
|
||||
"SE",
|
||||
"SI",
|
||||
"SK",
|
||||
"UA",
|
||||
)
|
||||
|
||||
|
||||
def _expected_country_mapping() -> dict[str, str]:
|
||||
return {
|
||||
**{country_code: "dadata" for country_code in _DADATA_COUNTRIES},
|
||||
**{
|
||||
country_code: "yandex_geosuggest"
|
||||
for country_code in _YANDEX_COUNTRIES
|
||||
},
|
||||
**{country_code: "tomtom" for country_code in _TOMTOM_COUNTRIES},
|
||||
}
|
||||
|
||||
|
||||
def _use_runtime_config_files(
|
||||
@@ -81,18 +126,7 @@ def test_configuration_sections_are_loaded_from_yaml_file(
|
||||
assert settings.adapter.cdek_retry_backoff_seconds == 0.2
|
||||
assert settings.adapter.cdek_timeout_seconds == 10.0
|
||||
assert settings.adapter.cdek_cache_ttl_seconds == 900
|
||||
assert settings.address_suggestions.country_to_provider == {
|
||||
"RU": "dadata",
|
||||
"BY": "dadata",
|
||||
"KZ": "dadata",
|
||||
"AM": "yandex_geosuggest",
|
||||
"AZ": "yandex_geosuggest",
|
||||
"KG": "yandex_geosuggest",
|
||||
"MD": "yandex_geosuggest",
|
||||
"TJ": "yandex_geosuggest",
|
||||
"TM": "yandex_geosuggest",
|
||||
"UZ": "yandex_geosuggest",
|
||||
}
|
||||
assert settings.address_suggestions.country_to_provider == _expected_country_mapping()
|
||||
assert (
|
||||
settings.address_suggestions.dadata.url
|
||||
== "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address"
|
||||
@@ -108,6 +142,9 @@ def test_configuration_sections_are_loaded_from_yaml_file(
|
||||
== "test-yandex-geosuggest-api-key"
|
||||
)
|
||||
assert settings.address_suggestions.yandex_geosuggest.timeout_seconds == 6.5
|
||||
assert settings.address_suggestions.tomtom.url == "https://api.tomtom.com/search/2/search"
|
||||
assert settings.address_suggestions.tomtom.api_key == "test-tomtom-api-key"
|
||||
assert settings.address_suggestions.tomtom.timeout_seconds == 5.5
|
||||
assert settings.observability.enabled is False
|
||||
assert settings.observability.service_name == "g2s-aggregator-test"
|
||||
assert settings.observability.otlp_endpoint == "http://localhost:4317"
|
||||
@@ -178,11 +215,14 @@ def test_get_settings_uses_config_test_yaml_in_pytest_environment(
|
||||
assert settings.address_suggestions.country_to_provider == {
|
||||
"RU": "dadata",
|
||||
"AM": "yandex_geosuggest",
|
||||
"DE": "tomtom",
|
||||
}
|
||||
assert settings.address_suggestions.dadata.api_key == "override-dadata-key"
|
||||
assert settings.address_suggestions.dadata.timeout_seconds == 3.0
|
||||
assert settings.address_suggestions.yandex_geosuggest.api_key == "override-yandex-key"
|
||||
assert settings.address_suggestions.yandex_geosuggest.timeout_seconds == 4.5
|
||||
assert settings.address_suggestions.tomtom.api_key == "override-tomtom-key"
|
||||
assert settings.address_suggestions.tomtom.timeout_seconds == 5.25
|
||||
assert settings.observability.enabled is True
|
||||
assert settings.observability.service_name == "override-config-service"
|
||||
assert settings.observability.otlp_endpoint == "http://override-collector:4317"
|
||||
|
||||
@@ -99,6 +99,18 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency(
|
||||
_ = config
|
||||
return stub_yandex_provider
|
||||
|
||||
class StubTomTomAddressSuggestionProvider:
|
||||
@classmethod
|
||||
def from_config(
|
||||
cls,
|
||||
*,
|
||||
http_client,
|
||||
config,
|
||||
) -> StubAddressProvider:
|
||||
assert http_client is stub_http_client
|
||||
_ = config
|
||||
return stub_tomtom_provider
|
||||
|
||||
class StubCache:
|
||||
async def get(self, key: str) -> object | None:
|
||||
_ = key
|
||||
@@ -138,6 +150,17 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency(
|
||||
)
|
||||
],
|
||||
)
|
||||
stub_tomtom_provider = StubAddressProvider(
|
||||
"tomtom",
|
||||
[
|
||||
AddressSuggestion(
|
||||
address="Alexanderplatz 1, 10178 Berlin",
|
||||
street="Alexanderplatz",
|
||||
house="1",
|
||||
postal_code="10178",
|
||||
)
|
||||
],
|
||||
)
|
||||
http_client_timeouts: list[float] = []
|
||||
|
||||
def fake_build_controller_http_client(timeout_seconds: float) -> StubHttpClient:
|
||||
@@ -160,13 +183,18 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency(
|
||||
"YandexGeosuggestAddressSuggestionProvider",
|
||||
StubYandexGeosuggestAddressSuggestionProvider,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
delivery_controller,
|
||||
"TomTomAddressSuggestionProvider",
|
||||
StubTomTomAddressSuggestionProvider,
|
||||
)
|
||||
monkeypatch.setattr(delivery_controller, "PriceCache", StubPriceCache)
|
||||
|
||||
app = create_app()
|
||||
request_payload = _valid_payload(
|
||||
country_code="AM",
|
||||
city="Yerevan",
|
||||
query="Tumanyan 12",
|
||||
country_code="DE",
|
||||
city="Berlin",
|
||||
query="Alexanderplatz 1",
|
||||
)
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
@@ -185,16 +213,17 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency(
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [
|
||||
{
|
||||
"address": "Tumanyan Street, 12, Yerevan",
|
||||
"street": "Tumanyan Street",
|
||||
"house": "12",
|
||||
"address": "Alexanderplatz 1, 10178 Berlin",
|
||||
"street": "Alexanderplatz",
|
||||
"house": "1",
|
||||
"flat": None,
|
||||
"postal_code": None,
|
||||
"postal_code": "10178",
|
||||
}
|
||||
]
|
||||
assert http_client_timeouts == [10.0]
|
||||
assert stub_dadata_provider.calls == []
|
||||
assert stub_yandex_provider.calls == [AddressSuggestRequest(**request_payload)]
|
||||
assert stub_yandex_provider.calls == []
|
||||
assert stub_tomtom_provider.calls == [AddressSuggestRequest(**request_payload)]
|
||||
|
||||
|
||||
def test_post_address_suggest_returns_response_and_delegates_to_service() -> None:
|
||||
|
||||
@@ -17,6 +17,38 @@ from app.services.aggregator import (
|
||||
|
||||
_DADATA_COUNTRIES = ("RU", "BY", "KZ")
|
||||
_YANDEX_COUNTRIES = ("AM", "AZ", "KG", "MD", "TJ", "TM", "UZ")
|
||||
_TOMTOM_COUNTRIES = (
|
||||
"AL",
|
||||
"AT",
|
||||
"BE",
|
||||
"BG",
|
||||
"CH",
|
||||
"CZ",
|
||||
"DE",
|
||||
"DK",
|
||||
"EE",
|
||||
"ES",
|
||||
"FI",
|
||||
"FR",
|
||||
"GB",
|
||||
"GR",
|
||||
"HR",
|
||||
"HU",
|
||||
"IE",
|
||||
"IT",
|
||||
"LT",
|
||||
"LV",
|
||||
"NL",
|
||||
"NO",
|
||||
"PL",
|
||||
"PT",
|
||||
"RO",
|
||||
"RS",
|
||||
"SE",
|
||||
"SI",
|
||||
"SK",
|
||||
"UA",
|
||||
)
|
||||
|
||||
|
||||
class StubAddressSuggestionProvider:
|
||||
@@ -74,6 +106,7 @@ def _make_country_mapping() -> dict[str, str]:
|
||||
country_code: "yandex_geosuggest"
|
||||
for country_code in _YANDEX_COUNTRIES
|
||||
},
|
||||
**{country_code: "tomtom" for country_code in _TOMTOM_COUNTRIES},
|
||||
}
|
||||
|
||||
|
||||
@@ -97,9 +130,13 @@ def test_suggest_addresses_routes_dadata_countries_to_dadata(
|
||||
"yandex_geosuggest",
|
||||
response=[_make_suggestion("Yerevan, Tumanyan 1")],
|
||||
)
|
||||
tomtom = StubAddressSuggestionProvider(
|
||||
"tomtom",
|
||||
response=[_make_suggestion("Alexanderplatz 1, 10178 Berlin")],
|
||||
)
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
address_suggestion_providers=[dadata, yandex_geosuggest],
|
||||
address_suggestion_providers=[dadata, yandex_geosuggest, tomtom],
|
||||
address_suggestion_country_to_provider=_make_country_mapping(),
|
||||
)
|
||||
request = _make_request(
|
||||
@@ -121,6 +158,7 @@ def test_suggest_addresses_routes_dadata_countries_to_dadata(
|
||||
]
|
||||
assert dadata.calls == [request]
|
||||
assert yandex_geosuggest.calls == []
|
||||
assert tomtom.calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("country_code", _YANDEX_COUNTRIES)
|
||||
@@ -135,9 +173,13 @@ def test_suggest_addresses_routes_cis_countries_to_yandex_geosuggest(
|
||||
"yandex_geosuggest",
|
||||
response=[_make_suggestion("Yerevan, Tumanyan 1")],
|
||||
)
|
||||
tomtom = StubAddressSuggestionProvider(
|
||||
"tomtom",
|
||||
response=[_make_suggestion("Alexanderplatz 1, 10178 Berlin")],
|
||||
)
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
address_suggestion_providers=[dadata, yandex_geosuggest],
|
||||
address_suggestion_providers=[dadata, yandex_geosuggest, tomtom],
|
||||
address_suggestion_country_to_provider=_make_country_mapping(),
|
||||
)
|
||||
request = _make_request(country_code=country_code, city="Yerevan", query="Tumanyan")
|
||||
@@ -147,6 +189,57 @@ def test_suggest_addresses_routes_cis_countries_to_yandex_geosuggest(
|
||||
assert result == [AddressSuggestion(address="Yerevan, Tumanyan 1")]
|
||||
assert dadata.calls == []
|
||||
assert yandex_geosuggest.calls == [request]
|
||||
assert tomtom.calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("country_code", _TOMTOM_COUNTRIES)
|
||||
def test_suggest_addresses_routes_european_countries_to_tomtom(
|
||||
country_code: str,
|
||||
) -> None:
|
||||
dadata = StubAddressSuggestionProvider(
|
||||
"dadata",
|
||||
response=[_make_suggestion("Moscow, Lenina 1")],
|
||||
)
|
||||
yandex_geosuggest = StubAddressSuggestionProvider(
|
||||
"yandex_geosuggest",
|
||||
response=[_make_suggestion("Yerevan, Tumanyan 1")],
|
||||
)
|
||||
tomtom = StubAddressSuggestionProvider(
|
||||
"tomtom",
|
||||
response=[
|
||||
_make_suggestion(
|
||||
"Alexanderplatz 1, 10178 Berlin",
|
||||
street="Alexanderplatz",
|
||||
house="1",
|
||||
postal_code="10178",
|
||||
)
|
||||
],
|
||||
)
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
address_suggestion_providers=[dadata, yandex_geosuggest, tomtom],
|
||||
address_suggestion_country_to_provider=_make_country_mapping(),
|
||||
)
|
||||
request = _make_request(
|
||||
country_code=country_code,
|
||||
city="Berlin",
|
||||
query="Alexanderplatz 1",
|
||||
)
|
||||
|
||||
result = asyncio.run(service.suggest_addresses(request))
|
||||
|
||||
assert result == [
|
||||
AddressSuggestion(
|
||||
address="Alexanderplatz 1, 10178 Berlin",
|
||||
street="Alexanderplatz",
|
||||
house="1",
|
||||
flat=None,
|
||||
postal_code="10178",
|
||||
)
|
||||
]
|
||||
assert dadata.calls == []
|
||||
assert yandex_geosuggest.calls == []
|
||||
assert tomtom.calls == [request]
|
||||
|
||||
|
||||
def test_suggest_addresses_raises_for_unsupported_country() -> None:
|
||||
|
||||
Reference in New Issue
Block a user