wbapi 0.0.1__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wbapi-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,366 @@
1
+ Metadata-Version: 2.3
2
+ Name: wbapi
3
+ Version: 0.2.1
4
+ Summary: Typed async client for the Wildberries seller API
5
+ Author: Dmitry Drozdov
6
+ Author-email: Dmitry Drozdov <ds.drozdov@gmail.com>
7
+ Requires-Dist: httpx2>=2.13.1,<3
8
+ Requires-Python: >=3.14
9
+ Description-Content-Type: text/markdown
10
+
11
+ # wbapi
12
+
13
+ Внутренний типизированный Python-клиент для официального API продавцов Wildberries.
14
+
15
+ Текущая версия реализует три read-only метода раздела «Общее» из
16
+ `specs/upstream/01-general.yaml`, получение детализации отчётов реализации из
17
+ `specs/upstream/13-finances.yaml`, статистику карточек товаров из
18
+ `specs/upstream/11-analytics.yaml`, получение товаров с ценами из `specs/upstream/02-items.yaml`
19
+ и отчёт о платном хранении из `specs/upstream/12-reports.yaml`.
20
+ Ответы возвращаются без преобразования в виде обычных `dict` и `list`. `TypedDict` используется
21
+ только для подсказок IDE и статической проверки.
22
+
23
+ ## Установка
24
+
25
+ ```bash
26
+ uv add --editable /path/to/wbapi
27
+ ```
28
+
29
+ Требуется Python 3.14 или новее.
30
+
31
+ Для HTTP-запросов используется `httpx2`. Если передаёте собственный клиент через
32
+ `AsyncWBClient(http_client=...)`, создавайте его как `httpx2.AsyncClient`.
33
+
34
+ ## Асинхронный клиент
35
+
36
+ ```python
37
+ import asyncio
38
+
39
+ from wbapi import AsyncWBClient
40
+
41
+
42
+ async def main() -> None:
43
+ async with AsyncWBClient("personal-token") as client:
44
+ result = await client.general.ping()
45
+ print(result)
46
+
47
+
48
+ asyncio.run(main())
49
+ ```
50
+
51
+ Все примеры ниже предполагают, что вызов выполняется внутри блока
52
+ `async with AsyncWBClient(...) as client`.
53
+
54
+ ## Поддерживаемые эндпоинты
55
+
56
+ ### Проверка подключения
57
+
58
+ `client.general.ping()` выполняет `GET https://common-api.wildberries.ru/ping`. Метод проверяет
59
+ доступность API и валидность токена, не принимает аргументов и возвращает исходный JSON-ответ WB.
60
+
61
+ ```python
62
+ result = await client.general.ping()
63
+
64
+ print(result.get("Status"))
65
+ print(result.get("TS"))
66
+ ```
67
+
68
+ Лимит: максимум 3 запроса за 30 секунд, отдельно для каждого домена API.
69
+
70
+ ### Новости портала продавцов
71
+
72
+ Для последовательного получения всех доступных новостей используйте `iter_news()`. Необходимо
73
+ передать ровно один из двух параметров:
74
+
75
+ - `from_date` — дата начала выборки как `datetime.date` или строка `YYYY-MM-DD`;
76
+ - `from_id` — неотрицательный ID новости, с которого нужно начать выборку.
77
+
78
+ Параметр `max_pages` ограничивает количество запросов:
79
+
80
+ ```python
81
+ from datetime import date
82
+
83
+ async for news in client.general.iter_news(
84
+ from_date=date(2026, 6, 1),
85
+ max_pages=10,
86
+ ):
87
+ print(news.get("header"))
88
+ ```
89
+
90
+ Низкоуровневый метод `get_news()` выполняет один запрос
91
+ `GET https://common-api.wildberries.ru/api/communications/v2/news` и возвращает одну страницу в
92
+ исходной структуре ответа WB. Используйте его, если требуется самостоятельно управлять курсором,
93
+ границами страниц или обработкой отдельных запросов:
94
+
95
+ ```python
96
+ page = await client.general.get_news(from_id=1)
97
+
98
+ for news in page.get("data", []):
99
+ print(news.get("id"), news.get("header"))
100
+ ```
101
+
102
+ Лимит для персонального токена: 1 запрос в минуту, всплеск до 10 запросов.
103
+
104
+ ### Товары с ценами
105
+
106
+ `client.items.get_goods_prices()` выполняет
107
+ `GET https://discounts-prices-api.wildberries.ru/api/v2/list/goods/filter` и возвращает одну
108
+ страницу товаров с ценами и скидками в исходной структуре WB. `limit` обязателен и не должен
109
+ превышать 1000, `offset` задаёт смещение, а `filter_nm_id` позволяет найти один артикул WB.
110
+
111
+ Для автоматической загрузки всех страниц используйте `iter_goods_prices()`:
112
+
113
+ ```python
114
+ async for item in client.items.iter_goods_prices(max_pages=10):
115
+ print(item.get("nmID"), item.get("currencyIsoCode4217"), item.get("sizes"))
116
+ ```
117
+
118
+ ```python
119
+ page = await client.items.get_goods_prices(
120
+ limit=1000,
121
+ offset=0,
122
+ )
123
+
124
+ for item in page.get("data", {}).get("listGoods", []):
125
+ print(item.get("nmID"), item.get("currencyIsoCode4217"), item.get("sizes"))
126
+ ```
127
+
128
+ Итератор увеличивает `offset` на `limit` после каждого запроса, останавливается на пустом
129
+ `listGoods` и предотвращает повтор артикулов WB. `max_pages` ограничивает количество страниц.
130
+ Лимит для персонального токена общий для категории «Цены и скидки»: всплеск до 5 запросов с
131
+ восполнением одного запроса каждые 600 мс.
132
+
133
+ ### Информация о продавце
134
+
135
+ `client.general.get_seller_info()` выполняет
136
+ `GET https://common-api.wildberries.ru/api/v1/seller-info`. Метод не принимает аргументов и
137
+ возвращает наименование, ID, ИНН и торговое имя продавца.
138
+
139
+ ```python
140
+ seller = await client.general.get_seller_info()
141
+
142
+ print(seller.get("name"))
143
+ print(seller.get("sid"))
144
+ print(seller.get("tin"))
145
+ print(seller.get("tradeMark"))
146
+ ```
147
+
148
+ Лимит для персонального токена: 1 запрос в минуту, всплеск до 10 запросов.
149
+
150
+ ### Детализация отчётов реализации
151
+
152
+ Для автоматической загрузки всех строк детализации используйте `iter_sales_report_details()`.
153
+ Обязательные аргументы:
154
+
155
+ - `date_from` — начало периода;
156
+ - `date_to` — конец периода.
157
+
158
+ Дата может быть строкой RFC 3339, `datetime.date` или `datetime.datetime`. Время WB интерпретирует
159
+ в часовом поясе Москвы UTC+3. Доступны необязательные аргументы:
160
+
161
+ - `limit` — количество строк, от 1 до 100 000;
162
+ - `rrd_id` — неотрицательный ID строки для продолжения загрузки, начальное значение — `0`;
163
+ - `period` — периодичность отчёта: `"daily"` или `"weekly"`;
164
+ - `fields` — список полей, которые нужно вернуть.
165
+
166
+ ```python
167
+ async for row in client.finances.iter_sales_report_details(
168
+ date_from="2026-03-17",
169
+ date_to="2026-03-20",
170
+ period="daily",
171
+ limit=1_000,
172
+ fields=["rrdId", "nmId", "retailAmount", "acquiringFee"],
173
+ max_pages=10,
174
+ ):
175
+ print(row.get("rrdId"), row.get("retailAmount"))
176
+ ```
177
+
178
+ Итератор загружает страницы до ответа HTTP 204, передаёт `rrdId` последней строки в следующий
179
+ запрос и предотвращает повтор строк и отсутствие продвижения курсора. Параметр `max_pages`
180
+ ограничивает количество страниц. Если итератор вызывается с `fields`, список обязан содержать
181
+ `rrdId`.
182
+
183
+ Низкоуровневый метод `get_sales_report_details()` выполняет один запрос
184
+ `POST https://finance-api.wildberries.ru/api/finance/v1/sales-reports/detailed` и возвращает одну
185
+ страницу в исходной структуре ответа WB. Используйте его, если требуется самостоятельно управлять
186
+ `rrd_id`, границами страниц или обработкой HTTP 204:
187
+
188
+ ```python
189
+ rows = await client.finances.get_sales_report_details(
190
+ date_from="2026-03-17",
191
+ date_to="2026-03-20",
192
+ period="daily",
193
+ limit=1_000,
194
+ rrd_id=0,
195
+ fields=["rrdId", "nmId", "retailAmount"],
196
+ )
197
+
198
+ if rows is None: # HTTP 204 — данных нет
199
+ print("Данных нет")
200
+ else:
201
+ for row in rows:
202
+ print(row.get("rrdId"), row.get("retailAmount"))
203
+ ```
204
+
205
+ Лимит для персонального токена: 1 запрос в минуту. Встроенный ограничитель автоматически ожидает
206
+ освобождения лимита перед загрузкой следующей страницы.
207
+
208
+ ### Воронка продаж по карточкам товаров
209
+
210
+ Для последовательной загрузки всех карточек используйте `iter_sales_funnel_products()`:
211
+
212
+ ```python
213
+ async for item in client.analytics.iter_sales_funnel_products(
214
+ date_from="2026-06-01",
215
+ date_to="2026-06-30",
216
+ past_date_from="2026-05-01",
217
+ past_date_to="2026-05-30",
218
+ nm_ids=[], # Пустой список означает все карточки продавца
219
+ order_by={"field": "orderSum", "mode": "desc"},
220
+ limit=1000,
221
+ max_pages=10,
222
+ ):
223
+ product = item.get("product", {})
224
+ statistic = item.get("statistic", {}).get("selected", {})
225
+ print(product.get("nmId"), statistic.get("orderSum"))
226
+ ```
227
+
228
+ Дополнительно поддерживаются фильтры `brand_names`, `subject_ids`, `tag_ids` и
229
+ `skip_deleted_nm`. Период — максимум последние 365 дней. Параметр `limit` должен быть от 1 до
230
+ 1000, `offset` задаёт начальное смещение, `max_pages` ограничивает количество страниц. Итератор
231
+ увеличивает offset на фактическое число карточек и предотвращает повтор артикулов WB.
232
+
233
+ Низкоуровневый `get_sales_funnel_products()` выполняет один запрос и сохраняет исходную структуру,
234
+ включая общую валюту отчёта:
235
+
236
+ ```python
237
+ page = await client.analytics.get_sales_funnel_products(
238
+ date_from="2026-06-01",
239
+ date_to="2026-06-30",
240
+ limit=100,
241
+ offset=0,
242
+ )
243
+
244
+ print(page.get("data", {}).get("currency"))
245
+ ```
246
+
247
+ Метод выполняет `POST https://seller-analytics-api.wildberries.ru/api/analytics/v3/sales-funnel/products`.
248
+ Лимит для персонального токена: всплеск до 3 запросов с восполнением одного запроса раз в 20 секунд.
249
+
250
+ ### Отчёт о платном хранении
251
+
252
+ `get_paid_storage_report()` выполняет весь сценарий: создаёт задание, проверяет его статус до
253
+ готовности и скачивает результат. Период может охватывать максимум 8 дней. Даты принимаются как
254
+ строки RFC 3339, `datetime.date` или `datetime.datetime`.
255
+
256
+ ```python
257
+ rows = await client.reports.get_paid_storage_report(
258
+ date_from="2026-07-01",
259
+ date_to="2026-07-08",
260
+ max_status_checks=120,
261
+ )
262
+
263
+ if rows is None: # HTTP 204 — данных нет
264
+ print("Данных нет")
265
+ else:
266
+ for row in rows:
267
+ print(row.get("date"), row.get("nmId"), row.get("warehousePrice"))
268
+ ```
269
+
270
+ По умолчанию статус проверяется раз в 5 секунд, максимум 120 раз. Интервал и предел можно изменить
271
+ параметрами `poll_interval` и `max_status_checks`. Статусы `canceled` и `purged` приводят к
272
+ `WBReportTaskFailedError`, исчерпание проверок — к `WBReportTaskTimeoutError`.
273
+
274
+ Для ручного управления этапами доступны низкоуровневые методы:
275
+
276
+ ```python
277
+ task = await client.reports.create_paid_storage_report(
278
+ date_from="2026-07-01",
279
+ date_to="2026-07-08",
280
+ )
281
+ task_id = task["data"]["taskId"]
282
+
283
+ status = await client.reports.get_paid_storage_report_status(task_id)
284
+ if status.get("data", {}).get("status") == "done":
285
+ rows = await client.reports.download_paid_storage_report(task_id)
286
+ ```
287
+
288
+ Создание задания ограничено всплеском до 5 запросов с восполнением раз в минуту, проверка статуса —
289
+ до 5 запросов с восполнением раз в 5 секунд, скачивание — 1 запрос в минуту. Создание задания не
290
+ повторяется автоматически после транспортной ошибки или `5xx`, поскольку операция создаёт новый
291
+ ресурс и спецификация не гарантирует её идемпотентность.
292
+
293
+ ## HTTP-метаданные
294
+
295
+ Обычный метод возвращает JSON-тело. Вариант с суффиксом `_detailed` дополнительно возвращает
296
+ `WBResponse` с HTTP-статусом, заголовками и исходным телом. Варианты доступны для каждого
297
+ низкоуровневого метода:
298
+
299
+ - `general.ping_detailed()`;
300
+ - `general.get_news_detailed()`;
301
+ - `general.get_seller_info_detailed()`;
302
+ - `items.get_goods_prices_detailed()`;
303
+ - `finances.get_sales_report_details_detailed()`;
304
+ - `analytics.get_sales_funnel_products_detailed()`;
305
+ - `reports.create_paid_storage_report_detailed()`;
306
+ - `reports.get_paid_storage_report_status_detailed()`;
307
+ - `reports.download_paid_storage_report_detailed()`.
308
+
309
+ ```python
310
+ response = await client.finances.get_sales_report_details_detailed(
311
+ date_from="2026-03-17",
312
+ date_to="2026-03-20",
313
+ )
314
+
315
+ print(response.status_code)
316
+ print(response.headers)
317
+ print(response.body)
318
+ ```
319
+
320
+ ## Ошибки
321
+
322
+ Для неуспешных HTTP-ответов поднимаются исключения из `wbapi.exceptions`. Исключение сохраняет
323
+ `status_code`, `headers` и исходное JSON-тело в `body`.
324
+
325
+ ```python
326
+ from wbapi.exceptions import WBAuthenticationError
327
+
328
+ try:
329
+ await client.general.ping()
330
+ except WBAuthenticationError as error:
331
+ print(error.status_code, error.body)
332
+ ```
333
+
334
+ ## Лимиты и повторы
335
+
336
+ По умолчанию все клиенты внутри одного Python-процесса используют общий token bucket. Он учитывает
337
+ лимиты конкретного метода и заголовки `X-Ratelimit-*`. Ответы `429`, временные `5xx` и транспортные
338
+ ошибки методов, явно отмеченных как безопасные для повтора, повторяются с ограниченным числом
339
+ попыток.
340
+
341
+ In-memory limiter не координирует разные процессы. Для этого предусмотрен интерфейс
342
+ `wbapi.rate_limit.RateLimiter`, через который позднее можно подключить общий backend.
343
+
344
+ ## Разработка
345
+
346
+ ```bash
347
+ uv sync
348
+ uv lock --check
349
+ uv run pytest -q --cov=wbapi --cov-report=term-missing
350
+ uv run ruff check src tests
351
+ uv run ruff format --check src tests
352
+ uv run python -m compileall -q src
353
+ uv build --no-sources
354
+ uv run ty check
355
+ ```
356
+
357
+
358
+ ## OpenAPI SWAGGER files
359
+
360
+ - Сваггер файлы находится в этой папке: `/Users/dmitry/Documents/code/_DATA/wb_api_swagger_files/`
361
+
362
+ - Чтобы обновить Swagger файл, запустить эту команду:
363
+ ```bash
364
+ uv run --directory "/Users/dmitry/Documents/code/PYTHON_SCRIPTS_v2" \
365
+ SCRIPTS/wb_api_swagger_files_downloading.py
366
+ ```