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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wbapi-async
3
- Version: 0.7.4
3
+ Version: 0.7.5
4
4
  Summary: Asynchronous client for Wildberries API
5
5
  Project-URL: Documentation, https://serdukow.github.io/wbapi-async/
6
6
  Project-URL: Repository, https://github.com/serdukow/wbapi-async
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wbapi-async"
7
- version = "0.7.4"
7
+ version = "0.7.5"
8
8
  description = "Asynchronous client for Wildberries API"
9
9
  authors = [
10
10
  { name = "Andrei Serdiukov", email = "asyncdf@gmail.com" },
@@ -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
@@ -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
- _PATH_TO_BASE: dict[str, str] = {
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
- _PATH_TO_LIMIT: dict[str, tuple[int, int, int, int]] = {
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
- _DEFAULT_LIMIT: tuple[int, int, int, int] = (60000, 60, 1000, 5)
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
- class MethodDispatcher:
535
- def __init__(self, session: BaseSession, token: str) -> None:
536
- self._session = session
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
- Full API reference and available paths: https://dev.wildberries.ru/openapi/
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
- news = await api.get(
54
- "/api/communications/v2/news", fromID=0
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
- raise AttributeError(name) from None
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