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.
Files changed (65) hide show
  1. lingxingapi/__init__.py +7 -0
  2. lingxingapi/ads/__init__.py +0 -0
  3. lingxingapi/ads/api.py +5946 -0
  4. lingxingapi/ads/param.py +192 -0
  5. lingxingapi/ads/route.py +134 -0
  6. lingxingapi/ads/schema.py +2615 -0
  7. lingxingapi/api.py +557 -0
  8. lingxingapi/base/__init__.py +0 -0
  9. lingxingapi/base/api.py +568 -0
  10. lingxingapi/base/param.py +59 -0
  11. lingxingapi/base/route.py +11 -0
  12. lingxingapi/base/schema.py +198 -0
  13. lingxingapi/basic/__init__.py +0 -0
  14. lingxingapi/basic/api.py +466 -0
  15. lingxingapi/basic/param.py +72 -0
  16. lingxingapi/basic/route.py +20 -0
  17. lingxingapi/basic/schema.py +218 -0
  18. lingxingapi/errors.py +152 -0
  19. lingxingapi/fba/__init__.py +0 -0
  20. lingxingapi/fba/api.py +1691 -0
  21. lingxingapi/fba/param.py +250 -0
  22. lingxingapi/fba/route.py +30 -0
  23. lingxingapi/fba/schema.py +987 -0
  24. lingxingapi/fields.py +50 -0
  25. lingxingapi/finance/__init__.py +0 -0
  26. lingxingapi/finance/api.py +3091 -0
  27. lingxingapi/finance/param.py +616 -0
  28. lingxingapi/finance/route.py +44 -0
  29. lingxingapi/finance/schema.py +1243 -0
  30. lingxingapi/product/__init__.py +0 -0
  31. lingxingapi/product/api.py +2643 -0
  32. lingxingapi/product/param.py +934 -0
  33. lingxingapi/product/route.py +49 -0
  34. lingxingapi/product/schema.py +1004 -0
  35. lingxingapi/purchase/__init__.py +0 -0
  36. lingxingapi/purchase/api.py +496 -0
  37. lingxingapi/purchase/param.py +126 -0
  38. lingxingapi/purchase/route.py +11 -0
  39. lingxingapi/purchase/schema.py +215 -0
  40. lingxingapi/sales/__init__.py +0 -0
  41. lingxingapi/sales/api.py +3200 -0
  42. lingxingapi/sales/param.py +723 -0
  43. lingxingapi/sales/route.py +70 -0
  44. lingxingapi/sales/schema.py +1718 -0
  45. lingxingapi/source/__init__.py +0 -0
  46. lingxingapi/source/api.py +1799 -0
  47. lingxingapi/source/param.py +176 -0
  48. lingxingapi/source/route.py +38 -0
  49. lingxingapi/source/schema.py +1011 -0
  50. lingxingapi/tools/__init__.py +0 -0
  51. lingxingapi/tools/api.py +291 -0
  52. lingxingapi/tools/param.py +73 -0
  53. lingxingapi/tools/route.py +8 -0
  54. lingxingapi/tools/schema.py +169 -0
  55. lingxingapi/utils.py +456 -0
  56. lingxingapi/warehourse/__init__.py +0 -0
  57. lingxingapi/warehourse/api.py +1778 -0
  58. lingxingapi/warehourse/param.py +506 -0
  59. lingxingapi/warehourse/route.py +28 -0
  60. lingxingapi/warehourse/schema.py +926 -0
  61. lingxingapi-1.1.4.dist-info/METADATA +73 -0
  62. lingxingapi-1.1.4.dist-info/RECORD +65 -0
  63. lingxingapi-1.1.4.dist-info/WHEEL +5 -0
  64. lingxingapi-1.1.4.dist-info/licenses/LICENSE +22 -0
  65. lingxingapi-1.1.4.dist-info/top_level.txt +1 -0
@@ -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"