Metadata-Version: 2.3
Name: wbapi
Version: 0.2.1
Summary: Typed async client for the Wildberries seller API
Author: Dmitry Drozdov
Author-email: Dmitry Drozdov <ds.drozdov@gmail.com>
Requires-Dist: httpx2>=2.13.1,<3
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# wbapi

Внутренний типизированный Python-клиент для официального API продавцов Wildberries.

Текущая версия реализует три read-only метода раздела «Общее» из
`specs/upstream/01-general.yaml`, получение детализации отчётов реализации из
`specs/upstream/13-finances.yaml`, статистику карточек товаров из
`specs/upstream/11-analytics.yaml`, получение товаров с ценами из `specs/upstream/02-items.yaml`
и отчёт о платном хранении из `specs/upstream/12-reports.yaml`.
Ответы возвращаются без преобразования в виде обычных `dict` и `list`. `TypedDict` используется
только для подсказок IDE и статической проверки.

## Установка

```bash
uv add --editable /path/to/wbapi
```

Требуется Python 3.14 или новее.

Для HTTP-запросов используется `httpx2`. Если передаёте собственный клиент через
`AsyncWBClient(http_client=...)`, создавайте его как `httpx2.AsyncClient`.

## Асинхронный клиент

```python
import asyncio

from wbapi import AsyncWBClient


async def main() -> None:
    async with AsyncWBClient("personal-token") as client:
        result = await client.general.ping()
        print(result)


asyncio.run(main())
```

Все примеры ниже предполагают, что вызов выполняется внутри блока
`async with AsyncWBClient(...) as client`.

## Поддерживаемые эндпоинты

### Проверка подключения

`client.general.ping()` выполняет `GET https://common-api.wildberries.ru/ping`. Метод проверяет
доступность API и валидность токена, не принимает аргументов и возвращает исходный JSON-ответ WB.

```python
result = await client.general.ping()

print(result.get("Status"))
print(result.get("TS"))
```

Лимит: максимум 3 запроса за 30 секунд, отдельно для каждого домена API.

### Новости портала продавцов

Для последовательного получения всех доступных новостей используйте `iter_news()`. Необходимо
передать ровно один из двух параметров:

- `from_date` — дата начала выборки как `datetime.date` или строка `YYYY-MM-DD`;
- `from_id` — неотрицательный ID новости, с которого нужно начать выборку.

Параметр `max_pages` ограничивает количество запросов:

```python
from datetime import date

async for news in client.general.iter_news(
    from_date=date(2026, 6, 1),
    max_pages=10,
):
    print(news.get("header"))
```

Низкоуровневый метод `get_news()` выполняет один запрос
`GET https://common-api.wildberries.ru/api/communications/v2/news` и возвращает одну страницу в
исходной структуре ответа WB. Используйте его, если требуется самостоятельно управлять курсором,
границами страниц или обработкой отдельных запросов:

```python
page = await client.general.get_news(from_id=1)

for news in page.get("data", []):
    print(news.get("id"), news.get("header"))
```

Лимит для персонального токена: 1 запрос в минуту, всплеск до 10 запросов.

### Товары с ценами

`client.items.get_goods_prices()` выполняет
`GET https://discounts-prices-api.wildberries.ru/api/v2/list/goods/filter` и возвращает одну
страницу товаров с ценами и скидками в исходной структуре WB. `limit` обязателен и не должен
превышать 1000, `offset` задаёт смещение, а `filter_nm_id` позволяет найти один артикул WB.

Для автоматической загрузки всех страниц используйте `iter_goods_prices()`:

```python
async for item in client.items.iter_goods_prices(max_pages=10):
    print(item.get("nmID"), item.get("currencyIsoCode4217"), item.get("sizes"))
```

```python
page = await client.items.get_goods_prices(
    limit=1000,
    offset=0,
)

for item in page.get("data", {}).get("listGoods", []):
    print(item.get("nmID"), item.get("currencyIsoCode4217"), item.get("sizes"))
```

Итератор увеличивает `offset` на `limit` после каждого запроса, останавливается на пустом
`listGoods` и предотвращает повтор артикулов WB. `max_pages` ограничивает количество страниц.
Лимит для персонального токена общий для категории «Цены и скидки»: всплеск до 5 запросов с
восполнением одного запроса каждые 600 мс.

