Добавлен 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
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user