lingxingapi 1.1.4__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.
- lingxingapi/__init__.py +7 -0
- lingxingapi/ads/__init__.py +0 -0
- lingxingapi/ads/api.py +5946 -0
- lingxingapi/ads/param.py +192 -0
- lingxingapi/ads/route.py +134 -0
- lingxingapi/ads/schema.py +2615 -0
- lingxingapi/api.py +557 -0
- lingxingapi/base/__init__.py +0 -0
- lingxingapi/base/api.py +568 -0
- lingxingapi/base/param.py +59 -0
- lingxingapi/base/route.py +11 -0
- lingxingapi/base/schema.py +198 -0
- lingxingapi/basic/__init__.py +0 -0
- lingxingapi/basic/api.py +466 -0
- lingxingapi/basic/param.py +72 -0
- lingxingapi/basic/route.py +20 -0
- lingxingapi/basic/schema.py +218 -0
- lingxingapi/errors.py +152 -0
- lingxingapi/fba/__init__.py +0 -0
- lingxingapi/fba/api.py +1691 -0
- lingxingapi/fba/param.py +250 -0
- lingxingapi/fba/route.py +30 -0
- lingxingapi/fba/schema.py +987 -0
- lingxingapi/fields.py +50 -0
- lingxingapi/finance/__init__.py +0 -0
- lingxingapi/finance/api.py +3091 -0
- lingxingapi/finance/param.py +616 -0
- lingxingapi/finance/route.py +44 -0
- lingxingapi/finance/schema.py +1243 -0
- lingxingapi/product/__init__.py +0 -0
- lingxingapi/product/api.py +2643 -0
- lingxingapi/product/param.py +934 -0
- lingxingapi/product/route.py +49 -0
- lingxingapi/product/schema.py +1004 -0
- lingxingapi/purchase/__init__.py +0 -0
- lingxingapi/purchase/api.py +496 -0
- lingxingapi/purchase/param.py +126 -0
- lingxingapi/purchase/route.py +11 -0
- lingxingapi/purchase/schema.py +215 -0
- lingxingapi/sales/__init__.py +0 -0
- lingxingapi/sales/api.py +3200 -0
- lingxingapi/sales/param.py +723 -0
- lingxingapi/sales/route.py +70 -0
- lingxingapi/sales/schema.py +1718 -0
- lingxingapi/source/__init__.py +0 -0
- lingxingapi/source/api.py +1799 -0
- lingxingapi/source/param.py +176 -0
- lingxingapi/source/route.py +38 -0
- lingxingapi/source/schema.py +1011 -0
- lingxingapi/tools/__init__.py +0 -0
- lingxingapi/tools/api.py +291 -0
- lingxingapi/tools/param.py +73 -0
- lingxingapi/tools/route.py +8 -0
- lingxingapi/tools/schema.py +169 -0
- lingxingapi/utils.py +456 -0
- lingxingapi/warehourse/__init__.py +0 -0
- lingxingapi/warehourse/api.py +1778 -0
- lingxingapi/warehourse/param.py +506 -0
- lingxingapi/warehourse/route.py +28 -0
- lingxingapi/warehourse/schema.py +926 -0
- lingxingapi-1.1.4.dist-info/METADATA +73 -0
- lingxingapi-1.1.4.dist-info/RECORD +65 -0
- lingxingapi-1.1.4.dist-info/WHEEL +5 -0
- lingxingapi-1.1.4.dist-info/licenses/LICENSE +22 -0
- lingxingapi-1.1.4.dist-info/top_level.txt +1 -0
lingxingapi/base/api.py
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-c
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from typing_extensions import Self
|
|
4
|
+
from asyncio import sleep as _aio_sleep
|
|
5
|
+
from orjson import loads as _orjson_loads
|
|
6
|
+
from Crypto.Cipher._mode_ecb import EcbMode
|
|
7
|
+
from aiohttp import (
|
|
8
|
+
TCPConnector,
|
|
9
|
+
ClientSession,
|
|
10
|
+
ClientConnectorError,
|
|
11
|
+
ClientTimeout,
|
|
12
|
+
ServerDisconnectedError,
|
|
13
|
+
)
|
|
14
|
+
from lingxingapi import utils, errors
|
|
15
|
+
from lingxingapi.base import route, schema
|
|
16
|
+
|
|
17
|
+
# Type Aliases ---------------------------------------------------------------------------------------------------------
|
|
18
|
+
REQUEST_METHOD = Literal["GET", "POST", "PUT", "DELETE"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# API ------------------------------------------------------------------------------------------------------------------
|
|
22
|
+
class BaseAPI:
|
|
23
|
+
"""领星 API 基础类, 提供公共方法和属性, 用于子类继承
|
|
24
|
+
|
|
25
|
+
## Notice
|
|
26
|
+
请勿直接实例化此类
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# HTTP 会话
|
|
30
|
+
_session: ClientSession = None
|
|
31
|
+
# Token 令牌
|
|
32
|
+
_access_token: str = None
|
|
33
|
+
_refresh_token: str = None
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
app_id: str,
|
|
38
|
+
app_secret: str,
|
|
39
|
+
app_cipher: EcbMode,
|
|
40
|
+
timeout: int | float,
|
|
41
|
+
ignore_timeout: bool,
|
|
42
|
+
ignore_timeout_wait: int | float,
|
|
43
|
+
ignore_timeout_retry: int,
|
|
44
|
+
ignore_api_limit: bool,
|
|
45
|
+
ignore_api_limit_wait: int | float,
|
|
46
|
+
ignore_api_limit_retry: int,
|
|
47
|
+
ignore_internal_server_error: bool,
|
|
48
|
+
ignore_internal_server_error_wait: int | float,
|
|
49
|
+
ignore_internal_server_error_retry: int,
|
|
50
|
+
ignore_internet_connection: bool,
|
|
51
|
+
ignore_internet_connection_wait: int | float,
|
|
52
|
+
ignore_internet_connection_retry: int,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""领星 API 基础类, 提供公共方法和属性供子类继承使用
|
|
55
|
+
|
|
56
|
+
## Notice
|
|
57
|
+
请勿直接实例化此类
|
|
58
|
+
|
|
59
|
+
:param app_id `<'str'>`: 应用ID, 用于鉴权
|
|
60
|
+
|
|
61
|
+
:param app_secret `<'str'>`: 应用密钥, 用于鉴权
|
|
62
|
+
|
|
63
|
+
:param app_cipher `<'EcbMode'>`: 基于 appId 创建的 AES-ECB 加密器
|
|
64
|
+
|
|
65
|
+
:param timeout `<'int/float'>`: 请求超时 (单位: 秒)
|
|
66
|
+
|
|
67
|
+
:param ignore_timeout `<'bool'>`: 是否忽略请求超时错误
|
|
68
|
+
|
|
69
|
+
- 如果设置为 `True`, 则在遇到请求超时错误时不会抛出异常, 而是会等待
|
|
70
|
+
`ignore_timeout_wait` 秒后重试请求, 重试次数不超过 `ignore_timeout_retry`
|
|
71
|
+
- 如果设置为 `False`, 则在遇到请求超时错误时直接抛出 `ApiTimeoutError` 异常
|
|
72
|
+
|
|
73
|
+
:param ignore_timeout_wait `<'int/float'>`: 忽略请求超时错误时的等待时间 (单位: 秒),
|
|
74
|
+
仅在 `ignore_timeout` 为 `True` 时生效
|
|
75
|
+
|
|
76
|
+
:param ignore_timeout_retry `<'int'>`: 忽略请求超时错误时的最大重试次数,
|
|
77
|
+
仅在 `ignore_timeout` 为 `True` 时生效, 若设置为 `-1` 则表示无限重试
|
|
78
|
+
|
|
79
|
+
:param ignore_api_limit `<'bool'>`: 是否忽略 API 限流错误
|
|
80
|
+
|
|
81
|
+
- 如果设置为 `True`, 则在遇到限流错误时不会抛出异常, 而是会等待
|
|
82
|
+
`ignore_api_limit_wait` 秒后重试请求, 重试次数不超过 `ignore_api_limit_retry`
|
|
83
|
+
- 如果设置为 `False`, 则在遇到限流错误时直接抛出 `ApiLimitError` 异常
|
|
84
|
+
|
|
85
|
+
:param ignore_api_limit_wait `<'int/float'>`: 忽略 API 限流错误时的等待时间 (单位: 秒),
|
|
86
|
+
仅在 `ignore_api_limit` 为 `True` 时生效
|
|
87
|
+
|
|
88
|
+
:param ignore_api_limit_retry `<'int'>`: 忽略 API 限流错误时的最大重试次数,
|
|
89
|
+
仅在 `ignore_api_limit` 为 `True` 时生效, 若设置为 `-1` 则表示无限重试
|
|
90
|
+
|
|
91
|
+
:param ignore_internal_server_error `<'bool'>`: 是否忽略服务器内部错误 (500错误码, 仅限 Internal Server Error 类型)
|
|
92
|
+
|
|
93
|
+
- 如果设置为 `True`, 则在遇到服务器内部错误时不会抛出异常, 而是会等待
|
|
94
|
+
`ignore_internal_server_error_wait` 秒后重试请求, 重试次数不超过 `ignore_internal_server_error_retry`
|
|
95
|
+
- 如果设置为 `False`, 则在遇到服务器内部错误时直接抛出 `InternalServerError` 异常
|
|
96
|
+
|
|
97
|
+
:param ignore_internal_server_error_wait `<'int/float'>`: 忽略服务器内部错误时的等待时间 (单位: 秒),
|
|
98
|
+
仅在 `ignore_internal_server_error` 为 `True` 时生效
|
|
99
|
+
|
|
100
|
+
:param ignore_internal_server_error_retry `<'int'>`: 忽略服务器内部错误时的最大重试次数,
|
|
101
|
+
仅在 `ignore_internal_server_error` 为 `True` 时生效, 若设置为 `-1` 则表示无限重试
|
|
102
|
+
"""
|
|
103
|
+
# API 凭证
|
|
104
|
+
self._app_id: str = app_id
|
|
105
|
+
self._app_secret: str = app_secret
|
|
106
|
+
self._app_cipher: EcbMode = app_cipher
|
|
107
|
+
# HTTP 会话
|
|
108
|
+
self._timeout: ClientTimeout = timeout
|
|
109
|
+
# 错误处理
|
|
110
|
+
# . 请求超时
|
|
111
|
+
self._ignore_timeout: bool = ignore_timeout
|
|
112
|
+
self._ignore_timeout_wait: float = ignore_timeout_wait
|
|
113
|
+
self._ignore_timeout_retry: int = ignore_timeout_retry
|
|
114
|
+
self._infinite_timeout_retry: bool = ignore_timeout_retry == -1
|
|
115
|
+
# . API 限流
|
|
116
|
+
self._ignore_api_limit: bool = ignore_api_limit
|
|
117
|
+
self._ignore_api_limit_wait: float = ignore_api_limit_wait
|
|
118
|
+
self._ignore_api_limit_retry: int = ignore_api_limit_retry
|
|
119
|
+
self._infinite_retry: bool = ignore_api_limit_retry == -1
|
|
120
|
+
# . 服务器内部错误 (500错误码, 仅限 Internal Server Error 类型)
|
|
121
|
+
self._ignore_internal_server_error: bool = ignore_internal_server_error
|
|
122
|
+
self._ignore_internal_server_error_wait: float = (
|
|
123
|
+
ignore_internal_server_error_wait
|
|
124
|
+
)
|
|
125
|
+
self._ignore_internal_server_error_retry: int = (
|
|
126
|
+
ignore_internal_server_error_retry
|
|
127
|
+
)
|
|
128
|
+
self._infinite_internal_server_error_retry: bool = (
|
|
129
|
+
ignore_internal_server_error_retry == -1
|
|
130
|
+
)
|
|
131
|
+
# . 无法链接互联网
|
|
132
|
+
self._ignore_internet_connection: bool = ignore_internet_connection
|
|
133
|
+
self._ignore_internet_connection_wait: float = ignore_internet_connection_wait
|
|
134
|
+
self._ignore_internet_connection_retry: int = ignore_internet_connection_retry
|
|
135
|
+
self._infinite_internet_connection_retry: bool = (
|
|
136
|
+
ignore_internet_connection_retry == -1
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def __aenter__(self) -> Self:
|
|
140
|
+
"""进入 API 客户端异步上下文管理器
|
|
141
|
+
|
|
142
|
+
:returns `<'API'>`: 返回 API 客户端实例
|
|
143
|
+
"""
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
147
|
+
"""退出 API 客户端异步上下文管理器, 并关闭 HTTP 会话"""
|
|
148
|
+
await self.close()
|
|
149
|
+
|
|
150
|
+
async def close(self) -> None:
|
|
151
|
+
"""关闭 API 客户端的 HTTP 会话"""
|
|
152
|
+
if BaseAPI._session is not None:
|
|
153
|
+
await BaseAPI._session.close()
|
|
154
|
+
BaseAPI._session = None
|
|
155
|
+
|
|
156
|
+
# 公共 API --------------------------------------------------------------------------------------
|
|
157
|
+
# . Token 令牌
|
|
158
|
+
async def _AccessToken(self) -> schema.Token:
|
|
159
|
+
"""(Internal) 获取领星 API 的访问令牌
|
|
160
|
+
|
|
161
|
+
## Docs
|
|
162
|
+
- 授权: [获取 access-token和refresh-token](https://apidoc.lingxing.com/#/docs/Authorization/GetToken)
|
|
163
|
+
|
|
164
|
+
:returns `<'Token'>`: 返回接口的访问令牌
|
|
165
|
+
```python
|
|
166
|
+
{
|
|
167
|
+
# 接口的访问令牌
|
|
168
|
+
"access_token": "your_access_token",
|
|
169
|
+
# 用于续约 access_token 的更新令牌
|
|
170
|
+
"refresh_token": "your_refresh_token",
|
|
171
|
+
# 访问令牌的有效时间 (单位: 秒)
|
|
172
|
+
"expires_in": 3600
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
"""
|
|
176
|
+
data = await self._request(
|
|
177
|
+
"POST",
|
|
178
|
+
route.AUTH_GET_TOKEN,
|
|
179
|
+
{"appId": self._app_id, "appSecret": self._app_secret},
|
|
180
|
+
extract_data=True,
|
|
181
|
+
)
|
|
182
|
+
res = schema.Token.model_validate(data)
|
|
183
|
+
BaseAPI._access_token = res.access_token
|
|
184
|
+
BaseAPI._refresh_token = res.refresh_token
|
|
185
|
+
return res
|
|
186
|
+
|
|
187
|
+
async def _RefreshToken(self, refresh_token: str) -> schema.Token:
|
|
188
|
+
"""(Internal) 基于 refresh_token 刷新领星 API 的访问令牌
|
|
189
|
+
|
|
190
|
+
## Docs
|
|
191
|
+
- 授权: [续约接口令牌](https://apidoc.lingxing.com/#/docs/Authorization/RefreshToken)
|
|
192
|
+
|
|
193
|
+
:param refresh_token `<'str'>`: 用于续约 refresh_token 的令牌
|
|
194
|
+
:returns `<'Token'>`: 返回接口的访问令牌
|
|
195
|
+
```python
|
|
196
|
+
{
|
|
197
|
+
# 接口的访问令牌
|
|
198
|
+
"access_token": "your_access_token",
|
|
199
|
+
# 用于续约 access_token 的更新令牌
|
|
200
|
+
"refresh_token": "your_refresh_token",
|
|
201
|
+
# 访问令牌的有效时间 (单位: 秒)
|
|
202
|
+
"expires_in": 3600
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
"""
|
|
206
|
+
data = await self._request(
|
|
207
|
+
"POST",
|
|
208
|
+
route.AUTH_REFRESH_TOKEN,
|
|
209
|
+
{"appId": self._app_id, "refreshToken": str(refresh_token)},
|
|
210
|
+
extract_data=True,
|
|
211
|
+
)
|
|
212
|
+
res = schema.Token.model_validate(data)
|
|
213
|
+
BaseAPI._access_token = res.access_token
|
|
214
|
+
BaseAPI._refresh_token = res.refresh_token
|
|
215
|
+
return res
|
|
216
|
+
|
|
217
|
+
async def _UpdateToken(self) -> None:
|
|
218
|
+
"""(internal) 获取或刷新 access_token, 如果缓存了 refresh_token,
|
|
219
|
+
则优先基于 refresh_token 进行刷新
|
|
220
|
+
"""
|
|
221
|
+
# 如果存在 refresh token, 则使用它刷新 access token
|
|
222
|
+
if BaseAPI._refresh_token is not None:
|
|
223
|
+
try:
|
|
224
|
+
await self._RefreshToken(BaseAPI._refresh_token)
|
|
225
|
+
return None
|
|
226
|
+
except errors.TokenExpiredError:
|
|
227
|
+
pass
|
|
228
|
+
# 如果不存在 refresh token, 则重新获取 access token
|
|
229
|
+
await self._AccessToken()
|
|
230
|
+
|
|
231
|
+
# 核心 HTTP 逻辑 ---------------------------------------------------------------------------------
|
|
232
|
+
async def _request(
|
|
233
|
+
self,
|
|
234
|
+
method: REQUEST_METHOD,
|
|
235
|
+
url: str,
|
|
236
|
+
params: dict,
|
|
237
|
+
body: dict | None = None,
|
|
238
|
+
headers: dict | None = None,
|
|
239
|
+
extract_data: bool = False,
|
|
240
|
+
) -> dict | list:
|
|
241
|
+
"""(internal) 发送基础 HTTP 请求, 并对状态码与业务响应数据进行校验
|
|
242
|
+
|
|
243
|
+
:param method `<'str'>`: HTTP 请求方法, 如: `"GET"`, `"POST"`, `"PUT"`, `"DELETE"`
|
|
244
|
+
:param url `<'str'>`: 业务请求路径, 如: `"/api/auth-server/oauth/access-token"`
|
|
245
|
+
:param params `<'dict'>`: 必填公共参数, 包含 (`app_key`, `access_token`, `timestamp`, `sign`)
|
|
246
|
+
:param body `<'dict/None'>`: 可选业务请求参数, 用于 POST/PUT 请求, 默认 `None`
|
|
247
|
+
:param headers `<'dict/None'>`: 可选请求头, 如: `{"X-API-VERSION": "2"}`, 默认 `None`
|
|
248
|
+
:param extract_data `<'bool'>`: 是否提取响应数据中的 `data` 字段并直接返回, 默认为 `False`
|
|
249
|
+
:returns `<'list/dict'>`: 返回解析并验证后响应数据
|
|
250
|
+
"""
|
|
251
|
+
# 确保 HTTP 会话可用
|
|
252
|
+
if BaseAPI._session is None or BaseAPI._session.closed:
|
|
253
|
+
BaseAPI._session = ClientSession(
|
|
254
|
+
route.API_SERVER,
|
|
255
|
+
headers={"Content-Type": "application/json"},
|
|
256
|
+
timeout=self._timeout,
|
|
257
|
+
connector=TCPConnector(limit=100),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# 发送请求
|
|
261
|
+
retry_count = 0
|
|
262
|
+
while True:
|
|
263
|
+
try:
|
|
264
|
+
async with BaseAPI._session.request(
|
|
265
|
+
method,
|
|
266
|
+
url,
|
|
267
|
+
params=params,
|
|
268
|
+
json=body,
|
|
269
|
+
headers=headers,
|
|
270
|
+
) as res:
|
|
271
|
+
# . 检查响应状态码
|
|
272
|
+
if res.status != 200:
|
|
273
|
+
raise errors.ServerError(
|
|
274
|
+
"领星API服务器响应错误", url, res.reason, res.status
|
|
275
|
+
)
|
|
276
|
+
# . 解析并验证响应数据
|
|
277
|
+
return self._handle_response_data(
|
|
278
|
+
url, await res.read(), extract_data
|
|
279
|
+
)
|
|
280
|
+
# fmt: off
|
|
281
|
+
except errors.ApiLimitError as err:
|
|
282
|
+
if (
|
|
283
|
+
self._ignore_api_limit
|
|
284
|
+
and (self._infinite_retry or retry_count < self._ignore_api_limit_retry)
|
|
285
|
+
):
|
|
286
|
+
await _aio_sleep(self._ignore_api_limit_wait)
|
|
287
|
+
retry_count += 1
|
|
288
|
+
continue
|
|
289
|
+
if params is not None:
|
|
290
|
+
err.add_note("请求参数: %r" % params)
|
|
291
|
+
if body is not None:
|
|
292
|
+
err.add_note("请求实体: %r" % body)
|
|
293
|
+
if retry_count > 0:
|
|
294
|
+
err.add_note("请求重试: %d" % retry_count)
|
|
295
|
+
raise err
|
|
296
|
+
except errors.InternalServerError as err:
|
|
297
|
+
if (
|
|
298
|
+
self._ignore_internal_server_error
|
|
299
|
+
and (self._infinite_internal_server_error_retry or retry_count < self._ignore_internal_server_error_retry)
|
|
300
|
+
):
|
|
301
|
+
await _aio_sleep(self._ignore_internal_server_error_wait)
|
|
302
|
+
retry_count += 1
|
|
303
|
+
continue
|
|
304
|
+
if params is not None:
|
|
305
|
+
err.add_note("请求参数: %r" % params)
|
|
306
|
+
if body is not None:
|
|
307
|
+
err.add_note("请求实体: %r" % body)
|
|
308
|
+
if retry_count > 0:
|
|
309
|
+
err.add_note("请求重试: %d" % retry_count)
|
|
310
|
+
raise err
|
|
311
|
+
except (TimeoutError, ServerDisconnectedError) as err:
|
|
312
|
+
# 无法链接互联网
|
|
313
|
+
if not await utils.check_internet_tcp():
|
|
314
|
+
if (
|
|
315
|
+
self._ignore_internet_connection
|
|
316
|
+
and (self._infinite_internet_connection_retry or retry_count < self._ignore_internet_connection_retry)
|
|
317
|
+
):
|
|
318
|
+
await _aio_sleep(self._ignore_internet_connection_wait)
|
|
319
|
+
retry_count += 1
|
|
320
|
+
continue
|
|
321
|
+
exc = errors.InternetConnectionError("无法链接互联网, 请检查网络连接", url, str(err))
|
|
322
|
+
if params is not None:
|
|
323
|
+
exc.add_note("请求参数: %r" % params)
|
|
324
|
+
if body is not None:
|
|
325
|
+
exc.add_note("请求实体: %r" % body)
|
|
326
|
+
if retry_count > 0:
|
|
327
|
+
exc.add_note("请求重试: %d" % retry_count)
|
|
328
|
+
raise exc from err
|
|
329
|
+
# 请求超时
|
|
330
|
+
if (
|
|
331
|
+
self._ignore_timeout
|
|
332
|
+
and (self._infinite_timeout_retry or retry_count < self._ignore_timeout_retry)
|
|
333
|
+
):
|
|
334
|
+
await _aio_sleep(self._ignore_timeout_wait)
|
|
335
|
+
retry_count += 1
|
|
336
|
+
continue
|
|
337
|
+
exc = errors.ApiTimeoutError("领星 API 请求超时", url, str(err))
|
|
338
|
+
if params is not None:
|
|
339
|
+
exc.add_note("请求参数: %r" % params)
|
|
340
|
+
if body is not None:
|
|
341
|
+
exc.add_note("请求实体: %r" % body)
|
|
342
|
+
if retry_count > 0:
|
|
343
|
+
exc.add_note("请求重试: %d" % retry_count)
|
|
344
|
+
exc.add_note("超时时间: %s" % self._timeout)
|
|
345
|
+
raise exc from err
|
|
346
|
+
except ClientConnectorError as err:
|
|
347
|
+
# 无法链接互联网
|
|
348
|
+
if not await utils.check_internet_tcp():
|
|
349
|
+
if (
|
|
350
|
+
self._ignore_internet_connection
|
|
351
|
+
and (self._infinite_internet_connection_retry or retry_count < self._ignore_internet_connection_retry)
|
|
352
|
+
):
|
|
353
|
+
await _aio_sleep(self._ignore_internet_connection_wait)
|
|
354
|
+
retry_count += 1
|
|
355
|
+
continue
|
|
356
|
+
exc = errors.InternetConnectionError("无法链接互联网, 请检查网络连接", url, str(err))
|
|
357
|
+
if params is not None:
|
|
358
|
+
exc.add_note("请求参数: %r" % params)
|
|
359
|
+
if body is not None:
|
|
360
|
+
exc.add_note("请求实体: %r" % body)
|
|
361
|
+
if retry_count > 0:
|
|
362
|
+
exc.add_note("请求重试: %d" % retry_count)
|
|
363
|
+
raise exc from err
|
|
364
|
+
# Server 无响应
|
|
365
|
+
raise errors.ServerError("领星 API 服务器无响应, 若无网络问题, 请检查账号 IP 白名单设置", url, err) from err
|
|
366
|
+
except Exception as err:
|
|
367
|
+
if params is not None:
|
|
368
|
+
err.add_note("请求参数: %r" % params)
|
|
369
|
+
if body is not None:
|
|
370
|
+
err.add_note("请求实体: %r" % body)
|
|
371
|
+
raise err
|
|
372
|
+
# fmt: on
|
|
373
|
+
|
|
374
|
+
async def _request_with_sign(
|
|
375
|
+
self,
|
|
376
|
+
method: REQUEST_METHOD,
|
|
377
|
+
url: str,
|
|
378
|
+
params: dict = None,
|
|
379
|
+
body: dict | None = None,
|
|
380
|
+
headers: dict | None = None,
|
|
381
|
+
extract_data: bool = False,
|
|
382
|
+
) -> dict | list:
|
|
383
|
+
"""(internal) 基于 params 和 body 生成签名, 并发送带签名的 HTTP 请求,自动处理过期 Access Token 的获取与刷新
|
|
384
|
+
|
|
385
|
+
:param method `<'str'>`: HTTP 请求方法, 如: `"GET"`, `"POST"`, `"PUT"`, `"DELETE"`
|
|
386
|
+
:param url `<'str'>`: 业务请求路径, 如: `"/api/auth-server/oauth/access-token"`
|
|
387
|
+
:param params `<'dict'>`: 可选请求参数, 默认 `None`
|
|
388
|
+
:param body `<'dict/None'>`: 可选业务请参数, 默认 `None`
|
|
389
|
+
:param headers `<'dict/None'>`: 可选请求头, 如: `{"X-API-VERSION": "2"}`, 默认 `None`
|
|
390
|
+
:param extract_data `<'bool'>`: 是否提取响应数据中的 `data` 字段并直接返回, 默认为 `False`
|
|
391
|
+
:returns `<'list/dict'>`: 返回解析并验证后响应数据
|
|
392
|
+
"""
|
|
393
|
+
# 确保 access token 可用
|
|
394
|
+
if BaseAPI._access_token is None:
|
|
395
|
+
await self._UpdateToken()
|
|
396
|
+
|
|
397
|
+
# 构建参数
|
|
398
|
+
reqs_params = {
|
|
399
|
+
"app_key": self._app_id,
|
|
400
|
+
"access_token": BaseAPI._access_token,
|
|
401
|
+
"timestamp": utils.now_ts(),
|
|
402
|
+
}
|
|
403
|
+
if isinstance(params, dict):
|
|
404
|
+
reqs_params.update(params)
|
|
405
|
+
sign_params = {k: v for k, v in reqs_params.items()}
|
|
406
|
+
if isinstance(body, dict):
|
|
407
|
+
sign_params.update(body)
|
|
408
|
+
|
|
409
|
+
# 生成签名
|
|
410
|
+
sign = utils.generate_sign(sign_params, self._app_cipher)
|
|
411
|
+
reqs_params["sign"] = sign
|
|
412
|
+
|
|
413
|
+
# 发送请求
|
|
414
|
+
try:
|
|
415
|
+
return await self._request(
|
|
416
|
+
method,
|
|
417
|
+
url,
|
|
418
|
+
reqs_params,
|
|
419
|
+
body=body,
|
|
420
|
+
headers=headers,
|
|
421
|
+
extract_data=extract_data,
|
|
422
|
+
)
|
|
423
|
+
except errors.TokenExpiredError:
|
|
424
|
+
await self._UpdateToken() # 刷新 Access Token
|
|
425
|
+
return await self._request_with_sign(method, url, params, body)
|
|
426
|
+
|
|
427
|
+
def _handle_response_data(
|
|
428
|
+
self,
|
|
429
|
+
url: str,
|
|
430
|
+
res_data: bytes,
|
|
431
|
+
extract_data: bool,
|
|
432
|
+
) -> dict | list:
|
|
433
|
+
"""解析并验证响应数据, 并数据中的 `data` 字段
|
|
434
|
+
|
|
435
|
+
:param url `<'str'>`: 对应业务请求路径, 用于构建错误信息
|
|
436
|
+
:param res_data `<'bytes'>`: 原始响应数据
|
|
437
|
+
:param extract_data `<'bool'>`: 是否提取响应数据中的 `data` 字段
|
|
438
|
+
:returns `<'dict/list'>`: 返回解析并验证后的响应数据中的 `data` 字段
|
|
439
|
+
"""
|
|
440
|
+
# 解析响应数据
|
|
441
|
+
try:
|
|
442
|
+
data: dict = _orjson_loads(res_data)
|
|
443
|
+
except Exception as err:
|
|
444
|
+
raise errors.ResponseDataError(
|
|
445
|
+
"响应数据解析错误, 可能不是有效的JSON格式", url, res_data
|
|
446
|
+
) from err
|
|
447
|
+
|
|
448
|
+
# 验证响应数据
|
|
449
|
+
code = data.get("code")
|
|
450
|
+
if code is None:
|
|
451
|
+
raise errors.ResponseDataError("响应数据错误, 缺少 code 字段", url, data)
|
|
452
|
+
if code not in (0, 1, "200"):
|
|
453
|
+
try:
|
|
454
|
+
errno: int = int(code)
|
|
455
|
+
except ValueError:
|
|
456
|
+
raise errors.ResponseDataError(
|
|
457
|
+
"响应数据错误, code 字段不是整数", url, data, code
|
|
458
|
+
)
|
|
459
|
+
# 常见错误码
|
|
460
|
+
if errno == 2001003:
|
|
461
|
+
raise errors.AccessTokenExpiredError(
|
|
462
|
+
"access token 过期, 请重新获取", url, data, code
|
|
463
|
+
)
|
|
464
|
+
if errno == 2001008:
|
|
465
|
+
raise errors.RefreshTokenExpiredError(
|
|
466
|
+
"refresh token过期, 请重新获取", url, data, code
|
|
467
|
+
)
|
|
468
|
+
if errno == 2001007:
|
|
469
|
+
raise errors.SignatureExpiredError(
|
|
470
|
+
"签名过期, 请重新发起请求", url, data, code
|
|
471
|
+
)
|
|
472
|
+
if errno == 3001008:
|
|
473
|
+
raise errors.TooManyRequestsError(
|
|
474
|
+
"接口请求太频繁触发限流", url, data, code
|
|
475
|
+
)
|
|
476
|
+
if errno == 103:
|
|
477
|
+
raise errors.ApiLimitError(
|
|
478
|
+
"请求速率过高导致被限流拦截", url, data, code
|
|
479
|
+
)
|
|
480
|
+
# 其他错误码
|
|
481
|
+
if errno in (400, 405):
|
|
482
|
+
raise errors.InvalidApiUrlError(
|
|
483
|
+
"请求 url 或 params 不正确", url, data, code
|
|
484
|
+
)
|
|
485
|
+
if errno == 500:
|
|
486
|
+
if data.get("message") in (
|
|
487
|
+
"Internal Server Error",
|
|
488
|
+
"请求连接异常,请稍后再试",
|
|
489
|
+
):
|
|
490
|
+
raise errors.InternalServerError(
|
|
491
|
+
"领星 API 服务器发生内部错误", url, data, code
|
|
492
|
+
)
|
|
493
|
+
raise errors.InvalidApiUrlError(
|
|
494
|
+
"请求 url 或 params 不正确", url, data, code
|
|
495
|
+
)
|
|
496
|
+
if errno == 2001001:
|
|
497
|
+
raise errors.AppIdOrSecretError(
|
|
498
|
+
"appId 不存在, 请检查值有效性", url, data, code
|
|
499
|
+
)
|
|
500
|
+
if errno == 1001:
|
|
501
|
+
raise errors.AppIdOrSecretError(
|
|
502
|
+
"appSecret 中可能有特殊字符", url, data, code
|
|
503
|
+
)
|
|
504
|
+
if errno == 2001002:
|
|
505
|
+
raise errors.AppIdOrSecretError(
|
|
506
|
+
"appSecret 不正确, 请检查值有效性", url, data, code
|
|
507
|
+
)
|
|
508
|
+
if errno == 2001004:
|
|
509
|
+
raise errors.UnauthorizedApiError(
|
|
510
|
+
"请求的 api 被未授权, 请联系领星确认", url, data, code
|
|
511
|
+
)
|
|
512
|
+
if errno == 401:
|
|
513
|
+
raise errors.UnauthorizedApiError(
|
|
514
|
+
"api 授权被禁用, 请检查授权状态", url, data, code
|
|
515
|
+
)
|
|
516
|
+
if errno == 403:
|
|
517
|
+
raise errors.UnauthorizedApiError(
|
|
518
|
+
"api 授权失效, 请检查授权状态", url, data, code
|
|
519
|
+
)
|
|
520
|
+
if errno == 2001005:
|
|
521
|
+
raise errors.InvalidAccessTokenError(
|
|
522
|
+
"access token 不正确, 请检查有效性", url, data, code
|
|
523
|
+
)
|
|
524
|
+
if errno == 2001009:
|
|
525
|
+
raise errors.InvalidRefreshTokenError(
|
|
526
|
+
"refresh token 不正确, 请检查有效性", url, data, code
|
|
527
|
+
)
|
|
528
|
+
if errno == 2001006:
|
|
529
|
+
raise errors.InvalidSignatureError(
|
|
530
|
+
"接口签名不正确, 请检查生成签名的正确性", url, data, code
|
|
531
|
+
)
|
|
532
|
+
if errno == 102:
|
|
533
|
+
raise errors.InvalidParametersError("参数不合法", url, data, code)
|
|
534
|
+
if errno == 3001001:
|
|
535
|
+
# fmt: off
|
|
536
|
+
raise errors.InvalidParametersError(
|
|
537
|
+
"必传参数缺失, 公共参数必须包含 (access_token, app_key, timestamp, sign)",
|
|
538
|
+
url, data, code,
|
|
539
|
+
)
|
|
540
|
+
# fmt: on
|
|
541
|
+
if errno == 3001002:
|
|
542
|
+
raise errors.UnauthorizedRequestIpError(
|
|
543
|
+
"发起请求的 ip 未加入领星 api 白名单", url, data, code
|
|
544
|
+
)
|
|
545
|
+
# 未知错误码
|
|
546
|
+
raise errors.UnknownRequestError("未知的 api 错误", url, data, code)
|
|
547
|
+
|
|
548
|
+
# 是否成功 (特殊返回检查情况)
|
|
549
|
+
if not data.get("success", True):
|
|
550
|
+
raise errors.InvalidParametersError("参数不合法", url, data, code)
|
|
551
|
+
|
|
552
|
+
# 返回响应数据
|
|
553
|
+
return data if not extract_data else self._extract_data(data, url, code)
|
|
554
|
+
|
|
555
|
+
def _extract_data(self, res_data: dict, url: str, code: int | None = None) -> dict:
|
|
556
|
+
"""(internal) 提取响应数据中的 `data` 字段
|
|
557
|
+
|
|
558
|
+
:param res_data `<'dict'>`: 原始响应数据
|
|
559
|
+
:param url `<'str'>`: 对应业务请求路径, 用于构建错误信息
|
|
560
|
+
:param code `<'int'>`: 可选的状态码, 用于构建错误信息
|
|
561
|
+
:returns `<'dict'>`: 返回解析并验证后的响应数据中的 `data` 字段
|
|
562
|
+
"""
|
|
563
|
+
try:
|
|
564
|
+
return res_data["data"]
|
|
565
|
+
except KeyError:
|
|
566
|
+
raise errors.ResponseDataError(
|
|
567
|
+
"响应数据错误, 缺少 data 字段", url, res_data, code
|
|
568
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
from lingxingapi.fields import NonNegativeInt
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# 基础模型 -----------------------------------------------------------------------------------------------------------------------
|
|
8
|
+
class Parameter(BaseModel):
|
|
9
|
+
model_config = ConfigDict(
|
|
10
|
+
populate_by_name=True,
|
|
11
|
+
str_strip_whitespace=True,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def model_dump_params(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
include=None,
|
|
18
|
+
exclude=None,
|
|
19
|
+
context=None,
|
|
20
|
+
by_alias=True,
|
|
21
|
+
exclude_unset=False,
|
|
22
|
+
exclude_defaults=False,
|
|
23
|
+
exclude_none=True,
|
|
24
|
+
round_trip=False,
|
|
25
|
+
warnings=True,
|
|
26
|
+
fallback=None,
|
|
27
|
+
serialize_as_any=False,
|
|
28
|
+
) -> dict:
|
|
29
|
+
"""将模型转换为字典, 并按字母顺序排序键 `<'dict'>`."""
|
|
30
|
+
res = self.model_dump(
|
|
31
|
+
mode="python",
|
|
32
|
+
include=include,
|
|
33
|
+
exclude=exclude,
|
|
34
|
+
context=context,
|
|
35
|
+
by_alias=by_alias,
|
|
36
|
+
exclude_unset=exclude_unset,
|
|
37
|
+
exclude_defaults=exclude_defaults,
|
|
38
|
+
exclude_none=exclude_none,
|
|
39
|
+
round_trip=round_trip,
|
|
40
|
+
warnings=warnings,
|
|
41
|
+
fallback=fallback,
|
|
42
|
+
serialize_as_any=serialize_as_any,
|
|
43
|
+
)
|
|
44
|
+
return dict(sorted(res.items()))
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def model_validate_params(cls, data: object) -> dict:
|
|
48
|
+
"""将传入的数据进行验证, 转换为字典, 且按字母顺序排序键"""
|
|
49
|
+
if isinstance(data, BaseModel) and not isinstance(data, cls):
|
|
50
|
+
data = data.model_dump()
|
|
51
|
+
return cls.model_validate(data).model_dump_params()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# 公用参数 -----------------------------------------------------------------------------------------------------------------------
|
|
55
|
+
class PageOffestAndLength(Parameter):
|
|
56
|
+
# 分页偏移量
|
|
57
|
+
offset: Optional[NonNegativeInt] = None
|
|
58
|
+
# 分页长度
|
|
59
|
+
length: Optional[NonNegativeInt] = None
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# fmt: off
|
|
4
|
+
# API 域名 ----------------------------------------------------------------------------------------------------------------------
|
|
5
|
+
API_SERVER: str = "https://openapi.lingxing.com"
|
|
6
|
+
|
|
7
|
+
# Token 令牌 --------------------------------------------------------------------------------------------------------------------
|
|
8
|
+
# https://apidoc.lingxing.com/#/docs/Authorization/GetToken
|
|
9
|
+
AUTH_GET_TOKEN: str = "/api/auth-server/oauth/access-token"
|
|
10
|
+
# https://apidoc.lingxing.com/#/docs/Authorization/RefreshToken
|
|
11
|
+
AUTH_REFRESH_TOKEN: str = "/api/auth-server/oauth/refresh"
|