### Информация о продавце

`client.general.get_seller_info()` выполняет
`GET https://common-api.wildberries.ru/api/v1/seller-info`. Метод не принимает аргументов и
возвращает наименование, ID, ИНН и торговое имя продавца.

```python
seller = await client.general.get_seller_info()

print(seller.get("name"))
print(seller.get("sid"))
print(seller.get("tin"))
print(seller.get("tradeMark"))
```

Лимит для персонального токена: 1 запрос в минуту, всплеск до 10 запросов.

### Детализация отчётов реализации

Для автоматической загрузки всех строк детализации используйте `iter_sales_report_details()`.
Обязательные аргументы:

- `date_from` — начало периода;
- `date_to` — конец периода.

Дата может быть строкой RFC 3339, `datetime.date` или `datetime.datetime`. Время WB интерпретирует
в часовом поясе Москвы UTC+3. Доступны необязательные аргументы:

- `limit` — количество строк, от 1 до 100 000;
- `rrd_id` — неотрицательный ID строки для продолжения загрузки, начальное значение — `0`;
- `period` — периодичность отчёта: `"daily"` или `"weekly"`;
- `fields` — список полей, которые нужно вернуть.

```python
async for row in client.finances.iter_sales_report_details(
    date_from="2026-03-17",
    date_to="2026-03-20",
    period="daily",
    limit=1_000,
    fields=["rrdId", "nmId", "retailAmount", "acquiringFee"],
    max_pages=10,
):
    print(row.get("rrdId"), row.get("retailAmount"))
```

Итератор загружает страницы до ответа HTTP 204, передаёт `rrdId` последней строки в следующий
запрос и предотвращает повтор строк и отсутствие продвижения курсора. Параметр `max_pages`
ограничивает количество страниц. Если итератор вызывается с `fields`, список обязан содержать
`rrdId`.

Низкоуровневый метод `get_sales_report_details()` выполняет один запрос
`POST https://finance-api.wildberries.ru/api/finance/v1/sales-reports/detailed` и возвращает одну
страницу в исходной структуре ответа WB. Используйте его, если требуется самостоятельно управлять
`rrd_id`, границами страниц или обработкой HTTP 204:

```python
rows = await client.finances.get_sales_report_details(
    date_from="2026-03-17",
    date_to="2026-03-20",
    period="daily",
    limit=1_000,
    rrd_id=0,
    fields=["rrdId", "nmId", "retailAmount"],
)

if rows is None:  # HTTP 204 — данных нет
    print("Данных нет")
else:
    for row in rows:
        print(row.get("rrdId"), row.get("retailAmount"))
```

Лимит для персонального токена: 1 запрос в минуту. Встроенный ограничитель автоматически ожидает
освобождения лимита перед загрузкой следующей страницы.

### Воронка продаж по карточкам товаров

Для последовательной загрузки всех карточек используйте `iter_sales_funnel_products()`:

```python
async for item in client.analytics.iter_sales_funnel_products(
    date_from="2026-06-01",
    date_to="2026-06-30",
    past_date_from="2026-05-01",
    past_date_to="2026-05-30",
    nm_ids=[],  # Пустой список означает все карточки продавца
    order_by={"field": "orderSum", "mode": "desc"},
    limit=1000,
    max_pages=10,
):
    product = item.get("product", {})
    statistic = item.get("statistic", {}).get("selected", {})
    print(product.get("nmId"), statistic.get("orderSum"))
```

Дополнительно поддерживаются фильтры `brand_names`, `subject_ids`, `tag_ids` и
`skip_deleted_nm`. Период — максимум последние 365 дней. Параметр `limit` должен быть от 1 до
1000, `offset` задаёт начальное смещение, `max_pages` ограничивает количество страниц. Итератор
увеличивает offset на фактическое число карточек и предотвращает повтор артикулов WB.

Низкоуровневый `get_sales_funnel_products()` выполняет один запрос и сохраняет исходную структуру,
включая общую валюту отчёта:

```python
page = await client.analytics.get_sales_funnel_products(
    date_from="2026-06-01",
    date_to="2026-06-30",
    limit=100,
    offset=0,
)

print(page.get("data", {}).get("currency"))
```

Метод выполняет `POST https://seller-analytics-api.wildberries.ru/api/analytics/v3/sales-funnel/products`.
Лимит для персонального токена: всплеск до 3 запросов с восполнением одного запроса раз в 20 секунд.

