import re from datetime import date from typing import Any import httpx from django.conf import settings class WildberriesError(RuntimeError): pass class WildberriesClient: """Small client for the current WB Finance API and seller identity API.""" DEFAULT_FINANCE_URL = "https://finance-api.wildberries.ru" DEFAULT_COMMON_URL = "https://common-api.wildberries.ru" REPORT_LIMIT = 100_000 def __init__(self, *, credentials: dict[str, str]): token = re.sub( r"^Bearer\s+", "", credentials.get("token", "").strip(), flags=re.IGNORECASE, ).strip() if not token: raise WildberriesError("Не указан API-токен Wildberries.") self.finance_url = getattr( settings, "WILDBERRIES_FINANCE_API_BASE_URL", self.DEFAULT_FINANCE_URL, ).rstrip("/") self.common_url = getattr( settings, "WILDBERRIES_COMMON_API_BASE_URL", self.DEFAULT_COMMON_URL, ).rstrip("/") self.client = httpx.Client( headers={ "Authorization": token, "Accept": "application/json", "User-Agent": "EUSRR-Commerce/1.0", }, timeout=httpx.Timeout(60.0), follow_redirects=False, ) def close(self): self.client.close() def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() @staticmethod def _error_detail(response: httpx.Response) -> str: try: payload = response.json() except ValueError: return response.text.strip()[:500] if isinstance(payload, dict): for key in ("detail", "title", "message", "errorText", "error"): if payload.get(key): return str(payload[key])[:500] return "" def _request(self, method: str, url: str, **kwargs) -> Any: try: response = self.client.request(method, url, **kwargs) if response.status_code == 204: return None response.raise_for_status() return response.json() except httpx.HTTPStatusError as exc: status = exc.response.status_code detail = self._error_detail(exc.response) if status in {401, 403}: message = ( "Wildberries отклонил токен. Для финансового импорта нужен " "доступ к категории «Финансы»." ) elif status == 400: message = "Wildberries не принял параметры финансового отчёта." elif status == 429: message = "Wildberries ограничил частоту запросов. Повторите импорт через минуту." else: message = f"Wildberries вернул ошибку HTTP {status}." if detail: message = f"{message} Ответ сервиса: {detail}" raise WildberriesError(message) from exc except (httpx.HTTPError, ValueError) as exc: raise WildberriesError( "Не удалось получить корректный ответ от Wildberries." ) from exc def seller_info(self) -> dict[str, Any]: payload = self._request( "GET", f"{self.common_url}/api/v1/seller-info", ) if not isinstance(payload, dict) or not payload.get("sid"): raise WildberriesError( "Wildberries вернул неполную информацию об учётной записи." ) return payload def finance_ping(self) -> None: """Validate that the token is accepted by the Finance API domain.""" payload = self._request("GET", f"{self.finance_url}/ping") if not isinstance(payload, dict) or payload.get("Status") != "OK": raise WildberriesError( "Wildberries не подтвердил доступ токена к категории «Финансы»." ) def financial_report( self, *, date_from: date | str, date_to: date | str, rrd_id: int = 0, limit: int = REPORT_LIMIT, ) -> list[dict[str, Any]]: payload = self._request( "POST", f"{self.finance_url}/api/finance/v1/sales-reports/detailed", json={ "dateFrom": str(date_from), "dateTo": str(date_to), "limit": min(max(int(limit), 1), self.REPORT_LIMIT), "rrdId": max(int(rrd_id), 0), "period": "weekly", }, ) if payload is None: return [] if not isinstance(payload, list) or any( not isinstance(item, dict) for item in payload ): raise WildberriesError( "Wildberries вернул финансовый отчёт неожиданного формата." ) return payload