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}") 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}')