lingxingapi 1.0.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.

Potentially problematic release.


This version of lingxingapi might be problematic. Click here for more details.

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 +443 -0
  8. lingxingapi/base/__init__.py +0 -0
  9. lingxingapi/base/api.py +409 -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 +212 -0
  18. lingxingapi/errors.py +143 -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 +411 -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.0.0.dist-info/METADATA +67 -0
  62. lingxingapi-1.0.0.dist-info/RECORD +65 -0
  63. lingxingapi-1.0.0.dist-info/WHEEL +5 -0
  64. lingxingapi-1.0.0.dist-info/licenses/LICENSE +22 -0
  65. lingxingapi-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,409 @@
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 TCPConnector, ClientSession, ClientConnectorError, ClientTimeout
8
+ from lingxingapi import utils, errors
9
+ from lingxingapi.base import route, schema
10
+
11
+ # Type Aliases ---------------------------------------------------------------------------------------------------------
12
+ REQUEST_METHOD = Literal["GET", "POST", "PUT", "DELETE"]
13
+
14
+
15
+ # API ------------------------------------------------------------------------------------------------------------------
16
+ class BaseAPI:
17
+ """领星 API 基础类, 提供公共方法和属性, 用于子类继承
18
+
19
+ ## Notice
20
+ 请勿直接实例化此类
21
+ """
22
+
23
+ # HTTP 会话
24
+ _session: ClientSession = None
25
+ # Token 令牌
26
+ _access_token: str = None
27
+ _refresh_token: str = None
28
+
29
+ def __init__(
30
+ self,
31
+ app_id: str,
32
+ app_secret: str,
33
+ app_cipher: EcbMode,
34
+ timeout: int | float,
35
+ ignore_api_limit: bool,
36
+ ignore_api_limit_wait: int | float,
37
+ ignore_api_limit_retry: int,
38
+ ) -> None:
39
+ """领星 API 基础类, 提供公共方法和属性供子类继承使用
40
+
41
+ ## Notice
42
+ 请勿直接实例化此类
43
+
44
+ :param app_id `<'str'>`: 应用ID, 用于鉴权
45
+ :param app_secret `<'str'>`: 应用密钥, 用于鉴权
46
+ :param app_cipher `<'EcbMode'>`: 基于 appId 创建的 AES-ECB 加密器
47
+ :param timeout `<'int/float'>`: 请求超时 (单位: 秒)
48
+ :param ignore_api_limit `<'bool'>`: 是否忽略 API 限流错误
49
+
50
+ - 如果设置为 `True`, 则在遇到限流错误时不会抛出异常, 而是会等待
51
+ `ignore_api_limit_wait` 秒后重试请求, 重试次数不超过 `ignore_api_limit_retry`
52
+ - 如果设置为 `False`, 则在遇到限流错误时直接抛出 `ApiLimitError` 异常
53
+
54
+ :param ignore_api_limit_wait `<'float'>`: 忽略 API 限流错误时的等待时间 (单位: 秒),
55
+ 仅在 `ignore_api_limit` 为 `True` 时生效
56
+
57
+ :param ignore_api_limit_retry `<'int'>`: 忽略 API 限流错误时的最大重试次数,
58
+ 仅在 `ignore_api_limit` 为 `True` 时生效, 若设置为 `-1` 则表示无限重试
59
+ """
60
+ # API 凭证
61
+ self._app_id: str = app_id
62
+ self._app_secret: str = app_secret
63
+ self._app_cipher: EcbMode = app_cipher
64
+ # HTTP 会话
65
+ self._timeout: ClientTimeout = timeout
66
+ # API 限流
67
+ self._ignore_api_limit: bool = ignore_api_limit
68
+ self._ignore_api_limit_wait: float = ignore_api_limit_wait
69
+ self._ignore_api_limit_retry: int = ignore_api_limit_retry
70
+ self._infinite_retry: bool = ignore_api_limit_retry == -1
71
+
72
+ async def __aenter__(self) -> Self:
73
+ """进入 API 客户端异步上下文管理器
74
+
75
+ :returns `<'API'>`: 返回 API 客户端实例
76
+ """
77
+ return self
78
+
79
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
80
+ """退出 API 客户端异步上下文管理器, 并关闭 HTTP 会话"""
81
+ await self.close()
82
+
83
+ async def close(self) -> None:
84
+ """关闭 API 客户端的 HTTP 会话"""
85
+ if BaseAPI._session is not None:
86
+ await BaseAPI._session.close()
87
+ BaseAPI._session = None
88
+
89
+ # 公共 API --------------------------------------------------------------------------------------
90
+ # . Token 令牌
91
+ async def _AccessToken(self) -> schema.Token:
92
+ """(Internal) 获取领星 API 的访问令牌
93
+
94
+ ## Docs
95
+ - 授权: [获取 access-token和refresh-token](https://apidoc.lingxing.com/#/docs/Authorization/GetToken)
96
+
97
+ :returns `<'Token'>`: 返回接口的访问令牌
98
+ ```python
99
+ {
100
+ # 接口的访问令牌
101
+ "access_token": "your_access_token",
102
+ # 用于续约 access_token 的更新令牌
103
+ "refresh_token": "your_refresh_token",
104
+ # 访问令牌的有效时间 (单位: 秒)
105
+ "expires_in": 3600
106
+ }
107
+ ```
108
+ """
109
+ data = await self._request(
110
+ "POST",
111
+ route.AUTH_GET_TOKEN,
112
+ {"appId": self._app_id, "appSecret": self._app_secret},
113
+ extract_data=True,
114
+ )
115
+ res = schema.Token.model_validate(data)
116
+ BaseAPI._access_token = res.access_token
117
+ BaseAPI._refresh_token = res.refresh_token
118
+ return res
119
+
120
+ async def _RefreshToken(self, refresh_token: str) -> schema.Token:
121
+ """(Internal) 基于 refresh_token 刷新领星 API 的访问令牌
122
+
123
+ ## Docs
124
+ - 授权: [续约接口令牌](https://apidoc.lingxing.com/#/docs/Authorization/RefreshToken)
125
+
126
+ :param refresh_token `<'str'>`: 用于续约 refresh_token 的令牌
127
+ :returns `<'Token'>`: 返回接口的访问令牌
128
+ ```python
129
+ {
130
+ # 接口的访问令牌
131
+ "access_token": "your_access_token",
132
+ # 用于续约 access_token 的更新令牌
133
+ "refresh_token": "your_refresh_token",
134
+ # 访问令牌的有效时间 (单位: 秒)
135
+ "expires_in": 3600
136
+ }
137
+ ```
138
+ """
139
+ data = await self._request(
140
+ "POST",
141
+ route.AUTH_REFRESH_TOKEN,
142
+ {"appId": self._app_id, "refreshToken": str(refresh_token)},
143
+ extract_data=True,
144
+ )
145
+ res = schema.Token.model_validate(data)
146
+ BaseAPI._access_token = res.access_token
147
+ BaseAPI._refresh_token = res.refresh_token
148
+ return res
149
+
150
+ async def _UpdateToken(self) -> None:
151
+ """(internal) 获取或刷新 access_token, 如果缓存了 refresh_token,
152
+ 则优先基于 refresh_token 进行刷新
153
+ """
154
+ # 如果存在 refresh token, 则使用它刷新 access token
155
+ if BaseAPI._refresh_token is not None:
156
+ try:
157
+ await self._RefreshToken(BaseAPI._refresh_token)
158
+ return None
159
+ except errors.RefreshTokenExpiredError:
160
+ pass
161
+ # 如果不存在 refresh token, 则重新获取 access token
162
+ await self._AccessToken()
163
+
164
+ # 核心 HTTP 逻辑 ---------------------------------------------------------------------------------
165
+ async def _request(
166
+ self,
167
+ method: REQUEST_METHOD,
168
+ url: str,
169
+ params: dict,
170
+ body: dict | None = None,
171
+ headers: dict | None = None,
172
+ extract_data: bool = False,
173
+ ) -> dict | list:
174
+ """(internal) 发送基础 HTTP 请求, 并对状态码与业务响应数据进行校验
175
+
176
+ :param method `<'str'>`: HTTP 请求方法, 如: `"GET"`, `"POST"`, `"PUT"`, `"DELETE"`
177
+ :param url `<'str'>`: 业务请求路径, 如: `"/api/auth-server/oauth/access-token"`
178
+ :param params `<'dict'>`: 必填公共参数, 包含 (`app_key`, `access_token`, `timestamp`, `sign`)
179
+ :param body `<'dict/None'>`: 可选业务请求参数, 用于 POST/PUT 请求, 默认 `None`
180
+ :param headers `<'dict/None'>`: 可选请求头, 如: `{"X-API-VERSION": "2"}`, 默认 `None`
181
+ :param extract_data `<'bool'>`: 是否提取响应数据中的 `data` 字段并直接返回, 默认为 `False`
182
+ :returns `<'list/dict'>`: 返回解析并验证后响应数据
183
+ """
184
+ # 确保 HTTP 会话可用
185
+ if BaseAPI._session is None or BaseAPI._session.closed:
186
+ BaseAPI._session = ClientSession(
187
+ route.API_SERVER,
188
+ headers={"Content-Type": "application/json"},
189
+ timeout=self._timeout,
190
+ connector=TCPConnector(limit=100),
191
+ )
192
+
193
+ # 发送请求
194
+ retry_count = 0
195
+ while True:
196
+ try:
197
+ async with BaseAPI._session.request(
198
+ method,
199
+ url,
200
+ params=params,
201
+ json=body,
202
+ headers=headers,
203
+ ) as res:
204
+ # . 检查响应状态码
205
+ if res.status != 200:
206
+ raise errors.ServerError(
207
+ "领星API服务器响应错误", url, res.reason, res.status
208
+ )
209
+ # . 解析并验证响应数据
210
+ return self._handle_response_data(
211
+ url, await res.read(), extract_data
212
+ )
213
+ # fmt: off
214
+ except errors.ApiLimitError as err:
215
+ if self._ignore_api_limit and (self._infinite_retry or retry_count < self._ignore_api_limit_retry):
216
+ await _aio_sleep(self._ignore_api_limit_wait)
217
+ retry_count += 1
218
+ continue
219
+ raise err
220
+ except TimeoutError as err:
221
+ raise errors.ApiTimeoutError("领星 API 请求超时", url, self._timeout) from err
222
+ except ClientConnectorError as err:
223
+ raise errors.ServerError("领星 API 服务器无响应, 若无网络问题, 请检查账号 ID 白名单设置", url, err) from err
224
+ # fmt: on
225
+
226
+ async def _request_with_sign(
227
+ self,
228
+ method: REQUEST_METHOD,
229
+ url: str,
230
+ params: dict = None,
231
+ body: dict | None = None,
232
+ headers: dict | None = None,
233
+ extract_data: bool = False,
234
+ ) -> dict | list:
235
+ """(internal) 基于 params 和 body 生成签名, 并发送带签名的 HTTP 请求,自动处理过期 Access Token 的获取与刷新
236
+
237
+ :param method `<'str'>`: HTTP 请求方法, 如: `"GET"`, `"POST"`, `"PUT"`, `"DELETE"`
238
+ :param url `<'str'>`: 业务请求路径, 如: `"/api/auth-server/oauth/access-token"`
239
+ :param params `<'dict'>`: 可选请求参数, 默认 `None`
240
+ :param body `<'dict/None'>`: 可选业务请参数, 默认 `None`
241
+ :param headers `<'dict/None'>`: 可选请求头, 如: `{"X-API-VERSION": "2"}`, 默认 `None`
242
+ :param extract_data `<'bool'>`: 是否提取响应数据中的 `data` 字段并直接返回, 默认为 `False`
243
+ :returns `<'list/dict'>`: 返回解析并验证后响应数据
244
+ """
245
+ # 确保 access token 可用
246
+ if BaseAPI._access_token is None:
247
+ await self._UpdateToken()
248
+
249
+ # 构建参数
250
+ reqs_params = {
251
+ "app_key": self._app_id,
252
+ "access_token": BaseAPI._access_token,
253
+ "timestamp": utils.now_ts(),
254
+ }
255
+ if isinstance(params, dict):
256
+ reqs_params.update(params)
257
+ sign_params = {k: v for k, v in reqs_params.items()}
258
+ if isinstance(body, dict):
259
+ sign_params.update(body)
260
+
261
+ # 生成签名
262
+ sign = utils.generate_sign(sign_params, self._app_cipher)
263
+ reqs_params["sign"] = sign
264
+
265
+ # 发送请求
266
+ try:
267
+ return await self._request(
268
+ method,
269
+ url,
270
+ reqs_params,
271
+ body=body,
272
+ headers=headers,
273
+ extract_data=extract_data,
274
+ )
275
+ except errors.AccessTokenExpiredError:
276
+ await self._UpdateToken() # 刷新 Access Token
277
+ return await self._request_with_sign(method, url, params, body)
278
+
279
+ def _handle_response_data(
280
+ self,
281
+ url: str,
282
+ res_data: bytes,
283
+ extract_data: bool,
284
+ ) -> dict | list:
285
+ """解析并验证响应数据, 并数据中的 `data` 字段
286
+
287
+ :param url `<'str'>`: 对应业务请求路径, 用于构建错误信息
288
+ :param res_data `<'bytes'>`: 原始响应数据
289
+ :param extract_data `<'bool'>`: 是否提取响应数据中的 `data` 字段
290
+ :returns `<'dict/list'>`: 返回解析并验证后的响应数据中的 `data` 字段
291
+ """
292
+ # 解析响应数据
293
+ try:
294
+ data: dict = _orjson_loads(res_data)
295
+ except Exception as err:
296
+ raise errors.ResponseDataError(
297
+ "响应数据解析错误, 可能不是有效的JSON格式", url, res_data
298
+ ) from err
299
+
300
+ # 验证响应数据
301
+ code = data.get("code")
302
+ if code is None:
303
+ raise errors.ResponseDataError("响应数据错误, 缺少 code 字段", url, data)
304
+ if code not in (0, 1, "200"):
305
+ try:
306
+ errno: int = int(code)
307
+ except ValueError:
308
+ raise errors.ResponseDataError(
309
+ "响应数据错误, code 字段不是整数", url, data, code
310
+ )
311
+ # 常见错误码
312
+ if errno == 2001003:
313
+ raise errors.AccessTokenExpiredError(
314
+ "access token 过期, 请重新获取", url, data, code
315
+ )
316
+ if errno == 2001008:
317
+ raise errors.RefreshTokenExpiredError(
318
+ "refresh token过期, 请重新获取", url, data, code
319
+ )
320
+ if errno == 2001007:
321
+ raise errors.SignatureExpiredError(
322
+ "签名过期, 请重新发起请求", url, data, code
323
+ )
324
+ if errno == 3001008:
325
+ raise errors.TooManyRequestsError(
326
+ "接口请求太频繁触发限流", url, data, code
327
+ )
328
+ if errno == 103:
329
+ raise errors.ApiLimitError(
330
+ "请求速率过高导致被限流拦截", url, data, code
331
+ )
332
+ # 其他错误码
333
+ if errno in (400, 405, 500):
334
+ raise errors.InvalidApiUrlError(
335
+ "请求 url 或 params 不正确", url, data, code
336
+ )
337
+ if errno == 2001001:
338
+ raise errors.AppIdOrSecretError(
339
+ "appId 不存在, 请检查值有效性", url, data, code
340
+ )
341
+ if errno == 1001:
342
+ raise errors.AppIdOrSecretError(
343
+ "appSecret 中可能有特殊字符", url, data, code
344
+ )
345
+ if errno == 2001002:
346
+ raise errors.AppIdOrSecretError(
347
+ "appSecret 不正确, 请检查值有效性", url, data, code
348
+ )
349
+ if errno == 2001004:
350
+ raise errors.UnauthorizedApiError(
351
+ "请求的 api 被未授权, 请联系领星确认", url, data, code
352
+ )
353
+ if errno == 401:
354
+ raise errors.UnauthorizedApiError(
355
+ "api 授权被禁用, 请检查授权状态", url, data, code
356
+ )
357
+ if errno == 403:
358
+ raise errors.UnauthorizedApiError(
359
+ "api 授权失效, 请检查授权状态", url, data, code
360
+ )
361
+ if errno == 2001005:
362
+ raise errors.InvalidAccessTokenError(
363
+ "access token 不正确, 请检查有效性", url, data, code
364
+ )
365
+ if errno == 2001009:
366
+ raise errors.InvalidRefreshTokenError(
367
+ "refresh token 不正确, 请检查有效性", url, data, code
368
+ )
369
+ if errno == 2001006:
370
+ raise errors.InvalidSignatureError(
371
+ "接口签名不正确, 请检查生成签名的正确性", url, data, code
372
+ )
373
+ if errno == 102:
374
+ raise errors.InvalidParametersError("参数不合法", url, data, code)
375
+ if errno == 3001001:
376
+ # fmt: off
377
+ raise errors.InvalidParametersError(
378
+ "必传参数缺失, 公共参数必须包含 (access_token, app_key, timestamp, sign)",
379
+ url, data, code,
380
+ )
381
+ # fmt: on
382
+ if errno == 3001002:
383
+ raise errors.UnauthorizedRequestIpError(
384
+ "发起请求的 ip 未加入领星 api 白名单", url, data, code
385
+ )
386
+ # 未知错误码
387
+ raise errors.UnknownRequestError("未知的 api 错误", url, data, code)
388
+
389
+ # 是否成功 (特殊返回检查情况)
390
+ if not data.get("success", True):
391
+ raise errors.InvalidParametersError("参数不合法", url, data, code)
392
+
393
+ # 返回响应数据
394
+ return data if not extract_data else self._extract_data(data, url, code)
395
+
396
+ def _extract_data(self, res_data: dict, url: str, code: int | None = None) -> dict:
397
+ """(internal) 提取响应数据中的 `data` 字段
398
+
399
+ :param res_data `<'dict'>`: 原始响应数据
400
+ :param url `<'str'>`: 对应业务请求路径, 用于构建错误信息
401
+ :param code `<'int'>`: 可选的状态码, 用于构建错误信息
402
+ :returns `<'dict'>`: 返回解析并验证后的响应数据中的 `data` 字段
403
+ """
404
+ try:
405
+ return res_data["data"]
406
+ except KeyError:
407
+ raise errors.ResponseDataError(
408
+ "响应数据错误, 缺少 data 字段", url, res_data, code
409
+ )
@@ -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"
@@ -0,0 +1,198 @@
1
+ # -*- coding: utf-8 -*-
2
+ import datetime
3
+ from typing import Any, Optional
4
+ from typing_extensions import Self
5
+ from pydantic import BaseModel, Field, model_validator
6
+ from lingxingapi import errors
7
+ from lingxingapi.fields import StrOrNone2Blank
8
+
9
+
10
+ # Token 令牌 --------------------------------------------------------------------------------------------------------------------
11
+ class Token(BaseModel):
12
+ # 接口的访问令牌
13
+ access_token: str
14
+ # 用于续约 access_token 的更新令牌
15
+ refresh_token: str
16
+ # 访问令牌的有效时间 (单位: 秒)
17
+ expires_in: int
18
+
19
+
20
+ # 公用 Schema -------------------------------------------------------------------------------------------------------------------
21
+ class TagInfo(BaseModel):
22
+ """商品的标签信息."""
23
+
24
+ # 领星标签ID (GlobalTag.tag_id) [原字段 'global_tag_id']
25
+ tag_id: str = Field(validation_alias="global_tag_id")
26
+ # 领星标签名称 (GlobalTag.tag_name) [原字段 'tag_name']
27
+ tag_name: str = Field(validation_alias="tag_name")
28
+ # 领星标签颜色 (如: "#FF0000") [原字段 'color']
29
+ tag_color: str = Field(validation_alias="color")
30
+
31
+
32
+ class AttachmentFile(BaseModel):
33
+ """附件信息"""
34
+
35
+ # 文件ID
36
+ file_id: int
37
+ # 文件名称
38
+ file_name: str
39
+ # 文件类型 (0: 未知, 1: 图片, 2: 压缩包)
40
+ file_type: int = 0
41
+ # 文件链接
42
+ file_url: str = ""
43
+
44
+
45
+ class SpuProductAttribute(BaseModel):
46
+ """SPU 商品属性"""
47
+
48
+ # 属性ID
49
+ attr_id: int
50
+ # 属性名称
51
+ attr_name: str
52
+ # 属性值
53
+ attr_value: str
54
+
55
+
56
+ class CustomField(BaseModel):
57
+ """自定义字段"""
58
+
59
+ # 自定义字段ID
60
+ field_id: int = Field(validation_alias="id")
61
+ # 自定义字段名称
62
+ field_name: str = Field(validation_alias="name")
63
+ # 自定义字段值
64
+ field_value: str = Field(validation_alias="val_text")
65
+
66
+
67
+ class BaseResponse(BaseModel):
68
+ """基础响应数据"""
69
+
70
+ # 状态码
71
+ code: int = 0
72
+ # 提示信息
73
+ message: Optional[StrOrNone2Blank] = None
74
+ # 错误信息
75
+ errors: Optional[list] = Field(None, validation_alias="error_details")
76
+ # 请求链路id
77
+ request_id: Optional[str] = None
78
+ # 响应时间
79
+ response_time: Optional[str] = None
80
+
81
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
82
+ @model_validator(mode="after")
83
+ def _adjustments(self) -> Self:
84
+ # 设置默认数值
85
+ if self.message is None:
86
+ self.message = "success"
87
+ if self.errors is None:
88
+ self.errors = []
89
+ if self.request_id is None:
90
+ self.request_id = ""
91
+ if self.response_time is None:
92
+ # fmt: off
93
+ dt = datetime.datetime.now()
94
+ self.response_time = "%04d-%02d-%02d %02d:%02d:%02d" % (
95
+ dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second
96
+ )
97
+ # fmt: on
98
+
99
+ # 返回
100
+ return self
101
+
102
+
103
+ class ResponseV1(BaseResponse):
104
+ """响应数据 - 下划线命名"""
105
+
106
+ # 响应数据量
107
+ response_count: int = 0
108
+ # 总数据量
109
+ total_count: int = Field(0, validation_alias="total")
110
+ # 响应数据
111
+ data: Any = None
112
+
113
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
114
+ @model_validator(mode="after")
115
+ def _set_count(self) -> Self:
116
+ # 计算响应数据量
117
+ if isinstance(self.data, list):
118
+ self.response_count = len(self.data)
119
+ elif isinstance(self.data, (dict, BaseModel)):
120
+ self.response_count = 1
121
+ else:
122
+ return self
123
+
124
+ # 调整总数据量
125
+ if self.response_count > 0:
126
+ self.total_count = max(self.total_count, self.response_count)
127
+ return self
128
+
129
+
130
+ class ResponseV1Token(ResponseV1):
131
+ """响应数据 - 下划线命名 + 分页游标 (next_token)"""
132
+
133
+ # 分页游标
134
+ next_token: StrOrNone2Blank
135
+
136
+
137
+ class ResponseV1TraceId(ResponseV1):
138
+ """响应数据 - request_id 替换为 traceId"""
139
+
140
+ # 请求链路id
141
+ request_id: Optional[str] = Field(None, validation_alias="traceId")
142
+
143
+
144
+ class ResponseV2(ResponseV1):
145
+ """响应数据 - 驼峰命名"""
146
+
147
+ # 错误信息
148
+ errors: Optional[list] = Field(None, validation_alias="errorDetails")
149
+ # 请求链路id
150
+ request_id: Optional[str] = Field(None, validation_alias="requestId")
151
+ # 响应时间
152
+ response_time: Optional[str] = Field(None, validation_alias="responseTime")
153
+
154
+
155
+ class ResponseResult(BaseResponse):
156
+ """响应结果"""
157
+
158
+ # 响应结果
159
+ data: Any = None
160
+
161
+
162
+ # 特殊 Schema -------------------------------------------------------------------------------------------------------------------
163
+ class FlattenDataRecords(BaseModel):
164
+ """从嵌套数据中提取列表数据 (records)
165
+
166
+ - 1. 从 data 字段中, 提取 total 并赋值至基础层
167
+ - 2. 从 data 字段中, 提取 records 并覆盖 data 字段
168
+ """
169
+
170
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
171
+ @model_validator(mode="before")
172
+ def _flatten_data(cls, data: dict) -> dict:
173
+ try:
174
+ inner: dict = data.pop("data", {})
175
+ data["total"] = max(inner.get("total", 0), data.get("total", 0))
176
+ data["data"] = inner.get("records", [])
177
+ except Exception:
178
+ raise errors.ResponseDataError(cls.__name__, data=data)
179
+ return data
180
+
181
+
182
+ class FlattenDataList(BaseModel):
183
+ """从嵌套数据中提取列表数据 (list)
184
+
185
+ - 1. 从 data 字段中, 提取 total 并赋值至基础层
186
+ - 2. 从 data 字段中, 提取 list 并覆盖 data 字段
187
+ """
188
+
189
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
190
+ @model_validator(mode="before")
191
+ def _flatten_data(cls, data: dict) -> dict:
192
+ try:
193
+ inner: dict = data.pop("data", {})
194
+ data["total"] = max(inner.get("total", 0), data.get("total", 0))
195
+ data["data"] = inner.get("list", [])
196
+ except Exception:
197
+ raise errors.ResponseDataError(cls.__name__, data=data)
198
+ return data
File without changes