147 lines
4.8 KiB
Python
147 lines
4.8 KiB
Python
"""Dadata address suggestion adapter."""
|
|
|
|
from typing import Any
|
|
from urllib.parse import urljoin
|
|
|
|
import httpx
|
|
|
|
from app.adapters.address_suggestions.base import (
|
|
AddressSuggestionClientError,
|
|
AddressSuggestionProvider,
|
|
AddressSuggestionRequestError,
|
|
)
|
|
from app.config import DadataAddressSuggestionsConfig
|
|
from app.schemas.request import AddressSuggestRequest
|
|
from app.schemas.response import AddressSuggestion
|
|
|
|
|
|
class DadataClientError(AddressSuggestionClientError):
|
|
"""Raised when Dadata address suggestions fail."""
|
|
|
|
|
|
class DadataRequestError(DadataClientError, AddressSuggestionRequestError):
|
|
"""Raised when Dadata rejects a suggestion request."""
|
|
|
|
|
|
class DadataAddressSuggestionProvider(AddressSuggestionProvider):
|
|
name = "dadata"
|
|
|
|
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
|
|
self._api_key = api_key
|
|
self._timeout_seconds = timeout_seconds
|
|
|
|
@classmethod
|
|
def from_config(
|
|
cls,
|
|
*,
|
|
http_client: httpx.AsyncClient,
|
|
config: DadataAddressSuggestionsConfig,
|
|
) -> "DadataAddressSuggestionProvider":
|
|
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]:
|
|
payload = self._build_payload(request)
|
|
try:
|
|
response = await self._http_client.post(
|
|
self._url,
|
|
json=payload,
|
|
headers={
|
|
"Authorization": f"Token {self._api_key}",
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
},
|
|
timeout=self._timeout_seconds,
|
|
)
|
|
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
|
raise DadataClientError(
|
|
"Dadata address suggestion request failed."
|
|
) from exc
|
|
|
|
if 400 <= response.status_code < 500:
|
|
raise DadataRequestError(
|
|
"Dadata address suggestion request was rejected with status "
|
|
f"{response.status_code}."
|
|
)
|
|
|
|
if response.status_code >= 500:
|
|
raise DadataClientError(
|
|
"Dadata address suggestion 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 DadataClientError(
|
|
"Dadata address suggestion returned invalid payload."
|
|
) from exc
|
|
|
|
try:
|
|
return _map_dadata_response(raw_payload)
|
|
except ValueError as exc:
|
|
raise DadataClientError(
|
|
"Dadata address suggestion response payload is invalid."
|
|
) from exc
|
|
|
|
@staticmethod
|
|
def _build_payload(request: AddressSuggestRequest) -> dict[str, Any]:
|
|
payload: dict[str, Any] = {
|
|
"query": f"{request.city.strip()} {request.query.strip()}".strip(),
|
|
}
|
|
if request.limit is not None:
|
|
payload["count"] = request.limit
|
|
if request.country_code.strip().upper() != "RU":
|
|
payload["locations"] = [{"country": "*"}]
|
|
return payload
|
|
|
|
|
|
def _map_dadata_response(payload: object) -> list[AddressSuggestion]:
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("Dadata response must be a JSON object.")
|
|
|
|
raw_suggestions = payload.get("suggestions")
|
|
if not isinstance(raw_suggestions, list):
|
|
raise ValueError("Dadata response must include suggestions list.")
|
|
|
|
return [_map_dadata_suggestion(item) for item in raw_suggestions]
|
|
|
|
|
|
def _map_dadata_suggestion(payload: object) -> AddressSuggestion:
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("Dadata suggestion entry must be an object.")
|
|
|
|
raw_address = payload.get("unrestricted_value") or payload.get("value")
|
|
if not isinstance(raw_address, str) or not raw_address.strip():
|
|
raise ValueError("Dadata suggestion has no valid address value.")
|
|
|
|
raw_data = payload.get("data")
|
|
if raw_data is not None and not isinstance(raw_data, dict):
|
|
raise ValueError("Dadata suggestion data must be an object when provided.")
|
|
|
|
postal_code = None
|
|
if isinstance(raw_data, dict):
|
|
raw_postal_code = raw_data.get("postal_code")
|
|
if raw_postal_code is not None:
|
|
postal_code = str(raw_postal_code).strip() or None
|
|
|
|
return AddressSuggestion(
|
|
provider="dadata",
|
|
address=raw_address.strip(),
|
|
postal_code=postal_code,
|
|
)
|