python-wb 0.1.0__py3-none-any.whl
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.
- python_wb-0.1.0.dist-info/METADATA +185 -0
- python_wb-0.1.0.dist-info/RECORD +25 -0
- python_wb-0.1.0.dist-info/WHEEL +5 -0
- python_wb-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_wb-0.1.0.dist-info/top_level.txt +2 -0
- pywb/__init__.py +12 -0
- pywb/__meta__.py +1 -0
- pywb/client/__init__.py +0 -0
- pywb/client/session/__init__.py +0 -0
- pywb/client/session/aiohttp.py +131 -0
- pywb/client/session/base.py +185 -0
- pywb/client/wb_client.py +122 -0
- pywb/enums/__init__.py +7 -0
- pywb/enums/urls.py +39 -0
- pywb/exceptions.py +83 -0
- pywb/methods/__init__.py +12 -0
- pywb/methods/base.py +16 -0
- pywb/methods/ping.py +24 -0
- pywb/methods/statistics.py +19 -0
- pywb/methods/update_product_card.py +16 -0
- pywb/types/__init__.py +7 -0
- pywb/types/order.py +34 -0
- pywb/types/ping_response.py +6 -0
- pywb/utils/errors.py +46 -0
- test.py +18 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-wb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Библиотека для работы с API Wildberries на Python.
|
|
5
|
+
Author-email: MistakeTZ <mistaketz@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/mistaketz/pywb
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/mistaketz/pywb/issues
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: aiohttp>=3.13.5
|
|
15
|
+
Requires-Dist: build>=1.4.2
|
|
16
|
+
Requires-Dist: httpx>=0.28.1
|
|
17
|
+
Requires-Dist: pydantic>=2.12.5
|
|
18
|
+
Requires-Dist: setuptools>=82.0.1
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
## pywb
|
|
22
|
+
|
|
23
|
+
Asynchronous Python client for working with the Wildberries Seller API.
|
|
24
|
+
|
|
25
|
+
The library provides:
|
|
26
|
+
|
|
27
|
+
- async HTTP client based on `aiohttp`
|
|
28
|
+
- typed request/response models via `pydantic`
|
|
29
|
+
- centralized API error mapping to Python exceptions
|
|
30
|
+
- support for multiple Wildberries API domains
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- Easy entry point through `WBClient`
|
|
35
|
+
- Domain routing (`common`, `content`, `statistics`, etc.)
|
|
36
|
+
- Built-in methods:
|
|
37
|
+
- ping (`client.ping()`)
|
|
38
|
+
- content ping (`client.ping_content()`)
|
|
39
|
+
- statistics orders report (`client.get_orders(...)`)
|
|
40
|
+
- etc.
|
|
41
|
+
- Generic low-level method execution:
|
|
42
|
+
- `await client(SomeWBMethod(...))`
|
|
43
|
+
- Context manager support:
|
|
44
|
+
- `async with WBClient(...) as client:`
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- Python 3.12+
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
Using `uv`:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv add python-wb
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
With `pip`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install python-wb
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import asyncio
|
|
68
|
+
from pywb import WBClient
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def main() -> None:
|
|
72
|
+
token = "YOUR_WB_API_TOKEN"
|
|
73
|
+
|
|
74
|
+
async with WBClient(token) as client:
|
|
75
|
+
result = await client.ping()
|
|
76
|
+
print(result.ts, result.status)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
asyncio.run(main())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
### 1) Health check
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
result = await client.ping()
|
|
89
|
+
print(result.status)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 2) Content API health check
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
result = await client.ping_content()
|
|
96
|
+
print(result.status)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3) Get orders from Statistics API
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from datetime import datetime
|
|
103
|
+
|
|
104
|
+
orders = await client.get_orders(
|
|
105
|
+
date_from=datetime(2026, 1, 1, 0, 0, 0),
|
|
106
|
+
flag=0,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if orders:
|
|
110
|
+
print(orders[0].srid, orders[0].brand)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`date_from` accepts either:
|
|
114
|
+
|
|
115
|
+
- ISO 8601 string (example: `"2022-03-04T18:08:31"`)
|
|
116
|
+
- `datetime` object
|
|
117
|
+
|
|
118
|
+
## Low-Level Method Call
|
|
119
|
+
|
|
120
|
+
You can call method objects directly through the client:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from pywb.methods import Ping
|
|
124
|
+
|
|
125
|
+
response = await client(Ping())
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This is useful when adding new method classes while keeping one transport layer.
|
|
129
|
+
|
|
130
|
+
## Error Handling
|
|
131
|
+
|
|
132
|
+
HTTP errors are mapped to dedicated exceptions:
|
|
133
|
+
|
|
134
|
+
- `BadRequestError` (400)
|
|
135
|
+
- `UnauthorizedError` (401)
|
|
136
|
+
- `PaymentRequiredError` (402)
|
|
137
|
+
- `AccessDeniedError` (403)
|
|
138
|
+
- `NotFoundError` (404)
|
|
139
|
+
- `ConflictError` (409)
|
|
140
|
+
- `PayloadTooLargeError` (413)
|
|
141
|
+
- `UnprocessableEntityError` (422)
|
|
142
|
+
- `TooManyRequestsError` (429)
|
|
143
|
+
- `InternalServerError` (5xx)
|
|
144
|
+
|
|
145
|
+
Base type: `WBApiError`
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from pywb.exceptions import BadRequestError
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
await client.get_orders(date_from="2022-03-04T18:08:31")
|
|
154
|
+
except BadRequestError as e:
|
|
155
|
+
print(e)
|
|
156
|
+
print(e.payload)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Network/transport failures in the `aiohttp` session are raised as `WBNetworkError`.
|
|
160
|
+
|
|
161
|
+
## Sandbox and Domains
|
|
162
|
+
|
|
163
|
+
The client supports domain-based URL routing through `WBDomain` and `WB_ROUTER`.
|
|
164
|
+
|
|
165
|
+
To enable sandbox mode (only where available for a domain):
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
client = WBClient(token="YOUR_TOKEN", is_sandbox=True)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
If sandbox is unavailable for a domain, a `ValueError` is raised by the session router.
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
Run the example script:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
python examples/ping.py
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Notes
|
|
182
|
+
|
|
183
|
+
- Keep your API token secret and do not commit it to git.
|
|
184
|
+
- Respect Wildberries API rate limits for each endpoint.
|
|
185
|
+
- For the statistics orders endpoint, use pagination strategy based on the last record timestamp when handling large datasets.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
test.py,sha256=Swoh8k3MnJNbc9uWPPKNHmnawy6bJU6yelmWdPeOwJw,845
|
|
2
|
+
python_wb-0.1.0.dist-info/licenses/LICENSE,sha256=ZVCm8rowdh_H4PlGUkYZ3KUvwsgdfWcmBTgpsuwT-3I,1072
|
|
3
|
+
pywb/__init__.py,sha256=Zbw-zByB-HJO3LCx8f0dqaUXTSu2LYjFntXlOTgecIk,200
|
|
4
|
+
pywb/__meta__.py,sha256=sXLh7g3KC4QCFxcZGBTpG2scR7hmmBsMjq6LqRptkRg,22
|
|
5
|
+
pywb/exceptions.py,sha256=SvWSGh6GO4Qszf3pBmj4VppV-syltkzayhr8WDJeZhI,1961
|
|
6
|
+
pywb/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
pywb/client/wb_client.py,sha256=RfoANIt8Y0nSd_VTNrvujNGu9EJhZQsG11juK5gXLvY,5378
|
|
8
|
+
pywb/client/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
pywb/client/session/aiohttp.py,sha256=tAMIx3NCkKVmYawUiwLq2JAJpZXSBINS915cCQSjGb4,4595
|
|
10
|
+
pywb/client/session/base.py,sha256=7R8KtmemOtvb8uxaBUMmwfhSAyp_w3FI3zDXwS14vXU,6371
|
|
11
|
+
pywb/enums/__init__.py,sha256=Fc9c2c9HoGz-yVf1r1x6_pZSFbLxBgsaBcNGv2aSMuM,116
|
|
12
|
+
pywb/enums/urls.py,sha256=FKwmJFno_viYE0LGJLpzP-9dJ_1Ymp7aP1m_5F4gOGI,2079
|
|
13
|
+
pywb/methods/__init__.py,sha256=pJcfhaujOVlKVK_YvWEi4HyGI_3AdmlL3RV5TSdVhpk,234
|
|
14
|
+
pywb/methods/base.py,sha256=77ZkL3Yl0PlBbDTGkJD5ggl1nWhDm3IdfZJE4RREIz4,416
|
|
15
|
+
pywb/methods/ping.py,sha256=Uce67ZgZ4ZPc9O9dw5txTP04WJJSHpAjEeBeipRdlyw,586
|
|
16
|
+
pywb/methods/statistics.py,sha256=UTMXKmGrcoCcB0IasHUlKwPKB4zPKU4CmTufHq6TIoI,530
|
|
17
|
+
pywb/methods/update_product_card.py,sha256=fsIHhTzlT12t-hmjWrZVDR9Gj-r4-0_py9A-9vxuzP4,372
|
|
18
|
+
pywb/types/__init__.py,sha256=TEubPAI_bqgqC2Ron_62YHZoxt0TiDDigd_B-fOrG0g,131
|
|
19
|
+
pywb/types/order.py,sha256=hJSBLPVoXouy4seo3fh3-L_hqqo_KTVXqt2qAXxpoPg,1316
|
|
20
|
+
pywb/types/ping_response.py,sha256=SIpcGZBWx0GNbNt23XqPBzH-4AIUxXmbK4PQGyC4wt0,143
|
|
21
|
+
pywb/utils/errors.py,sha256=6fPTI7XvAyI8f53xl329aDemUaHBnmdRNEUSVrt1CgU,1131
|
|
22
|
+
python_wb-0.1.0.dist-info/METADATA,sha256=7Fghwm6MxRNeIKtv4iC_nRpYQ7_5rHJqb-WCI_1EMm4,3967
|
|
23
|
+
python_wb-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
24
|
+
python_wb-0.1.0.dist-info/top_level.txt,sha256=ga0M4jXex5QccfVXqDbGMV3mmIaKvNg1HLOGclNqytE,10
|
|
25
|
+
python_wb-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Browser Use Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pywb/__init__.py
ADDED
pywb/__meta__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
pywb/client/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import ssl
|
|
5
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
6
|
+
|
|
7
|
+
import certifi
|
|
8
|
+
from aiohttp import ClientError, ClientSession, TCPConnector
|
|
9
|
+
from typing_extensions import Self
|
|
10
|
+
|
|
11
|
+
from .base import BaseSession, WBType
|
|
12
|
+
from ...enums import WB_ROUTER, WBDomain
|
|
13
|
+
from ...exceptions import WBApiError
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ...methods.base import WBMethod
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WBNetworkError(WBApiError):
|
|
20
|
+
"""Ошибка сети (таймаут, обрыв соединения, DNS и т.д.)"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, message: str, original_exception: Exception | None = None):
|
|
23
|
+
super().__init__(
|
|
24
|
+
status_code=0, payload={"title": "Network Error", "detail": message}
|
|
25
|
+
)
|
|
26
|
+
self.original_exception = original_exception
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AiohttpWBSession(BaseSession):
|
|
30
|
+
"""
|
|
31
|
+
HTTP-сессия на базе aiohttp для работы с Wildberries API.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, is_sandbox: bool = False, limit: int = 100, **kwargs):
|
|
35
|
+
"""
|
|
36
|
+
:param limit: Максимальное количество одновременных соединений (Connection Pooling).
|
|
37
|
+
:param kwargs: Аргументы для BaseSession (base_url, timeout и т.д.).
|
|
38
|
+
"""
|
|
39
|
+
super().__init__(**kwargs)
|
|
40
|
+
|
|
41
|
+
self.is_sandbox = is_sandbox
|
|
42
|
+
self._session: ClientSession | None = None
|
|
43
|
+
self._connector_type: type[TCPConnector] = TCPConnector
|
|
44
|
+
self._connector_init: dict[str, Any] = {
|
|
45
|
+
"ssl": ssl.create_default_context(cafile=certifi.where()),
|
|
46
|
+
"limit": limit,
|
|
47
|
+
"ttl_dns_cache": 3600,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async def create_session(self) -> ClientSession:
|
|
51
|
+
"""Ленивая инициализация aiohttp сессии."""
|
|
52
|
+
if self._session is None or self._session.closed:
|
|
53
|
+
self._session = ClientSession(
|
|
54
|
+
connector=self._connector_type(**self._connector_init),
|
|
55
|
+
headers={"Content-Type": "application/json"},
|
|
56
|
+
)
|
|
57
|
+
return self._session
|
|
58
|
+
|
|
59
|
+
async def close(self) -> None:
|
|
60
|
+
"""Аккуратное закрытие сессии и всех TCP соединений."""
|
|
61
|
+
if self._session is not None and not self._session.closed:
|
|
62
|
+
await self._session.close()
|
|
63
|
+
|
|
64
|
+
await asyncio.sleep(0.25)
|
|
65
|
+
|
|
66
|
+
def _get_url(self, domain: WBDomain, path: str) -> str:
|
|
67
|
+
"""Определяет базовый URL на основе домена и флага Sandbox"""
|
|
68
|
+
domain_urls = WB_ROUTER[domain]
|
|
69
|
+
|
|
70
|
+
if self.is_sandbox:
|
|
71
|
+
if domain_urls["sandbox"] is None:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Sandbox environment is not available for domain: {domain}"
|
|
74
|
+
)
|
|
75
|
+
base_url = domain_urls["sandbox"]
|
|
76
|
+
else:
|
|
77
|
+
base_url = domain_urls["prod"]
|
|
78
|
+
|
|
79
|
+
return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
80
|
+
|
|
81
|
+
async def make_request(
|
|
82
|
+
self,
|
|
83
|
+
token: str,
|
|
84
|
+
method: WBMethod[WBType],
|
|
85
|
+
timeout: int | None = None,
|
|
86
|
+
) -> WBType:
|
|
87
|
+
"""
|
|
88
|
+
Подготавливает данные, выполняет запрос и передает результат валидатору.
|
|
89
|
+
"""
|
|
90
|
+
session = await self.create_session()
|
|
91
|
+
|
|
92
|
+
url = self._get_url(method.__domain__, method.__api_path__)
|
|
93
|
+
|
|
94
|
+
http_method = method.__http_method__.upper()
|
|
95
|
+
|
|
96
|
+
raw_payload = method.model_dump(exclude_none=True)
|
|
97
|
+
|
|
98
|
+
prepared_payload = self.prepare_value(raw_payload)
|
|
99
|
+
|
|
100
|
+
request_kwargs: dict[str, Any] = {}
|
|
101
|
+
if http_method in ("GET", "DELETE"):
|
|
102
|
+
request_kwargs["params"] = prepared_payload
|
|
103
|
+
else:
|
|
104
|
+
request_kwargs["json"] = prepared_payload
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
async with session.request(
|
|
108
|
+
method=http_method,
|
|
109
|
+
url=url,
|
|
110
|
+
headers={"Authorization": token},
|
|
111
|
+
timeout=self.timeout if timeout is None else timeout,
|
|
112
|
+
**request_kwargs,
|
|
113
|
+
) as resp:
|
|
114
|
+
raw_result = await resp.text()
|
|
115
|
+
|
|
116
|
+
except asyncio.TimeoutError as e:
|
|
117
|
+
raise WBNetworkError("Request timeout error", e) from e
|
|
118
|
+
except ClientError as e:
|
|
119
|
+
raise WBNetworkError(f"Network error: {type(e).__name__} - {e}", e) from e
|
|
120
|
+
|
|
121
|
+
response_data = self.check_response(
|
|
122
|
+
method=method,
|
|
123
|
+
status_code=resp.status,
|
|
124
|
+
content=raw_result,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return cast(WBType, response_data)
|
|
128
|
+
|
|
129
|
+
async def __aenter__(self) -> Self:
|
|
130
|
+
await self.create_session()
|
|
131
|
+
return self
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import datetime
|
|
5
|
+
import json
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
10
|
+
from typing_extensions import Self
|
|
11
|
+
|
|
12
|
+
from ...exceptions import (
|
|
13
|
+
AccessDeniedError,
|
|
14
|
+
BadRequestError,
|
|
15
|
+
ConflictError,
|
|
16
|
+
InternalServerError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
PaymentRequiredError,
|
|
19
|
+
PayloadTooLargeError,
|
|
20
|
+
TooManyRequestsError,
|
|
21
|
+
UnauthorizedError,
|
|
22
|
+
UnprocessableEntityError,
|
|
23
|
+
WBApiError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from types import TracebackType
|
|
28
|
+
from ...methods import WBMethod
|
|
29
|
+
|
|
30
|
+
WBType = TypeVar("WBType")
|
|
31
|
+
|
|
32
|
+
_JsonLoads = Callable[..., Any]
|
|
33
|
+
_JsonDumps = Callable[..., str]
|
|
34
|
+
|
|
35
|
+
DEFAULT_TIMEOUT: float = 60.0
|
|
36
|
+
WB_PRODUCTION_API: str = "https://common-api.wildberries.ru"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ClientDecodeError(Exception):
|
|
40
|
+
"""Ошибка при парсинге или валидации ответа от серверов WB"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, message: str, original: Exception, data: Any):
|
|
43
|
+
super().__init__(message)
|
|
44
|
+
self.original = original
|
|
45
|
+
self.data = data
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BaseSession(abc.ABC):
|
|
49
|
+
"""
|
|
50
|
+
Базовый класс для всех HTTP сессий клиента Wildberries.
|
|
51
|
+
Наследуйтесь от него, чтобы реализовать конкретную сессию (например, HttpxSession).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
base_url: str = WB_PRODUCTION_API,
|
|
57
|
+
json_loads: _JsonLoads = json.loads,
|
|
58
|
+
json_dumps: _JsonDumps = json.dumps,
|
|
59
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
60
|
+
) -> None:
|
|
61
|
+
self.base_url = base_url
|
|
62
|
+
self.json_loads = json_loads
|
|
63
|
+
self.json_dumps = json_dumps
|
|
64
|
+
self.timeout = timeout
|
|
65
|
+
|
|
66
|
+
def check_response(
|
|
67
|
+
self,
|
|
68
|
+
method: WBMethod[WBType],
|
|
69
|
+
status_code: int,
|
|
70
|
+
content: str,
|
|
71
|
+
) -> WBType:
|
|
72
|
+
"""
|
|
73
|
+
Проверяет статус ответа и десериализует его в нужный тип Pydantic.
|
|
74
|
+
"""
|
|
75
|
+
if status_code == 204:
|
|
76
|
+
return cast(WBType, None)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
json_data = self.json_loads(content) if content else {}
|
|
80
|
+
except Exception as e:
|
|
81
|
+
raise ClientDecodeError("Failed to decode JSON response", e, content) from e
|
|
82
|
+
|
|
83
|
+
if status_code >= 400:
|
|
84
|
+
self._raise_for_status(status_code, json_data)
|
|
85
|
+
|
|
86
|
+
return_type = method.__returning__
|
|
87
|
+
|
|
88
|
+
if return_type in (bool, dict, list, Any):
|
|
89
|
+
return cast(WBType, json_data)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
adapter = TypeAdapter(return_type)
|
|
93
|
+
validated_data = adapter.validate_python(json_data)
|
|
94
|
+
return cast(WBType, validated_data)
|
|
95
|
+
except ValidationError as e:
|
|
96
|
+
raise ClientDecodeError("Failed to deserialize response into Pydantic model", e, json_data) from e
|
|
97
|
+
|
|
98
|
+
def _raise_for_status(self, status_code: int, payload: dict[str, Any]) -> None:
|
|
99
|
+
"""Внутренний маппинг ошибок HTTP на исключения Python"""
|
|
100
|
+
if status_code == 400:
|
|
101
|
+
raise BadRequestError(status_code, payload)
|
|
102
|
+
if status_code == 401:
|
|
103
|
+
raise UnauthorizedError(status_code, payload)
|
|
104
|
+
if status_code == 402:
|
|
105
|
+
raise PaymentRequiredError(status_code, payload)
|
|
106
|
+
if status_code == 403:
|
|
107
|
+
raise AccessDeniedError(status_code, payload)
|
|
108
|
+
if status_code == 404:
|
|
109
|
+
raise NotFoundError(status_code, payload)
|
|
110
|
+
if status_code == 409:
|
|
111
|
+
raise ConflictError(status_code, payload)
|
|
112
|
+
if status_code == 413:
|
|
113
|
+
raise PayloadTooLargeError(status_code, payload)
|
|
114
|
+
if status_code == 422:
|
|
115
|
+
raise UnprocessableEntityError(status_code, payload)
|
|
116
|
+
if status_code == 429:
|
|
117
|
+
raise TooManyRequestsError(status_code, payload)
|
|
118
|
+
if status_code >= 500:
|
|
119
|
+
raise InternalServerError(status_code, payload)
|
|
120
|
+
|
|
121
|
+
raise WBApiError(status_code, payload)
|
|
122
|
+
|
|
123
|
+
def prepare_value(self, value: Any) -> Any:
|
|
124
|
+
"""
|
|
125
|
+
Подготавливает значения перед отправкой в WB API.
|
|
126
|
+
В отличие от Telegram, WB требует строгий JSON, поэтому
|
|
127
|
+
в основном мы форматируем даты и Enums.
|
|
128
|
+
"""
|
|
129
|
+
if value is None:
|
|
130
|
+
return None
|
|
131
|
+
if isinstance(value, str):
|
|
132
|
+
return value
|
|
133
|
+
if isinstance(value, datetime.datetime):
|
|
134
|
+
# WB обычно ожидает ISO 8601 (например "2024-09-30T06:52:38Z")
|
|
135
|
+
return value.isoformat() + "Z"
|
|
136
|
+
if isinstance(value, Enum):
|
|
137
|
+
return value.value
|
|
138
|
+
if isinstance(value, BaseModel):
|
|
139
|
+
return self.prepare_value(value.model_dump(exclude_none=True))
|
|
140
|
+
if isinstance(value, dict):
|
|
141
|
+
return {k: self.prepare_value(v) for k, v in value.items() if v is not None}
|
|
142
|
+
if isinstance(value, list):
|
|
143
|
+
return [self.prepare_value(v) for v in value if v is not None]
|
|
144
|
+
|
|
145
|
+
return value
|
|
146
|
+
|
|
147
|
+
@abc.abstractmethod
|
|
148
|
+
async def close(self) -> None:
|
|
149
|
+
"""Закрытие HTTP сессии (aiohttp/httpx)"""
|
|
150
|
+
|
|
151
|
+
@abc.abstractmethod
|
|
152
|
+
async def make_request(
|
|
153
|
+
self,
|
|
154
|
+
token: str,
|
|
155
|
+
method: WBMethod[WBType],
|
|
156
|
+
timeout: int | None = None,
|
|
157
|
+
) -> WBType:
|
|
158
|
+
"""
|
|
159
|
+
Фактическое выполнение HTTP запроса.
|
|
160
|
+
Должно быть реализовано в наследнике (HttpxSession или AiohttpSession).
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
async def __call__(
|
|
164
|
+
self,
|
|
165
|
+
token: str,
|
|
166
|
+
method: WBMethod[WBType],
|
|
167
|
+
timeout: int | None = None,
|
|
168
|
+
) -> WBType:
|
|
169
|
+
"""
|
|
170
|
+
Точка входа для выполнения запроса.
|
|
171
|
+
"""
|
|
172
|
+
# TODO: Здесь можно добавить мидлвари (логирование, ретраи, метрики) перед вызовом make_request
|
|
173
|
+
|
|
174
|
+
return await self.make_request(token, method, timeout=timeout)
|
|
175
|
+
|
|
176
|
+
async def __aenter__(self) -> Self:
|
|
177
|
+
return self
|
|
178
|
+
|
|
179
|
+
async def __aexit__(
|
|
180
|
+
self,
|
|
181
|
+
exc_type: type[BaseException] | None,
|
|
182
|
+
exc_value: BaseException | None,
|
|
183
|
+
traceback: TracebackType | None,
|
|
184
|
+
) -> None:
|
|
185
|
+
await self.close()
|
pywb/client/wb_client.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# client.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
from pywb.methods.ping import PingContent
|
|
7
|
+
from pywb.methods.statistics import GetOrders
|
|
8
|
+
from pywb.types.order import StatisticOrder
|
|
9
|
+
|
|
10
|
+
from .session.base import BaseSession
|
|
11
|
+
from .session.aiohttp import AiohttpWBSession
|
|
12
|
+
|
|
13
|
+
from ..methods import Ping, WT
|
|
14
|
+
from ..types import PingResponse
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..methods.base import WBMethod
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WBClient:
|
|
21
|
+
"""
|
|
22
|
+
Асинхронный клиент для работы с API Wildberries.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
token: str,
|
|
28
|
+
is_sandbox: bool = False,
|
|
29
|
+
session: BaseSession | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.token = token
|
|
32
|
+
|
|
33
|
+
if session is None:
|
|
34
|
+
session = AiohttpWBSession(is_sandbox=is_sandbox)
|
|
35
|
+
|
|
36
|
+
self.session = session
|
|
37
|
+
|
|
38
|
+
async def __call__(
|
|
39
|
+
self, method: WBMethod[WT], request_timeout: int | None = None
|
|
40
|
+
) -> WT:
|
|
41
|
+
"""
|
|
42
|
+
Единая точка входа.
|
|
43
|
+
Вызывает метод API и возвращает результат нужного типа.
|
|
44
|
+
"""
|
|
45
|
+
return await self.session(
|
|
46
|
+
token=self.token, method=method, timeout=request_timeout
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def aclose(self) -> None:
|
|
50
|
+
"""Делегируем закрытие соединений сессии"""
|
|
51
|
+
await self.session.close()
|
|
52
|
+
|
|
53
|
+
async def __aenter__(self):
|
|
54
|
+
return self
|
|
55
|
+
|
|
56
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
57
|
+
await self.aclose()
|
|
58
|
+
|
|
59
|
+
async def ping(self, request_timeout: int | None = None) -> PingResponse:
|
|
60
|
+
"""
|
|
61
|
+
Проверяет доступность серверов Wildberries и валидность токена.
|
|
62
|
+
"""
|
|
63
|
+
call = Ping()
|
|
64
|
+
|
|
65
|
+
return await self(call, request_timeout=request_timeout)
|
|
66
|
+
|
|
67
|
+
async def ping_content(self, request_timeout: int | None = None) -> PingResponse:
|
|
68
|
+
"""
|
|
69
|
+
Специальный метод для проверки доступности Content API (для карточек, цен и т.д.).
|
|
70
|
+
Полезно, если у вас есть методы, которые работают только с этим доменом.
|
|
71
|
+
"""
|
|
72
|
+
call = PingContent()
|
|
73
|
+
|
|
74
|
+
return await self(call, request_timeout=request_timeout)
|
|
75
|
+
|
|
76
|
+
async def get_orders(
|
|
77
|
+
self,
|
|
78
|
+
date_from: datetime | str,
|
|
79
|
+
flag: int = 0,
|
|
80
|
+
request_timeout: Optional[int] = None
|
|
81
|
+
) -> list[StatisticOrder]:
|
|
82
|
+
"""
|
|
83
|
+
Возвращает информацию о заказах.
|
|
84
|
+
Данные в этом отчете предварительные и используются для оперативного контроля.
|
|
85
|
+
|
|
86
|
+
⚠️ ЛИМИТЫ И ПРАВИЛА ИСПОЛЬЗОВАНИЯ API:
|
|
87
|
+
---------------------------------------
|
|
88
|
+
- Обновление данных: Каждые 30 минут.
|
|
89
|
+
- Хранение данных: Гарантируется не более 90 дней со дня продажи.
|
|
90
|
+
- Rate Limit: 1 запрос в 1 минуту на один аккаунт продавца (Burst: 1 запрос).
|
|
91
|
+
|
|
92
|
+
ОСОБЕННОСТИ ПАГИНАЦИИ (Лимит строк):
|
|
93
|
+
---------------------------------------
|
|
94
|
+
При запросе с flag=0 или без него установлен условный лимит в 80 000 строк.
|
|
95
|
+
Для получения всех заказов:
|
|
96
|
+
1. В первом запросе передайте начальную дату в `date_from`.
|
|
97
|
+
2. Если вернулось 80 000 строк, возьмите значение `last_change_date`
|
|
98
|
+
из ПОСЛЕДНЕЙ строки ответа и передайте его как `date_from` в следующий запрос.
|
|
99
|
+
3. Если ответ вернул пустой массив `[]` — все заказы получены.
|
|
100
|
+
|
|
101
|
+
ПРИМЕЧАНИЯ:
|
|
102
|
+
---------------------------------------
|
|
103
|
+
- 1 строка = 1 заказ = 1 единица товара.
|
|
104
|
+
- `srid` — уникальный идентификатор заказа.
|
|
105
|
+
- В отчет НЕ попадают заказы без подтвержденной оплаты (например, рассрочка).
|
|
106
|
+
Такие продажи можно найти в детализации отчета о реализации.
|
|
107
|
+
|
|
108
|
+
:param date_from: Дата в формате ISO 8601 (например, "2022-03-04T18:08:31")
|
|
109
|
+
или объект datetime.
|
|
110
|
+
:param flag: 0 (по умолчанию) - получить данные, у которых lastChangeDate >= dateFrom.
|
|
111
|
+
1 - получить данные, у которых date >= dateFrom.
|
|
112
|
+
:param request_timeout: Таймаут запроса в секундах.
|
|
113
|
+
:return: Список объектов StatisticOrder.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
if isinstance(date_from, datetime):
|
|
117
|
+
date_from_str = date_from.replace(microsecond=0).isoformat()
|
|
118
|
+
else:
|
|
119
|
+
date_from_str = date_from
|
|
120
|
+
|
|
121
|
+
call = GetOrders(dateFrom=date_from_str, flag=flag)
|
|
122
|
+
return await self(call, request_timeout=request_timeout)
|
pywb/enums/__init__.py
ADDED
pywb/enums/urls.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Dict, TypedDict
|
|
3
|
+
|
|
4
|
+
class WBDomain(str, Enum):
|
|
5
|
+
CONTENT = "content"
|
|
6
|
+
ANALYTICS = "analytics"
|
|
7
|
+
PRICES = "prices"
|
|
8
|
+
MARKETPLACE = "marketplace"
|
|
9
|
+
STATISTICS = "statistics"
|
|
10
|
+
PROMOTION = "promotion"
|
|
11
|
+
FEEDBACKS = "feedbacks"
|
|
12
|
+
BUYER_CHAT = "buyer_chat"
|
|
13
|
+
SUPPLIES = "supplies"
|
|
14
|
+
RETURNS = "returns"
|
|
15
|
+
DOCUMENTS = "documents"
|
|
16
|
+
FINANCE = "finance"
|
|
17
|
+
COMMON = "common"
|
|
18
|
+
USER_MANAGEMENT = "user_management"
|
|
19
|
+
|
|
20
|
+
class DomainUrls(TypedDict):
|
|
21
|
+
prod: str
|
|
22
|
+
sandbox: str | None
|
|
23
|
+
|
|
24
|
+
WB_ROUTER: Dict[WBDomain, DomainUrls] = {
|
|
25
|
+
WBDomain.CONTENT: {"prod": "https://content-api.wildberries.ru", "sandbox": "https://content-api-sandbox.wildberries.ru"},
|
|
26
|
+
WBDomain.ANALYTICS: {"prod": "https://seller-analytics-api.wildberries.ru", "sandbox": None},
|
|
27
|
+
WBDomain.PRICES: {"prod": "https://discounts-prices-api.wildberries.ru", "sandbox": "https://discounts-prices-api-sandbox.wildberries.ru"},
|
|
28
|
+
WBDomain.MARKETPLACE: {"prod": "https://marketplace-api.wildberries.ru", "sandbox": None},
|
|
29
|
+
WBDomain.STATISTICS: {"prod": "https://statistics-api.wildberries.ru", "sandbox": "https://statistics-api-sandbox.wildberries.ru"},
|
|
30
|
+
WBDomain.PROMOTION: {"prod": "https://advert-api.wildberries.ru", "sandbox": "https://advert-api-sandbox.wildberries.ru"},
|
|
31
|
+
WBDomain.FEEDBACKS: {"prod": "https://feedbacks-api.wildberries.ru", "sandbox": "https://feedbacks-api-sandbox.wildberries.ru"},
|
|
32
|
+
WBDomain.BUYER_CHAT: {"prod": "https://buyer-chat-api.wildberries.ru", "sandbox": None},
|
|
33
|
+
WBDomain.SUPPLIES: {"prod": "https://supplies-api.wildberries.ru", "sandbox": None},
|
|
34
|
+
WBDomain.RETURNS: {"prod": "https://returns-api.wildberries.ru", "sandbox": None},
|
|
35
|
+
WBDomain.DOCUMENTS: {"prod": "https://documents-api.wildberries.ru", "sandbox": None},
|
|
36
|
+
WBDomain.FINANCE: {"prod": "https://finance-api.wildberries.ru", "sandbox": None},
|
|
37
|
+
WBDomain.COMMON: {"prod": "https://common-api.wildberries.ru", "sandbox": None},
|
|
38
|
+
WBDomain.USER_MANAGEMENT: {"prod": "https://user-management-api.wildberries.ru", "sandbox": None},
|
|
39
|
+
}
|
pywb/exceptions.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WBApiError(Exception):
|
|
5
|
+
"""
|
|
6
|
+
Base exception for all Wildberries API errors.
|
|
7
|
+
Automatically parses the WB error JSON schema to provide clear error messages.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, status_code: int, payload: Optional[Dict[str, Any]] = None):
|
|
11
|
+
self.status_code = status_code
|
|
12
|
+
self.payload = payload or {}
|
|
13
|
+
|
|
14
|
+
self.title = self.payload.get("title", "Unknown API Error")
|
|
15
|
+
self.detail = self.payload.get("detail", "")
|
|
16
|
+
self.timestamp = self.payload.get("timestamp", "")
|
|
17
|
+
self.status_text = self.payload.get("statusText", "")
|
|
18
|
+
|
|
19
|
+
message = f"[{self.status_code}] {self.title}"
|
|
20
|
+
if self.detail:
|
|
21
|
+
message += f" | Detail: {self.detail}"
|
|
22
|
+
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BadRequestError(WBApiError):
|
|
27
|
+
"""400: Bad request. Check the request syntax."""
|
|
28
|
+
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UnauthorizedError(WBApiError):
|
|
33
|
+
"""401: Unauthorized. Token is missing, expired, or incorrect."""
|
|
34
|
+
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class PaymentRequiredError(WBApiError):
|
|
39
|
+
"""402: Payment required. Insufficient funds on the Catalog balance."""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AccessDeniedError(WBApiError):
|
|
45
|
+
"""403: Access denied. Deleted user, blocked access, or missing Jam subscription."""
|
|
46
|
+
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NotFoundError(WBApiError):
|
|
51
|
+
"""404: Not found. Check the request URL."""
|
|
52
|
+
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ConflictError(WBApiError):
|
|
57
|
+
"""409: Status update error / Error adding label. Data contradicts limits."""
|
|
58
|
+
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PayloadTooLargeError(WBApiError):
|
|
63
|
+
"""413: The request body size exceeds the given limit."""
|
|
64
|
+
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class UnprocessableEntityError(WBApiError):
|
|
69
|
+
"""422: Error processing request parameters or unexpected result."""
|
|
70
|
+
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TooManyRequestsError(WBApiError):
|
|
75
|
+
"""429: Too many requests. Rate limit exceeded."""
|
|
76
|
+
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class InternalServerError(WBApiError):
|
|
81
|
+
"""5XX: Internal service error. Service is unavailable."""
|
|
82
|
+
|
|
83
|
+
pass
|
pywb/methods/__init__.py
ADDED
pywb/methods/base.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from typing import TypeVar, Generic, ClassVar
|
|
3
|
+
from ..enums import WBDomain
|
|
4
|
+
|
|
5
|
+
WT = TypeVar("WT")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class WBMethod(BaseModel, Generic[WT]):
|
|
9
|
+
"""Базовый класс для всех методов API"""
|
|
10
|
+
|
|
11
|
+
__http_method__: ClassVar[str]
|
|
12
|
+
__api_path__: ClassVar[str]
|
|
13
|
+
__domain__: ClassVar[WBDomain]
|
|
14
|
+
__returning__: ClassVar[type]
|
|
15
|
+
|
|
16
|
+
model_config = {"extra": "forbid"}
|
pywb/methods/ping.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .base import WBMethod
|
|
2
|
+
from ..enums import WBDomain
|
|
3
|
+
from ..types import PingResponse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Ping(WBMethod[PingResponse]):
|
|
7
|
+
__http_method__ = "GET"
|
|
8
|
+
__api_path__ = "/ping"
|
|
9
|
+
__domain__ = WBDomain.COMMON
|
|
10
|
+
__returning__ = PingResponse
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PingContent(WBMethod[PingResponse]):
|
|
14
|
+
__http_method__ = "GET"
|
|
15
|
+
__api_path__ = "/ping"
|
|
16
|
+
__domain__ = WBDomain.CONTENT
|
|
17
|
+
__returning__ = PingResponse
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PingAnalytics(WBMethod[PingResponse]):
|
|
21
|
+
__http_method__ = "GET"
|
|
22
|
+
__api_path__ = "/ping"
|
|
23
|
+
__domain__ = WBDomain.ANALYTICS
|
|
24
|
+
__returning__ = PingResponse
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import ClassVar
|
|
2
|
+
|
|
3
|
+
from pywb.types.order import StatisticOrder
|
|
4
|
+
from .base import WBMethod
|
|
5
|
+
from ..enums import WBDomain
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GetOrders(WBMethod[list[StatisticOrder]]):
|
|
9
|
+
"""
|
|
10
|
+
Команда для получения отчета по заказам.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__http_method__: ClassVar[str] = "GET"
|
|
14
|
+
__api_path__: ClassVar[str] = "/api/v1/supplier/orders"
|
|
15
|
+
__domain__: ClassVar[WBDomain] = WBDomain.STATISTICS
|
|
16
|
+
__returning__: ClassVar[type] = list[StatisticOrder]
|
|
17
|
+
|
|
18
|
+
dateFrom: str
|
|
19
|
+
flag: int = 0
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from .base import WBMethod
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class UpdateProductCard(WBMethod[bool]):
|
|
6
|
+
"""
|
|
7
|
+
Класс-команда для обновления карточки товара.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__http_method__ = "POST"
|
|
11
|
+
__api_path__ = "/content/v1/cards/update"
|
|
12
|
+
__returning__ = bool
|
|
13
|
+
|
|
14
|
+
card_id: str
|
|
15
|
+
price: int
|
|
16
|
+
discount: Optional[int] = None
|
pywb/types/__init__.py
ADDED
pywb/types/order.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class StatisticOrder(BaseModel):
|
|
6
|
+
"""Модель информации о заказе из API Статистики."""
|
|
7
|
+
|
|
8
|
+
date: datetime
|
|
9
|
+
last_change_date: datetime = Field(alias="lastChangeDate")
|
|
10
|
+
warehouse_name: str = Field(alias="warehouseName")
|
|
11
|
+
warehouse_type: str = Field(alias="warehouseType")
|
|
12
|
+
country_name: str = Field(alias="countryName")
|
|
13
|
+
oblast_okrug_name: str = Field(alias="oblastOkrugName")
|
|
14
|
+
region_name: str = Field(alias="regionName")
|
|
15
|
+
supplier_article: str = Field(alias="supplierArticle")
|
|
16
|
+
nm_id: int = Field(alias="nmId")
|
|
17
|
+
barcode: str
|
|
18
|
+
category: str
|
|
19
|
+
subject: str
|
|
20
|
+
brand: str
|
|
21
|
+
tech_size: str = Field(alias="techSize")
|
|
22
|
+
income_id: int = Field(alias="incomeID")
|
|
23
|
+
is_supply: bool = Field(alias="isSupply")
|
|
24
|
+
is_realization: bool = Field(alias="isRealization")
|
|
25
|
+
total_price: float = Field(alias="totalPrice")
|
|
26
|
+
discount_percent: int = Field(alias="discountPercent")
|
|
27
|
+
spp: float
|
|
28
|
+
finished_price: float = Field(alias="finishedPrice")
|
|
29
|
+
price_with_disc: float = Field(alias="priceWithDisc")
|
|
30
|
+
is_cancel: bool = Field(alias="isCancel")
|
|
31
|
+
cancel_date: datetime = Field(alias="cancelDate")
|
|
32
|
+
sticker: str
|
|
33
|
+
g_number: str = Field(alias="gNumber")
|
|
34
|
+
srid: str
|
pywb/utils/errors.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from ..exceptions import (
|
|
4
|
+
BadRequestError,
|
|
5
|
+
ConflictError,
|
|
6
|
+
InternalServerError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
PayloadTooLargeError,
|
|
9
|
+
PaymentRequiredError,
|
|
10
|
+
TooManyRequestsError,
|
|
11
|
+
UnauthorizedError,
|
|
12
|
+
UnprocessableEntityError,
|
|
13
|
+
AccessDeniedError,
|
|
14
|
+
WBApiError,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_EXCEPTION_MAPPING = {
|
|
19
|
+
400: BadRequestError,
|
|
20
|
+
401: UnauthorizedError,
|
|
21
|
+
402: PaymentRequiredError,
|
|
22
|
+
403: AccessDeniedError,
|
|
23
|
+
404: NotFoundError,
|
|
24
|
+
409: ConflictError,
|
|
25
|
+
413: PayloadTooLargeError,
|
|
26
|
+
422: UnprocessableEntityError,
|
|
27
|
+
429: TooManyRequestsError,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def raise_for_status(
|
|
32
|
+
status_code: int,
|
|
33
|
+
response_json: Optional[Dict[str, Any]] = None,
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Helper function to raise the appropriate exception based on the status code.
|
|
37
|
+
If the status code is 200 or 204, it does nothing.
|
|
38
|
+
"""
|
|
39
|
+
if status_code in (200, 204):
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if 500 <= status_code < 600:
|
|
43
|
+
raise InternalServerError(status_code, response_json)
|
|
44
|
+
|
|
45
|
+
exception_class = _EXCEPTION_MAPPING.get(status_code, WBApiError)
|
|
46
|
+
raise exception_class(status_code, response_json)
|
test.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pywb import WBClient
|
|
3
|
+
from pywb.exceptions import BadRequestError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def main():
|
|
7
|
+
token = "eyJhbGciOiJFUzI1NiIsImtpZCI6IjIwMjYwMzAydjEiLCJ0eXAiOiJKV1QifQ.eyJhY2MiOjEsImVudCI6MSwiZXhwIjoxNzkwMzk3MTQxLCJpZCI6IjAxOWQzMDIzLWJkMGMtNzYyNy1hZmQ4LTVmOGIwMWVmOGEwYyIsImlpZCI6MTI4NDg1NDE5LCJvaWQiOjI1MDEyNDEwOCwicyI6MTYxMjYsInNpZCI6ImJjMDRiNDUyLTlkMWMtNDM3MC1hZDUwLTc1NDQzYmJiZWQzMCIsInQiOmZhbHNlLCJ1aWQiOjEyODQ4NTQxOX0.r2N1FXLRM8PZM43PzkKa4HgrdFSYf5Nz3s2JXUPrcUOZ0RicfvWqa4JowK6FVDps8pHoqsnHDukuRLvNKqvCbQ"
|
|
8
|
+
|
|
9
|
+
async with WBClient(token, is_sandbox=False) as client:
|
|
10
|
+
try:
|
|
11
|
+
result = await client.get_orders(date_from="2022-03-04T18:08:31")
|
|
12
|
+
print(result[0].brand)
|
|
13
|
+
except BadRequestError as e:
|
|
14
|
+
print(f"Error: {e}", e.payload)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
asyncio.run(main())
|