Files
octopus/myproject/integrations/recommerce/client.py
Andrey Smakotin 74d7d1186a fix(recommerce): сброс зачеркнутой цены через price_old=0
- Передаем price_old[amount]="0" для сброса старой цены
- Добавлены флаги is_new и is_popular в маппер
- Добавлен debug логгер для отладки типов данных

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:26:29 +03:00

139 lines
6.1 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 logging
import requests
from typing import Dict, Any, Optional, List
from .exceptions import (
RecommerceConnectionError,
RecommerceAuthError,
RecommerceAPIError
)
logger = logging.getLogger(__name__)
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 - requests сам поставит правильный:
# - application/x-www-form-urlencoded для data=...
# - application/json для 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}")
logger.error(f"Recommerce API error {response.status_code}: {response.text}")
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]:
"""Обновить товар (использует form-data как указано в документации)"""
# Recommerce API требует form-data для POST запросов
# data должен быть плоским словарём с ключами вида price[amount], price[currency]
logger.info(f"Recommerce update_product {sku}: {data}")
logger.debug(f"Data types in update_product: {[(k, type(v).__name__) for k, v in data.items()]}")
return self._request('POST', f'catalog/products/{sku}', data=data)
def create_product(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Создать товар (использует form-data как указано в документации)"""
# Recommerce API требует form-data для POST запросов
return self._request('POST', 'catalog/products', data=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}')