wbapi-async 0.7.4__tar.gz → 0.7.5__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_async-0.7.4 → wbapi_async-0.7.5}/PKG-INFO +1 -1
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/pyproject.toml +1 -1
- wbapi_async-0.7.5/src/wbapi_async/_method.py +178 -0
- wbapi_async-0.7.4/src/wbapi_async/method.py → wbapi_async-0.7.5/src/wbapi_async/_registry.py +7 -168
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/src/wbapi_async/api.py +4 -20
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/src/wbapi_async/type.py +11 -1
- wbapi_async-0.7.5/src/wbapi_async/utils/paginate.py +155 -0
- wbapi_async-0.7.4/src/wbapi_async/utils/paginate.py +0 -114
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/.gitignore +0 -0
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/LICENSE +0 -0
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/README.md +0 -0
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/src/wbapi_async/__init__.py +0 -0
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/src/wbapi_async/client/__init__.py +0 -0
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/src/wbapi_async/client/session/__init__.py +0 -0
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/src/wbapi_async/client/session/base.py +0 -0
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/src/wbapi_async/client/session/headers.py +0 -0
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/src/wbapi_async/exceptions.py +0 -0
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/src/wbapi_async/py.typed +0 -0
- {wbapi_async-0.7.4 → wbapi_async-0.7.5}/src/wbapi_async/utils/__init__.py +0 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from aiolimiter import AsyncLimiter
|
|
6
|
+
|
|
7
|
+
from ._registry import _BASES, _LIMITS, _PAGE_SIZES, _PUBLIC
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .client.session.base import BaseSession
|
|
12
|
+
|
|
13
|
+
_DEFAULT_LIMIT: tuple[int, int, int, int] = (60000, 60, 1000, 5)
|
|
14
|
+
|
|
15
|
+
_limiters: dict[tuple[int, int], AsyncLimiter] = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_limiter(path: str) -> AsyncLimiter:
|
|
19
|
+
period, limit, interval, burst = _LIMITS.get(path, _DEFAULT_LIMIT)
|
|
20
|
+
key = (interval, burst)
|
|
21
|
+
if key not in _limiters:
|
|
22
|
+
_limiters[key] = AsyncLimiter(max_rate=burst, time_period=interval / 1000)
|
|
23
|
+
return _limiters[key]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def resolve_url(path: str) -> str:
|
|
27
|
+
"""Resolve full URL for a spec path like /api/v3/supplies.
|
|
28
|
+
|
|
29
|
+
If a full URL is passed, it is returned as-is provided its host is a known
|
|
30
|
+
wildberries.ru domain or is listed in ``_PUBLIC``.
|
|
31
|
+
"""
|
|
32
|
+
from urllib.parse import urlparse
|
|
33
|
+
|
|
34
|
+
from .exceptions import WbAPIError
|
|
35
|
+
|
|
36
|
+
if path.startswith("https://") or path.startswith("http://"):
|
|
37
|
+
host = urlparse(path).netloc
|
|
38
|
+
known_hosts = {urlparse(v).netloc for v in _BASES.values()} | _PUBLIC
|
|
39
|
+
if host not in known_hosts:
|
|
40
|
+
raise WbAPIError(detail=f"Unknown host {host!r}.")
|
|
41
|
+
return path
|
|
42
|
+
|
|
43
|
+
base = _BASES.get(path)
|
|
44
|
+
if base:
|
|
45
|
+
return base + path
|
|
46
|
+
# Path may contain dynamic segments (e.g. /api/v3/supplies/WB-123/orders).
|
|
47
|
+
# Walk up the path until we find a match.
|
|
48
|
+
parts = path.rstrip("/").split("/")
|
|
49
|
+
for i in range(len(parts) - 1, 0, -1):
|
|
50
|
+
candidate = "/".join(parts[:i])
|
|
51
|
+
if candidate in _BASES:
|
|
52
|
+
return _BASES[candidate] + path
|
|
53
|
+
raise WbAPIError(detail=f"Unknown path {path!r}. Check available paths at https://dev.wildberries.ru")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _extract_list(raw: Any) -> list[Any] | None:
|
|
57
|
+
if isinstance(raw, list):
|
|
58
|
+
return raw
|
|
59
|
+
if isinstance(raw, dict):
|
|
60
|
+
for v in raw.values():
|
|
61
|
+
if isinstance(v, list):
|
|
62
|
+
return v
|
|
63
|
+
for v in raw.values():
|
|
64
|
+
if isinstance(v, dict):
|
|
65
|
+
found = _extract_list(v)
|
|
66
|
+
if found is not None:
|
|
67
|
+
return found
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MethodDispatcher:
|
|
72
|
+
def __init__(self, session: BaseSession, token: str) -> None:
|
|
73
|
+
self._session = session
|
|
74
|
+
self._token = token
|
|
75
|
+
|
|
76
|
+
async def dispatch(
|
|
77
|
+
self,
|
|
78
|
+
http_method: str,
|
|
79
|
+
path: str,
|
|
80
|
+
*,
|
|
81
|
+
params: dict[str, Any] | None = None,
|
|
82
|
+
json: Any | None = None,
|
|
83
|
+
no_auth: bool = False,
|
|
84
|
+
) -> Any:
|
|
85
|
+
import re
|
|
86
|
+
from urllib.parse import urlparse
|
|
87
|
+
|
|
88
|
+
placeholders = re.findall(r"\{(\w+)\}", path)
|
|
89
|
+
if placeholders and params:
|
|
90
|
+
params = dict(params)
|
|
91
|
+
for key in placeholders:
|
|
92
|
+
if key in params:
|
|
93
|
+
path = path.replace(f"{{{key}}}", str(params.pop(key)))
|
|
94
|
+
params = params or None
|
|
95
|
+
|
|
96
|
+
if not no_auth and path.startswith(("https://", "http://")):
|
|
97
|
+
no_auth = urlparse(path).netloc in _PUBLIC
|
|
98
|
+
|
|
99
|
+
if no_auth:
|
|
100
|
+
self._session.headers.authorization = None
|
|
101
|
+
else:
|
|
102
|
+
self._session.headers.set_token(self._token)
|
|
103
|
+
url = resolve_url(path)
|
|
104
|
+
limiter = _get_limiter(path)
|
|
105
|
+
return await self._session._request(http_method, url, params=params, json=json, limit=limiter)
|
|
106
|
+
|
|
107
|
+
async def fetch_all(self, path: str, **kwargs: Any) -> list[Any]:
|
|
108
|
+
"""Fetch all pages. Accepts an optional ``paginator`` kwarg —
|
|
109
|
+
a callable ``(response) -> (items, next_params | None)`` for custom pagination logic.
|
|
110
|
+
|
|
111
|
+
Example::
|
|
112
|
+
|
|
113
|
+
def my_paginator(raw):
|
|
114
|
+
items = raw.get("result", [])
|
|
115
|
+
cursor = raw.get("cursor") or None
|
|
116
|
+
next_params = {"cursor": cursor} if cursor else None
|
|
117
|
+
return items, next_params
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
all_items = await api.get_all(
|
|
121
|
+
"/api/v3/custom", paginator=my_paginator
|
|
122
|
+
)
|
|
123
|
+
"""
|
|
124
|
+
from .exceptions import PaginationNotSupported
|
|
125
|
+
|
|
126
|
+
paginator = kwargs.pop("paginator", None)
|
|
127
|
+
body = kwargs.pop("body", None)
|
|
128
|
+
http_method = "POST" if body is not None else "GET"
|
|
129
|
+
|
|
130
|
+
def _do_request(
|
|
131
|
+
extra: dict[str, Any] | None = None,
|
|
132
|
+
extra_body: dict[str, Any] | None = None,
|
|
133
|
+
full_body: dict[str, Any] | None = None,
|
|
134
|
+
) -> Any:
|
|
135
|
+
if full_body is not None:
|
|
136
|
+
return self.dispatch(http_method, path, params=kwargs or None, json=full_body)
|
|
137
|
+
if body is not None:
|
|
138
|
+
json = {**body, **(extra or {}), **(extra_body or {})}
|
|
139
|
+
return self.dispatch(http_method, path, params=kwargs or None, json=json)
|
|
140
|
+
params = {**kwargs, **(extra or {})} or None
|
|
141
|
+
return self.dispatch(http_method, path, params=params)
|
|
142
|
+
|
|
143
|
+
if paginator is not None:
|
|
144
|
+
result: list[Any] = []
|
|
145
|
+
next_extra: dict[str, Any] = {}
|
|
146
|
+
while True:
|
|
147
|
+
if body is not None:
|
|
148
|
+
json = next_extra if next_extra else body
|
|
149
|
+
raw = await self.dispatch(http_method, path, params=kwargs or None, json=json)
|
|
150
|
+
else:
|
|
151
|
+
raw = await _do_request(next_extra or None)
|
|
152
|
+
items, next_extra = paginator(raw)
|
|
153
|
+
result.extend(items)
|
|
154
|
+
if not next_extra:
|
|
155
|
+
break
|
|
156
|
+
return result
|
|
157
|
+
|
|
158
|
+
from .utils.paginate import PAGINATION_STRATEGIES
|
|
159
|
+
|
|
160
|
+
page_size = _PAGE_SIZES.get(path, 1000)
|
|
161
|
+
first_extra: dict[str, Any] = {"limit": page_size}
|
|
162
|
+
if body is not None:
|
|
163
|
+
first_extra["offset"] = 0
|
|
164
|
+
raw = await _do_request(first_extra)
|
|
165
|
+
page = _extract_list(raw)
|
|
166
|
+
|
|
167
|
+
if page is None:
|
|
168
|
+
raise PaginationNotSupported(f"No list data found in response for {path!r}")
|
|
169
|
+
|
|
170
|
+
result: list[Any] = list(page)
|
|
171
|
+
|
|
172
|
+
for strategy in PAGINATION_STRATEGIES:
|
|
173
|
+
if strategy.detect(raw, page, body, page_size, path):
|
|
174
|
+
return await strategy.paginate(result, page, raw, _do_request, page_size, body)
|
|
175
|
+
|
|
176
|
+
if not result:
|
|
177
|
+
raise PaginationNotSupported(f"{path!r} returned empty first page — pagination not supported")
|
|
178
|
+
return result
|
wbapi_async-0.7.4/src/wbapi_async/method.py → wbapi_async-0.7.5/src/wbapi_async/_registry.py
RENAMED
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Any
|
|
4
|
-
|
|
5
|
-
from aiolimiter import AsyncLimiter
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from .client.session.base import BaseSession
|
|
10
|
-
|
|
11
1
|
# fmt: off
|
|
12
2
|
# AUTO-GENERATED — DO NOT EDIT MANUALLY!!!
|
|
13
3
|
|
|
14
|
-
|
|
4
|
+
_BASES: dict[str, str] = {
|
|
15
5
|
"/adv/v0/auction/nms": "https://advert-api.wildberries.ru",
|
|
16
6
|
"/adv/v0/auction/placements": "https://advert-api.wildberries.ru",
|
|
17
7
|
"/adv/v0/delete": "https://advert-api.wildberries.ru",
|
|
@@ -242,7 +232,7 @@ _PATH_TO_BASE: dict[str, str] = {
|
|
|
242
232
|
"/ping": "https://common-api.wildberries.ru",
|
|
243
233
|
}
|
|
244
234
|
|
|
245
|
-
|
|
235
|
+
_LIMITS: dict[str, tuple[int, int, int, int]] = {
|
|
246
236
|
"/adv/v0/auction/nms": (1000, 1, 1000, 1),
|
|
247
237
|
"/adv/v0/auction/placements": (1000, 1, 1000, 1),
|
|
248
238
|
"/adv/v0/delete": (1000, 5, 200, 5),
|
|
@@ -462,160 +452,9 @@ _PATH_TO_LIMIT: dict[str, tuple[int, int, int, int]] = {
|
|
|
462
452
|
}
|
|
463
453
|
# fmt: on
|
|
464
454
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
# Shared AsyncLimiter instances keyed by (interval_ms, burst).
|
|
468
|
-
_limiters: dict[tuple[int, int], AsyncLimiter] = {}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
def _get_limiter(path: str) -> AsyncLimiter:
|
|
472
|
-
period_ms, limit, interval_ms, burst = _PATH_TO_LIMIT.get(path, _DEFAULT_LIMIT)
|
|
473
|
-
key = (interval_ms, burst)
|
|
474
|
-
if key not in _limiters:
|
|
475
|
-
_limiters[key] = AsyncLimiter(max_rate=burst, time_period=interval_ms / 1000)
|
|
476
|
-
return _limiters[key]
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
_EXTRA_ALLOWED_HOSTS: frozenset[str] = frozenset(
|
|
480
|
-
{
|
|
481
|
-
"card.wb.ru",
|
|
482
|
-
}
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
def resolve_url(path: str) -> str:
|
|
487
|
-
"""Resolve full URL for a spec path like /api/v3/supplies.
|
|
488
|
-
|
|
489
|
-
If a full URL is passed, it is returned as-is provided its host is a known
|
|
490
|
-
wildberries.ru domain or is listed in ``_EXTRA_ALLOWED_HOSTS``.
|
|
491
|
-
"""
|
|
492
|
-
from urllib.parse import urlparse
|
|
493
|
-
|
|
494
|
-
from .exceptions import WbAPIError
|
|
495
|
-
|
|
496
|
-
if path.startswith("https://") or path.startswith("http://"):
|
|
497
|
-
host = urlparse(path).netloc
|
|
498
|
-
known_hosts = {urlparse(v).netloc for v in _PATH_TO_BASE.values()} | _EXTRA_ALLOWED_HOSTS
|
|
499
|
-
if host not in known_hosts:
|
|
500
|
-
raise WbAPIError(detail=f"Unknown host {host!r}.")
|
|
501
|
-
return path
|
|
502
|
-
|
|
503
|
-
base = _PATH_TO_BASE.get(path)
|
|
504
|
-
if base:
|
|
505
|
-
return base + path
|
|
506
|
-
# Path may contain dynamic segments (e.g. /api/v3/supplies/WB-123/orders).
|
|
507
|
-
# Walk up the path until we find a match.
|
|
508
|
-
parts = path.rstrip("/").split("/")
|
|
509
|
-
for i in range(len(parts) - 1, 0, -1):
|
|
510
|
-
candidate = "/".join(parts[:i])
|
|
511
|
-
if candidate in _PATH_TO_BASE:
|
|
512
|
-
return _PATH_TO_BASE[candidate] + path
|
|
513
|
-
raise WbAPIError(detail=f"Unknown path {path!r}. Check available paths at https://dev.wildberries.ru")
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
_PAGE_SIZE = 1000
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
def _extract_list(raw: Any) -> list[Any] | None:
|
|
520
|
-
if isinstance(raw, list):
|
|
521
|
-
return raw
|
|
522
|
-
if isinstance(raw, dict):
|
|
523
|
-
for v in raw.values():
|
|
524
|
-
if isinstance(v, list):
|
|
525
|
-
return v
|
|
526
|
-
for v in raw.values():
|
|
527
|
-
if isinstance(v, dict):
|
|
528
|
-
found = _extract_list(v)
|
|
529
|
-
if found is not None:
|
|
530
|
-
return found
|
|
531
|
-
return None
|
|
455
|
+
_PUBLIC: frozenset[str] = frozenset({"card.wb.ru"})
|
|
532
456
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
self._token = token
|
|
538
|
-
|
|
539
|
-
async def dispatch(
|
|
540
|
-
self,
|
|
541
|
-
http_method: str,
|
|
542
|
-
path: str,
|
|
543
|
-
*,
|
|
544
|
-
params: dict[str, Any] | None = None,
|
|
545
|
-
json: Any | None = None,
|
|
546
|
-
no_auth: bool = False,
|
|
547
|
-
) -> Any:
|
|
548
|
-
from urllib.parse import urlparse
|
|
549
|
-
|
|
550
|
-
if not no_auth and path.startswith(("https://", "http://")):
|
|
551
|
-
no_auth = urlparse(path).netloc in _EXTRA_ALLOWED_HOSTS
|
|
552
|
-
|
|
553
|
-
if no_auth:
|
|
554
|
-
self._session.headers.authorization = None
|
|
555
|
-
else:
|
|
556
|
-
self._session.headers.set_token(self._token)
|
|
557
|
-
url = resolve_url(path)
|
|
558
|
-
limiter = _get_limiter(path)
|
|
559
|
-
return await self._session._request(http_method, url, params=params, json=json, limit=limiter)
|
|
560
|
-
|
|
561
|
-
async def fetch_all(self, path: str, **kwargs: Any) -> list[Any]:
|
|
562
|
-
"""Fetch all pages. Accepts an optional ``paginator`` kwarg —
|
|
563
|
-
a callable ``(response) -> (items, next_params | None)`` for custom pagination logic.
|
|
564
|
-
|
|
565
|
-
Example::
|
|
566
|
-
|
|
567
|
-
def my_paginator(raw):
|
|
568
|
-
items = raw.get("result", [])
|
|
569
|
-
cursor = raw.get("cursor") or None
|
|
570
|
-
next_params = {"cursor": cursor} if cursor else None
|
|
571
|
-
return items, next_params
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
all_items = await api.get_all(
|
|
575
|
-
"/api/v3/custom", paginator=my_paginator
|
|
576
|
-
)
|
|
577
|
-
"""
|
|
578
|
-
from .exceptions import PaginationNotSupported
|
|
579
|
-
|
|
580
|
-
paginator = kwargs.pop("paginator", None)
|
|
581
|
-
body = kwargs.pop("body", None)
|
|
582
|
-
http_method = "POST" if body is not None else "GET"
|
|
583
|
-
|
|
584
|
-
def _do_request(extra: dict[str, Any] | None = None, extra_body: dict[str, Any] | None = None) -> Any:
|
|
585
|
-
if body is not None:
|
|
586
|
-
json = {**body, **(extra or {}), **(extra_body or {})}
|
|
587
|
-
return self.dispatch(http_method, path, params=kwargs or None, json=json)
|
|
588
|
-
params = {**kwargs, **(extra or {})} or None
|
|
589
|
-
return self.dispatch(http_method, path, params=params)
|
|
590
|
-
|
|
591
|
-
if paginator is not None:
|
|
592
|
-
result: list[Any] = []
|
|
593
|
-
next_params: dict[str, Any] = {}
|
|
594
|
-
while True:
|
|
595
|
-
raw = await _do_request(next_params)
|
|
596
|
-
items, next_params = paginator(raw)
|
|
597
|
-
result.extend(items)
|
|
598
|
-
if not next_params:
|
|
599
|
-
break
|
|
600
|
-
return result
|
|
601
|
-
|
|
602
|
-
from .utils.paginate import PAGINATION_STRATEGIES
|
|
603
|
-
|
|
604
|
-
first_extra: dict[str, Any] = {"limit": _PAGE_SIZE}
|
|
605
|
-
if body is not None:
|
|
606
|
-
first_extra["offset"] = 0
|
|
607
|
-
raw = await _do_request(first_extra)
|
|
608
|
-
page = _extract_list(raw)
|
|
609
|
-
|
|
610
|
-
if page is None:
|
|
611
|
-
raise PaginationNotSupported(f"No list data found in response for {path!r}")
|
|
612
|
-
|
|
613
|
-
result: list[Any] = list(page)
|
|
614
|
-
|
|
615
|
-
for strategy in PAGINATION_STRATEGIES:
|
|
616
|
-
if strategy.detect(raw, page, body):
|
|
617
|
-
return await strategy.paginate(result, page, raw, _do_request)
|
|
618
|
-
|
|
619
|
-
if not result:
|
|
620
|
-
raise PaginationNotSupported(f"{path!r} returned empty first page — pagination not supported")
|
|
621
|
-
return result
|
|
457
|
+
_PAGE_SIZES: dict[str, int] = {
|
|
458
|
+
"/content/v2/get/cards/list": 100,
|
|
459
|
+
"/api/v1/documents/list": 50,
|
|
460
|
+
}
|
|
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
+
from ._method import MethodDispatcher
|
|
5
6
|
from .client.session.base import BaseSession
|
|
6
|
-
from .method import MethodDispatcher
|
|
7
7
|
from .type import ApiResponse, _wrap
|
|
8
8
|
|
|
9
9
|
|
|
@@ -11,23 +11,7 @@ class WbAPI:
|
|
|
11
11
|
"""
|
|
12
12
|
Async client for the Wildberries Seller API.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
Swagger specs (YAML): https://dev.wildberries.ru/api/swagger/yaml/en/
|
|
16
|
-
|
|
17
|
-
Swagger specs (YAML) — use these to look up paths, parameters, and response schemas:
|
|
18
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/01-general.yaml — warehouses, ping
|
|
19
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/02-products.yaml — cards, stocks, prices, tags
|
|
20
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/03-orders-fbs.yaml — orders, supplies, shipments (FBS)
|
|
21
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/04-orders-dbw.yaml — orders DBW
|
|
22
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/05-orders-dbs.yaml — orders DBS
|
|
23
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/06-in-store-pickup.yaml — in-store pickup
|
|
24
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/07-orders-fbw.yaml — orders FBW
|
|
25
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/08-promotion.yaml — ad campaigns, budgets, bids
|
|
26
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/09-communications.yaml — questions, reviews, news
|
|
27
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/10-tariffs.yaml — tariffs
|
|
28
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/11-analytics.yaml — sales funnel, turnover, stocks
|
|
29
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/12-reports.yaml — detailed sales report
|
|
30
|
-
- https://dev.wildberries.ru/api/swagger/yaml/en/13-finances.yaml — payments, penalties
|
|
14
|
+
Source: https://dev.wildberries.ru/en/docs/openapi/api-information
|
|
31
15
|
"""
|
|
32
16
|
|
|
33
17
|
def __init__(
|
|
@@ -50,8 +34,8 @@ class WbAPI:
|
|
|
50
34
|
orders = await api.get(
|
|
51
35
|
"/api/v3/orders/new", limit=10, next=0
|
|
52
36
|
)
|
|
53
|
-
|
|
54
|
-
"/api/
|
|
37
|
+
meta = await api.get(
|
|
38
|
+
"/api/v3/orders/{orderId}/meta", orderId=123
|
|
55
39
|
)
|
|
56
40
|
"""
|
|
57
41
|
return _wrap(await self._dispatcher.dispatch("GET", path, params=kwargs or None))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
5
6
|
|
|
@@ -17,7 +18,8 @@ class ApiResponse:
|
|
|
17
18
|
try:
|
|
18
19
|
val = self._data[name]
|
|
19
20
|
except (KeyError, TypeError):
|
|
20
|
-
|
|
21
|
+
available = list(self._data) if isinstance(self._data, dict) else type(self._data).__name__
|
|
22
|
+
raise AttributeError(f"{name!r} not found. Got: {available}") from None
|
|
21
23
|
return _wrap(val)
|
|
22
24
|
|
|
23
25
|
def __getitem__(self, key: str | int) -> Any:
|
|
@@ -39,3 +41,11 @@ class ApiResponse:
|
|
|
39
41
|
|
|
40
42
|
def unwrap(self) -> Any:
|
|
41
43
|
return self._data
|
|
44
|
+
|
|
45
|
+
def to_snake(self) -> dict[str, Any]:
|
|
46
|
+
def convert(key: str) -> str:
|
|
47
|
+
return re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", key).lower()
|
|
48
|
+
|
|
49
|
+
if isinstance(self._data, dict):
|
|
50
|
+
return {convert(k): v for k, v in self._data.items()}
|
|
51
|
+
return self._data
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_Requester = Callable[..., Any]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PaginationStrategy:
|
|
11
|
+
"""Base class for pagination strategies used by fetch_all."""
|
|
12
|
+
|
|
13
|
+
def detect(self, raw: Any, page: list[Any], body: Any, page_size: int, path: str) -> bool:
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
async def paginate(
|
|
17
|
+
self, result: list[Any], page: list[Any], raw: Any, request: _Requester, page_size: int, body: Any
|
|
18
|
+
) -> list[Any]:
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RrdIdCursorStrategy(PaginationStrategy):
|
|
23
|
+
def detect(self, raw: Any, page: list[Any], body: Any, page_size: int, path: str) -> bool:
|
|
24
|
+
return bool(page and isinstance(page[-1], dict) and "rrd_id" in page[-1])
|
|
25
|
+
|
|
26
|
+
async def paginate(
|
|
27
|
+
self, result: list[Any], page: list[Any], raw: Any, request: _Requester, page_size: int, body: Any
|
|
28
|
+
) -> list[Any]:
|
|
29
|
+
from .._method import _extract_list
|
|
30
|
+
|
|
31
|
+
while page:
|
|
32
|
+
rrd_id = page[-1]["rrd_id"]
|
|
33
|
+
page = _extract_list(await request({"rrdid": rrd_id})) or []
|
|
34
|
+
result.extend(page)
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LastChangeDateStrategy(PaginationStrategy):
|
|
39
|
+
def detect(self, raw: Any, page: list[Any], body: Any, page_size: int, path: str) -> bool:
|
|
40
|
+
return bool(page and isinstance(page[-1], dict) and "lastChangeDate" in page[-1])
|
|
41
|
+
|
|
42
|
+
async def paginate(
|
|
43
|
+
self, result: list[Any], page: list[Any], raw: Any, request: _Requester, page_size: int, body: Any
|
|
44
|
+
) -> list[Any]:
|
|
45
|
+
from .._method import _extract_list
|
|
46
|
+
|
|
47
|
+
while page:
|
|
48
|
+
date_from = page[-1]["lastChangeDate"]
|
|
49
|
+
page = _extract_list(await request({"dateFrom": date_from})) or []
|
|
50
|
+
result.extend(page)
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BodyCursorStrategy(PaginationStrategy):
|
|
55
|
+
def detect(self, raw: Any, page: list[Any], body: Any, page_size: int, path: str) -> bool:
|
|
56
|
+
return body is not None and isinstance(raw, dict) and bool(raw.get("cursor"))
|
|
57
|
+
|
|
58
|
+
async def paginate(
|
|
59
|
+
self, result: list[Any], page: list[Any], raw: Any, request: _Requester, page_size: int, body: Any
|
|
60
|
+
) -> list[Any]:
|
|
61
|
+
from .._method import _extract_list
|
|
62
|
+
|
|
63
|
+
cursor_val = raw.get("cursor")
|
|
64
|
+
while cursor_val:
|
|
65
|
+
raw = await request(extra_body={"cursor": cursor_val})
|
|
66
|
+
page = _extract_list(raw)
|
|
67
|
+
if not page:
|
|
68
|
+
break
|
|
69
|
+
result.extend(page)
|
|
70
|
+
cursor_val = raw.get("cursor") if isinstance(raw, dict) else None
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class NextCursorStrategy(PaginationStrategy):
|
|
75
|
+
def detect(self, raw: Any, page: list[Any], body: Any, page_size: int, path: str) -> bool:
|
|
76
|
+
return isinstance(raw, dict) and "next" in raw
|
|
77
|
+
|
|
78
|
+
async def paginate(
|
|
79
|
+
self, result: list[Any], page: list[Any], raw: Any, request: _Requester, page_size: int, body: Any
|
|
80
|
+
) -> list[Any]:
|
|
81
|
+
from .._method import _extract_list
|
|
82
|
+
|
|
83
|
+
cursor = raw["next"]
|
|
84
|
+
while cursor:
|
|
85
|
+
raw = await request({"limit": page_size, "next": cursor})
|
|
86
|
+
page = _extract_list(raw)
|
|
87
|
+
if not page:
|
|
88
|
+
break
|
|
89
|
+
result.extend(page)
|
|
90
|
+
cursor = raw.get("next") if isinstance(raw, dict) else None
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class OffsetStrategy(PaginationStrategy):
|
|
95
|
+
def detect(self, raw: Any, page: list[Any], body: Any, page_size: int, path: str) -> bool:
|
|
96
|
+
return len(page) >= page_size
|
|
97
|
+
|
|
98
|
+
async def paginate(
|
|
99
|
+
self, result: list[Any], page: list[Any], raw: Any, request: _Requester, page_size: int, body: Any
|
|
100
|
+
) -> list[Any]:
|
|
101
|
+
from .._method import _extract_list
|
|
102
|
+
|
|
103
|
+
offset = page_size
|
|
104
|
+
while True:
|
|
105
|
+
raw = await request({"limit": page_size, "offset": offset})
|
|
106
|
+
page = _extract_list(raw)
|
|
107
|
+
if not page:
|
|
108
|
+
break
|
|
109
|
+
result.extend(page)
|
|
110
|
+
if len(page) < page_size:
|
|
111
|
+
break
|
|
112
|
+
offset += page_size
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class CardsListStrategy(PaginationStrategy):
|
|
117
|
+
path = "/content/v2/get/cards/list"
|
|
118
|
+
|
|
119
|
+
def detect(self, raw: Any, page: list[Any], body: Any, page_size: int, path: str) -> bool:
|
|
120
|
+
if path != self.path:
|
|
121
|
+
return False
|
|
122
|
+
cursor = raw.get("cursor", {}) if isinstance(raw, dict) else {}
|
|
123
|
+
return isinstance(cursor, dict) and "updatedAt" in cursor
|
|
124
|
+
|
|
125
|
+
async def paginate(
|
|
126
|
+
self, result: list[Any], page: list[Any], raw: Any, request: _Requester, page_size: int, body: Any
|
|
127
|
+
) -> list[Any]:
|
|
128
|
+
cursor = raw.get("cursor", {}) if isinstance(raw, dict) else {}
|
|
129
|
+
while cursor.get("total", 0) >= page_size:
|
|
130
|
+
next_body = {
|
|
131
|
+
**body,
|
|
132
|
+
"settings": {
|
|
133
|
+
**body.get("settings", {}),
|
|
134
|
+
"cursor": {
|
|
135
|
+
"limit": page_size,
|
|
136
|
+
"updatedAt": cursor["updatedAt"],
|
|
137
|
+
"nmID": cursor["nmID"],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
raw = await request(full_body=next_body)
|
|
142
|
+
page = raw.get("cards", []) if isinstance(raw, dict) else []
|
|
143
|
+
result.extend(page)
|
|
144
|
+
cursor = raw.get("cursor", {}) if isinstance(raw, dict) else {}
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
PAGINATION_STRATEGIES: list[PaginationStrategy] = [
|
|
149
|
+
CardsListStrategy(),
|
|
150
|
+
RrdIdCursorStrategy(),
|
|
151
|
+
LastChangeDateStrategy(),
|
|
152
|
+
BodyCursorStrategy(),
|
|
153
|
+
NextCursorStrategy(),
|
|
154
|
+
OffsetStrategy(),
|
|
155
|
+
]
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from collections.abc import Callable
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
_Requester = Callable[..., Any]
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class PaginationStrategy:
|
|
11
|
-
"""Base class for pagination strategies used by fetch_all."""
|
|
12
|
-
|
|
13
|
-
def detect(self, raw: Any, page: list[Any], body: Any) -> bool:
|
|
14
|
-
"""Return True if this strategy applies to the given response."""
|
|
15
|
-
raise NotImplementedError
|
|
16
|
-
|
|
17
|
-
async def paginate(self, result: list[Any], page: list[Any], raw: Any, request: _Requester) -> list[Any]:
|
|
18
|
-
"""Continue fetching pages and append to result. Return final list."""
|
|
19
|
-
raise NotImplementedError
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class RrdIdCursorStrategy(PaginationStrategy):
|
|
23
|
-
def detect(self, raw: Any, page: list[Any], body: Any) -> bool:
|
|
24
|
-
return bool(page and isinstance(page[-1], dict) and "rrd_id" in page[-1])
|
|
25
|
-
|
|
26
|
-
async def paginate(self, result: list[Any], page: list[Any], raw: Any, request: _Requester) -> list[Any]:
|
|
27
|
-
from ..method import _extract_list
|
|
28
|
-
|
|
29
|
-
while page:
|
|
30
|
-
rrd_id = page[-1]["rrd_id"]
|
|
31
|
-
page = _extract_list(await request({"rrdid": rrd_id})) or []
|
|
32
|
-
result.extend(page)
|
|
33
|
-
return result
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class LastChangeDateStrategy(PaginationStrategy):
|
|
37
|
-
def detect(self, raw: Any, page: list[Any], body: Any) -> bool:
|
|
38
|
-
return bool(page and isinstance(page[-1], dict) and "lastChangeDate" in page[-1])
|
|
39
|
-
|
|
40
|
-
async def paginate(self, result: list[Any], page: list[Any], raw: Any, request: _Requester) -> list[Any]:
|
|
41
|
-
from ..method import _extract_list
|
|
42
|
-
|
|
43
|
-
while page:
|
|
44
|
-
date_from = page[-1]["lastChangeDate"]
|
|
45
|
-
page = _extract_list(await request({"dateFrom": date_from})) or []
|
|
46
|
-
result.extend(page)
|
|
47
|
-
return result
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class BodyCursorStrategy(PaginationStrategy):
|
|
51
|
-
def detect(self, raw: Any, page: list[Any], body: Any) -> bool:
|
|
52
|
-
return body is not None and isinstance(raw, dict) and bool(raw.get("cursor"))
|
|
53
|
-
|
|
54
|
-
async def paginate(self, result: list[Any], page: list[Any], raw: Any, request: _Requester) -> list[Any]:
|
|
55
|
-
from ..method import _extract_list
|
|
56
|
-
|
|
57
|
-
cursor_val = raw.get("cursor")
|
|
58
|
-
while cursor_val:
|
|
59
|
-
raw = await request(extra_body={"cursor": cursor_val})
|
|
60
|
-
page = _extract_list(raw)
|
|
61
|
-
if not page:
|
|
62
|
-
break
|
|
63
|
-
result.extend(page)
|
|
64
|
-
cursor_val = raw.get("cursor") if isinstance(raw, dict) else None
|
|
65
|
-
return result
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class NextCursorStrategy(PaginationStrategy):
|
|
69
|
-
def detect(self, raw: Any, page: list[Any], body: Any) -> bool:
|
|
70
|
-
return isinstance(raw, dict) and "next" in raw
|
|
71
|
-
|
|
72
|
-
async def paginate(self, result: list[Any], page: list[Any], raw: Any, request: _Requester) -> list[Any]:
|
|
73
|
-
from ..method import _PAGE_SIZE, _extract_list
|
|
74
|
-
|
|
75
|
-
cursor = raw["next"]
|
|
76
|
-
while cursor:
|
|
77
|
-
raw = await request({"limit": _PAGE_SIZE, "next": cursor})
|
|
78
|
-
page = _extract_list(raw)
|
|
79
|
-
if not page:
|
|
80
|
-
break
|
|
81
|
-
result.extend(page)
|
|
82
|
-
cursor = raw.get("next") if isinstance(raw, dict) else None
|
|
83
|
-
return result
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class OffsetStrategy(PaginationStrategy):
|
|
87
|
-
def detect(self, raw: Any, page: list[Any], body: Any) -> bool:
|
|
88
|
-
from ..method import _PAGE_SIZE
|
|
89
|
-
|
|
90
|
-
return len(page) >= _PAGE_SIZE
|
|
91
|
-
|
|
92
|
-
async def paginate(self, result: list[Any], page: list[Any], raw: Any, request: _Requester) -> list[Any]:
|
|
93
|
-
from ..method import _PAGE_SIZE, _extract_list
|
|
94
|
-
|
|
95
|
-
offset = _PAGE_SIZE
|
|
96
|
-
while True:
|
|
97
|
-
raw = await request({"limit": _PAGE_SIZE, "offset": offset})
|
|
98
|
-
page = _extract_list(raw)
|
|
99
|
-
if not page:
|
|
100
|
-
break
|
|
101
|
-
result.extend(page)
|
|
102
|
-
if len(page) < _PAGE_SIZE:
|
|
103
|
-
break
|
|
104
|
-
offset += _PAGE_SIZE
|
|
105
|
-
return result
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
PAGINATION_STRATEGIES: list[PaginationStrategy] = [
|
|
109
|
-
RrdIdCursorStrategy(),
|
|
110
|
-
LastChangeDateStrategy(),
|
|
111
|
-
BodyCursorStrategy(),
|
|
112
|
-
NextCursorStrategy(),
|
|
113
|
-
OffsetStrategy(),
|
|
114
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|