Files
g2s-aggregator/spec/overview.md
T
Раис Юсупалиев adfa3e26de 001 Project structure
2026-03-07 13:41:30 +03:00

7.7 KiB

spec/overview.md

Контекст, специфичный для проекта агрегатора служб доставки. Глобальные правила определены в AGENTS.md.


Проект

Название: Агрегатор служб доставки (бэкенд) Язык: Python 3.14 Назначение: Агрегировать расчёты стоимости доставки от нескольких служб и отдавать единый ответ фронтенду через унифицированный API.


Продуктовые требования

  • Принимать запрос на расчёт стоимости доставки (откуда, куда, вес, габариты)
  • Опрашивать всех зарегистрированных провайдеров параллельно
  • Возвращать унифицированный список тарифов, отсортированных по цене
  • Если провайдер вернул ошибку или не ответил вовремя — исключить его из результата, не падая целиком
  • Кешировать ответы провайдеров для исключения повторных внешних запросов. Время кэширования вынести в конфиг
  • Обеспечивать структурированную наблюдаемость: трейсы, метрики, логи — связанные по request ID
  • Отправлять алерты в Telegram при аномалиях (ошибки, всплески задержки, недоступность провайдера). token вынести в конфиг

Провайдеры

Фаза 1

  • CDEKhttps://apidoc.cdek.ru/
    • Аутентификация: OAuth2 (client credentials)
    • Операции: расчёт тарифа
    • Cache TTL: 15 минут

Фаза 2+

  • Дополнительные провайдеры (Boxberry, DHL и др.) — подключаются через интерфейс DeliveryProvider без изменений в логике агрегации

Компоненты

Controller (app/controllers/v1/delivery.py)

  • POST /api/v1/delivery/price — принимает DeliveryRequest, возвращает list[DeliveryPrice]
  • Парсинг и валидация входных данных через Pydantic
  • Маппинг исключений сервиса в HTTP-ответы
  • Вызывает ровно один метод Service: AggregatorService.get_all_prices()

Service (app/services/aggregator.py)

  • AggregatorService.get_all_prices(request: DeliveryRequest) -> list[DeliveryPrice]
  • Распределяет запросы по всем зарегистрированным провайдерам через asyncio.gather(..., return_exceptions=True)
  • Фильтрует упавшие результаты
  • Сортирует тарифы по цене
  • Не содержит бизнес-логики и логики, специфичной для провайдеров

Business Logic (app/domain/)

  • Правила сортировки и фильтрации тарифов
  • Логика сравнения цен
  • Нормализация входных данных (например, правила округления веса)
  • Чистые функции, без IO, без зависимостей от фреймворков

Repository (app/repositories/cache/)

  • PriceCache — хранилище ключ/значение на базе Redis
  • Операции: get(key), set(key, value, ttl), invalidate(key)
  • Без бизнес-решений; формирование ключа — ответственность Adapter

Adapter (app/adapters/delivery_providers)

  • base.py — абстрактный интерфейс DeliveryProvider:
    class DeliveryProvider(ABC):
        name: str
        async def get_price(self, request: DeliveryRequest) -> DeliveryPrice: ...
    
  • cdek/client.py — HTTP-клиент (httpx AsyncClient), аутентификация, ретраи, таймаут (10с)
  • cdek/auth.py — управление OAuth2-токеном
  • cdek/mapper.py — ответ CDEK → DeliveryPrice
  • Каждый адаптер владеет своей конфигурацией; наружу экспонирует только get_price()

Модели данных

Входная: DeliveryRequest

entity: Enum(individual, legal)
from_city: str
to_city: str
weight_kg: float
length_cm: float
width_cm: float
height_cm: float

Выходная: DeliveryPrice

provider: str
service_name: str
price: Decimal
currency: str
delivery_days_min: int
delivery_days_max: int

Технологический стек

Задача Инструмент
Web-фреймворк FastAPI
HTTP-клиент httpx (async)
Валидация Pydantic v2
Кеш Redis
Конфигурация pydantic-settings
Сервер Uvicorn
Инструментация OpenTelemetry SDK
Бэкенд наблюдаемости SigNoz (self-hosted)
Структурные логи structlog
Алерты SigNoz Alert Rules → Webhook → Telegram Bot API

Наблюдаемость

  • Каждому запросу присваивается request_id (UUID) через middleware
  • request_id привязывается ко всем логам через structlog.contextvars
  • Трейсы отправляются через OpenTelemetry; FastAPI и httpx инструментируются автоматически
  • Ручные спаны оборачивают: AggregatorService.get_all_prices, get_price каждого провайдера, обращения к кешу
  • Атрибуты спанов: provider, from_city, to_city, weight_kg, cache_hit, tariffs_found
  • Отслеживаемые метрики: количество запросов, количество ошибок, время ответа (p50/p99), доступность каждого провайдера
  • Все три сигнала (трейсы, метрики, логи) связаны через trace_id

Условия алертов (Telegram)

  • Провайдер вернул 5xx — порог: 5 ошибок за 5 минут
  • p99 времени ответа провайдера > 5000ms в течение 10 минут
  • Провайдер недоступен более 5 минут

Структура проекта

app/
├── controllers/
│   ├── http_client.py
│   ├── middleware.py
│   └── v1/
│       └── delivery.py        # Controller
├── services/
│   └── aggregator.py              # Service
├── domain/
│   └── price.py                  # Business Logic
├── adapters/
│   └── delivery_providers/
│       ├── base.py                    # Интерфейс Adapter
│       └── cdek/
│           ├── client.py
│           ├── auth.py
│           └── mapper.py
├── repositories/
│   └── cache/
│       └── redis_cache.py             # Repository
├── schemas/
│   ├── request.py
│   └── response.py
└── config.py

Инфраструктура

# docker-compose сервисы
app        # FastAPI-приложение
redis      # Кеш тарифов
signoz     # Бэкенд наблюдаемости (трейсы + метрики + логи + алерты)