wbapi-async 0.7.0__tar.gz → 0.7.2__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.0
3
+ Version: 0.7.2
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
@@ -47,7 +47,7 @@ Description-Content-Type: text/markdown
47
47
 
48
48
  <div align="center">
49
49
 
50
- ### Lightweight async client for Wildberries Seller API
50
+ #### Lightweight async client for Wildberries Seller API
51
51
 
52
52
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
53
53
  [![PyPI version](https://img.shields.io/pypi/v/wild-api.svg)](https://pypi.org/project/wild-api/)
@@ -59,20 +59,27 @@ Description-Content-Type: text/markdown
59
59
  Lightweight async Python client for the [Wildberries Seller API](https://dev.wildberries.ru).
60
60
  Just pass the path from the docs and get data back.
61
61
 
62
+ ## Features
63
+
64
+ - **Zero boilerplate** — WB API changes constantly; typed models break on every schema update. Instead, responses are plain attribute-accessible dicts — `result.fieldName` just works, no models to maintain
65
+ - **Auto-pagination** — `get_all()` fetches all page. Auto-detect for all known strategies
66
+ - **Rate limiting** — per-endpoint limits from the spec, powered by `aiolimiter`
67
+ - **Auto-retry** — automatic retry on HTTP 429 with `X-Ratelimit-Retry` backoff
68
+ - **Always up to date** — path registry is [auto-generated](https://github.com/serdukow/wbapi-codegen) from WB OpenAPI specs daily
69
+ - **Fully async** — built on `httpx` + `asyncio`
70
+
62
71
  ## Install
63
72
 
64
73
  ```bash
65
74
  pip install wbapi-async
66
75
  ```
67
76
 
68
- ## Usage
77
+ ## Quick start
69
78
 
70
79
  ```python
71
80
  from wbapi_async import WbAPI
72
81
 
73
82
  async with WbAPI(token="...") as api:
74
- orders = await api.get("/api/v3/orders/new", limit=10, next=0)
75
- print(f"orders: {orders.orders!r}")
76
83
 
77
84
  supplies = await api.get_all("/api/v3/supplies")
78
85
  print(f"supplies: {supplies!r}")
@@ -84,28 +91,4 @@ async with WbAPI(token="...") as api:
84
91
  await api.delete("/content/v2/tag/99")
85
92
  ```
86
93
 
87
- Paths are used exactly as they appear in the [wb api](https://dev.wildberries.ru) —
88
- no need to know which subdomain serves which endpoint, it's resolved automatically.
89
-
90
- ## Features
91
-
92
- - **Zero boilerplate** — no type models, no method classes, just `api.get(path, **params)`
93
- - **Auto-pagination** — `get_all()` fetches all page. Currently auto-detects cursor or offset style
94
- - **Rate limiting** — per-endpoint limits from the spec, powered by `aiolimiter`
95
- - **Auto-retry** — automatic retry on HTTP 429 with `X-Ratelimit-Retry` backoff
96
- - **Always up to date** — path registry is [auto-generated](https://github.com/serdukow/wbapi-codegen) from WB OpenAPI specs daily
97
- - **Fully async** — built on `httpx` + `asyncio`
98
-
99
- ## Custom pagination
100
-
101
- For endpoints with non-standard pagination, pass a `paginator=` callable.
102
- If you've figured out a pattern that works well — feel free to open a [PR](https://github.com/serdukow/wbapi-async/pulls):
103
-
104
- ```python
105
- def my_paginator(response):
106
- items = response.get("result", [])
107
- cursor = response.get("cursor") or None
108
- return items, {"cursor": cursor} if cursor else None
109
-
110
- all_items = await api.get_all("/api/v3/custom", paginator=my_paginator)
111
- ```
94
+ Just copy the path straight from the [WB API](https://dev.wildberries.ru) — the correct base URL and subdomain are resolved automatically.
@@ -4,7 +4,7 @@
4
4
 
5
5
  <div align="center">
6
6
 
7
- ### Lightweight async client for Wildberries Seller API
7
+ #### Lightweight async client for Wildberries Seller API
8
8
 
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
10
10
  [![PyPI version](https://img.shields.io/pypi/v/wild-api.svg)](https://pypi.org/project/wild-api/)
@@ -16,20 +16,27 @@
16
16
  Lightweight async Python client for the [Wildberries Seller API](https://dev.wildberries.ru).
17
17
  Just pass the path from the docs and get data back.
18
18
 
19
+ ## Features
20
+
21
+ - **Zero boilerplate** — WB API changes constantly; typed models break on every schema update. Instead, responses are plain attribute-accessible dicts — `result.fieldName` just works, no models to maintain
22
+ - **Auto-pagination** — `get_all()` fetches all page. Auto-detect for all known strategies
23
+ - **Rate limiting** — per-endpoint limits from the spec, powered by `aiolimiter`
24
+ - **Auto-retry** — automatic retry on HTTP 429 with `X-Ratelimit-Retry` backoff
25
+ - **Always up to date** — path registry is [auto-generated](https://github.com/serdukow/wbapi-codegen) from WB OpenAPI specs daily
26
+ - **Fully async** — built on `httpx` + `asyncio`
27
+
19
28
  ## Install
20
29
 
21
30
  ```bash
22
31
  pip install wbapi-async
23
32
  ```
24
33
 
25
- ## Usage
34
+ ## Quick start
26
35
 
27
36
  ```python
28
37
  from wbapi_async import WbAPI
29
38
 
30
39
  async with WbAPI(token="...") as api:
31
- orders = await api.get("/api/v3/orders/new", limit=10, next=0)
32
- print(f"orders: {orders.orders!r}")
33
40
 
34
41
  supplies = await api.get_all("/api/v3/supplies")
35
42
  print(f"supplies: {supplies!r}")
@@ -41,28 +48,4 @@ async with WbAPI(token="...") as api:
41
48
  await api.delete("/content/v2/tag/99")
42
49
  ```
43
50
 
44
- Paths are used exactly as they appear in the [wb api](https://dev.wildberries.ru) —
45
- no need to know which subdomain serves which endpoint, it's resolved automatically.
46
-
47
- ## Features
48
-
49
- - **Zero boilerplate** — no type models, no method classes, just `api.get(path, **params)`
50
- - **Auto-pagination** — `get_all()` fetches all page. Currently auto-detects cursor or offset style
51
- - **Rate limiting** — per-endpoint limits from the spec, powered by `aiolimiter`
52
- - **Auto-retry** — automatic retry on HTTP 429 with `X-Ratelimit-Retry` backoff
53
- - **Always up to date** — path registry is [auto-generated](https://github.com/serdukow/wbapi-codegen) from WB OpenAPI specs daily
54
- - **Fully async** — built on `httpx` + `asyncio`
55
-
56
- ## Custom pagination
57
-
58
- For endpoints with non-standard pagination, pass a `paginator=` callable.
59
- If you've figured out a pattern that works well — feel free to open a [PR](https://github.com/serdukow/wbapi-async/pulls):
60
-
61
- ```python
62
- def my_paginator(response):
63
- items = response.get("result", [])
64
- cursor = response.get("cursor") or None
65
- return items, {"cursor": cursor} if cursor else None
66
-
67
- all_items = await api.get_all("/api/v3/custom", paginator=my_paginator)
68
- ```
51
+ Just copy the path straight from the [WB API](https://dev.wildberries.ru) — the correct base URL and subdomain are resolved automatically.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "wbapi-async"
7
- version = "0.7.0"
7
+ version = "0.7.2"
8
8
  description = "Asynchronous client for Wildberries API"
9
9
  authors = [
10
10
  { name = "Andrei Serdiukov", email = "asyncdf@gmail.com" },
@@ -5,7 +5,6 @@ from typing import Any
5
5
  from .client.session.base import BaseSession
6
6
  from .method import MethodDispatcher
7
7
  from .type import ApiResponse, _wrap
8
- from .utils.token import validate_token
9
8
 
10
9
 
11
10
  class WbAPI:
@@ -38,7 +37,6 @@ class WbAPI:
38
37
  *,
39
38
  timeout: int = 60,
40
39
  ) -> None:
41
- validate_token(token)
42
40
  self._token = token
43
41
  self.session = session or BaseSession(base="https://wildberries.ru", timeout=timeout)
44
42
  self._dispatcher = MethodDispatcher(self.session, token)
@@ -56,7 +56,10 @@ class BaseSession:
56
56
  raise
57
57
 
58
58
  if response.status_code == 429:
59
- retry_after = int(response.headers.get("X-Ratelimit-Retry", 1))
59
+ retry_after = int(response.headers.get("X-Ratelimit-Retry", "1"))
60
+ log = logging.getLogger("wbapi.session")
61
+ if retry_after > 60:
62
+ log.warning("Rate limit exceeded. Retrying in %s seconds", retry_after)
60
63
  await asyncio.sleep(retry_after)
61
64
  return await self._request(method, url, params=params, json=json, limit=limit)
62
65
 
@@ -69,10 +72,14 @@ class BaseSession:
69
72
 
70
73
  if not response.content:
71
74
  return None
75
+
72
76
  try:
73
77
  return response.json()
74
- except Exception:
75
- return None
78
+ except Exception as e:
79
+ raise WbAPIError(
80
+ http_status=response.status_code,
81
+ detail=f"Failed to decode JSON response: {response.text[:200]!r}",
82
+ ) from e
76
83
 
77
84
  async def get(
78
85
  self,
@@ -242,7 +242,6 @@ _PATH_TO_BASE: dict[str, str] = {
242
242
  "/ping": "https://common-api.wildberries.ru",
243
243
  }
244
244
 
245
- # (period_ms, limit, interval_ms, burst)
246
245
  _PATH_TO_LIMIT: dict[str, tuple[int, int, int, int]] = {
247
246
  "/adv/v0/auction/nms": (1000, 1, 1000, 1),
248
247
  "/adv/v0/auction/placements": (1000, 1, 1000, 1),
@@ -575,9 +574,9 @@ class MethodDispatcher:
575
574
  body = kwargs.pop("body", None)
576
575
  http_method = "POST" if body is not None else "GET"
577
576
 
578
- def _do_request(extra: dict[str, Any] | None = None) -> Any:
577
+ def _do_request(extra: dict[str, Any] | None = None, extra_body: dict[str, Any] | None = None) -> Any:
579
578
  if body is not None:
580
- json = {**body, **(extra or {})}
579
+ json = {**body, **(extra or {}), **(extra_body or {})}
581
580
  return self.dispatch(http_method, path, params=kwargs or None, json=json)
582
581
  params = {**kwargs, **(extra or {})} or None
583
582
  return self.dispatch(http_method, path, params=params)
@@ -593,7 +592,12 @@ class MethodDispatcher:
593
592
  break
594
593
  return result
595
594
 
596
- raw = await _do_request({"limit": _PAGE_SIZE})
595
+ from .utils.paginate import PAGINATION_STRATEGIES
596
+
597
+ first_extra: dict[str, Any] = {"limit": _PAGE_SIZE}
598
+ if body is not None:
599
+ first_extra["offset"] = 0
600
+ raw = await _do_request(first_extra)
597
601
  page = _extract_list(raw)
598
602
 
599
603
  if page is None:
@@ -601,49 +605,10 @@ class MethodDispatcher:
601
605
 
602
606
  result: list[Any] = list(page)
603
607
 
604
- # rrd_id cursor — detected by presence of rrd_id in last item
605
- if page and isinstance(page[-1], dict) and "rrd_id" in page[-1]:
606
- while page:
607
- rrd_id = page[-1]["rrd_id"]
608
- page = _extract_list(await _do_request({"rrdid": rrd_id})) or []
609
- result.extend(page)
610
- return result
611
-
612
- # lastChangeDate cursor — detected by presence of lastChangeDate in last item
613
- if page and isinstance(page[-1], dict) and "lastChangeDate" in page[-1]:
614
- while page:
615
- date_from = page[-1]["lastChangeDate"]
616
- page = _extract_list(await _do_request({"dateFrom": date_from})) or []
617
- result.extend(page)
618
- return result
619
-
620
- # Cursor pagination
621
- if isinstance(raw, dict) and "next" in raw:
622
- cursor = raw["next"]
623
- while cursor:
624
- raw = await _do_request({"limit": _PAGE_SIZE, "next": cursor})
625
- page = _extract_list(raw)
626
- if not page:
627
- break
628
- result.extend(page)
629
- cursor = raw.get("next") if isinstance(raw, dict) else None
630
- return result
631
-
632
- # Offset pagination — first page was full
633
- if len(page) < _PAGE_SIZE:
634
- if not result:
635
- raise PaginationNotSupported(f"{path!r} returned empty first page — pagination not supported")
636
- return result
637
-
638
- offset = _PAGE_SIZE
639
- while True:
640
- raw = await _do_request({"limit": _PAGE_SIZE, "offset": offset})
641
- page = _extract_list(raw)
642
- if not page:
643
- break
644
- result.extend(page)
645
- if len(page) < _PAGE_SIZE:
646
- break
647
- offset += _PAGE_SIZE
608
+ for strategy in PAGINATION_STRATEGIES:
609
+ if strategy.detect(raw, page, body):
610
+ return await strategy.paginate(result, page, raw, _do_request)
648
611
 
612
+ if not result:
613
+ raise PaginationNotSupported(f"{path!r} returned empty first page — pagination not supported")
649
614
  return result
@@ -0,0 +1,114 @@
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
+ ]
@@ -1,86 +0,0 @@
1
- """paginate() — fetch all pages from a paginated API method.
2
-
3
- Example::
4
-
5
- from wbapi_async import WbAPI, paginate
6
-
7
- async with WbAPI(token="...") as api:
8
- products = await paginate(
9
- api.get_products_with_prices, nm_id=123
10
- )
11
- orders = await paginate(
12
- api.get_assembly_orders, date_from=1700000000
13
- )
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- from typing import TYPE_CHECKING, Any
19
-
20
- from pydantic import TypeAdapter
21
-
22
-
23
- if TYPE_CHECKING:
24
- from ..client.api import WbAPI
25
-
26
-
27
- async def paginate(method: Any, **kwargs: Any) -> list[Any]:
28
- """Fetch all pages from a paginated API method and return a combined list.
29
-
30
- Args:
31
- method: A bound method from WbAPI (e.g. ``api.get_products_with_prices``).
32
- **kwargs: Parameters to pass to the method (excluding pagination params).
33
-
34
- Returns:
35
- Combined list of all items from all pages.
36
-
37
- Raises:
38
- TypeError: If the method does not support pagination.
39
-
40
- Example::
41
-
42
- products = await paginate(
43
- api.get_products_with_prices, nm_id=123
44
- )
45
- """
46
- from ..methods.pagination import PAGINATION_STRATEGIES # lazy — avoids circular import
47
-
48
- # Resolve WbAPI instance and WbMethod class from the bound method
49
- wb_api: WbAPI = method.__self__
50
- method_cls = getattr(method, "__wrapped_cls__", None)
51
- if method_cls is None:
52
- raise TypeError(
53
- f"{method.__name__!r} does not expose __wrapped_cls__. "
54
- "Only auto-generated WbAPI methods support paginate()."
55
- )
56
-
57
- pagination_key: str | None = method_cls.__dict__.get("__pagination__")
58
- if not isinstance(pagination_key, str):
59
- raise TypeError(f"{method_cls.__name__} does not support pagination (__pagination__ is not set)")
60
-
61
- strategy = PAGINATION_STRATEGIES[pagination_key]
62
- pagination_params = strategy.first_params()
63
-
64
- # Use model_construct to skip validation — pagination params are injected separately
65
- instance = method_cls.model_construct(**kwargs)
66
- return_type = method_cls.__return__
67
- adapter: TypeAdapter[Any] = TypeAdapter(list[return_type]) # type: ignore[valid-type]
68
-
69
- wb_api.session.headers.set_token(wb_api._token)
70
- url = instance._get_url(wb_api)
71
- request_limit = getattr(instance, "request_limit", None)
72
- http_method = getattr(instance, "__http_method__", "GET").upper()
73
- base_params = instance.model_dump(by_alias=True, exclude_none=True, exclude={"request_limit"})
74
- result: list[Any] = []
75
-
76
- while True:
77
- raw = await instance._dispatch(
78
- wb_api, http_method, url, {**base_params, **pagination_params}, request_limit
79
- )
80
- page_raw = instance._extract(raw)
81
- if not page_raw:
82
- return result
83
- result.extend(adapter.validate_python(page_raw))
84
- pagination_params = strategy.next_params(pagination_params, raw, page_raw)
85
- if pagination_params is None:
86
- return result
@@ -1,63 +0,0 @@
1
- import base64
2
- from functools import lru_cache
3
- import json
4
- import time
5
-
6
- from ..exceptions import TokenValidationError
7
-
8
-
9
- def _decode_part(part: str) -> dict:
10
- padded = part + "=" * (-len(part) % 4)
11
- try:
12
- return json.loads(base64.urlsafe_b64decode(padded))
13
- except Exception as exc:
14
- raise TokenValidationError("Token is invalid! Failed to decode JWT part.") from exc
15
-
16
-
17
- @lru_cache
18
- def validate_token(token: str) -> bool:
19
- """
20
- Validate WB API JWT token (RFC 7519).
21
-
22
- Checks header typ=JWT, required payload fields (id, sid, acc, t, s, exp),
23
- valid acc value (1-4), and token expiry.
24
-
25
- Source: https://dev.wildberries.ru/en/docs/openapi/api-information#tag/Authorization/About-the-token
26
-
27
- :param token: Access token
28
- :return: True if token is valid
29
- :raises TokenValidationError: If token is malformed or expired
30
- """
31
- # "test" is a special token that bypasses validation.
32
- # It exists to allow using unofficial (undocumented) API methods
33
- # that don't require authorization. Not intended for public use.
34
- if token == "test":
35
- return True
36
-
37
- if not isinstance(token, str) or not token:
38
- raise TokenValidationError("Token is invalid! Must be a non-empty string.")
39
-
40
- if any(c.isspace() for c in token):
41
- raise TokenValidationError("Token is invalid! Must not contain whitespace.")
42
-
43
- parts = token.split(".")
44
- if len(parts) != 3:
45
- raise TokenValidationError(f"Token is invalid! JWT must have 3 parts, got {len(parts)}.")
46
-
47
- header = _decode_part(parts[0])
48
- if header.get("typ", "").upper() != "JWT":
49
- raise TokenValidationError(f"Token is invalid! Header typ must be 'JWT', got {header.get('typ')!r}.")
50
-
51
- payload = _decode_part(parts[1])
52
-
53
- for field in ("id", "sid", "acc", "t", "s", "exp"):
54
- if field not in payload:
55
- raise TokenValidationError(f"Token is invalid! Payload missing '{field}' field.")
56
-
57
- if payload["acc"] not in (1, 2, 3, 4):
58
- raise TokenValidationError(f"Token is invalid! Unknown token type acc={payload['acc']!r}.")
59
-
60
- if time.time() > payload["exp"]:
61
- raise TokenValidationError("Token is invalid! Token has expired.")
62
-
63
- return True
@@ -1,20 +0,0 @@
1
- from collections.abc import Callable
2
- from functools import wraps
3
- from typing import Any
4
- import warnings
5
-
6
-
7
- def unofficial(func: Callable[..., Any]) -> Callable[..., Any]:
8
- """Mark a method as unofficial (not documented in WB API docs)."""
9
-
10
- @wraps(func)
11
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
12
- warnings.warn(
13
- f"{func.__name__} is an unofficial method and may stop working at any time.",
14
- stacklevel=2,
15
- )
16
- return await func(*args, **kwargs)
17
-
18
- wrapper.__unofficial__ = True # type: ignore[attr-defined]
19
- wrapper.__doc__ = f".. warning:: Unofficial method — not documented in WB API.\n\n{func.__doc__ or ''}"
20
- return wrapper
File without changes
File without changes