Добавлен yandex geosuggest
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
"""Yandex Geosuggest address suggestion adapter package."""
|
||||
|
||||
from app.adapters.address_suggestions.yandex_geosuggest.client import (
|
||||
YandexGeosuggestAddressSuggestionProvider,
|
||||
YandexGeosuggestClientError,
|
||||
YandexGeosuggestRequestError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"YandexGeosuggestAddressSuggestionProvider",
|
||||
"YandexGeosuggestClientError",
|
||||
"YandexGeosuggestRequestError",
|
||||
]
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Yandex Geosuggest address suggestion adapter."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app.adapters.address_suggestions.base import (
|
||||
AddressSuggestionClientError,
|
||||
AddressSuggestionProvider,
|
||||
AddressSuggestionRequestError,
|
||||
)
|
||||
from app.config import YandexGeosuggestAddressSuggestionsConfig
|
||||
from app.schemas.request import AddressSuggestRequest
|
||||
from app.schemas.response import AddressSuggestion
|
||||
|
||||
_STREET_KINDS = frozenset({"street"})
|
||||
_HOUSE_KINDS = frozenset({"house"})
|
||||
_FLAT_KINDS = frozenset({"apartment", "flat"})
|
||||
|
||||
|
||||
class YandexGeosuggestClientError(AddressSuggestionClientError):
|
||||
"""Raised when Yandex Geosuggest requests fail."""
|
||||
|
||||
|
||||
class YandexGeosuggestRequestError(
|
||||
YandexGeosuggestClientError,
|
||||
AddressSuggestionRequestError,
|
||||
):
|
||||
"""Raised when Yandex Geosuggest rejects request parameters."""
|
||||
|
||||
|
||||
class YandexGeosuggestAddressSuggestionProvider(AddressSuggestionProvider):
|
||||
name = "yandex_geosuggest"
|
||||
|
||||
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: YandexGeosuggestAddressSuggestionsConfig,
|
||||
) -> "YandexGeosuggestAddressSuggestionProvider":
|
||||
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]:
|
||||
params = self._build_query_params(request)
|
||||
try:
|
||||
response = await self._http_client.get(
|
||||
self._url,
|
||||
params=params,
|
||||
timeout=self._timeout_seconds,
|
||||
)
|
||||
except (httpx.TimeoutException, httpx.TransportError) as exc:
|
||||
raise YandexGeosuggestClientError(
|
||||
"Yandex Geosuggest request failed."
|
||||
) from exc
|
||||
|
||||
if response.status_code == 400:
|
||||
raise YandexGeosuggestRequestError(
|
||||
"Yandex Geosuggest request was rejected with status 400."
|
||||
)
|
||||
|
||||
if response.status_code == 403 or response.status_code == 429:
|
||||
raise YandexGeosuggestClientError(
|
||||
"Yandex Geosuggest request failed with status "
|
||||
f"{response.status_code}."
|
||||
)
|
||||
|
||||
if response.status_code >= 500 or 400 <= response.status_code < 500:
|
||||
raise YandexGeosuggestClientError(
|
||||
"Yandex Geosuggest 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 YandexGeosuggestClientError(
|
||||
"Yandex Geosuggest returned invalid payload."
|
||||
) from exc
|
||||
|
||||
try:
|
||||
return _map_yandex_geosuggest_response(raw_payload)
|
||||
except ValueError as exc:
|
||||
raise YandexGeosuggestClientError(
|
||||
"Yandex Geosuggest response payload is invalid."
|
||||
) from exc
|
||||
|
||||
def _build_query_params(self, request: AddressSuggestRequest) -> dict[str, Any]:
|
||||
params: dict[str, Any] = {
|
||||
"apikey": self._api_key,
|
||||
"text": f"{request.city.strip()} {request.query.strip()}".strip(),
|
||||
"print_address": 1,
|
||||
}
|
||||
if request.limit is not None:
|
||||
params["results"] = request.limit
|
||||
return params
|
||||
|
||||
|
||||
def _map_yandex_geosuggest_response(payload: object) -> list[AddressSuggestion]:
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("Yandex Geosuggest 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("Yandex Geosuggest response must include results list.")
|
||||
|
||||
return [_map_yandex_geosuggest_result(item) for item in raw_results]
|
||||
|
||||
|
||||
def _map_yandex_geosuggest_result(payload: object) -> AddressSuggestion:
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("Yandex Geosuggest result entry must be an object.")
|
||||
|
||||
raw_address_payload = payload.get("address")
|
||||
if raw_address_payload is not None and not isinstance(raw_address_payload, dict):
|
||||
raise ValueError("Yandex Geosuggest address payload must be an object.")
|
||||
|
||||
address_payload = raw_address_payload if isinstance(raw_address_payload, dict) else {}
|
||||
raw_components = address_payload.get("component")
|
||||
if raw_components is None:
|
||||
components: list[object] = []
|
||||
elif isinstance(raw_components, list):
|
||||
components = raw_components
|
||||
else:
|
||||
raise ValueError("Yandex Geosuggest address components must be a list.")
|
||||
|
||||
address = _extract_formatted_address(address_payload) or _extract_title_text(payload)
|
||||
if address is None:
|
||||
raise ValueError("Yandex Geosuggest result has no valid address value.")
|
||||
|
||||
return AddressSuggestion(
|
||||
address=address,
|
||||
street=_extract_component_value(components, _STREET_KINDS),
|
||||
house=_extract_component_value(components, _HOUSE_KINDS),
|
||||
flat=_extract_component_value(components, _FLAT_KINDS),
|
||||
postal_code=None,
|
||||
)
|
||||
|
||||
|
||||
def _extract_formatted_address(payload: dict[str, Any]) -> str | None:
|
||||
value = payload.get("formatted_address")
|
||||
return _coerce_optional_text(value)
|
||||
|
||||
|
||||
def _extract_title_text(payload: dict[str, Any]) -> str | None:
|
||||
raw_title = payload.get("title")
|
||||
if raw_title is None:
|
||||
return None
|
||||
if not isinstance(raw_title, dict):
|
||||
raise ValueError("Yandex Geosuggest title payload must be an object.")
|
||||
return _coerce_optional_text(raw_title.get("text"))
|
||||
|
||||
|
||||
def _extract_component_value(
|
||||
components: list[object],
|
||||
expected_kinds: frozenset[str],
|
||||
) -> str | None:
|
||||
for component in components:
|
||||
if not isinstance(component, dict):
|
||||
raise ValueError(
|
||||
"Yandex Geosuggest address component entry must be an object."
|
||||
)
|
||||
|
||||
if not _component_has_kind(component, expected_kinds):
|
||||
continue
|
||||
|
||||
raw_name = component.get("name")
|
||||
if raw_name is None:
|
||||
return None
|
||||
if not isinstance(raw_name, str):
|
||||
raise ValueError(
|
||||
"Yandex Geosuggest address component name must be a string."
|
||||
)
|
||||
return raw_name.strip() or None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _component_has_kind(component: dict[str, Any], expected_kinds: frozenset[str]) -> bool:
|
||||
raw_kind = component.get("kind")
|
||||
if isinstance(raw_kind, str):
|
||||
kinds = [raw_kind]
|
||||
elif isinstance(raw_kind, list):
|
||||
kinds = raw_kind
|
||||
else:
|
||||
raise ValueError("Yandex Geosuggest address component kind must be provided.")
|
||||
|
||||
normalized_kinds: set[str] = set()
|
||||
for kind in kinds:
|
||||
if not isinstance(kind, str):
|
||||
raise ValueError(
|
||||
"Yandex Geosuggest address component kind must be a string."
|
||||
)
|
||||
normalized_kind = kind.strip().lower()
|
||||
if normalized_kind:
|
||||
normalized_kinds.add(normalized_kind)
|
||||
|
||||
return not expected_kinds.isdisjoint(normalized_kinds)
|
||||
|
||||
|
||||
def _coerce_optional_text(value: object) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("Yandex Geosuggest text field must be a string.")
|
||||
return value.strip() or None
|
||||
@@ -54,11 +54,20 @@ class DadataAddressSuggestionsConfig(BaseModel):
|
||||
timeout_seconds: float = Field(default=10.0, gt=0)
|
||||
|
||||
|
||||
class YandexGeosuggestAddressSuggestionsConfig(BaseModel):
|
||||
url: str = "https://suggest-maps.yandex.ru/v1/suggest"
|
||||
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(
|
||||
default_factory=DadataAddressSuggestionsConfig
|
||||
)
|
||||
yandex_geosuggest: YandexGeosuggestAddressSuggestionsConfig = Field(
|
||||
default_factory=YandexGeosuggestAddressSuggestionsConfig
|
||||
)
|
||||
|
||||
@field_validator("country_to_provider")
|
||||
@classmethod
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
|
||||
from app.adapters.address_suggestions.dadata import DadataAddressSuggestionProvider
|
||||
from app.adapters.address_suggestions.yandex_geosuggest import (
|
||||
YandexGeosuggestAddressSuggestionProvider,
|
||||
)
|
||||
from app.adapters.delivery_providers.cdek import CDEKProvider
|
||||
from app.config import Settings
|
||||
from app.controllers.http_client import build_controller_http_client
|
||||
@@ -38,13 +41,17 @@ def _build_aggregator_service(settings: Settings) -> AggregatorService:
|
||||
http_client=http_client,
|
||||
config=settings.address_suggestions.dadata,
|
||||
)
|
||||
yandex_geosuggest_provider = YandexGeosuggestAddressSuggestionProvider.from_config(
|
||||
http_client=http_client,
|
||||
config=settings.address_suggestions.yandex_geosuggest,
|
||||
)
|
||||
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,),
|
||||
address_suggestion_providers=(dadata_provider, yandex_geosuggest_provider),
|
||||
address_suggestion_country_to_provider=(
|
||||
settings.address_suggestions.country_to_provider
|
||||
),
|
||||
|
||||
@@ -23,6 +23,27 @@ adapter:
|
||||
cdek_timeout_seconds: 10.0
|
||||
cdek_cache_ttl_seconds: 900
|
||||
|
||||
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"
|
||||
dadata:
|
||||
url: "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address"
|
||||
api_key: ""
|
||||
timeout_seconds: 10.0
|
||||
yandex_geosuggest:
|
||||
url: "https://suggest-maps.yandex.ru/v1/suggest"
|
||||
api_key: ""
|
||||
timeout_seconds: 10.0
|
||||
|
||||
observability:
|
||||
enabled: false
|
||||
service_name: "g2s-aggregator"
|
||||
|
||||
@@ -28,10 +28,21 @@ address_suggestions:
|
||||
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"
|
||||
dadata:
|
||||
url: "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address"
|
||||
api_key: "test-dadata-api-key"
|
||||
timeout_seconds: 7.5
|
||||
yandex_geosuggest:
|
||||
url: "https://suggest-maps.yandex.ru/v1/suggest"
|
||||
api_key: "test-yandex-geosuggest-api-key"
|
||||
timeout_seconds: 6.5
|
||||
|
||||
observability:
|
||||
enabled: false
|
||||
|
||||
@@ -176,13 +176,10 @@ def load_tasks() -> List[TaskMeta]:
|
||||
|
||||
|
||||
def render_index(tasks: List[TaskMeta]) -> str:
|
||||
generated_at = dt.datetime.now(tz=dt.timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
lines: List[str] = []
|
||||
lines.append("# Spec Tasks Index")
|
||||
lines.append("")
|
||||
lines.append("> ⚠️ This file is generated. Do not edit manually.")
|
||||
lines.append(f"> Generated at (UTC): `{generated_at}`")
|
||||
lines.append("")
|
||||
lines.append("## Tasks")
|
||||
lines.append("")
|
||||
|
||||
+4
-3
@@ -1,7 +1,7 @@
|
||||
# Spec Tasks Index
|
||||
|
||||
> ⚠️ This file is generated. Do not edit manually.
|
||||
> Generated at (UTC): `2026-03-28T21:02:29+00:00`
|
||||
> Generated at (UTC): `2026-03-29T15:00:18+00:00`
|
||||
|
||||
## Tasks
|
||||
|
||||
@@ -30,9 +30,10 @@
|
||||
| 020 | DONE | 2026-03-21 | Rename price request model and use cities_map for CDEK codes | `spec/tasks/020_rename_price_request_model_and_use_cities_map_for_cdek_codes.md` |
|
||||
| 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` |
|
||||
|
||||
## Summary
|
||||
|
||||
- Total: **23**
|
||||
- Total: **24**
|
||||
- TODO: **1**
|
||||
- DONE: **22**
|
||||
- DONE: **23**
|
||||
|
||||
+8
-2
@@ -20,7 +20,9 @@
|
||||
- Предоставлять отдельный endpoint подсказок адреса, чтобы frontend мог получить точное значение для `from_location.address` и `to_location.address` перед созданием заказа
|
||||
- Принимать запрос на создание заказа CDEK по контракту из `http-client.http` для сценария "доставка, до двери"
|
||||
- Выбирать сервис подсказок адреса по `country_code` через маппинг стран в конфиге
|
||||
- Для стран, сопоставленных с provider id `dadata`, использовать `dadata.ru`; конфигурация и wiring должны допускать отдельный address suggestion provider для европейских стран
|
||||
- Для `RU`, `BY` и `KZ`, сопоставленных с provider id `dadata`, использовать `dadata.ru`
|
||||
- Для `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM` и `UZ`, сопоставленных с provider id `yandex_geosuggest`, использовать Yandex Geosuggest
|
||||
- Конфигурация и wiring должны допускать отдельные address suggestion providers для других регионов
|
||||
- Опрашивать всех зарегистрированных провайдеров параллельно
|
||||
- Возвращать унифицированный список тарифов, отсортированных по цене
|
||||
- Если провайдер вернул ошибку или не ответил вовремя — исключить его из результата, не падая целиком
|
||||
@@ -103,9 +105,11 @@
|
||||
async def suggest(self, request: AddressSuggestRequest) -> list[AddressSuggestion]: ...
|
||||
```
|
||||
- `dadata/client.py` — HTTP-клиент `dadata.ru` для address suggestions
|
||||
- `yandex_geosuggest/client.py` — HTTP-клиент Yandex Geosuggest `GET https://suggest-maps.yandex.ru/v1/suggest`
|
||||
- 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 не используется как источник почтового индекса
|
||||
|
||||
---
|
||||
|
||||
@@ -222,7 +226,9 @@ app/
|
||||
├── adapters/
|
||||
│ ├── address_suggestions/
|
||||
│ │ ├── base.py # Интерфейс Adapter
|
||||
│ │ └── dadata/
|
||||
│ │ ├── dadata/
|
||||
│ │ └── client.py
|
||||
│ │ └── yandex_geosuggest/
|
||||
│ │ └── client.py
|
||||
│ └── delivery_providers/
|
||||
│ ├── base.py # Интерфейс Adapter
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
id: 023
|
||||
title: Add Yandex Geosuggest address suggestion adapter and CIS routing
|
||||
status: DONE
|
||||
created: 2026-03-29
|
||||
---
|
||||
|
||||
## Context
|
||||
Текущий flow подсказок адреса поддерживает только `dadata` и уже использует routing по `country_code` через YAML-конфиг. Появилось новое требование: для части стран СНГ использовать отдельный provider `Yandex Geosuggest`, чтобы разгрузить `dadata` и зафиксировать routing по странам на уровне конфигурации.
|
||||
|
||||
## Goal
|
||||
Добавить новый adapter `yandex_geosuggest` для address suggestions, настроить routing по `country_code` так, чтобы `RU`, `BY` и `KZ` оставались на `dadata`, а `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM` и `UZ` шли через `Yandex Geosuggest`, и покрыть это тестами без изменения публичного 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/yandex_geosuggest/`.
|
||||
- Для `RU`, `BY` и `KZ` existing mapping на `dadata` должен сохраниться без изменения контракта `dadata` adapter.
|
||||
- Для `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM` и `UZ` конфиг должен выбирать provider id `yandex_geosuggest`.
|
||||
- Новый adapter должен следовать официальному HTTP-контракту Yandex Geosuggest: `GET https://suggest-maps.yandex.ru/v1/suggest`, обязательные query-параметры `apikey` и `text`; для полного адреса использовать `print_address=1`; если в internal request передан `limit`, он должен маппиться в query-параметр `results`.
|
||||
- Unified mapping наружу должен по-прежнему возвращать только `AddressSuggestion` без утечки provider-specific payload.
|
||||
- Для Yandex Geosuggest поле `postal_code` не извлекается из provider response и должно возвращаться как `None`.
|
||||
- Ошибки Yandex Geosuggest `400` должны маппиться в deterministic provider request error; `403`, `429`, transport errors и `5xx` должны маппиться в adapter client error/unavailable path.
|
||||
- Scope задачи не включает добавление европейского provider, изменение order flow, price flow, новых endpoint'ов и расширение internal model `AddressSuggestion`.
|
||||
- Не изменять файлы в `spec/`.
|
||||
|
||||
## Acceptance criteria
|
||||
- В конфиге address suggestion providers добавлен provider id `yandex_geosuggest` с параметрами, необходимыми для вызова Yandex Geosuggest.
|
||||
- Country mapping в конфиге маршрутизирует `RU`, `BY`, `KZ` на `dadata`, а `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` на `yandex_geosuggest`.
|
||||
- Реализован adapter `app/adapters/address_suggestions/yandex_geosuggest/client.py`, который отправляет запрос в Yandex Geosuggest по официальному контракту и возвращает `list[AddressSuggestion]`.
|
||||
- Adapter использует `request.city` и `request.query` как значение `text`, передаёт `print_address=1`, а `request.limit` при наличии маппит в `results`.
|
||||
- Поля `address`, `street`, `house` и `flat` детерминированно извлекаются из ответа Yandex Geosuggest без утечки внешнего payload за пределы adapter contract, а `postal_code` возвращается как `None`.
|
||||
- `AggregatorService.suggest_addresses()` по `country_code` выбирает `yandex_geosuggest` для стран `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` и сохраняет routing на `dadata` для `RU`, `BY`, `KZ`.
|
||||
- При ответе Yandex Geosuggest с `400` service flow возвращает deterministic bad-request path, а при `403`, `429`, `5xx` и transport failure — deterministic unavailable path.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Добавлена конфигурация `yandex_geosuggest` и обновлён mapping стран для address suggestions.
|
||||
- [ ] Реализован новый Yandex Geosuggest adapter без утечки HTTP-деталей в Service.
|
||||
- [ ] Обновлён wiring address suggestion providers без изменения публичного endpoint контракта.
|
||||
- [ ] Добавлены tests для config, adapter mapping/error handling и service routing по странам СНГ, включая `postal_code=None` для Yandex Geosuggest.
|
||||
- [ ] Пройдены все команды из раздела Commands.
|
||||
|
||||
## Tests
|
||||
- Обновить `tests/config/test_config_sections.py` для проверки секции `yandex_geosuggest` и явного country mapping: `RU`, `BY`, `KZ` -> `dadata`; `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` -> `yandex_geosuggest`.
|
||||
- Добавить `tests/adapters/address_suggestions/yandex_geosuggest/test_client.py` для success case, mapping `limit -> results`, извлечения `address`/`street`/`house`/`flat`, возврата `postal_code=None` и error scenarios `400`, `403`, `429`, `5xx`.
|
||||
- Обновить `tests/services/test_address_suggestions.py` для проверки routing на `yandex_geosuggest` по странам `AM`, `AZ`, `KG`, `MD`, `TJ`, `TM`, `UZ` и сохранения routing на `dadata` для `RU`, `BY`, `KZ`.
|
||||
- При необходимости обновить `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/yandex_geosuggest/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`
|
||||
@@ -0,0 +1,266 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from app.adapters.address_suggestions.yandex_geosuggest.client import (
|
||||
YandexGeosuggestAddressSuggestionProvider,
|
||||
YandexGeosuggestClientError,
|
||||
YandexGeosuggestRequestError,
|
||||
)
|
||||
from app.config import YandexGeosuggestAddressSuggestionsConfig
|
||||
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": "AM",
|
||||
"city": "Yerevan",
|
||||
"query": "Tumanyan 12",
|
||||
"limit": 5,
|
||||
}
|
||||
payload.update(overrides)
|
||||
return AddressSuggestRequest(**payload)
|
||||
|
||||
|
||||
def test_yandex_geosuggest_provider_maps_response_to_unified_model() -> None:
|
||||
response = httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"results": [
|
||||
{
|
||||
"title": {"text": "Tumanyan Street, 12, Yerevan"},
|
||||
"address": {
|
||||
"formatted_address": "Tumanyan Street, 12, apt 34, Yerevan",
|
||||
"component": [
|
||||
{"name": "Tumanyan Street", "kind": ["STREET"]},
|
||||
{"name": "12", "kind": ["HOUSE"]},
|
||||
{"name": "34", "kind": ["APARTMENT"]},
|
||||
{"name": "0001", "kind": ["POSTAL_CODE"]},
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://suggest-maps.yandex.test/v1/suggest",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = YandexGeosuggestAddressSuggestionProvider(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
url="https://suggest-maps.yandex.test/v1/suggest",
|
||||
api_key="test-api-key",
|
||||
timeout_seconds=6.5,
|
||||
)
|
||||
|
||||
result = asyncio.run(provider.suggest(_make_request()))
|
||||
|
||||
assert result == [
|
||||
AddressSuggestion(
|
||||
address="Tumanyan Street, 12, apt 34, Yerevan",
|
||||
street="Tumanyan Street",
|
||||
house="12",
|
||||
flat="34",
|
||||
postal_code=None,
|
||||
)
|
||||
]
|
||||
assert http_client.calls == [
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "https://suggest-maps.yandex.test/v1/suggest",
|
||||
"params": {
|
||||
"apikey": "test-api-key",
|
||||
"text": "Yerevan Tumanyan 12",
|
||||
"print_address": 1,
|
||||
"results": 5,
|
||||
},
|
||||
"timeout": 6.5,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_yandex_geosuggest_provider_uses_config_factory_and_omits_results_without_limit() -> None:
|
||||
response = httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"results": [
|
||||
{
|
||||
"title": {"text": "Republic Square, Yerevan"},
|
||||
"address": {
|
||||
"formatted_address": "Republic Square, Yerevan",
|
||||
"component": [
|
||||
{"name": "Republic Square", "kind": ["street"]},
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://suggest-maps.yandex.test/v1/suggest",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = YandexGeosuggestAddressSuggestionProvider.from_config(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
config=YandexGeosuggestAddressSuggestionsConfig(
|
||||
url="https://suggest-maps.yandex.test/v1/suggest",
|
||||
api_key="config-api-key",
|
||||
timeout_seconds=4.5,
|
||||
),
|
||||
)
|
||||
|
||||
result = asyncio.run(
|
||||
provider.suggest(
|
||||
_make_request(
|
||||
query="Republic Square",
|
||||
limit=None,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert result == [
|
||||
AddressSuggestion(
|
||||
address="Republic Square, Yerevan",
|
||||
street="Republic Square",
|
||||
)
|
||||
]
|
||||
assert http_client.calls == [
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "https://suggest-maps.yandex.test/v1/suggest",
|
||||
"params": {
|
||||
"apikey": "config-api-key",
|
||||
"text": "Yerevan Republic Square",
|
||||
"print_address": 1,
|
||||
},
|
||||
"timeout": 4.5,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_yandex_geosuggest_provider_maps_400_to_request_error() -> None:
|
||||
response = httpx.Response(
|
||||
400,
|
||||
json={"error": "bad request"},
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://suggest-maps.yandex.test/v1/suggest",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = YandexGeosuggestAddressSuggestionProvider(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
url="https://suggest-maps.yandex.test/v1/suggest",
|
||||
api_key="test-api-key",
|
||||
)
|
||||
|
||||
with pytest.raises(YandexGeosuggestRequestError, match="status 400"):
|
||||
asyncio.run(provider.suggest(_make_request()))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status_code", [403, 429, 503])
|
||||
def test_yandex_geosuggest_provider_maps_unavailable_statuses_to_client_error(
|
||||
status_code: int,
|
||||
) -> None:
|
||||
response = httpx.Response(
|
||||
status_code,
|
||||
json={"error": "provider error"},
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://suggest-maps.yandex.test/v1/suggest",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = YandexGeosuggestAddressSuggestionProvider(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
url="https://suggest-maps.yandex.test/v1/suggest",
|
||||
api_key="test-api-key",
|
||||
)
|
||||
|
||||
with pytest.raises(YandexGeosuggestClientError, match=f"status {status_code}"):
|
||||
asyncio.run(provider.suggest(_make_request()))
|
||||
|
||||
|
||||
def test_yandex_geosuggest_provider_maps_transport_error_to_client_error() -> None:
|
||||
transport_error = httpx.ReadTimeout(
|
||||
"read timeout",
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://suggest-maps.yandex.test/v1/suggest",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([transport_error])
|
||||
provider = YandexGeosuggestAddressSuggestionProvider(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
url="https://suggest-maps.yandex.test/v1/suggest",
|
||||
api_key="test-api-key",
|
||||
)
|
||||
|
||||
with pytest.raises(YandexGeosuggestClientError, match="request failed"):
|
||||
asyncio.run(provider.suggest(_make_request()))
|
||||
|
||||
|
||||
def test_yandex_geosuggest_provider_raises_on_invalid_payload_shape() -> None:
|
||||
response = httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"results": [
|
||||
{
|
||||
"address": {
|
||||
"formatted_address": "Tumanyan Street, 12, Yerevan",
|
||||
"component": [{"name": 12, "kind": ["HOUSE"]}],
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
request=httpx.Request(
|
||||
"GET",
|
||||
"https://suggest-maps.yandex.test/v1/suggest",
|
||||
),
|
||||
)
|
||||
http_client = SequenceHTTPClient([response])
|
||||
provider = YandexGeosuggestAddressSuggestionProvider(
|
||||
http_client=http_client, # type: ignore[arg-type]
|
||||
url="https://suggest-maps.yandex.test/v1/suggest",
|
||||
api_key="test-api-key",
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
YandexGeosuggestClientError,
|
||||
match="response payload is invalid",
|
||||
):
|
||||
asyncio.run(provider.suggest(_make_request()))
|
||||
@@ -18,9 +18,14 @@ adapter:
|
||||
address_suggestions:
|
||||
country_to_provider:
|
||||
RU: "dadata"
|
||||
AM: "yandex_geosuggest"
|
||||
dadata:
|
||||
url: "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address"
|
||||
api_key: "default-dadata-key"
|
||||
yandex_geosuggest:
|
||||
url: "https://suggest-maps.yandex.ru/v1/suggest"
|
||||
api_key: "default-yandex-key"
|
||||
timeout_seconds: 4.0
|
||||
|
||||
observability:
|
||||
enabled: false
|
||||
|
||||
@@ -18,11 +18,15 @@ adapter:
|
||||
address_suggestions:
|
||||
country_to_provider:
|
||||
RU: "dadata"
|
||||
AM: "europe"
|
||||
AM: "yandex_geosuggest"
|
||||
dadata:
|
||||
url: "https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address"
|
||||
api_key: "override-dadata-key"
|
||||
timeout_seconds: 3.0
|
||||
yandex_geosuggest:
|
||||
url: "https://suggest-maps.yandex.ru/v1/suggest"
|
||||
api_key: "override-yandex-key"
|
||||
timeout_seconds: 4.5
|
||||
|
||||
observability:
|
||||
enabled: true
|
||||
|
||||
@@ -85,6 +85,13 @@ def test_configuration_sections_are_loaded_from_yaml_file(
|
||||
"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.dadata.url
|
||||
@@ -92,6 +99,15 @@ def test_configuration_sections_are_loaded_from_yaml_file(
|
||||
)
|
||||
assert settings.address_suggestions.dadata.api_key == "test-dadata-api-key"
|
||||
assert settings.address_suggestions.dadata.timeout_seconds == 7.5
|
||||
assert (
|
||||
settings.address_suggestions.yandex_geosuggest.url
|
||||
== "https://suggest-maps.yandex.ru/v1/suggest"
|
||||
)
|
||||
assert (
|
||||
settings.address_suggestions.yandex_geosuggest.api_key
|
||||
== "test-yandex-geosuggest-api-key"
|
||||
)
|
||||
assert settings.address_suggestions.yandex_geosuggest.timeout_seconds == 6.5
|
||||
assert settings.observability.enabled is False
|
||||
assert settings.observability.service_name == "g2s-aggregator-test"
|
||||
assert settings.observability.otlp_endpoint == "http://localhost:4317"
|
||||
@@ -161,10 +177,12 @@ def test_get_settings_uses_config_test_yaml_in_pytest_environment(
|
||||
assert settings.adapter.cdek_client_id == "test-id"
|
||||
assert settings.address_suggestions.country_to_provider == {
|
||||
"RU": "dadata",
|
||||
"AM": "europe",
|
||||
"AM": "yandex_geosuggest",
|
||||
}
|
||||
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.observability.enabled is True
|
||||
assert settings.observability.service_name == "override-config-service"
|
||||
assert settings.observability.otlp_endpoint == "http://override-collector:4317"
|
||||
|
||||
@@ -35,13 +35,15 @@ def _install_service_override(app, service: StubAggregatorService) -> None:
|
||||
app.dependency_overrides[get_aggregator_service] = override_service
|
||||
|
||||
|
||||
def _valid_payload() -> dict[str, object]:
|
||||
return {
|
||||
def _valid_payload(**overrides: object) -> dict[str, object]:
|
||||
payload = {
|
||||
"country_code": "RU",
|
||||
"city": "Moscow",
|
||||
"query": "Lenina",
|
||||
"limit": 5,
|
||||
}
|
||||
payload.update(overrides)
|
||||
return payload
|
||||
|
||||
|
||||
def test_post_address_suggest_uses_registered_provider_in_default_dependency(
|
||||
@@ -62,24 +64,16 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency(
|
||||
return cls()
|
||||
|
||||
class StubAddressProvider:
|
||||
name = "dadata"
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, name: str, response: list[AddressSuggestion]) -> None:
|
||||
self.name = name
|
||||
self._response = response
|
||||
self.calls: list[AddressSuggestRequest] = []
|
||||
|
||||
async def suggest(
|
||||
self, request: AddressSuggestRequest
|
||||
) -> list[AddressSuggestion]:
|
||||
self.calls.append(request)
|
||||
return [
|
||||
AddressSuggestion(
|
||||
address="107241, Moscow, Khabarovskaya 1",
|
||||
street="Khabarovskaya",
|
||||
house="1",
|
||||
flat="25",
|
||||
postal_code="107241",
|
||||
)
|
||||
]
|
||||
return self._response
|
||||
|
||||
class StubDadataAddressSuggestionProvider:
|
||||
@classmethod
|
||||
@@ -91,7 +85,19 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency(
|
||||
) -> StubAddressProvider:
|
||||
assert http_client is stub_http_client
|
||||
_ = config
|
||||
return stub_address_provider
|
||||
return stub_dadata_provider
|
||||
|
||||
class StubYandexGeosuggestAddressSuggestionProvider:
|
||||
@classmethod
|
||||
def from_config(
|
||||
cls,
|
||||
*,
|
||||
http_client,
|
||||
config,
|
||||
) -> StubAddressProvider:
|
||||
assert http_client is stub_http_client
|
||||
_ = config
|
||||
return stub_yandex_provider
|
||||
|
||||
class StubCache:
|
||||
async def get(self, key: str) -> object | None:
|
||||
@@ -110,7 +116,28 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency(
|
||||
return StubCache()
|
||||
|
||||
stub_http_client = StubHttpClient()
|
||||
stub_address_provider = StubAddressProvider()
|
||||
stub_dadata_provider = StubAddressProvider(
|
||||
"dadata",
|
||||
[
|
||||
AddressSuggestion(
|
||||
address="107241, Moscow, Khabarovskaya 1",
|
||||
street="Khabarovskaya",
|
||||
house="1",
|
||||
flat="25",
|
||||
postal_code="107241",
|
||||
)
|
||||
],
|
||||
)
|
||||
stub_yandex_provider = StubAddressProvider(
|
||||
"yandex_geosuggest",
|
||||
[
|
||||
AddressSuggestion(
|
||||
address="Tumanyan Street, 12, Yerevan",
|
||||
street="Tumanyan Street",
|
||||
house="12",
|
||||
)
|
||||
],
|
||||
)
|
||||
http_client_timeouts: list[float] = []
|
||||
|
||||
def fake_build_controller_http_client(timeout_seconds: float) -> StubHttpClient:
|
||||
@@ -128,9 +155,19 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency(
|
||||
"DadataAddressSuggestionProvider",
|
||||
StubDadataAddressSuggestionProvider,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
delivery_controller,
|
||||
"YandexGeosuggestAddressSuggestionProvider",
|
||||
StubYandexGeosuggestAddressSuggestionProvider,
|
||||
)
|
||||
monkeypatch.setattr(delivery_controller, "PriceCache", StubPriceCache)
|
||||
|
||||
app = create_app()
|
||||
request_payload = _valid_payload(
|
||||
country_code="AM",
|
||||
city="Yerevan",
|
||||
query="Tumanyan 12",
|
||||
)
|
||||
|
||||
async def run_request() -> httpx.Response:
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
@@ -140,7 +177,7 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency(
|
||||
) as client:
|
||||
return await client.post(
|
||||
"/api/v1/delivery/suggest-address",
|
||||
json=_valid_payload(),
|
||||
json=request_payload,
|
||||
)
|
||||
|
||||
response = asyncio.run(run_request())
|
||||
@@ -148,15 +185,16 @@ def test_post_address_suggest_uses_registered_provider_in_default_dependency(
|
||||
assert response.status_code == 200
|
||||
assert response.json() == [
|
||||
{
|
||||
"address": "107241, Moscow, Khabarovskaya 1",
|
||||
"street": "Khabarovskaya",
|
||||
"house": "1",
|
||||
"flat": "25",
|
||||
"postal_code": "107241",
|
||||
"address": "Tumanyan Street, 12, Yerevan",
|
||||
"street": "Tumanyan Street",
|
||||
"house": "12",
|
||||
"flat": None,
|
||||
"postal_code": None,
|
||||
}
|
||||
]
|
||||
assert http_client_timeouts == [10.0]
|
||||
assert stub_address_provider.calls == [AddressSuggestRequest(**_valid_payload())]
|
||||
assert stub_dadata_provider.calls == []
|
||||
assert stub_yandex_provider.calls == [AddressSuggestRequest(**request_payload)]
|
||||
|
||||
|
||||
def test_post_address_suggest_returns_response_and_delegates_to_service() -> None:
|
||||
|
||||
@@ -15,6 +15,9 @@ from app.services.aggregator import (
|
||||
UnsupportedAddressSuggestionCountryError,
|
||||
)
|
||||
|
||||
_DADATA_COUNTRIES = ("RU", "BY", "KZ")
|
||||
_YANDEX_COUNTRIES = ("AM", "AZ", "KG", "MD", "TJ", "TM", "UZ")
|
||||
|
||||
|
||||
class StubAddressSuggestionProvider:
|
||||
def __init__(
|
||||
@@ -64,7 +67,20 @@ def _make_suggestion(
|
||||
)
|
||||
|
||||
|
||||
def test_suggest_addresses_routes_ru_to_dadata() -> None:
|
||||
def _make_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
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("country_code", _DADATA_COUNTRIES)
|
||||
def test_suggest_addresses_routes_dadata_countries_to_dadata(
|
||||
country_code: str,
|
||||
) -> None:
|
||||
dadata = StubAddressSuggestionProvider(
|
||||
"dadata",
|
||||
response=[
|
||||
@@ -77,16 +93,20 @@ def test_suggest_addresses_routes_ru_to_dadata() -> None:
|
||||
)
|
||||
],
|
||||
)
|
||||
europe = StubAddressSuggestionProvider(
|
||||
"europe",
|
||||
yandex_geosuggest = StubAddressSuggestionProvider(
|
||||
"yandex_geosuggest",
|
||||
response=[_make_suggestion("Yerevan, Tumanyan 1")],
|
||||
)
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
address_suggestion_providers=[dadata, europe],
|
||||
address_suggestion_country_to_provider={"RU": "dadata", "AM": "europe"},
|
||||
address_suggestion_providers=[dadata, yandex_geosuggest],
|
||||
address_suggestion_country_to_provider=_make_country_mapping(),
|
||||
)
|
||||
request = _make_request(
|
||||
country_code=country_code,
|
||||
city="Moscow",
|
||||
query="Khabarovskaya",
|
||||
)
|
||||
request = _make_request(country_code="RU", city="Moscow", query="Khabarovskaya")
|
||||
|
||||
result = asyncio.run(service.suggest_addresses(request))
|
||||
|
||||
@@ -100,30 +120,33 @@ def test_suggest_addresses_routes_ru_to_dadata() -> None:
|
||||
)
|
||||
]
|
||||
assert dadata.calls == [request]
|
||||
assert europe.calls == []
|
||||
assert yandex_geosuggest.calls == []
|
||||
|
||||
|
||||
def test_suggest_addresses_routes_second_provider_by_country_mapping() -> None:
|
||||
@pytest.mark.parametrize("country_code", _YANDEX_COUNTRIES)
|
||||
def test_suggest_addresses_routes_cis_countries_to_yandex_geosuggest(
|
||||
country_code: str,
|
||||
) -> None:
|
||||
dadata = StubAddressSuggestionProvider(
|
||||
"dadata",
|
||||
response=[_make_suggestion("Moscow, Lenina 1")],
|
||||
)
|
||||
europe = StubAddressSuggestionProvider(
|
||||
"europe",
|
||||
yandex_geosuggest = StubAddressSuggestionProvider(
|
||||
"yandex_geosuggest",
|
||||
response=[_make_suggestion("Yerevan, Tumanyan 1")],
|
||||
)
|
||||
service = AggregatorService(
|
||||
providers=[],
|
||||
address_suggestion_providers=[dadata, europe],
|
||||
address_suggestion_country_to_provider={"RU": "dadata", "AM": "europe"},
|
||||
address_suggestion_providers=[dadata, yandex_geosuggest],
|
||||
address_suggestion_country_to_provider=_make_country_mapping(),
|
||||
)
|
||||
request = _make_request(country_code="AM", city="Yerevan", query="Tumanyan")
|
||||
request = _make_request(country_code=country_code, city="Yerevan", query="Tumanyan")
|
||||
|
||||
result = asyncio.run(service.suggest_addresses(request))
|
||||
|
||||
assert result == [AddressSuggestion(address="Yerevan, Tumanyan 1")]
|
||||
assert dadata.calls == []
|
||||
assert europe.calls == [request]
|
||||
assert yandex_geosuggest.calls == [request]
|
||||
|
||||
|
||||
def test_suggest_addresses_raises_for_unsupported_country() -> None:
|
||||
@@ -155,7 +178,7 @@ def test_suggest_addresses_raises_for_unregistered_provider_mapping() -> None:
|
||||
response=[_make_suggestion("Moscow, Lenina 1")],
|
||||
)
|
||||
],
|
||||
address_suggestion_country_to_provider={"AM": "europe"},
|
||||
address_suggestion_country_to_provider={"AM": "yandex_geosuggest"},
|
||||
)
|
||||
|
||||
with pytest.raises(UnsupportedAddressSuggestionCountryError):
|
||||
|
||||
Reference in New Issue
Block a user