wbapi-async 0.7.7__tar.gz → 0.7.9__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.
Files changed (26) hide show
  1. wbapi_async-0.7.9/PKG-INFO +130 -0
  2. wbapi_async-0.7.9/README.md +86 -0
  3. {wbapi_async-0.7.7 → wbapi_async-0.7.9}/pyproject.toml +1 -1
  4. {wbapi_async-0.7.7 → wbapi_async-0.7.9}/src/wbapi/__init__.py +3 -4
  5. wbapi_async-0.7.9/src/wbapi/_core/__init__.py +5 -0
  6. wbapi_async-0.7.9/src/wbapi/_core/_method.py +62 -0
  7. wbapi_async-0.7.9/src/wbapi/_core/_paginators.py +165 -0
  8. wbapi_async-0.7.7/src/wbapi/client/session/base.py → wbapi_async-0.7.9/src/wbapi/_core/_session.py +26 -2
  9. wbapi_async-0.7.9/src/wbapi/_core/limiter.py +16 -0
  10. wbapi_async-0.7.9/src/wbapi/_core/paginate.py +27 -0
  11. wbapi_async-0.7.9/src/wbapi/_core/parse_url.py +31 -0
  12. wbapi_async-0.7.7/src/wbapi/api.py → wbapi_async-0.7.9/src/wbapi/_main.py +36 -37
  13. {wbapi_async-0.7.7 → wbapi_async-0.7.9}/src/wbapi/exceptions.py +0 -4
  14. wbapi_async-0.7.7/PKG-INFO +0 -94
  15. wbapi_async-0.7.7/README.md +0 -50
  16. wbapi_async-0.7.7/src/wbapi/_method.py +0 -180
  17. wbapi_async-0.7.7/src/wbapi/client/__init__.py +0 -0
  18. wbapi_async-0.7.7/src/wbapi/client/session/__init__.py +0 -4
  19. wbapi_async-0.7.7/src/wbapi/client/session/headers.py +0 -23
  20. wbapi_async-0.7.7/src/wbapi/utils/__init__.py +0 -0
  21. wbapi_async-0.7.7/src/wbapi/utils/paginate.py +0 -155
  22. {wbapi_async-0.7.7 → wbapi_async-0.7.9}/.gitignore +0 -0
  23. {wbapi_async-0.7.7 → wbapi_async-0.7.9}/LICENSE +0 -0
  24. {wbapi_async-0.7.7/src/wbapi → wbapi_async-0.7.9/src/wbapi/_core}/_registry.py +0 -0
  25. {wbapi_async-0.7.7/src/wbapi → wbapi_async-0.7.9/src/wbapi/_core}/type.py +0 -0
  26. {wbapi_async-0.7.7 → wbapi_async-0.7.9}/src/wbapi/py.typed +0 -0
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: wbapi-async
3
+ Version: 0.7.9
4
+ Summary: Fast, lightweight async client for the Wildberries Seller API
5
+ Project-URL: Documentation, https://serdukow.github.io/wbapi-async/
6
+ Project-URL: Repository, https://github.com/serdukow/wbapi-async
7
+ Project-URL: Changelog, https://github.com/serdukow/wbapi-async/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/serdukow/wbapi-async/issues
9
+ Author-email: Andrei Serdiukov <asyncdf@gmail.com>
10
+ Maintainer-email: Andrei Serdiukov <asyncdf@gmail.com>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: api,async,client,wb,wildberries
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Framework :: Pydantic :: 2
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: <3.14,>=3.11
27
+ Requires-Dist: aiolimiter<2.0.0,>=1.2.1
28
+ Requires-Dist: httpx<0.29.0,>=0.28.1
29
+ Provides-Extra: dev
30
+ Requires-Dist: pre-commit<5.0.0,>=4.3.0; extra == 'dev'
31
+ Requires-Dist: ruff<0.15.0,>=0.14.1; extra == 'dev'
32
+ Provides-Extra: docs
33
+ Requires-Dist: mkdocs-material>=9.6.0; extra == 'docs'
34
+ Requires-Dist: mkdocstrings[python]>=0.29.0; extra == 'docs'
35
+ Provides-Extra: lint
36
+ Requires-Dist: pre-commit<5.0.0,>=4.3.0; extra == 'lint'
37
+ Requires-Dist: ruff<0.15.0,>=0.14.1; extra == 'lint'
38
+ Provides-Extra: test
39
+ Requires-Dist: pytest-asyncio<2.0.0,>=1.2.0; extra == 'test'
40
+ Requires-Dist: pytest-httpx<0.37.0,>=0.35.0; extra == 'test'
41
+ Requires-Dist: pytest<9.0.0,>=8.4.2; extra == 'test'
42
+ Requires-Dist: python-dotenv<2.0.0,>=1.2.1; extra == 'test'
43
+ Description-Content-Type: text/markdown
44
+
45
+ <p align="center">
46
+ <a href="https://dev.wildberries.ru"><img src="https://i.ibb.co/dwsMVbNC/logo.png" alt="wbapi"></a>
47
+ </p>
48
+
49
+ <p align="center">
50
+ <a href="https://img.shields.io/pypi/v/wbapi-async.svg">
51
+ <img src="https://img.shields.io/pypi/v/wbapi-async.svg" alt="Version">
52
+ </a>
53
+ <a href="https://pypi.python.org/pypi/wbapi-async">
54
+ <img src="https://img.shields.io/pypi/dm/wbapi-async.svg" alt="Downloads">
55
+ </a>
56
+ <a href="https://pypi.python.org/pypi/wbapi-async">
57
+ <img src="https://img.shields.io/badge/status-stable-green.svg?logo=git&logoColor=green" alt="Status">
58
+ </a>
59
+ <a href="https://pypi.org/project/wbapi-async">
60
+ <img src="https://img.shields.io/pypi/pyversions/wbapi-async.svg" alt="Python">
61
+ </a>
62
+ </p>
63
+
64
+ ---
65
+
66
+ **Documentation**: [https://dev.wildberries.ru](https://dev.wildberries.ru)
67
+
68
+ **Source Code**: [https://github.com/serdukow/wbapi-async](https://github.com/serdukow/wbapi-async)
69
+
70
+ ---
71
+
72
+ **wbapi** is a lightweight async client for the Wildberries Seller API, built on top of [httpx](https://www.python-httpx.org/).
73
+
74
+ It handles pagination, rate limiting, and base URL resolution automatically — so you can focus on your business logic instead of HTTP boilerplate.
75
+
76
+ ## Requirements
77
+
78
+ Python 3.10+
79
+
80
+ wbapi stands on the shoulders of giants:
81
+
82
+ - [httpx](https://www.python-httpx.org/) for async HTTP.
83
+ - [aiolimiter](https://github.com/mjpieters/aiolimiter) for rate limiting.
84
+
85
+ ## Installation
86
+
87
+ ```bash
88
+ pip install wbapi-async
89
+ ```
90
+
91
+ ## How to use
92
+
93
+ 1. Register in the Wildberries seller [personal account](https://seller.wildberries.ru/) if you haven't already.
94
+ 2. Go to store settings and [create an API token](https://dev.wildberries.ru/en/docs/openapi/api-information#tag/Authorization/How-to-create-a-personal-access-base-or-test-token).
95
+
96
+ ## Quick start
97
+
98
+ ```python
99
+ import asyncio
100
+ from wbapi import WbAPI
101
+
102
+ async def main():
103
+ async with WbAPI(token="your_api_token") as api:
104
+ # Simple request
105
+ warehouses = await api.get("/api/v3/warehouses")
106
+
107
+ # Auto-paginated — fetches all pages automatically
108
+ all_cards = await api.post(
109
+ "/content/v2/get/cards/list",
110
+ body={
111
+ "settings": {
112
+ "sort": {"ascending": True},
113
+ "cursor": {"limit": 100},
114
+ "filter": {"withPhoto": -1},
115
+ }
116
+ },
117
+ paginate=True,
118
+ )
119
+ print(all_cards)
120
+
121
+ asyncio.run(main())
122
+ ```
123
+
124
+ Pass `paginate=True` to any `get` or `post` call and the library fetches all pages automatically. Pagination strategy (cursor, token, or offset) is detected from the first response.
125
+
126
+ Base URL resolution and per-endpoint rate limiting are handled transparently.
127
+
128
+ ## License
129
+
130
+ This project is licensed under the terms of the [MIT license](https://github.com/serdukow/wbapi-async/blob/main/LICENSE).
@@ -0,0 +1,86 @@
1
+ <p align="center">
2
+ <a href="https://dev.wildberries.ru"><img src="https://i.ibb.co/dwsMVbNC/logo.png" alt="wbapi"></a>
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://img.shields.io/pypi/v/wbapi-async.svg">
7
+ <img src="https://img.shields.io/pypi/v/wbapi-async.svg" alt="Version">
8
+ </a>
9
+ <a href="https://pypi.python.org/pypi/wbapi-async">
10
+ <img src="https://img.shields.io/pypi/dm/wbapi-async.svg" alt="Downloads">
11
+ </a>
12
+ <a href="https://pypi.python.org/pypi/wbapi-async">
13
+ <img src="https://img.shields.io/badge/status-stable-green.svg?logo=git&logoColor=green" alt="Status">
14
+ </a>
15
+ <a href="https://pypi.org/project/wbapi-async">
16
+ <img src="https://img.shields.io/pypi/pyversions/wbapi-async.svg" alt="Python">
17
+ </a>
18
+ </p>
19
+
20
+ ---
21
+
22
+ **Documentation**: [https://dev.wildberries.ru](https://dev.wildberries.ru)
23
+
24
+ **Source Code**: [https://github.com/serdukow/wbapi-async](https://github.com/serdukow/wbapi-async)
25
+
26
+ ---
27
+
28
+ **wbapi** is a lightweight async client for the Wildberries Seller API, built on top of [httpx](https://www.python-httpx.org/).
29
+
30
+ It handles pagination, rate limiting, and base URL resolution automatically — so you can focus on your business logic instead of HTTP boilerplate.
31
+
32
+ ## Requirements
33
+
34
+ Python 3.10+
35
+
36
+ wbapi stands on the shoulders of giants:
37
+
38
+ - [httpx](https://www.python-httpx.org/) for async HTTP.
39
+ - [aiolimiter](https://github.com/mjpieters/aiolimiter) for rate limiting.
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install wbapi-async
45
+ ```
46
+
47
+ ## How to use
48
+
49
+ 1. Register in the Wildberries seller [personal account](https://seller.wildberries.ru/) if you haven't already.
50
+ 2. Go to store settings and [create an API token](https://dev.wildberries.ru/en/docs/openapi/api-information#tag/Authorization/How-to-create-a-personal-access-base-or-test-token).
51
+
52
+ ## Quick start
53
+
54
+ ```python
55
+ import asyncio
56
+ from wbapi import WbAPI
57
+
58
+ async def main():
59
+ async with WbAPI(token="your_api_token") as api:
60
+ # Simple request
61
+ warehouses = await api.get("/api/v3/warehouses")
62
+
63
+ # Auto-paginated — fetches all pages automatically
64
+ all_cards = await api.post(
65
+ "/content/v2/get/cards/list",
66
+ body={
67
+ "settings": {
68
+ "sort": {"ascending": True},
69
+ "cursor": {"limit": 100},
70
+ "filter": {"withPhoto": -1},
71
+ }
72
+ },
73
+ paginate=True,
74
+ )
75
+ print(all_cards)
76
+
77
+ asyncio.run(main())
78
+ ```
79
+
80
+ Pass `paginate=True` to any `get` or `post` call and the library fetches all pages automatically. Pagination strategy (cursor, token, or offset) is detected from the first response.
81
+
82
+ Base URL resolution and per-endpoint rate limiting are handled transparently.
83
+
84
+ ## License
85
+
86
+ This project is licensed under the terms of the [MIT license](https://github.com/serdukow/wbapi-async/blob/main/LICENSE).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wbapi-async"
7
- version = "0.7.7"
7
+ version = "0.7.9"
8
8
  description = "Fast, lightweight async client for the Wildberries Seller API"
9
9
 
10
10
  authors = [
@@ -1,8 +1,8 @@
1
1
  from importlib.metadata import version
2
2
 
3
- from .api import WbAPI
4
- from .exceptions import PaginationNotSupported, TokenValidationError, WBAPIError
5
- from .type import ApiResponse
3
+ from ._core.type import ApiResponse
4
+ from ._main import WbAPI
5
+ from .exceptions import TokenValidationError, WBAPIError
6
6
 
7
7
 
8
8
  __version__ = version("wbapi-async")
@@ -11,7 +11,6 @@ __all__ = (
11
11
  "WbAPI",
12
12
  "WBAPIError",
13
13
  "TokenValidationError",
14
- "PaginationNotSupported",
15
14
  "ApiResponse",
16
15
  "__version__",
17
16
  )
@@ -0,0 +1,5 @@
1
+ from ._method import MethodDispatcher
2
+ from ._session import BaseSession
3
+
4
+
5
+ __all__ = ("MethodDispatcher", "BaseSession")
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ._registry import _PAGE_SIZES, _PUBLIC
6
+ from ._session import BaseSession
7
+ from .limiter import _get_limiter
8
+ from .paginate import fetch_all
9
+ from .parse_url import parse_url
10
+ from .type import ApiResponse, _wrap
11
+
12
+
13
+ class MethodDispatcher:
14
+ def __init__(self, session: BaseSession, token: str) -> None:
15
+ self._session = session
16
+ self._token = token
17
+
18
+ async def dispatch(
19
+ self,
20
+ http_method: str,
21
+ path: str,
22
+ *,
23
+ params: dict[str, Any] | None = None,
24
+ json: Any | None = None,
25
+ paginate: bool = False,
26
+ no_auth: bool = False,
27
+ ) -> Any:
28
+ import re
29
+ from urllib.parse import urlparse
30
+
31
+ placeholders = re.findall(r"\{(\w+)\}", path)
32
+ if placeholders and params:
33
+ params = dict(params)
34
+ for key in placeholders:
35
+ if key in params:
36
+ path = path.replace(f"{{{key}}}", str(params.pop(key)))
37
+ params = params or None
38
+
39
+ if not no_auth and path.startswith(("https://", "http://")):
40
+ no_auth = urlparse(path).netloc in _PUBLIC
41
+
42
+ if no_auth:
43
+ self._session.headers.authorization = None
44
+ else:
45
+ self._session.headers.set_token(self._token)
46
+
47
+ if paginate:
48
+ page_size = _PAGE_SIZES.get(path, 1000)
49
+
50
+ async def _request(
51
+ params: dict[str, Any] | None = None,
52
+ body: dict[str, Any] | None = None,
53
+ ) -> Any:
54
+ url = parse_url(path)
55
+ limiter = _get_limiter(path)
56
+ return await self._session._request(http_method, url, params=params, json=body, limit=limiter)
57
+
58
+ return [ApiResponse(item) for item in await fetch_all(_request, path, page_size, params, json)]
59
+
60
+ url = parse_url(path)
61
+ limiter = _get_limiter(path)
62
+ return _wrap(await self._session._request(http_method, url, params=params, json=json, limit=limiter))
@@ -0,0 +1,165 @@
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
+ def _extract_list(raw: Any) -> list[Any] | None:
11
+ if isinstance(raw, list):
12
+ return raw
13
+ if isinstance(raw, dict):
14
+ for v in raw.values():
15
+ if isinstance(v, list):
16
+ return v
17
+ for v in raw.values():
18
+ if isinstance(v, dict):
19
+ found = _extract_list(v)
20
+ if found is not None:
21
+ return found
22
+ return None
23
+
24
+
25
+ async def next_token(
26
+ raw: Any,
27
+ page: list[Any],
28
+ request: _Requester,
29
+ params: dict[str, Any],
30
+ page_size: int,
31
+ ) -> list[Any]:
32
+ """
33
+ Token continuation pagination: API returns ``next`` in the response root,
34
+ which is passed as ``?next=<value>`` in subsequent requests.
35
+
36
+ Used by: Marketplace (FBS/DBS/DBW orders), Supplies FBW, Q&A, Reviews.
37
+ Stop condition: ``next`` is falsy or the page is empty.
38
+ """
39
+ result: list[Any] = list(page)
40
+ while True:
41
+ cursor = raw.get("next") if isinstance(raw, dict) else None
42
+ if not cursor or not page:
43
+ break
44
+ raw = await request(params={**params, "limit": page_size, "next": cursor})
45
+ page = _extract_list(raw) or []
46
+ result.extend(page)
47
+ return result
48
+
49
+
50
+ async def rrdid_token(
51
+ raw: Any,
52
+ page: list[Any],
53
+ request: _Requester,
54
+ params: dict[str, Any],
55
+ page_size: int,
56
+ ) -> list[Any]:
57
+ """
58
+ Finance report token pagination: last item in the page has ``rrd_id``,
59
+ which is passed as ``?rrdid=<value>`` in subsequent requests.
60
+
61
+ Used by: Finance (reportDetailByPeriod).
62
+ Stop condition: page is empty (API returns 204 No Content → empty list).
63
+ """
64
+ result: list[Any] = list(page)
65
+ while page:
66
+ rrd_id = page[-1].get("rrd_id") if isinstance(page[-1], dict) else None
67
+ if not rrd_id:
68
+ break
69
+ raw = await request(params={**params, "rrdid": rrd_id})
70
+ page = _extract_list(raw) or []
71
+ result.extend(page)
72
+ return result
73
+
74
+
75
+ async def offset(
76
+ raw: Any,
77
+ page: list[Any],
78
+ request: _Requester,
79
+ params: dict[str, Any],
80
+ page_size: int,
81
+ ) -> list[Any]:
82
+ """
83
+ Offset pagination: increments ``offset`` by ``limit`` each request.
84
+
85
+ Used by: Analytics, Promotions, Reports.
86
+ Stop condition: returned page is smaller than ``limit``.
87
+ """
88
+ result: list[Any] = list(page)
89
+ current_offset = page_size
90
+ while len(page) >= page_size:
91
+ raw = await request(params={**params, "limit": page_size, "offset": current_offset})
92
+ page = _extract_list(raw) or []
93
+ result.extend(page)
94
+ current_offset += page_size
95
+ return result
96
+
97
+
98
+ async def cursor(
99
+ raw: Any,
100
+ page: list[Any],
101
+ request: _Requester,
102
+ body: dict[str, Any],
103
+ page_size: int,
104
+ ) -> list[Any]:
105
+ """
106
+ Cursor pagination (POST body): API returns ``cursor`` object with
107
+ ``updatedAt``, ``nmID``, and ``total``. Next request merges cursor
108
+ fields into ``settings.cursor`` in the request body.
109
+
110
+ Used by: Content v2 (cards list).
111
+ Stop condition: ``cursor.total`` < ``limit``.
112
+ """
113
+ result: list[Any] = list(page)
114
+ while True:
115
+ cursor_obj = raw.get("cursor", {}) if isinstance(raw, dict) else {}
116
+ total = cursor_obj.get("total", 0) if isinstance(cursor_obj, dict) else 0
117
+ if total < page_size:
118
+ break
119
+ next_body = {
120
+ **body,
121
+ "settings": {
122
+ **body.get("settings", {}),
123
+ "cursor": {
124
+ "limit": page_size,
125
+ "updatedAt": cursor_obj["updatedAt"],
126
+ "nmID": cursor_obj["nmID"],
127
+ },
128
+ },
129
+ }
130
+ raw = await request(body=next_body)
131
+ page = _extract_list(raw) or []
132
+ result.extend(page)
133
+ return result
134
+
135
+
136
+ def detect_and_paginate(
137
+ raw: Any,
138
+ page: list[Any],
139
+ request: _Requester,
140
+ params: dict[str, Any],
141
+ body: dict[str, Any] | None,
142
+ page_size: int,
143
+ ) -> Any:
144
+ """
145
+ Auto-detect pagination type from first-page response and dispatch
146
+ to the appropriate paginator coroutine.
147
+
148
+ Detection order (first match wins):
149
+ 1. CursorPaginator — POST body with ``cursor.updatedAt`` in response
150
+ 2. NextTokenPaginator — ``next`` key in response root
151
+ 3. RrdIdTokenPaginator — last item in page has ``rrd_id``
152
+ 4. OffsetPaginator — page length == page_size (fallback)
153
+ """
154
+ if body is not None and isinstance(raw, dict):
155
+ cursor_obj = raw.get("cursor", {})
156
+ if isinstance(cursor_obj, dict) and "updatedAt" in cursor_obj:
157
+ return cursor(raw, page, request, body, page_size)
158
+
159
+ if isinstance(raw, dict) and "next" in raw:
160
+ return next_token(raw, page, request, params, page_size)
161
+
162
+ if page and isinstance(page[-1], dict) and "rrd_id" in page[-1]:
163
+ return rrdid_token(raw, page, request, params, page_size)
164
+
165
+ return offset(raw, page, request, params, page_size)
@@ -7,8 +7,7 @@ from aiolimiter import AsyncLimiter
7
7
  import httpx
8
8
  from httpx import RequestError
9
9
 
10
- from ...exceptions import WBAPIError
11
- from .headers import Headers
10
+ from ..exceptions import WBAPIError
12
11
 
13
12
 
14
13
  log = logging.getLogger("wbapi.session")
@@ -16,6 +15,31 @@ logging.getLogger("httpx").setLevel(logging.WARNING)
16
15
  logging.getLogger("httpcore").setLevel(logging.WARNING)
17
16
 
18
17
 
18
+ class Headers:
19
+ def __init__(self) -> None:
20
+ self.accept = "application/json;charset=utf-8"
21
+ self.content_type = "application/json"
22
+ self.authorization: str | None = None
23
+ self.user_agent = (
24
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
25
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
26
+ "Chrome/124.0.0.0 Safari/537.36"
27
+ )
28
+
29
+ def set_token(self, token: str) -> None:
30
+ self.authorization = f"Bearer {token}"
31
+
32
+ def as_dict(self) -> dict[str, str]:
33
+ headers: dict[str, str] = {
34
+ "Accept": self.accept,
35
+ "Content-Type": self.content_type,
36
+ "User-Agent": self.user_agent,
37
+ }
38
+ if self.authorization is not None:
39
+ headers["Authorization"] = self.authorization
40
+ return headers
41
+
42
+
19
43
  class BaseSession:
20
44
  base_url: str
21
45
  timeout: int
@@ -0,0 +1,16 @@
1
+ from aiolimiter import AsyncLimiter
2
+
3
+ from ._registry import _LIMITS
4
+
5
+
6
+ _DEFAULT_LIMIT: tuple[int, int, int, int] = (60000, 60, 1000, 5)
7
+
8
+ _limiters: dict[tuple[int, int], AsyncLimiter] = {}
9
+
10
+
11
+ def _get_limiter(path: str) -> AsyncLimiter:
12
+ period, limit, interval, burst = _LIMITS.get(path, _DEFAULT_LIMIT)
13
+ key = (interval, burst)
14
+ if key not in _limiters:
15
+ _limiters[key] = AsyncLimiter(max_rate=burst, time_period=interval / 1000)
16
+ return _limiters[key]
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from ._paginators import _extract_list, detect_and_paginate
7
+
8
+
9
+ _Requester = Callable[..., Any]
10
+
11
+
12
+ async def fetch_all(
13
+ request: _Requester,
14
+ path: str,
15
+ page_size: int,
16
+ params: dict[str, Any] | None,
17
+ body: dict[str, Any] | None,
18
+ ) -> list[Any]:
19
+ init_params = {**(params or {}), "limit": page_size}
20
+ if body is not None:
21
+ init_body = {**body}
22
+ raw = await request(params=init_params or None, body=init_body)
23
+ else:
24
+ raw = await request(params=init_params)
25
+
26
+ page = _extract_list(raw) or []
27
+ return await detect_and_paginate(raw, page, request, init_params, body, page_size)
@@ -0,0 +1,31 @@
1
+ from ._registry import _BASES, _PUBLIC
2
+
3
+
4
+ def parse_url(path: str) -> str:
5
+ """Resolve full URL for a spec path like /api/v3/supplies.
6
+
7
+ If a full URL is passed, it is returned as-is provided its host is a known
8
+ wildberries.ru domain or is listed in ``_PUBLIC``.
9
+ """
10
+ from urllib.parse import urlparse
11
+
12
+ from ..exceptions import WBAPIError
13
+
14
+ if path.startswith("https://") or path.startswith("http://"):
15
+ host = urlparse(path).netloc
16
+ known_hosts = {urlparse(v).netloc for v in _BASES.values()} | _PUBLIC
17
+ if host not in known_hosts:
18
+ raise WBAPIError(detail=f"Unknown host {host!r}.")
19
+ return path
20
+
21
+ base = _BASES.get(path)
22
+ if base:
23
+ return base + path
24
+ parts = path.rstrip("/").split("/")
25
+ for i in range(len(parts) - 1, 0, -1):
26
+ candidate = "/".join(parts[:i])
27
+ if candidate in _BASES:
28
+ return _BASES[candidate] + path
29
+ raise WBAPIError(
30
+ detail=f"{path!r} may be deprecated or removed. See https://dev.wildberries.ru/release-notes"
31
+ )
@@ -2,9 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
- from ._method import MethodDispatcher
6
- from .client.session.base import BaseSession
7
- from .type import ApiResponse, _wrap
5
+ from ._core import BaseSession, MethodDispatcher
8
6
 
9
7
 
10
8
  class WbAPI:
@@ -25,59 +23,60 @@ class WbAPI:
25
23
  self.session = session or BaseSession(base="https://wildberries.ru", timeout=timeout)
26
24
  self._dispatcher = MethodDispatcher(self.session, token)
27
25
 
28
- async def get(self, path: str, **kwargs: Any) -> Any:
26
+ async def get(self, path: str, *, paginate: bool = False, **kwargs: Any) -> Any:
29
27
  """
30
28
  Send a GET request. kwargs become query parameters.
31
29
 
30
+ Pass ``paginate=True`` to fetch all pages automatically.
31
+ Pagination type is detected from the first response:
32
+ - ``next`` in response → token continuation (Marketplace, Supplies, Q&A)
33
+ - ``rrd_id`` in last item → rrdid token (Finance reports)
34
+ - fallback → offset (Analytics, Promotions, Reports)
35
+
32
36
  Example::
37
+
33
38
  warehouses = await api.get("/api/v3/warehouses")
34
39
  orders = await api.get(
35
40
  "/api/v3/orders/new", limit=10, next=0
36
41
  )
37
- meta = await api.get(
38
- "/api/v3/orders/{orderId}/meta", orderId=123
39
- )
40
- """
41
- return _wrap(await self._dispatcher.dispatch("GET", path, params=kwargs or None))
42
42
 
43
- async def get_all(self, path: str, **kwargs: Any) -> list[ApiResponse]:
44
- """
45
- Fetch all pages from a paginated endpoint. Pass ``body=`` to use POST.
46
-
47
- Example::
48
- supplies = await api.get_all("/api/v3/supplies")
49
- cards = await api.get_all(
50
- "/content/v2/get/cards/list",
51
- body={"settings": {"sort": {"ascending": False}}},
43
+ all_orders = await api.get(
44
+ "/api/v3/orders", paginate=True, dateFrom=1698045576
45
+ )
46
+ all_sales = await api.get(
47
+ "/api/finance/v1/sales-reports/detailed",
48
+ paginate=True,
49
+ dateFrom="2024-01-01",
50
+ dateTo="2024-01-31",
52
51
  )
53
52
  """
54
- return [ApiResponse(item) for item in await self._dispatcher.fetch_all(path, **kwargs)]
53
+ return await self._dispatcher.dispatch("GET", path, params=kwargs or None, paginate=paginate)
55
54
 
56
- async def post(self, path: str, *, body: Any = None, **kwargs: Any) -> Any:
55
+ async def post(self, path: str, *, body: Any = None, paginate: bool = False, **kwargs: Any) -> Any:
57
56
  """
58
57
  Send a POST request. ``body`` is serialized as JSON. kwargs become query parameters.
59
58
 
59
+ Pass ``paginate=True`` to fetch all pages automatically.
60
+ Pagination type is detected from the first response:
61
+ - ``cursor.updatedAt`` in response → cursor pagination (Content, cards list)
62
+ - fallback → offset
63
+
60
64
  Example::
61
65
 
62
66
  await api.post(
63
67
  "/content/v2/get/cards/list",
64
- body={
65
- "settings": {
66
- "sort": {"ascending": False},
67
- "filter": {"textSearch": "12345"},
68
- }
69
- },
70
- )
71
- await api.post(
72
- "/content/v2/tag/nomenclature/link",
73
- body={"nmID": 179891389, "tagsIDs": [123456]},
68
+ body={"settings": {"sort": {"ascending": False}}},
74
69
  )
75
- await api.post(
76
- "/adv/v0/rename",
77
- body={"advertId": 2233344, "name": "newname"},
70
+
71
+ all_cards = await api.post(
72
+ "/content/v2/get/cards/list",
73
+ body={"settings": {"filter": {"withPhoto": -1}}},
74
+ paginate=True,
78
75
  )
79
76
  """
80
- return _wrap(await self._dispatcher.dispatch("POST", path, json=body, params=kwargs or None))
77
+ return await self._dispatcher.dispatch(
78
+ "POST", path, json=body, params=kwargs or None, paginate=paginate
79
+ )
81
80
 
82
81
  async def put(self, path: str, *, body: Any = None, **kwargs: Any) -> Any:
83
82
  """
@@ -90,7 +89,7 @@ class WbAPI:
90
89
  body={"stocks": [{"sku": "WB007", "amount": 10}]},
91
90
  )
92
91
  """
93
- return _wrap(await self._dispatcher.dispatch("PUT", path, json=body, params=kwargs or None))
92
+ return await self._dispatcher.dispatch("PUT", path, json=body, params=kwargs or None)
94
93
 
95
94
  async def patch(self, path: str, *, body: Any = None, **kwargs: Any) -> Any:
96
95
  """
@@ -101,7 +100,7 @@ class WbAPI:
101
100
  await api.patch("/api/v3/orders/13833711/cancel")
102
101
  await api.patch("/content/v2/tag/99", body={"name": "sale"})
103
102
  """
104
- return _wrap(await self._dispatcher.dispatch("PATCH", path, json=body, params=kwargs or None))
103
+ return await self._dispatcher.dispatch("PATCH", path, json=body, params=kwargs or None)
105
104
 
106
105
  async def delete(self, path: str, **kwargs: Any) -> Any:
107
106
  """
@@ -112,7 +111,7 @@ class WbAPI:
112
111
  await api.delete("/content/v2/tag/99")
113
112
  await api.delete("/api/v1/user", deletedUserID="abc-123")
114
113
  """
115
- return _wrap(await self._dispatcher.dispatch("DELETE", path, params=kwargs or None))
114
+ return await self._dispatcher.dispatch("DELETE", path, params=kwargs or None)
116
115
 
117
116
  async def __aenter__(self) -> WbAPI:
118
117
  return self
@@ -15,7 +15,3 @@ class WBAPIError(BaseWBAPIError):
15
15
 
16
16
  class TokenValidationError(BaseWBAPIError):
17
17
  pass
18
-
19
-
20
- class PaginationNotSupported(BaseWBAPIError):
21
- pass
@@ -1,94 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: wbapi-async
3
- Version: 0.7.7
4
- Summary: Fast, lightweight async client for the Wildberries Seller API
5
- Project-URL: Documentation, https://serdukow.github.io/wbapi-async/
6
- Project-URL: Repository, https://github.com/serdukow/wbapi-async
7
- Project-URL: Changelog, https://github.com/serdukow/wbapi-async/blob/main/CHANGELOG.md
8
- Project-URL: Issues, https://github.com/serdukow/wbapi-async/issues
9
- Author-email: Andrei Serdiukov <asyncdf@gmail.com>
10
- Maintainer-email: Andrei Serdiukov <asyncdf@gmail.com>
11
- License: MIT
12
- License-File: LICENSE
13
- Keywords: api,async,client,wb,wildberries
14
- Classifier: Development Status :: 4 - Beta
15
- Classifier: Framework :: AsyncIO
16
- Classifier: Framework :: Pydantic :: 2
17
- Classifier: Intended Audience :: Developers
18
- Classifier: License :: OSI Approved :: MIT License
19
- Classifier: Programming Language :: Python :: 3
20
- Classifier: Programming Language :: Python :: 3.10
21
- Classifier: Programming Language :: Python :: 3.11
22
- Classifier: Programming Language :: Python :: 3.12
23
- Classifier: Programming Language :: Python :: 3.13
24
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
- Classifier: Typing :: Typed
26
- Requires-Python: <3.14,>=3.11
27
- Requires-Dist: aiolimiter<2.0.0,>=1.2.1
28
- Requires-Dist: httpx<0.29.0,>=0.28.1
29
- Provides-Extra: dev
30
- Requires-Dist: pre-commit<5.0.0,>=4.3.0; extra == 'dev'
31
- Requires-Dist: ruff<0.15.0,>=0.14.1; extra == 'dev'
32
- Provides-Extra: docs
33
- Requires-Dist: mkdocs-material>=9.6.0; extra == 'docs'
34
- Requires-Dist: mkdocstrings[python]>=0.29.0; extra == 'docs'
35
- Provides-Extra: lint
36
- Requires-Dist: pre-commit<5.0.0,>=4.3.0; extra == 'lint'
37
- Requires-Dist: ruff<0.15.0,>=0.14.1; extra == 'lint'
38
- Provides-Extra: test
39
- Requires-Dist: pytest-asyncio<2.0.0,>=1.2.0; extra == 'test'
40
- Requires-Dist: pytest-httpx<0.37.0,>=0.35.0; extra == 'test'
41
- Requires-Dist: pytest<9.0.0,>=8.4.2; extra == 'test'
42
- Requires-Dist: python-dotenv<2.0.0,>=1.2.1; extra == 'test'
43
- Description-Content-Type: text/markdown
44
-
45
- ![Logo](https://dev.wildberries.ru/images/open-graph.png)
46
-
47
- <div align="center">
48
-
49
- #### Fast, lightweight async client for the Wildberries Seller API
50
-
51
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
52
- [![PyPI version](https://img.shields.io/pypi/v/wbapi-async.svg)](https://pypi.org/project/wild-api/)
53
- [![Downloads](https://img.shields.io/pypi/dm/wbapi-async.svg)](https://pypi.python.org/pypi/wild-api)
54
- [![Docs](https://img.shields.io/badge/docs-serdukow.github.io-blue.svg)](https://serdukow.github.io/wbapi-async/)
55
- [![Python](https://img.shields.io/pypi/pyversions/wbapi-async.svg)](https://pypi.org/project/wbapi-async/)
56
-
57
- </div>
58
-
59
- ## Features
60
-
61
- - **Zero boilerplate** — responses are plain attribute-accessible dicts
62
- - **Auto-pagination** — `get_all()` fetches all pages. Auto-detects all known strategies; you can easily add your own via a custom paginator — [see docs](https://github.com/serdukow/wbapi-async/blob/dev/docs/advanced/pagination.md)
63
- - **Rate limiting** — per-endpoint limits powered by `aiolimiter`
64
- - **Auto-retry** — automatic retry
65
- - **Always up to date** — path registry is updated every monday
66
- - **Fully async** — built on `httpx` + `asyncio`
67
-
68
- ## Install
69
-
70
- ```bash
71
- pip install wbapi-async
72
- ```
73
-
74
- ## Quick start
75
-
76
- ```python
77
- import asyncio
78
- from wbapi import WbAPI
79
-
80
- async def main():
81
- async with WbAPI(token="YOUR_TOKEN") as api:
82
- body = {
83
- "settings": {
84
- "sort": {"ascending": True},
85
- "cursor": {"limit": 100},
86
- "filter": {"withPhoto": -1},
87
- }
88
- }
89
-
90
- cards = await api.get_all("/content/v2/get/cards/list", body=body)
91
- print(f"cards: {cards!r}")
92
-
93
- asyncio.run(main())
94
- ```
@@ -1,50 +0,0 @@
1
- ![Logo](https://dev.wildberries.ru/images/open-graph.png)
2
-
3
- <div align="center">
4
-
5
- #### Fast, lightweight async client for the Wildberries Seller API
6
-
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
- [![PyPI version](https://img.shields.io/pypi/v/wbapi-async.svg)](https://pypi.org/project/wild-api/)
9
- [![Downloads](https://img.shields.io/pypi/dm/wbapi-async.svg)](https://pypi.python.org/pypi/wild-api)
10
- [![Docs](https://img.shields.io/badge/docs-serdukow.github.io-blue.svg)](https://serdukow.github.io/wbapi-async/)
11
- [![Python](https://img.shields.io/pypi/pyversions/wbapi-async.svg)](https://pypi.org/project/wbapi-async/)
12
-
13
- </div>
14
-
15
- ## Features
16
-
17
- - **Zero boilerplate** — responses are plain attribute-accessible dicts
18
- - **Auto-pagination** — `get_all()` fetches all pages. Auto-detects all known strategies; you can easily add your own via a custom paginator — [see docs](https://github.com/serdukow/wbapi-async/blob/dev/docs/advanced/pagination.md)
19
- - **Rate limiting** — per-endpoint limits powered by `aiolimiter`
20
- - **Auto-retry** — automatic retry
21
- - **Always up to date** — path registry is updated every monday
22
- - **Fully async** — built on `httpx` + `asyncio`
23
-
24
- ## Install
25
-
26
- ```bash
27
- pip install wbapi-async
28
- ```
29
-
30
- ## Quick start
31
-
32
- ```python
33
- import asyncio
34
- from wbapi import WbAPI
35
-
36
- async def main():
37
- async with WbAPI(token="YOUR_TOKEN") as api:
38
- body = {
39
- "settings": {
40
- "sort": {"ascending": True},
41
- "cursor": {"limit": 100},
42
- "filter": {"withPhoto": -1},
43
- }
44
- }
45
-
46
- cards = await api.get_all("/content/v2/get/cards/list", body=body)
47
- print(f"cards: {cards!r}")
48
-
49
- asyncio.run(main())
50
- ```
@@ -1,180 +0,0 @@
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(
54
- detail=f"{path!r} may be deprecated or removed. See https://dev.wildberries.ru/release-notes"
55
- )
56
-
57
-
58
- def _extract_list(raw: Any) -> list[Any] | None:
59
- if isinstance(raw, list):
60
- return raw
61
- if isinstance(raw, dict):
62
- for v in raw.values():
63
- if isinstance(v, list):
64
- return v
65
- for v in raw.values():
66
- if isinstance(v, dict):
67
- found = _extract_list(v)
68
- if found is not None:
69
- return found
70
- return None
71
-
72
-
73
- class MethodDispatcher:
74
- def __init__(self, session: BaseSession, token: str) -> None:
75
- self._session = session
76
- self._token = token
77
-
78
- async def dispatch(
79
- self,
80
- http_method: str,
81
- path: str,
82
- *,
83
- params: dict[str, Any] | None = None,
84
- json: Any | None = None,
85
- no_auth: bool = False,
86
- ) -> Any:
87
- import re
88
- from urllib.parse import urlparse
89
-
90
- placeholders = re.findall(r"\{(\w+)\}", path)
91
- if placeholders and params:
92
- params = dict(params)
93
- for key in placeholders:
94
- if key in params:
95
- path = path.replace(f"{{{key}}}", str(params.pop(key)))
96
- params = params or None
97
-
98
- if not no_auth and path.startswith(("https://", "http://")):
99
- no_auth = urlparse(path).netloc in _PUBLIC
100
-
101
- if no_auth:
102
- self._session.headers.authorization = None
103
- else:
104
- self._session.headers.set_token(self._token)
105
- url = resolve_url(path)
106
- limiter = _get_limiter(path)
107
- return await self._session._request(http_method, url, params=params, json=json, limit=limiter)
108
-
109
- async def fetch_all(self, path: str, **kwargs: Any) -> list[Any]:
110
- """Fetch all pages. Accepts an optional ``paginator`` kwarg —
111
- a callable ``(response) -> (items, next_params | None)`` for custom pagination logic.
112
-
113
- Example::
114
-
115
- def my_paginator(raw):
116
- items = raw.get("result", [])
117
- cursor = raw.get("cursor") or None
118
- next_params = {"cursor": cursor} if cursor else None
119
- return items, next_params
120
-
121
-
122
- all_items = await api.get_all(
123
- "/api/v3/custom", paginator=my_paginator
124
- )
125
- """
126
- from .exceptions import PaginationNotSupported
127
-
128
- paginator = kwargs.pop("paginator", None)
129
- body = kwargs.pop("body", None)
130
- http_method = "POST" if body is not None else "GET"
131
-
132
- def _do_request(
133
- extra: dict[str, Any] | None = None,
134
- extra_body: dict[str, Any] | None = None,
135
- full_body: dict[str, Any] | None = None,
136
- ) -> Any:
137
- if full_body is not None:
138
- return self.dispatch(http_method, path, params=kwargs or None, json=full_body)
139
- if body is not None:
140
- json = {**body, **(extra or {}), **(extra_body or {})}
141
- return self.dispatch(http_method, path, params=kwargs or None, json=json)
142
- params = {**kwargs, **(extra or {})} or None
143
- return self.dispatch(http_method, path, params=params)
144
-
145
- if paginator is not None:
146
- result: list[Any] = []
147
- next_extra: dict[str, Any] = {}
148
- while True:
149
- if body is not None:
150
- json = next_extra if next_extra else body
151
- raw = await self.dispatch(http_method, path, params=kwargs or None, json=json)
152
- else:
153
- raw = await _do_request(next_extra or None)
154
- items, next_extra = paginator(raw)
155
- result.extend(items)
156
- if not next_extra:
157
- break
158
- return result
159
-
160
- from .utils.paginate import PAGINATION_STRATEGIES
161
-
162
- page_size = _PAGE_SIZES.get(path, 1000)
163
- first_extra: dict[str, Any] = {"limit": page_size}
164
- if body is not None:
165
- first_extra["offset"] = 0
166
- raw = await _do_request(first_extra)
167
- page = _extract_list(raw)
168
-
169
- if page is None:
170
- raise PaginationNotSupported(f"No list data found in response for {path!r}")
171
-
172
- result: list[Any] = list(page)
173
-
174
- for strategy in PAGINATION_STRATEGIES:
175
- if strategy.detect(raw, page, body, page_size, path):
176
- return await strategy.paginate(result, page, raw, _do_request, page_size, body)
177
-
178
- if not result:
179
- raise PaginationNotSupported(f"{path!r} returned empty first page — pagination not supported")
180
- return result
File without changes
@@ -1,4 +0,0 @@
1
- from .base import BaseSession
2
-
3
-
4
- __all__ = ("BaseSession",)
@@ -1,23 +0,0 @@
1
- class Headers:
2
- def __init__(self) -> None:
3
- self.accept = "application/json;charset=utf-8"
4
- self.content_type = "application/json"
5
- self.authorization: str | None = None
6
- self.user_agent = (
7
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
8
- "AppleWebKit/537.36 (KHTML, like Gecko) "
9
- "Chrome/124.0.0.0 Safari/537.36"
10
- )
11
-
12
- def set_token(self, token: str) -> None:
13
- self.authorization = f"Bearer {token}"
14
-
15
- def as_dict(self) -> dict[str, str]:
16
- headers: dict[str, str] = {
17
- "Accept": self.accept,
18
- "Content-Type": self.content_type,
19
- "User-Agent": self.user_agent,
20
- }
21
- if self.authorization is not None:
22
- headers["Authorization"] = self.authorization
23
- return headers
File without changes
@@ -1,155 +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, 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
- ]
File without changes
File without changes