005 Implement service

This commit is contained in:
Раис Юсупалиев
2026-03-07 15:38:26 +03:00
parent 3fff4b8878
commit 082e5c11ec
4 changed files with 306 additions and 8 deletions
+146 -3
View File
@@ -1,10 +1,153 @@
"""Aggregator service orchestrator skeleton."""
"""Aggregator service orchestrating providers, cache and domain rules."""
import asyncio
import hashlib
import json
from collections.abc import Callable, Iterable, Sequence
from typing import Protocol
from app.adapters.delivery_providers.base import DeliveryProvider
from app.domain.price import (
DEFAULT_WEIGHT_ROUND_SCALE,
NormalizedDeliveryRequest,
filter_and_sort_prices,
normalize_delivery_request,
)
from app.schemas.request import DeliveryRequest
from app.schemas.response import DeliveryPrice
class PriceCacheProtocol(Protocol):
async def get(self, key: str) -> object | None: ...
async def set(self, key: str, value: object, ttl: int | None = None) -> None: ...
class AggregatorService:
def __init__(
self,
providers: Sequence[DeliveryProvider],
cache: PriceCacheProtocol | None = None,
*,
weight_round_scale: int = DEFAULT_WEIGHT_ROUND_SCALE,
filter_and_sort_prices_fn: Callable[[Iterable[object]], list[object]] = (
filter_and_sort_prices
),
) -> None:
self._providers = tuple(providers)
self._cache = cache
self._weight_round_scale = weight_round_scale
self._filter_and_sort_prices = filter_and_sort_prices_fn
async def get_all_prices(self, request: DeliveryRequest) -> list[DeliveryPrice]:
_ = request
raise NotImplementedError("Aggregator workflow is implemented in task 005.")
normalized_request = normalize_delivery_request(
request, weight_round_scale=self._weight_round_scale
)
provider_request = self._to_provider_request(normalized_request)
provider_results = await asyncio.gather(
*(
self._get_provider_price(
provider=provider,
request=provider_request,
cache_key=self._build_cache_key(
provider_name=provider.name,
request=normalized_request,
),
)
for provider in self._providers
),
return_exceptions=True,
)
successful_results = [
price
for price in provider_results
if isinstance(price, DeliveryPrice)
]
filtered_and_sorted = self._filter_and_sort_prices(successful_results)
return [self._coerce_delivery_price(price) for price in filtered_and_sorted]
async def _get_provider_price(
self,
*,
provider: DeliveryProvider,
request: DeliveryRequest,
cache_key: str,
) -> DeliveryPrice:
cached_price = await self._get_cached_price(cache_key)
if cached_price is not None:
return cached_price
fresh_price = await provider.get_price(request)
await self._set_cached_price(
cache_key,
fresh_price,
ttl=getattr(provider, "cache_ttl_seconds", None),
)
return fresh_price
async def _get_cached_price(self, cache_key: str) -> DeliveryPrice | None:
if self._cache is None:
return None
try:
payload = await self._cache.get(cache_key)
except Exception:
return None
if payload is None:
return None
try:
return self._coerce_delivery_price(payload)
except Exception:
return None
async def _set_cached_price(
self,
cache_key: str,
payload: DeliveryPrice,
*,
ttl: int | None,
) -> None:
if self._cache is None:
return
try:
await self._cache.set(cache_key, payload, ttl=ttl)
except Exception:
return
@staticmethod
def _to_provider_request(request: NormalizedDeliveryRequest) -> DeliveryRequest:
return DeliveryRequest(
entity=request.entity,
from_city=request.from_city,
to_city=request.to_city,
weight_kg=request.weight_kg,
length_cm=request.length_cm,
width_cm=request.width_cm,
height_cm=request.height_cm,
)
@staticmethod
def _build_cache_key(provider_name: str, request: NormalizedDeliveryRequest) -> str:
cache_payload = {
"provider": provider_name,
"entity": request.entity,
"from_city": request.from_city,
"to_city": request.to_city,
"weight_kg": str(request.weight_kg),
"length_cm": str(request.length_cm),
"width_cm": str(request.width_cm),
"height_cm": str(request.height_cm),
}
serialized_payload = json.dumps(
cache_payload,
ensure_ascii=True,
sort_keys=True,
separators=(",", ":"),
)
cache_hash = hashlib.sha256(serialized_payload.encode("utf-8")).hexdigest()
return f"delivery-price:{provider_name}:{cache_hash}"
@staticmethod
def _coerce_delivery_price(value: object) -> DeliveryPrice:
return DeliveryPrice.model_validate(value, from_attributes=True)