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