### Отчёт о платном хранении

`get_paid_storage_report()` выполняет весь сценарий: создаёт задание, проверяет его статус до
готовности и скачивает результат. Период может охватывать максимум 8 дней. Даты принимаются как
строки RFC 3339, `datetime.date` или `datetime.datetime`.

```python
rows = await client.reports.get_paid_storage_report(
    date_from="2026-07-01",
    date_to="2026-07-08",
    max_status_checks=120,
)

if rows is None:  # HTTP 204 — данных нет
    print("Данных нет")
else:
    for row in rows:
        print(row.get("date"), row.get("nmId"), row.get("warehousePrice"))
```

По умолчанию статус проверяется раз в 5 секунд, максимум 120 раз. Интервал и предел можно изменить
параметрами `poll_interval` и `max_status_checks`. Статусы `canceled` и `purged` приводят к
`WBReportTaskFailedError`, исчерпание проверок — к `WBReportTaskTimeoutError`.

Для ручного управления этапами доступны низкоуровневые методы:

```python
task = await client.reports.create_paid_storage_report(
    date_from="2026-07-01",
    date_to="2026-07-08",
)
task_id = task["data"]["taskId"]

status = await client.reports.get_paid_storage_report_status(task_id)
if status.get("data", {}).get("status") == "done":
    rows = await client.reports.download_paid_storage_report(task_id)
```

Создание задания ограничено всплеском до 5 запросов с восполнением раз в минуту, проверка статуса —
до 5 запросов с восполнением раз в 5 секунд, скачивание — 1 запрос в минуту. Создание задания не
повторяется автоматически после транспортной ошибки или `5xx`, поскольку операция создаёт новый
ресурс и спецификация не гарантирует её идемпотентность.

## HTTP-метаданные

Обычный метод возвращает JSON-тело. Вариант с суффиксом `_detailed` дополнительно возвращает
`WBResponse` с HTTP-статусом, заголовками и исходным телом. Варианты доступны для каждого
низкоуровневого метода:

- `general.ping_detailed()`;
- `general.get_news_detailed()`;
- `general.get_seller_info_detailed()`;
- `items.get_goods_prices_detailed()`;
- `finances.get_sales_report_details_detailed()`;
- `analytics.get_sales_funnel_products_detailed()`;
- `reports.create_paid_storage_report_detailed()`;
- `reports.get_paid_storage_report_status_detailed()`;
- `reports.download_paid_storage_report_detailed()`.

```python
response = await client.finances.get_sales_report_details_detailed(
    date_from="2026-03-17",
    date_to="2026-03-20",
)

print(response.status_code)
print(response.headers)
print(response.body)
```

## Ошибки

Для неуспешных HTTP-ответов поднимаются исключения из `wbapi.exceptions`. Исключение сохраняет
`status_code`, `headers` и исходное JSON-тело в `body`.

```python
from wbapi.exceptions import WBAuthenticationError

try:
    await client.general.ping()
except WBAuthenticationError as error:
    print(error.status_code, error.body)
```

## Лимиты и повторы

По умолчанию все клиенты внутри одного Python-процесса используют общий token bucket. Он учитывает
лимиты конкретного метода и заголовки `X-Ratelimit-*`. Ответы `429`, временные `5xx` и транспортные
ошибки методов, явно отмеченных как безопасные для повтора, повторяются с ограниченным числом
попыток.

In-memory limiter не координирует разные процессы. Для этого предусмотрен интерфейс
`wbapi.rate_limit.RateLimiter`, через который позднее можно подключить общий backend.

## Разработка

```bash
uv sync
uv lock --check
uv run pytest -q --cov=wbapi --cov-report=term-missing
uv run ruff check src tests
uv run ruff format --check src tests
uv run python -m compileall -q src
uv build --no-sources
uv run ty check
```


## OpenAPI SWAGGER files

- Сваггер файлы находится в этой папке: `/Users/dmitry/Documents/code/_DATA/wb_api_swagger_files/` 

- Чтобы обновить Swagger файл, запустить эту команду:
```bash
uv run --directory "/Users/dmitry/Documents/code/PYTHON_SCRIPTS_v2" \
  SCRIPTS/wb_api_swagger_files_downloading.py
```
