148 lines
5.2 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 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