Files
octopus/myproject/integrations/recommerce/client.py
Andrey Smakotin a5ab216934 feat(integrations): добавлена полная интеграция с Recommerce
Реализован клиент для работы с API Recommerce, включая:
- Клиент с методами для работы с товарами и заказами
- Сервисный слой для высокоуровневых операций
- Мапперы данных между форматами
- Обработку исключений

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 17:56:53 +03:00

130 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import requests
from typing import Dict, Any, Optional, List
from .exceptions import (
RecommerceConnectionError,
RecommerceAuthError,
RecommerceAPIError
)
class RecommerceClient:
"""
Низкоуровневый клиент для работы с Recommerce API.
Отвечает только за HTTP запросы и обработку ошибок.
"""
def __init__(self, store_url: str, api_token: str, timeout: int = 15):
self.store_url = store_url.rstrip('/')
self.api_token = api_token
self.timeout = timeout
self.base_url = f"{self.store_url}/api/v1"
def _get_headers(self) -> Dict[str, str]:
"""Заголовки для API запросов"""
# HTTP заголовки должны быть ASCII-совместимыми
try:
token = self.api_token.encode('ascii', 'ignore').decode('ascii')
except (AttributeError, ValueError):
token = ''
return {
'x-auth-token': token,
'Accept': 'application/json',
'Content-Type': 'application/json', # В доках сказано form-data для POST, но обычно JSON тоже работает. Проверим.
# Если API строго требует form-data, то requests сам поставит нужный Content-Type, если передать files или data вместо json
}
def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
"""
Выполнение HTTP запроса.
Args:
method: HTTP метод (GET, POST, DELETE, etc)
endpoint: Путь относительно /api/v1/ (напр. 'catalog/products')
**kwargs: Аргументы для requests (params, json, data, etc)
Returns:
Dict: Ответ API (JSON)
Raises:
RecommerceConnectionError: Ошибка сети
RecommerceAuthError: Ошибка авторизации (401, 403)
RecommerceAPIError: Другие ошибки API (4xx, 5xx)
"""
url = f"{self.base_url}/{endpoint.lstrip('/')}"
headers = self._get_headers()
# Если переданы headers в kwargs, обновляем их, а не перезаписываем
if 'headers' in kwargs:
headers.update(kwargs.pop('headers'))
try:
response = requests.request(
method,
url,
headers=headers,
timeout=self.timeout,
**kwargs
)
if response.status_code in [200, 201, 204]:
if not response.content:
return {}
try:
return response.json()
except ValueError:
return {}
# Обработка ошибок
if response.status_code in [401, 403]:
raise RecommerceAuthError(f"Auth Error: {response.status_code}")
raise RecommerceAPIError(
status_code=response.status_code,
message=f"API Request failed: {url}",
response_body=response.text
)
except requests.exceptions.Timeout:
raise RecommerceConnectionError("Connection timeout")
except requests.exceptions.ConnectionError:
raise RecommerceConnectionError("Connection failed")
except requests.exceptions.RequestException as e:
raise RecommerceConnectionError(f"Network error: {str(e)}")
def get_product(self, sku: str) -> Dict[str, Any]:
"""Получить товар по SKU"""
return self._request('GET', f'catalog/products/{sku}')
def update_product(self, sku: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""Обновить товар"""
# В документации POST используется для обновления
return self._request('POST', f'catalog/products/{sku}', json=data)
def create_product(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Создать товар"""
return self._request('POST', 'catalog/products', json=data)
def get_orders(self, updated_after: Optional[str] = None) -> List[Dict[str, Any]]:
"""
Получить список заказов.
Args:
updated_after: Дата в формате 'Y-m-d-H-i-s'
"""
params = {'page': 1}
if updated_after:
params['updated_after'] = updated_after
# Recommerce возвращает пагинацию.
# Для простоты пока берем первую страницу, но в идеале нужно итерироваться.
# В рамках "простой" интеграции пока оставим 1 страницу (последние 50-100 заказов).
response = self._request('GET', 'orders', params=params)
return response.get('data', [])
def get_order(self, order_id: int) -> Dict[str, Any]:
"""Получить заказ по ID"""
# В документации нет отдельного эндпоинта для одного заказа,
# но обычно в REST API он есть. Если нет - этот метод упадет с 404.
# Предположим что он есть для полноты клиента.
return self._request('GET', f'orders/{order_id}')