uapi-sdk-python 0.1.10__tar.gz → 0.1.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uapi-sdk-python
3
- Version: 0.1.10
3
+ Version: 0.1.14
4
4
  Summary: Idiomatic UAPI SDK for Python
5
5
  Author-email: UAPI <dev@uapis.cn>
6
6
  Requires-Python: >=3.9
@@ -32,11 +32,13 @@ pip install uapi-sdk-python
32
32
  ```python
33
33
  from uapi import UapiClient
34
34
 
35
- client = UapiClient("https://uapis.cn/api/v1")
36
- result = client.social.get_social_qq_userinfo(qq="10001")
35
+ client = UapiClient("https://uapis.cn", "YOUR_API_KEY")
36
+ result = client.misc.get_misc_hotboard(type="weibo")
37
37
  print(result)
38
38
  ```
39
39
 
40
+ 这个接口默认只要传 `type` 就可以拿当前热榜。`time`、`keyword`、`time_start`、`time_end`、`limit`、`sources` 都是按场景再传的可选参数。
41
+
40
42
  > [!TIP]
41
43
  > 请使用与运行脚本相同的 Python 解释器安装依赖,例如执行 `python -m pip install uapi-sdk-python` 后再运行 `python main.py`。在 VS Code / Pyright 中若提示 “Import uapi could not be resolved”,将解释器切换到当前虚拟环境即可恢复补全。
42
44
 
@@ -54,6 +56,60 @@ print(result)
54
56
 
55
57
  如果你需要查看字段细节或内部逻辑,仓库中的 `./internal` 目录同步保留了由 `openapi-generator` 生成的完整结构体,随时可供参考。
56
58
 
59
+ ## 响应元信息
60
+
61
+ 每次请求完成后,SDK 会自动把响应 Header 解析成结构化的 `ResponseMeta`,你不用自己拆原始字符串。
62
+
63
+ 成功时可以通过 `client.last_response_meta` 读取,失败时可以通过 `err.meta` 读取,两条路径拿到的是同一套字段。
64
+
65
+ ```python
66
+ from uapi import UapiClient, UapiError
67
+
68
+ client = UapiClient("https://uapis.cn", "YOUR_API_KEY")
69
+
70
+ # 成功路径
71
+ client.social.get_social_qq_userinfo(qq="10001")
72
+ meta = client.last_response_meta
73
+ if meta:
74
+ print("这次请求原价:", meta.credits_requested or 0, "积分")
75
+ print("这次实际扣费:", meta.credits_charged or 0, "积分")
76
+ print("特殊计价:", meta.credits_pricing or "原价")
77
+ print("余额剩余:", meta.balance_remaining_cents or 0, "分")
78
+ print("资源包剩余:", meta.quota_remaining_credits or 0, "积分")
79
+ print("当前有效额度桶:", meta.active_quota_buckets or 0)
80
+ print("额度用空即停:", meta.stop_on_empty)
81
+ print("Key QPS:", meta.billing_key_rate_remaining or 0, "/", meta.billing_key_rate_limit or 0, meta.billing_key_rate_unit or "req")
82
+ print("Request ID:", meta.request_id)
83
+
84
+ # 失败路径
85
+ try:
86
+ client.social.get_social_qq_userinfo(qq="10001")
87
+ except UapiError as err:
88
+ if err.meta:
89
+ print("Retry-After 秒数:", err.meta.retry_after_seconds)
90
+ print("Retry-After 原始值:", err.meta.retry_after_raw)
91
+ print("访客 QPS:", err.meta.visitor_rate_remaining or 0, "/", err.meta.visitor_rate_limit or 0)
92
+ print("Request ID:", err.meta.request_id)
93
+ ```
94
+
95
+ 常用字段一览:
96
+
97
+ | 字段 | 说明 |
98
+ |------|------|
99
+ | `credits_requested` | 这次请求原本要扣多少积分,也就是请求价 |
100
+ | `credits_charged` | 这次请求实际扣了多少积分 |
101
+ | `credits_pricing` | 特殊计价原因,例如缓存半价 `cache-hit-half-price` |
102
+ | `balance_remaining_cents` | 账户余额剩余(分) |
103
+ | `quota_remaining_credits` | 资源包剩余积分 |
104
+ | `active_quota_buckets` | 当前还有多少个有效额度桶参与计费 |
105
+ | `stop_on_empty` | 额度耗尽后是否直接停止服务 |
106
+ | `retry_after_seconds` / `retry_after_raw` | 限流后的等待时长;当服务端返回 HTTP 时间字符串时看 `retry_after_raw` |
107
+ | `request_id` | 请求唯一 ID,排障时使用 |
108
+ | `billing_key_rate_limit` / `billing_key_rate_remaining` | Billing Key 当前 QPS 规则的上限与剩余 |
109
+ | `billing_ip_rate_limit` / `billing_ip_rate_remaining` | Billing Key 单 IP 当前 QPS 规则的上限与剩余 |
110
+ | `visitor_rate_limit` / `visitor_rate_remaining` | 访客当前 QPS 规则的上限与剩余 |
111
+ | `rate_limit_policies` / `rate_limits` | 完整结构化限流策略数据 |
112
+
57
113
  ## 进阶实践
58
114
 
59
115
  ### 缓存与幂等
@@ -62,7 +118,7 @@ print(result)
62
118
  from functools import lru_cache
63
119
  from uapi import UapiClient
64
120
 
65
- client = UapiClient("https://uapis.cn/api/v1", token="<TOKEN>")
121
+ client = UapiClient("https://uapis.cn", token="YOUR_API_KEY")
66
122
 
67
123
  @lru_cache(maxsize=128)
68
124
  def cached_lookup(qq: str):
@@ -73,34 +129,19 @@ user = cached_lookup("10001")
73
129
 
74
130
  也可以在 FastAPI / Django 项目里配合 Redis,将 SDK 的响应序列化后写入缓存,命中即直接返回。
75
131
 
76
- ### 注入自定义 httpx.Client
132
+ ### 调整超时与环境
77
133
 
78
134
  ```python
79
- import httpx
80
- from httpx import Auth
81
135
  from uapi import UapiClient
82
136
 
83
- class StaticToken(Auth):
84
- def __init__(self, token: str):
85
- self.token = token
86
- def auth_flow(self, request):
87
- request.headers["Authorization"] = f"Bearer {self.token}"
88
- yield request
89
-
90
- http_client = httpx.Client(
91
- timeout=5,
92
- transport=httpx.HTTPTransport(retries=3),
93
- event_hooks={"request": [lambda request: print("->", request.url)]},
94
- )
95
-
96
137
  client = UapiClient(
97
- "https://uapis.cn/api/v1",
98
- client=http_client,
99
- auth=StaticToken("<TOKEN>"),
138
+ "https://uapis.cn",
139
+ token="YOUR_API_KEY",
140
+ timeout=5.0,
100
141
  )
101
142
  ```
102
143
 
103
- 通过自定义 `client` / `transport` / `auth`,可以无缝植入代理、重试策略或 APM 埋点。
144
+ 如果你需要切换到别的环境,直接改 `base_url` 就可以;如果你只想缩短等待时间,传 `timeout` 就够了。
104
145
 
105
146
  ## 错误模型概览
106
147
 
@@ -18,11 +18,13 @@ pip install uapi-sdk-python
18
18
  ```python
19
19
  from uapi import UapiClient
20
20
 
21
- client = UapiClient("https://uapis.cn/api/v1")
22
- result = client.social.get_social_qq_userinfo(qq="10001")
21
+ client = UapiClient("https://uapis.cn", "YOUR_API_KEY")
22
+ result = client.misc.get_misc_hotboard(type="weibo")
23
23
  print(result)
24
24
  ```
25
25
 
26
+ 这个接口默认只要传 `type` 就可以拿当前热榜。`time`、`keyword`、`time_start`、`time_end`、`limit`、`sources` 都是按场景再传的可选参数。
27
+
26
28
  > [!TIP]
27
29
  > 请使用与运行脚本相同的 Python 解释器安装依赖,例如执行 `python -m pip install uapi-sdk-python` 后再运行 `python main.py`。在 VS Code / Pyright 中若提示 “Import uapi could not be resolved”,将解释器切换到当前虚拟环境即可恢复补全。
28
30
 
@@ -40,6 +42,60 @@ print(result)
40
42
 
41
43
  如果你需要查看字段细节或内部逻辑,仓库中的 `./internal` 目录同步保留了由 `openapi-generator` 生成的完整结构体,随时可供参考。
42
44
 
45
+ ## 响应元信息
46
+
47
+ 每次请求完成后,SDK 会自动把响应 Header 解析成结构化的 `ResponseMeta`,你不用自己拆原始字符串。
48
+
49
+ 成功时可以通过 `client.last_response_meta` 读取,失败时可以通过 `err.meta` 读取,两条路径拿到的是同一套字段。
50
+
51
+ ```python
52
+ from uapi import UapiClient, UapiError
53
+
54
+ client = UapiClient("https://uapis.cn", "YOUR_API_KEY")
55
+
56
+ # 成功路径
57
+ client.social.get_social_qq_userinfo(qq="10001")
58
+ meta = client.last_response_meta
59
+ if meta:
60
+ print("这次请求原价:", meta.credits_requested or 0, "积分")
61
+ print("这次实际扣费:", meta.credits_charged or 0, "积分")
62
+ print("特殊计价:", meta.credits_pricing or "原价")
63
+ print("余额剩余:", meta.balance_remaining_cents or 0, "分")
64
+ print("资源包剩余:", meta.quota_remaining_credits or 0, "积分")
65
+ print("当前有效额度桶:", meta.active_quota_buckets or 0)
66
+ print("额度用空即停:", meta.stop_on_empty)
67
+ print("Key QPS:", meta.billing_key_rate_remaining or 0, "/", meta.billing_key_rate_limit or 0, meta.billing_key_rate_unit or "req")
68
+ print("Request ID:", meta.request_id)
69
+
70
+ # 失败路径
71
+ try:
72
+ client.social.get_social_qq_userinfo(qq="10001")
73
+ except UapiError as err:
74
+ if err.meta:
75
+ print("Retry-After 秒数:", err.meta.retry_after_seconds)
76
+ print("Retry-After 原始值:", err.meta.retry_after_raw)
77
+ print("访客 QPS:", err.meta.visitor_rate_remaining or 0, "/", err.meta.visitor_rate_limit or 0)
78
+ print("Request ID:", err.meta.request_id)
79
+ ```
80
+
81
+ 常用字段一览:
82
+
83
+ | 字段 | 说明 |
84
+ |------|------|
85
+ | `credits_requested` | 这次请求原本要扣多少积分,也就是请求价 |
86
+ | `credits_charged` | 这次请求实际扣了多少积分 |
87
+ | `credits_pricing` | 特殊计价原因,例如缓存半价 `cache-hit-half-price` |
88
+ | `balance_remaining_cents` | 账户余额剩余(分) |
89
+ | `quota_remaining_credits` | 资源包剩余积分 |
90
+ | `active_quota_buckets` | 当前还有多少个有效额度桶参与计费 |
91
+ | `stop_on_empty` | 额度耗尽后是否直接停止服务 |
92
+ | `retry_after_seconds` / `retry_after_raw` | 限流后的等待时长;当服务端返回 HTTP 时间字符串时看 `retry_after_raw` |
93
+ | `request_id` | 请求唯一 ID,排障时使用 |
94
+ | `billing_key_rate_limit` / `billing_key_rate_remaining` | Billing Key 当前 QPS 规则的上限与剩余 |
95
+ | `billing_ip_rate_limit` / `billing_ip_rate_remaining` | Billing Key 单 IP 当前 QPS 规则的上限与剩余 |
96
+ | `visitor_rate_limit` / `visitor_rate_remaining` | 访客当前 QPS 规则的上限与剩余 |
97
+ | `rate_limit_policies` / `rate_limits` | 完整结构化限流策略数据 |
98
+
43
99
  ## 进阶实践
44
100
 
45
101
  ### 缓存与幂等
@@ -48,7 +104,7 @@ print(result)
48
104
  from functools import lru_cache
49
105
  from uapi import UapiClient
50
106
 
51
- client = UapiClient("https://uapis.cn/api/v1", token="<TOKEN>")
107
+ client = UapiClient("https://uapis.cn", token="YOUR_API_KEY")
52
108
 
53
109
  @lru_cache(maxsize=128)
54
110
  def cached_lookup(qq: str):
@@ -59,34 +115,19 @@ user = cached_lookup("10001")
59
115
 
60
116
  也可以在 FastAPI / Django 项目里配合 Redis,将 SDK 的响应序列化后写入缓存,命中即直接返回。
61
117
 
62
- ### 注入自定义 httpx.Client
118
+ ### 调整超时与环境
63
119
 
64
120
  ```python
65
- import httpx
66
- from httpx import Auth
67
121
  from uapi import UapiClient
68
122
 
69
- class StaticToken(Auth):
70
- def __init__(self, token: str):
71
- self.token = token
72
- def auth_flow(self, request):
73
- request.headers["Authorization"] = f"Bearer {self.token}"
74
- yield request
75
-
76
- http_client = httpx.Client(
77
- timeout=5,
78
- transport=httpx.HTTPTransport(retries=3),
79
- event_hooks={"request": [lambda request: print("->", request.url)]},
80
- )
81
-
82
123
  client = UapiClient(
83
- "https://uapis.cn/api/v1",
84
- client=http_client,
85
- auth=StaticToken("<TOKEN>"),
124
+ "https://uapis.cn",
125
+ token="YOUR_API_KEY",
126
+ timeout=5.0,
86
127
  )
87
128
  ```
88
129
 
89
- 通过自定义 `client` / `transport` / `auth`,可以无缝植入代理、重试策略或 APM 埋点。
130
+ 如果你需要切换到别的环境,直接改 `base_url` 就可以;如果你只想缩短等待时间,传 `timeout` 就够了。
90
131
 
91
132
  ## 错误模型概览
92
133
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "uapi-sdk-python"
7
- version = "0.1.10"
7
+ version = "0.1.14"
8
8
  description = "Idiomatic UAPI SDK for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -12,10 +12,18 @@ class _Config:
12
12
  token: Optional[str] = None
13
13
  timeout: float = 15.0
14
14
 
15
+
16
+ def _normalize_base_url(base_url: str) -> str:
17
+ normalized = base_url.rstrip("/")
18
+ if normalized.endswith("/api/v1"):
19
+ normalized = normalized[: -len("/api/v1")]
20
+ return normalized
21
+
15
22
  class _HTTP:
16
23
  def __init__(self, cfg: _Config):
17
24
  self._cfg = cfg
18
- self._client = httpx.Client(timeout=cfg.timeout)
25
+ self._client = httpx.Client(timeout=cfg.timeout, trust_env=False)
26
+ self.last_response_meta: Optional[ResponseMeta] = None
19
27
 
20
28
  def request(self, method: str, path: str, *, params: Dict[str, Any] | None = None, json: Any | None = None, headers: Dict[str, str] | None = None):
21
29
  url = self._cfg.base_url.rstrip("/") + path
@@ -24,7 +32,10 @@ class _HTTP:
24
32
  headers["Authorization"] = f"Bearer {self._cfg.token}"
25
33
  r = self._client.request(method, url, params=params, json=json, headers=headers)
26
34
  if r.status_code >= 400:
27
- raise map_error(r)
35
+ err = map_error(r)
36
+ self.last_response_meta = err.meta
37
+ raise err
38
+ self.last_response_meta = extract_meta(r.headers)
28
39
  # try json else bytes
29
40
  try:
30
41
  return r.json()
@@ -39,7 +50,7 @@ class UapiClient:
39
50
  """
40
51
 
41
52
  def __init__(self, base_url: str, token: str | None = None, timeout: float = 15.0):
42
- self._http = _HTTP(_Config(base_url, token, timeout))
53
+ self._http = _HTTP(_Config(_normalize_base_url(base_url), token, timeout))
43
54
  # 动态挂载每个 Tag 的 API 门面
44
55
  _clipzy_zai_xian_jian_tie_ban = _ClipzyZaiXianJianTieBanApi(self._http)
45
56
  self.clipzy_zai_xian_jian_tie_ban = _clipzy_zai_xian_jian_tie_ban
@@ -90,6 +101,10 @@ class UapiClient:
90
101
  self.zhi_neng_sou_suo = _zhi_neng_sou_suo
91
102
  setattr(self, "智能搜索", _zhi_neng_sou_suo)
92
103
 
104
+ @property
105
+ def last_response_meta(self) -> Optional[ResponseMeta]:
106
+ return self._http.last_response_meta
107
+
93
108
 
94
109
  class _ClipzyZaiXianJianTieBanApi:
95
110
  def __init__(self, http: _HTTP):
@@ -428,12 +443,7 @@ class _ImageApi:
428
443
  这个接口会获取 Bing 搜索引擎当天全球同步的每日壁纸,并直接以图片形式返回。你可以用它来做应用的启动页、网站背景,或者任何需要每日更新精美图片的地方。
429
444
 
430
445
  ## 使用须知
431
-
432
- > [!NOTE]
433
- > **响应格式是图片**
434
- > 请注意,此接口成功时直接返回图片二进制数据(通常为 `image/jpeg`),而非 JSON 格式。请确保客户端能够正确处理。
435
-
436
- 我们内置了备用方案:如果从必应官方获取图片失败,系统会尝试返回一张预存的高质量风景图,以保证服务的稳定性。
446
+ 此接口成功时直接返回图片二进制数据,通常是 `image/jpeg`,不是 JSON 格式。接入时请按图片响应来处理。
437
447
  """
438
448
  params = {}
439
449
  body = {}
@@ -472,21 +482,6 @@ class _ImageApi:
472
482
 
473
483
  ## 功能概述
474
484
  你提供一段文本内容,我们为你生成对应的二维码图片。你可以自定义尺寸、前景色、背景色,还支持透明背景,并选择不同的返回格式以适应不同场景。
475
-
476
- ## 使用须知
477
-
478
- > [!IMPORTANT]
479
- > **关键参数 `format`**
480
- > 此参数决定了成功响应的内容类型和结构,请务必根据你的需求选择并正确处理响应:
481
- > - **`image`** (默认): 直接返回 `image/png` 格式的图片二进制数据,适合在 `<img>` 标签中直接使用。
482
- > - **`json`**: 返回一个包含 Base64 Data URI 的 JSON 对象,适合需要在前端直接嵌入CSS或HTML的场景。
483
- > - **`json_url`**: 返回一个包含图片临时URL的JSON对象,适合需要图片链接的场景。
484
-
485
- > [!TIP]
486
- > **颜色参数说明**
487
- > - 颜色参数使用十六进制格式(如 `#FF0000`)
488
- > - URL 中需要对 `#` 进行编码,即 `%23`(例如:`fgcolor=%23FF0000`)
489
- > - 当 `transparent=true` 时,`bgcolor` 参数会被忽略
490
485
  """
491
486
  params = {}
492
487
  body = {}
@@ -638,9 +633,6 @@ class _ImageApi:
638
633
  r"""图片敏感检测
639
634
  这是一个图片内容审核接口,自动识别图片中的违规内容并返回处理建议。
640
635
 
641
- > [!VIP]
642
- > 此接口限时免费开放,无需企业认证即可使用。
643
-
644
636
  ## 功能概述
645
637
  上传图片文件或提供图片URL,接口会自动分析图片内容,返回是否违规、风险等级和处理建议。适合对接到用户上传流程中,实现自动化内容审核。
646
638
 
@@ -872,19 +864,6 @@ class _MiscApi:
872
864
 
873
865
  ### 数据源列表
874
866
  传 `sources=true`,返回所有支持历史数据的平台列表。
875
-
876
- ## 可选值
877
- `type` 参数接受多种不同的值,每种值对应一个不同的热榜来源。以下是目前支持的所有值:
878
-
879
- | 分类 | 支持的 type 值 |
880
- |------------|-----------------------------------------------------------------------------------------------------------------------------------|
881
- | 视频/社区 | bilibili(哔哩哔哩弹幕网), acfun(A站弹幕视频网站), weibo(新浪微博热搜), zhihu(知乎热榜), zhihu-daily(知乎日报热榜), douyin(抖音热榜), kuaishou(快手热榜), douban-movie(豆瓣电影榜单), douban-group(豆瓣小组话题), tieba(百度贴吧热帖), hupu(虎扑热帖), ngabbs(NGA游戏论坛热帖), v2ex(V2EX技术社区热帖), 52pojie(吾爱破解热帖), hostloc(全球主机交流论坛), coolapk(酷安热榜) |
882
- | 新闻/资讯 | baidu(百度热搜), thepaper(澎湃新闻热榜), toutiao(今日头条热榜), qq-news(腾讯新闻热榜), sina(新浪热搜), sina-news(新浪新闻热榜), netease-news(网易新闻热榜), huxiu(虎嗅网热榜), ifanr(爱范儿热榜) |
883
- | 技术/IT | sspai(少数派热榜), ithome(IT之家热榜), ithome-xijiayi(IT之家·喜加一栏目), juejin(掘金社区热榜), jianshu(简书热榜), guokr(果壳热榜), 36kr(36氪热榜), 51cto(51CTO热榜), csdn(CSDN博客热榜), nodeseek(NodeSeek 技术社区), hellogithub(HelloGitHub 项目推荐) |
884
- | 游戏 | lol(英雄联盟热帖), genshin(原神热榜), honkai(崩坏3热榜), starrail(星穹铁道热榜) |
885
- | 音乐 | netease-music(网易云音乐热歌榜), qq-music(QQ音乐热歌榜) |
886
- | 其他 | weread(微信读书热门书籍), weatheralarm(天气预警信息), earthquake(地震速报), history(历史上的今天) |
887
-
888
867
  """
889
868
  params = {}
890
869
  body = {}
@@ -1034,9 +1013,6 @@ graph TD
1034
1013
  r"""获取支持的快递公司列表
1035
1014
  不确定系统支持哪些快递公司?这个接口返回完整的支持列表。
1036
1015
 
1037
- > [!VIP]
1038
- > 本API目前处于**限时免费**阶段,我们鼓励开发者集成和测试。未来,它将转为付费API,为用户提供更稳定和强大的服务。
1039
-
1040
1016
  ## 功能概述
1041
1017
  获取系统当前支持的所有快递公司列表,包括每家公司的标准编码(code)和中文名称(name)。
1042
1018
 
@@ -1056,9 +1032,6 @@ graph TD
1056
1032
  r"""识别快递公司
1057
1033
  不确定手里的快递单号属于哪家快递公司?这个接口专门做识别,不查物流。
1058
1034
 
1059
- > [!VIP]
1060
- > 本API目前处于**限时免费**阶段,我们鼓励开发者集成和测试。未来,它将转为付费API,为用户提供更稳定和强大的服务。
1061
-
1062
1035
  ## 功能概述
1063
1036
  输入快递单号,系统会根据单号规则快速识别出最可能的快递公司。如果存在多个可能的匹配结果,还会在 `alternatives` 字段中返回备选项,供你参考选择。
1064
1037
 
@@ -1081,16 +1054,15 @@ graph TD
1081
1054
  r"""查询快递物流信息
1082
1055
  买了东西想知道快递到哪儿了?这个接口帮你实时追踪物流状态。
1083
1056
 
1084
- > [!VIP]
1085
- > 本API目前处于**限时免费**阶段,我们鼓励开发者集成和测试。未来,它将转为付费API,为用户提供更稳定和强大的服务。
1086
-
1087
1057
  ## 功能概述
1088
- 提供一个快递单号,系统会自动识别快递公司并返回完整的物流轨迹信息。支持中通、圆通、韵达、申通、极兔、顺丰、京东、EMS、德邦等60+国内外主流快递公司。
1058
+ 提供一个快递单号,系统会自动识别快递公司并返回完整的物流轨迹信息。这个接口目前可以查询中通、圆通、韵达、申通、极兔、京东、EMS、德邦等主流快递公司的物流信息。
1089
1059
 
1090
1060
  ## 使用须知
1061
+ 目前暂不支持顺丰快递单号的物流查询。
1062
+
1091
1063
  - **自动识别**:不知道是哪家快递?系统会根据单号规则自动识别快递公司(推荐使用)
1092
1064
  - **手动指定**:如果已知快递公司,可以传递 `carrier_code` 参数,查询速度会更快
1093
- - **手机尾号验证**:部分快递公司需要验证收件人手机尾号才能查询详细物流,如果返回「暂无物流信息」,建议尝试传入 `phone` 参数
1065
+ - **手机尾号验证**:部分快递公司需要验证收件人手机尾号才能查询详细物流,如果返回 `暂无物流信息`,建议尝试传入 `phone` 参数
1094
1066
  - **查询时效**:物流信息实时查询,响应时间通常在1-2秒内
1095
1067
  """
1096
1068
  params = {}
@@ -1123,9 +1095,9 @@ graph TD
1123
1095
 
1124
1096
  ## 可选功能模块
1125
1097
  - `extended=true`:扩展气象字段(体感温度、能见度、气压、紫外线、空气质量及污染物分项数据)
1126
- - `forecast=true`:多天预报(最多7天,含日出日落、风速等详细数据)
1098
+ - `forecast=true`:多天预报(最多7天,会额外返回每天的最高温度、最低温度,以及日出日落、风速等详细数据)
1127
1099
  - `hourly=true`:逐小时预报(24小时)
1128
- - `minutely=true`:分钟级降水预报(仅国内城市)
1100
+ - `minutely=true`:分钟级降水预报(仅国内城市,精确到2分钟)
1129
1101
  - `indices=true`:18项生活指数(穿衣、紫外线、洗车、运动、花粉等)
1130
1102
 
1131
1103
  ## 天气字段说明
@@ -1133,7 +1105,7 @@ graph TD
1133
1105
 
1134
1106
  常见值包括:晴、多云、阴、小雨、中雨、大雨、雷阵雨、小雪、中雪、大雪、雨夹雪、雾、霾、沙尘。
1135
1107
 
1136
- 如果你的业务需要稳定分类,建议结合 `weather_code` 做自己的映射归类。
1108
+ 如果你的业务需要稳定的天气分类,建议使用 `weather_code` 进行映射。完整的天气图标代码请参考[天气图标代码表](#enum-list)。
1137
1109
  """
1138
1110
  params = {}
1139
1111
  body = {}
@@ -1770,10 +1742,7 @@ class _SocialApi:
1770
1742
 
1771
1743
  def get_social_qq_groupinfo(self, **kwargs):
1772
1744
  r"""查询 QQ 群信息
1773
- 想在你的应用里展示QQ群信息?这个接口让你轻松获取群名称、群头像、群简介、成员数量等详细公开信息。它能帮你快速构建社群导航站、群聊推荐系统,或是为你的数据分析工具提供可靠的数据源。
1774
-
1775
- > [!VIP]
1776
- > 本API目前处于**限时免费**阶段,我们鼓励开发者集成和测试。未来,它将转为付费API,为用户提供更稳定和强大的服务。
1745
+ 想在你的应用里展示QQ群信息?这个接口让你轻松获取群名称、群头像、群简介、成员数量等详细公开信息。
1777
1746
 
1778
1747
  ## 功能概述
1779
1748
  你只需要提供一个QQ群号(5-12位纯数字),接口就会返回该群的完整公开信息。我们会先验证群号的有效性,确保返回的数据准确可靠。接口响应速度快,数据结构清晰,非常适合集成到各类应用场景中。
@@ -1813,9 +1782,6 @@ class _SocialApi:
1813
1782
  r"""查询 QQ 信息
1814
1783
  这是一个功能丰富的QQ用户信息查询接口,能够获取QQ用户的详细公开信息。
1815
1784
 
1816
- > [!VIP]
1817
- > 我们在近日优化了此接口,速度应该会更加快了。
1818
-
1819
1785
  ## 功能概述
1820
1786
  通过QQ号查询用户的详细信息,包括基础资料、等级信息、VIP状态等。返回的信息丰富全面,适合用于用户画像分析、社交应用集成等场景。
1821
1787
 
@@ -2299,19 +2265,16 @@ class _TranslateApi:
2299
2265
  r"""AI智能翻译
2300
2266
  这是一个商业级的AI智能翻译服务,采用最新的神经网络翻译技术和大语言模型,提供远超传统机器翻译的质量。
2301
2267
 
2302
- > [!VIP]
2303
- > 本API目前处于**限时免费**阶段,我们鼓励开发者深度集成和测试。未来,它将转为付费API,为用户提供更稳定、更智能的翻译服务。
2304
-
2305
2268
  ## 功能概述
2306
2269
 
2307
- - **智能双模式**: 支持单个文本翻译和批量文本翻译的统一接口设计,自动识别请求类型并提供相应的翻译服务。系统会根据输入自动判断是处理单条文本还是批量文本,无需使用不同的接口。
2270
+ - **单文本翻译**: 专注处理单条文本翻译,适合需要高质量译文的业务场景。
2308
2271
  - **多风格适配**: 提供随意口语化、专业商务、学术正式、文学艺术四种翻译风格,能够根据不同场景需求调整翻译的语言风格和表达方式。
2309
2272
  - **上下文感知**: 支持通用、商务、技术、医疗、法律、市场营销、娱乐、教育、新闻等九种专业领域的上下文翻译,确保术语准确性和表达地道性。
2310
- - **高质量保证**: 内置质量评估系统,对每次翻译结果进行流畅度、准确度、完整性评分,并提供置信度分数和替代翻译建议。
2311
- - **智能解释**: 提供关键词组翻译注释、文化背景说明和语法结构分析,帮助用户理解翻译逻辑和文化差异。
2312
- - **高效批量**: 批量翻译支持最多50条文本,总计10万字符,配备智能并发控制(1-10并发)和失败重试机制。
2313
- - **快速模式**: 提供快速模式选项,在保证95%+准确率的前提下,响应时间缩短至800ms内,适合实时翻译和聊天应用。
2314
2273
  - **格式保留**: 智能识别并保持原文的格式结构,包括换行、缩进、特殊符号等,确保翻译后的文本保持良好的可读性。
2274
+
2275
+ ## 支持的语言
2276
+
2277
+ 我们支持超过100种语言的互译,详见下方参数列表。
2315
2278
  """
2316
2279
  params = {}
2317
2280
  body = {}
@@ -2323,12 +2286,6 @@ class _TranslateApi:
2323
2286
  if "context" in kwargs:
2324
2287
  body["context"] = kwargs["context"]
2325
2288
 
2326
- if "fast_mode" in kwargs:
2327
- body["fast_mode"] = kwargs["fast_mode"]
2328
-
2329
- if "max_concurrency" in kwargs:
2330
- body["max_concurrency"] = kwargs["max_concurrency"]
2331
-
2332
2289
  if "preserve_format" in kwargs:
2333
2290
  body["preserve_format"] = kwargs["preserve_format"]
2334
2291
 
@@ -2341,9 +2298,6 @@ class _TranslateApi:
2341
2298
  if "text" in kwargs:
2342
2299
  body["text"] = kwargs["text"]
2343
2300
 
2344
- if "texts" in kwargs:
2345
- body["texts"] = kwargs["texts"]
2346
-
2347
2301
  path = "/api/v1/ai/translate"
2348
2302
 
2349
2303
  return self._http.request("POST", path, params=params, json=body if body else None)
@@ -2490,9 +2444,6 @@ class _WebparseApi:
2490
2444
 
2491
2445
  ## 功能概述
2492
2446
 
2493
- > [!VIP]
2494
- > 本 API 目前处于**限时免费**阶段,未来将转为付费服务。
2495
-
2496
2447
  提交一个网页 URL,我们会自动抓取主体内容,剔除广告、导航栏等干扰元素,并转换为 Markdown 格式。同时会提取标题、作者、发布日期等元数据,生成 YAML Front Matter。
2497
2448
 
2498
2449
  任务提交后会立即返回任务 ID,你可以用它来查询处理进度和结果。单个任务最长处理 60 秒,结果缓存 30 分钟。
@@ -2531,14 +2482,12 @@ class _MinGanCiShiBieApi:
2531
2482
  r"""分析敏感词
2532
2483
  分析单个或多个关键词的敏感程度,返回标准化风险标签与置信度结果。
2533
2484
 
2534
- > [!VIP]
2535
- > 本API基于先进的分析模型,提供三级缓存策略和并发处理能力。
2536
-
2537
2485
  ## 功能概述
2538
2486
 
2539
2487
  - **模型驱动**: 使用先进的分析模型进行语义分析。
2540
2488
  - **高性能**: 采用三级缓存策略(持久化存储 → 统一缓存 → 模型分析),确保高频请求的响应速度。
2541
2489
  - **并发支持**: 支持批量并发处理,单次最多可分析100个关键词。
2490
+ - **输入限制**: 单条关键词最多 1,000 字符,总字符数最多 20,000。
2542
2491
  - **标准标签**: 返回 `label` 字段,明确区分 `sensitive` 与 `normal`。
2543
2492
  - **分类清晰**: 返回 `category` 字段,用于标识具体风险类别。
2544
2493
  - **置信度输出**: 返回 `confidence` 字段,范围为0.0到1.0。
@@ -2635,9 +2584,6 @@ UAPI Pro Search 是一个智能搜索引擎,采用机器学习算法对搜索
2635
2584
  - **时间范围过滤**: 支持按天/周/月/年过滤结果
2636
2585
  - **站内搜索**: 支持 `site:` 操作符,在指定网站内搜索
2637
2586
  - **文件类型过滤**: 支持 `filetype:` 操作符,快速找到 PDF、Word 等特定格式文件
2638
-
2639
- > [!VIP]
2640
- > 本API目前处于**限时免费**阶段,我们鼓励开发者集成和测试。未来,它将转为付费API,为用户提供更稳定和强大的服务。
2641
2587
 
2642
2588
  """
2643
2589
  params = {}
@@ -2669,4 +2615,3 @@ UAPI Pro Search 是一个智能搜索引擎,采用机器学习算法对搜索
2669
2615
 
2670
2616
  return self._http.request("POST", path, params=params, json=body if body else None)
2671
2617
 
2672
-
@@ -0,0 +1,374 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Dict, Mapping, Optional
4
+ import httpx
5
+
6
+ @dataclass
7
+ class RateLimitPolicyEntry:
8
+ name: str
9
+ quota: Optional[int] = None
10
+ unit: Optional[str] = None
11
+ window_seconds: Optional[int] = None
12
+
13
+ @dataclass
14
+ class RateLimitStateEntry:
15
+ name: str
16
+ remaining: Optional[int] = None
17
+ unit: Optional[str] = None
18
+ reset_after_seconds: Optional[int] = None
19
+
20
+ @dataclass
21
+ class ResponseMeta:
22
+ request_id: Optional[str] = None
23
+ retry_after_raw: Optional[str] = None
24
+ retry_after_seconds: Optional[int] = None
25
+ debit_status: Optional[str] = None
26
+ credits_requested: Optional[int] = None
27
+ credits_charged: Optional[int] = None
28
+ credits_pricing: Optional[str] = None
29
+ active_quota_buckets: Optional[int] = None
30
+ stop_on_empty: Optional[bool] = None
31
+ rate_limit_policy_raw: Optional[str] = None
32
+ rate_limit_raw: Optional[str] = None
33
+ rate_limit_policies: Dict[str, RateLimitPolicyEntry] = field(default_factory=dict)
34
+ rate_limits: Dict[str, RateLimitStateEntry] = field(default_factory=dict)
35
+ balance_limit_cents: Optional[int] = None
36
+ balance_remaining_cents: Optional[int] = None
37
+ quota_limit_credits: Optional[int] = None
38
+ quota_remaining_credits: Optional[int] = None
39
+ visitor_quota_limit_credits: Optional[int] = None
40
+ visitor_quota_remaining_credits: Optional[int] = None
41
+ billing_key_rate_limit: Optional[int] = None
42
+ billing_key_rate_remaining: Optional[int] = None
43
+ billing_key_rate_unit: Optional[str] = None
44
+ billing_key_rate_window_seconds: Optional[int] = None
45
+ billing_key_rate_reset_after_seconds: Optional[int] = None
46
+ billing_ip_rate_limit: Optional[int] = None
47
+ billing_ip_rate_remaining: Optional[int] = None
48
+ billing_ip_rate_unit: Optional[str] = None
49
+ billing_ip_rate_window_seconds: Optional[int] = None
50
+ billing_ip_rate_reset_after_seconds: Optional[int] = None
51
+ visitor_rate_limit: Optional[int] = None
52
+ visitor_rate_remaining: Optional[int] = None
53
+ visitor_rate_unit: Optional[str] = None
54
+ visitor_rate_window_seconds: Optional[int] = None
55
+ visitor_rate_reset_after_seconds: Optional[int] = None
56
+ raw_headers: Dict[str, str] = field(default_factory=dict)
57
+
58
+ class UapiError(Exception):
59
+ code: str
60
+ status: int
61
+ message: str
62
+ details: Any
63
+ payload: Any
64
+ meta: Optional[ResponseMeta]
65
+
66
+ def __init__(
67
+ self,
68
+ code: str,
69
+ status: int,
70
+ message: str,
71
+ details: Any = None,
72
+ payload: Any = None,
73
+ meta: Optional[ResponseMeta] = None,
74
+ ):
75
+ super().__init__(f"[{status}] {code}: {message}")
76
+ self.code = code
77
+ self.status = status
78
+ self.message = message
79
+ self.details = details
80
+ self.payload = payload
81
+ self.meta = meta
82
+
83
+
84
+ class ApiErrorError(UapiError):
85
+ """上游/内部错误 (API_ERROR)"""
86
+ DEFAULT_STATUS = 502
87
+
88
+ class AvatarNotFoundError(UapiError):
89
+ """头像未找到 (AVATAR_NOT_FOUND)"""
90
+ DEFAULT_STATUS = 404
91
+
92
+ class ConversionFailedError(UapiError):
93
+ """转换失败 (CONVERSION_FAILED)"""
94
+ DEFAULT_STATUS = 400
95
+
96
+ class FileOpenErrorError(UapiError):
97
+ """文件打开错误 (FILE_OPEN_ERROR)"""
98
+ DEFAULT_STATUS = 500
99
+
100
+ class FileRequiredError(UapiError):
101
+ """文件必需 (FILE_REQUIRED)"""
102
+ DEFAULT_STATUS = 400
103
+
104
+ class InsufficientCreditsError(UapiError):
105
+ """账户积分不足 (INSUFFICIENT_CREDITS)"""
106
+ DEFAULT_STATUS = 402
107
+
108
+ class InternalServerErrorError(UapiError):
109
+ """服务器内部错误 (INTERNAL_SERVER_ERROR)"""
110
+ DEFAULT_STATUS = 500
111
+
112
+ class InvalidParameterError(UapiError):
113
+ """请求参数错误 (INVALID_PARAMETER)"""
114
+ DEFAULT_STATUS = 400
115
+
116
+ class InvalidParamsError(UapiError):
117
+ """无效参数 (INVALID_PARAMS)"""
118
+ DEFAULT_STATUS = 400
119
+
120
+ class NotFoundError(UapiError):
121
+ """资源不存在 (NOT_FOUND)"""
122
+ DEFAULT_STATUS = 404
123
+
124
+ class NoMatchError(UapiError):
125
+ """无匹配 (NO_MATCH)"""
126
+ DEFAULT_STATUS = 404
127
+
128
+ class NoTrackingDataError(UapiError):
129
+ """无物流数据 (NO_TRACKING_DATA)"""
130
+ DEFAULT_STATUS = 404
131
+
132
+ class PhoneInfoFailedError(UapiError):
133
+ """手机号信息查询失败 (PHONE_INFO_FAILED)"""
134
+ DEFAULT_STATUS = 500
135
+
136
+ class RecognitionFailedError(UapiError):
137
+ """识别失败 (RECOGNITION_FAILED)"""
138
+ DEFAULT_STATUS = 404
139
+
140
+ class RequestEntityTooLargeError(UapiError):
141
+ """错误 (REQUEST_ENTITY_TOO_LARGE)"""
142
+ DEFAULT_STATUS = 413
143
+
144
+ class ServiceBusyError(UapiError):
145
+ """请求过于频繁 (SERVICE_BUSY)"""
146
+ DEFAULT_STATUS = 429
147
+
148
+ class TimezoneNotFoundError(UapiError):
149
+ """时区未找到 (TIMEZONE_NOT_FOUND)"""
150
+ DEFAULT_STATUS = 404
151
+
152
+ class UnauthorizedError(UapiError):
153
+ """请求未授权 (UNAUTHORIZED)"""
154
+ DEFAULT_STATUS = 401
155
+
156
+ class UnsupportedCarrierError(UapiError):
157
+ """不支持的承运商 (UNSUPPORTED_CARRIER)"""
158
+ DEFAULT_STATUS = 404
159
+
160
+ class UnsupportedFormatError(UapiError):
161
+ """格式不支持 (UNSUPPORTED_FORMAT)"""
162
+ DEFAULT_STATUS = 400
163
+
164
+ class VisitorMonthlyQuotaExhaustedError(UapiError):
165
+ """访客月度免费额度已用尽 (VISITOR_MONTHLY_QUOTA_EXHAUSTED)"""
166
+ DEFAULT_STATUS = 429
167
+
168
+
169
+ def _default_code(status: int) -> str:
170
+ if status == 400:
171
+ return "INVALID_PARAMETER"
172
+ if status == 401:
173
+ return "UNAUTHORIZED"
174
+ if status == 402:
175
+ return "INSUFFICIENT_CREDITS"
176
+ if status == 404:
177
+ return "NOT_FOUND"
178
+ if status == 413:
179
+ return "REQUEST_ENTITY_TOO_LARGE"
180
+ if status == 429:
181
+ return "SERVICE_BUSY"
182
+ if status >= 500:
183
+ return "INTERNAL_SERVER_ERROR"
184
+ return "API_ERROR"
185
+
186
+ def _parse_int(value: Optional[str]) -> Optional[int]:
187
+ if value is None:
188
+ return None
189
+ try:
190
+ return int(value)
191
+ except (TypeError, ValueError):
192
+ return None
193
+
194
+ def _parse_bool(value: Optional[str]) -> Optional[bool]:
195
+ if value is None:
196
+ return None
197
+ lowered = value.strip().lower()
198
+ if lowered == "true":
199
+ return True
200
+ if lowered == "false":
201
+ return False
202
+ return None
203
+
204
+ def _unquote(value: str) -> str:
205
+ text = value.strip()
206
+ if len(text) >= 2 and text[0] == '"' and text[-1] == '"':
207
+ return text[1:-1]
208
+ return text
209
+
210
+ def _parse_structured_items(raw: Optional[str]) -> list[tuple[str, Dict[str, str]]]:
211
+ if not raw:
212
+ return []
213
+ items: list[tuple[str, Dict[str, str]]] = []
214
+ for chunk in [part.strip() for part in raw.split(",") if part.strip()]:
215
+ segments = [segment.strip() for segment in chunk.split(";") if segment.strip()]
216
+ if not segments:
217
+ continue
218
+ name = _unquote(segments[0])
219
+ params: Dict[str, str] = {}
220
+ for segment in segments[1:]:
221
+ if "=" not in segment:
222
+ continue
223
+ key, value = segment.split("=", 1)
224
+ params[key.strip()] = _unquote(value)
225
+ items.append((name, params))
226
+ return items
227
+
228
+ def extract_meta(headers: Mapping[str, str]) -> ResponseMeta:
229
+ raw_headers = {str(key).lower(): str(value) for key, value in headers.items()}
230
+ rate_limit_policies: Dict[str, RateLimitPolicyEntry] = {}
231
+ rate_limits: Dict[str, RateLimitStateEntry] = {}
232
+
233
+ for name, params in _parse_structured_items(raw_headers.get("ratelimit-policy")):
234
+ rate_limit_policies[name] = RateLimitPolicyEntry(
235
+ name=name,
236
+ quota=_parse_int(params.get("q")),
237
+ unit=params.get("uapi-unit"),
238
+ window_seconds=_parse_int(params.get("w")),
239
+ )
240
+
241
+ for name, params in _parse_structured_items(raw_headers.get("ratelimit")):
242
+ rate_limits[name] = RateLimitStateEntry(
243
+ name=name,
244
+ remaining=_parse_int(params.get("r")),
245
+ unit=params.get("uapi-unit"),
246
+ reset_after_seconds=_parse_int(params.get("t")),
247
+ )
248
+
249
+ billing_key_rate_policy = rate_limit_policies.get("billing-key-rate")
250
+ billing_key_rate_state = rate_limits.get("billing-key-rate")
251
+ billing_ip_rate_policy = rate_limit_policies.get("billing-ip-rate")
252
+ billing_ip_rate_state = rate_limits.get("billing-ip-rate")
253
+ visitor_rate_policy = rate_limit_policies.get("visitor-rate")
254
+ visitor_rate_state = rate_limits.get("visitor-rate")
255
+
256
+ return ResponseMeta(
257
+ request_id=raw_headers.get("x-request-id"),
258
+ retry_after_raw=raw_headers.get("retry-after"),
259
+ retry_after_seconds=_parse_int(raw_headers.get("retry-after")),
260
+ debit_status=raw_headers.get("uapi-debit-status"),
261
+ credits_requested=_parse_int(raw_headers.get("uapi-credits-requested")),
262
+ credits_charged=_parse_int(raw_headers.get("uapi-credits-charged")),
263
+ credits_pricing=raw_headers.get("uapi-credits-pricing"),
264
+ active_quota_buckets=_parse_int(raw_headers.get("uapi-quota-active-buckets")),
265
+ stop_on_empty=_parse_bool(raw_headers.get("uapi-stop-on-empty")),
266
+ rate_limit_policy_raw=raw_headers.get("ratelimit-policy"),
267
+ rate_limit_raw=raw_headers.get("ratelimit"),
268
+ rate_limit_policies=rate_limit_policies,
269
+ rate_limits=rate_limits,
270
+ balance_limit_cents=rate_limit_policies.get("billing-balance").quota if "billing-balance" in rate_limit_policies else None,
271
+ balance_remaining_cents=rate_limits.get("billing-balance").remaining if "billing-balance" in rate_limits else None,
272
+ quota_limit_credits=rate_limit_policies.get("billing-quota").quota if "billing-quota" in rate_limit_policies else None,
273
+ quota_remaining_credits=rate_limits.get("billing-quota").remaining if "billing-quota" in rate_limits else None,
274
+ visitor_quota_limit_credits=rate_limit_policies.get("visitor-quota").quota if "visitor-quota" in rate_limit_policies else None,
275
+ visitor_quota_remaining_credits=rate_limits.get("visitor-quota").remaining if "visitor-quota" in rate_limits else None,
276
+ billing_key_rate_limit=billing_key_rate_policy.quota if billing_key_rate_policy else None,
277
+ billing_key_rate_remaining=billing_key_rate_state.remaining if billing_key_rate_state else None,
278
+ billing_key_rate_unit=billing_key_rate_policy.unit if billing_key_rate_policy and billing_key_rate_policy.unit is not None else (billing_key_rate_state.unit if billing_key_rate_state else None),
279
+ billing_key_rate_window_seconds=billing_key_rate_policy.window_seconds if billing_key_rate_policy else None,
280
+ billing_key_rate_reset_after_seconds=billing_key_rate_state.reset_after_seconds if billing_key_rate_state else None,
281
+ billing_ip_rate_limit=billing_ip_rate_policy.quota if billing_ip_rate_policy else None,
282
+ billing_ip_rate_remaining=billing_ip_rate_state.remaining if billing_ip_rate_state else None,
283
+ billing_ip_rate_unit=billing_ip_rate_policy.unit if billing_ip_rate_policy and billing_ip_rate_policy.unit is not None else (billing_ip_rate_state.unit if billing_ip_rate_state else None),
284
+ billing_ip_rate_window_seconds=billing_ip_rate_policy.window_seconds if billing_ip_rate_policy else None,
285
+ billing_ip_rate_reset_after_seconds=billing_ip_rate_state.reset_after_seconds if billing_ip_rate_state else None,
286
+ visitor_rate_limit=visitor_rate_policy.quota if visitor_rate_policy else None,
287
+ visitor_rate_remaining=visitor_rate_state.remaining if visitor_rate_state else None,
288
+ visitor_rate_unit=visitor_rate_policy.unit if visitor_rate_policy and visitor_rate_policy.unit is not None else (visitor_rate_state.unit if visitor_rate_state else None),
289
+ visitor_rate_window_seconds=visitor_rate_policy.window_seconds if visitor_rate_policy else None,
290
+ visitor_rate_reset_after_seconds=visitor_rate_state.reset_after_seconds if visitor_rate_state else None,
291
+ raw_headers=raw_headers,
292
+ )
293
+
294
+ def _pick_details(data: Any) -> Any:
295
+ if not isinstance(data, dict):
296
+ return None
297
+ if "details" in data:
298
+ return data["details"]
299
+ if "quota" in data:
300
+ return data["quota"]
301
+ if "docs" in data:
302
+ return data["docs"]
303
+ return None
304
+
305
+ def map_error(r: httpx.Response) -> UapiError:
306
+ code = None
307
+ msg = r.text
308
+ data: Any = None
309
+ try:
310
+ data = r.json()
311
+ code = data.get("code") or data.get("error") or data.get("errCode") or _default_code(r.status_code)
312
+ msg = data.get("message") or data.get("errMsg") or msg
313
+ except Exception:
314
+ code = _default_code(r.status_code)
315
+ status = r.status_code
316
+ meta = extract_meta(r.headers)
317
+ cls = _class_by_code(code, status)
318
+ return cls(code, status, msg, _pick_details(data), data, meta)
319
+
320
+ def _class_by_code(code: str, status: int):
321
+ c = (code or "").upper()
322
+ mapping = {
323
+
324
+ "API_ERROR": ApiErrorError,
325
+
326
+ "AVATAR_NOT_FOUND": AvatarNotFoundError,
327
+
328
+ "CONVERSION_FAILED": ConversionFailedError,
329
+
330
+ "FILE_OPEN_ERROR": FileOpenErrorError,
331
+
332
+ "FILE_REQUIRED": FileRequiredError,
333
+
334
+ "INSUFFICIENT_CREDITS": InsufficientCreditsError,
335
+
336
+ "INTERNAL_SERVER_ERROR": InternalServerErrorError,
337
+
338
+ "INVALID_PARAMETER": InvalidParameterError,
339
+
340
+ "INVALID_PARAMS": InvalidParamsError,
341
+
342
+ "NOT_FOUND": NotFoundError,
343
+
344
+ "NO_MATCH": NoMatchError,
345
+
346
+ "NO_TRACKING_DATA": NoTrackingDataError,
347
+
348
+ "PHONE_INFO_FAILED": PhoneInfoFailedError,
349
+
350
+ "RECOGNITION_FAILED": RecognitionFailedError,
351
+
352
+ "REQUEST_ENTITY_TOO_LARGE": RequestEntityTooLargeError,
353
+
354
+ "SERVICE_BUSY": ServiceBusyError,
355
+
356
+ "TIMEZONE_NOT_FOUND": TimezoneNotFoundError,
357
+
358
+ "UNAUTHORIZED": UnauthorizedError,
359
+
360
+ "UNSUPPORTED_CARRIER": UnsupportedCarrierError,
361
+
362
+ "UNSUPPORTED_FORMAT": UnsupportedFormatError,
363
+
364
+ "VISITOR_MONTHLY_QUOTA_EXHAUSTED": VisitorMonthlyQuotaExhaustedError,
365
+
366
+ }
367
+ return mapping.get(c) or ({
368
+ 400: InvalidParameterError,
369
+ 401: UnauthorizedError,
370
+ 402: InsufficientCreditsError,
371
+ 404: NotFoundError,
372
+ 429: ServiceBusyError,
373
+ 500: InternalServerErrorError,
374
+ }.get(status) or UapiError)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uapi-sdk-python
3
- Version: 0.1.10
3
+ Version: 0.1.14
4
4
  Summary: Idiomatic UAPI SDK for Python
5
5
  Author-email: UAPI <dev@uapis.cn>
6
6
  Requires-Python: >=3.9
@@ -32,11 +32,13 @@ pip install uapi-sdk-python
32
32
  ```python
33
33
  from uapi import UapiClient
34
34
 
35
- client = UapiClient("https://uapis.cn/api/v1")
36
- result = client.social.get_social_qq_userinfo(qq="10001")
35
+ client = UapiClient("https://uapis.cn", "YOUR_API_KEY")
36
+ result = client.misc.get_misc_hotboard(type="weibo")
37
37
  print(result)
38
38
  ```
39
39
 
40
+ 这个接口默认只要传 `type` 就可以拿当前热榜。`time`、`keyword`、`time_start`、`time_end`、`limit`、`sources` 都是按场景再传的可选参数。
41
+
40
42
  > [!TIP]
41
43
  > 请使用与运行脚本相同的 Python 解释器安装依赖,例如执行 `python -m pip install uapi-sdk-python` 后再运行 `python main.py`。在 VS Code / Pyright 中若提示 “Import uapi could not be resolved”,将解释器切换到当前虚拟环境即可恢复补全。
42
44
 
@@ -54,6 +56,60 @@ print(result)
54
56
 
55
57
  如果你需要查看字段细节或内部逻辑,仓库中的 `./internal` 目录同步保留了由 `openapi-generator` 生成的完整结构体,随时可供参考。
56
58
 
59
+ ## 响应元信息
60
+
61
+ 每次请求完成后,SDK 会自动把响应 Header 解析成结构化的 `ResponseMeta`,你不用自己拆原始字符串。
62
+
63
+ 成功时可以通过 `client.last_response_meta` 读取,失败时可以通过 `err.meta` 读取,两条路径拿到的是同一套字段。
64
+
65
+ ```python
66
+ from uapi import UapiClient, UapiError
67
+
68
+ client = UapiClient("https://uapis.cn", "YOUR_API_KEY")
69
+
70
+ # 成功路径
71
+ client.social.get_social_qq_userinfo(qq="10001")
72
+ meta = client.last_response_meta
73
+ if meta:
74
+ print("这次请求原价:", meta.credits_requested or 0, "积分")
75
+ print("这次实际扣费:", meta.credits_charged or 0, "积分")
76
+ print("特殊计价:", meta.credits_pricing or "原价")
77
+ print("余额剩余:", meta.balance_remaining_cents or 0, "分")
78
+ print("资源包剩余:", meta.quota_remaining_credits or 0, "积分")
79
+ print("当前有效额度桶:", meta.active_quota_buckets or 0)
80
+ print("额度用空即停:", meta.stop_on_empty)
81
+ print("Key QPS:", meta.billing_key_rate_remaining or 0, "/", meta.billing_key_rate_limit or 0, meta.billing_key_rate_unit or "req")
82
+ print("Request ID:", meta.request_id)
83
+
84
+ # 失败路径
85
+ try:
86
+ client.social.get_social_qq_userinfo(qq="10001")
87
+ except UapiError as err:
88
+ if err.meta:
89
+ print("Retry-After 秒数:", err.meta.retry_after_seconds)
90
+ print("Retry-After 原始值:", err.meta.retry_after_raw)
91
+ print("访客 QPS:", err.meta.visitor_rate_remaining or 0, "/", err.meta.visitor_rate_limit or 0)
92
+ print("Request ID:", err.meta.request_id)
93
+ ```
94
+
95
+ 常用字段一览:
96
+
97
+ | 字段 | 说明 |
98
+ |------|------|
99
+ | `credits_requested` | 这次请求原本要扣多少积分,也就是请求价 |
100
+ | `credits_charged` | 这次请求实际扣了多少积分 |
101
+ | `credits_pricing` | 特殊计价原因,例如缓存半价 `cache-hit-half-price` |
102
+ | `balance_remaining_cents` | 账户余额剩余(分) |
103
+ | `quota_remaining_credits` | 资源包剩余积分 |
104
+ | `active_quota_buckets` | 当前还有多少个有效额度桶参与计费 |
105
+ | `stop_on_empty` | 额度耗尽后是否直接停止服务 |
106
+ | `retry_after_seconds` / `retry_after_raw` | 限流后的等待时长;当服务端返回 HTTP 时间字符串时看 `retry_after_raw` |
107
+ | `request_id` | 请求唯一 ID,排障时使用 |
108
+ | `billing_key_rate_limit` / `billing_key_rate_remaining` | Billing Key 当前 QPS 规则的上限与剩余 |
109
+ | `billing_ip_rate_limit` / `billing_ip_rate_remaining` | Billing Key 单 IP 当前 QPS 规则的上限与剩余 |
110
+ | `visitor_rate_limit` / `visitor_rate_remaining` | 访客当前 QPS 规则的上限与剩余 |
111
+ | `rate_limit_policies` / `rate_limits` | 完整结构化限流策略数据 |
112
+
57
113
  ## 进阶实践
58
114
 
59
115
  ### 缓存与幂等
@@ -62,7 +118,7 @@ print(result)
62
118
  from functools import lru_cache
63
119
  from uapi import UapiClient
64
120
 
65
- client = UapiClient("https://uapis.cn/api/v1", token="<TOKEN>")
121
+ client = UapiClient("https://uapis.cn", token="YOUR_API_KEY")
66
122
 
67
123
  @lru_cache(maxsize=128)
68
124
  def cached_lookup(qq: str):
@@ -73,34 +129,19 @@ user = cached_lookup("10001")
73
129
 
74
130
  也可以在 FastAPI / Django 项目里配合 Redis,将 SDK 的响应序列化后写入缓存,命中即直接返回。
75
131
 
76
- ### 注入自定义 httpx.Client
132
+ ### 调整超时与环境
77
133
 
78
134
  ```python
79
- import httpx
80
- from httpx import Auth
81
135
  from uapi import UapiClient
82
136
 
83
- class StaticToken(Auth):
84
- def __init__(self, token: str):
85
- self.token = token
86
- def auth_flow(self, request):
87
- request.headers["Authorization"] = f"Bearer {self.token}"
88
- yield request
89
-
90
- http_client = httpx.Client(
91
- timeout=5,
92
- transport=httpx.HTTPTransport(retries=3),
93
- event_hooks={"request": [lambda request: print("->", request.url)]},
94
- )
95
-
96
137
  client = UapiClient(
97
- "https://uapis.cn/api/v1",
98
- client=http_client,
99
- auth=StaticToken("<TOKEN>"),
138
+ "https://uapis.cn",
139
+ token="YOUR_API_KEY",
140
+ timeout=5.0,
100
141
  )
101
142
  ```
102
143
 
103
- 通过自定义 `client` / `transport` / `auth`,可以无缝植入代理、重试策略或 APM 埋点。
144
+ 如果你需要切换到别的环境,直接改 `base_url` 就可以;如果你只想缩短等待时间,传 `timeout` 就够了。
104
145
 
105
146
  ## 错误模型概览
106
147
 
@@ -1,153 +0,0 @@
1
- from __future__ import annotations
2
- from typing import Any, Dict, Optional
3
- import httpx
4
-
5
- class UapiError(Exception):
6
- code: str
7
- status: int
8
- message: str
9
- details: Optional[Dict[str, Any]]
10
-
11
- def __init__(self, code: str, status: int, message: str, details: Optional[Dict[str, Any]] = None):
12
- super().__init__(f"[{status}] {code}: {message}")
13
- self.code = code
14
- self.status = status
15
- self.message = message
16
- self.details = details
17
-
18
-
19
- class ApiErrorError(UapiError):
20
- """上游/内部错误 (API_ERROR)"""
21
- DEFAULT_STATUS = 502
22
-
23
- class AvatarNotFoundError(UapiError):
24
- """头像未找到 (AVATAR_NOT_FOUND)"""
25
- DEFAULT_STATUS = 404
26
-
27
- class ConversionFailedError(UapiError):
28
- """转换失败 (CONVERSION_FAILED)"""
29
- DEFAULT_STATUS = 400
30
-
31
- class FileOpenErrorError(UapiError):
32
- """文件打开错误 (FILE_OPEN_ERROR)"""
33
- DEFAULT_STATUS = 500
34
-
35
- class FileRequiredError(UapiError):
36
- """文件必需 (FILE_REQUIRED)"""
37
- DEFAULT_STATUS = 400
38
-
39
- class InternalServerErrorError(UapiError):
40
- """服务器内部错误 (INTERNAL_SERVER_ERROR)"""
41
- DEFAULT_STATUS = 500
42
-
43
- class InvalidParameterError(UapiError):
44
- """请求参数错误 (INVALID_PARAMETER)"""
45
- DEFAULT_STATUS = 400
46
-
47
- class InvalidParamsError(UapiError):
48
- """无效参数 (INVALID_PARAMS)"""
49
- DEFAULT_STATUS = 400
50
-
51
- class NotFoundError(UapiError):
52
- """资源不存在 (NOT_FOUND)"""
53
- DEFAULT_STATUS = 404
54
-
55
- class NoMatchError(UapiError):
56
- """无匹配 (NO_MATCH)"""
57
- DEFAULT_STATUS = 404
58
-
59
- class NoTrackingDataError(UapiError):
60
- """无物流数据 (NO_TRACKING_DATA)"""
61
- DEFAULT_STATUS = 404
62
-
63
- class PhoneInfoFailedError(UapiError):
64
- """手机号信息查询失败 (PHONE_INFO_FAILED)"""
65
- DEFAULT_STATUS = 500
66
-
67
- class RecognitionFailedError(UapiError):
68
- """识别失败 (RECOGNITION_FAILED)"""
69
- DEFAULT_STATUS = 404
70
-
71
- class RequestEntityTooLargeError(UapiError):
72
- """错误 (REQUEST_ENTITY_TOO_LARGE)"""
73
- DEFAULT_STATUS = 413
74
-
75
- class ServiceBusyError(UapiError):
76
- """请求过于频繁 (SERVICE_BUSY)"""
77
- DEFAULT_STATUS = 429
78
-
79
- class TimezoneNotFoundError(UapiError):
80
- """时区未找到 (TIMEZONE_NOT_FOUND)"""
81
- DEFAULT_STATUS = 404
82
-
83
- class UnauthorizedError(UapiError):
84
- """请求未授权 (UNAUTHORIZED)"""
85
- DEFAULT_STATUS = 401
86
-
87
- class UnsupportedCarrierError(UapiError):
88
- """不支持的承运商 (UNSUPPORTED_CARRIER)"""
89
- DEFAULT_STATUS = 404
90
-
91
- class UnsupportedFormatError(UapiError):
92
- """格式不支持 (UNSUPPORTED_FORMAT)"""
93
- DEFAULT_STATUS = 400
94
-
95
-
96
- def map_error(r: httpx.Response) -> UapiError:
97
- code = None
98
- msg = r.text
99
- try:
100
- data = r.json()
101
- code = data.get("code") or data.get("error") or data.get("errCode") or "API_ERROR"
102
- msg = data.get("message") or data.get("errMsg") or msg
103
- details = data.get("details")
104
- except Exception:
105
- details = None
106
- status = r.status_code
107
- cls = _class_by_code(code, status)
108
- return cls(code, status, msg, details)
109
-
110
- def _class_by_code(code: str, status: int):
111
- c = (code or "").upper()
112
- mapping = {
113
-
114
- "API_ERROR": ApiErrorError,
115
-
116
- "AVATAR_NOT_FOUND": AvatarNotFoundError,
117
-
118
- "CONVERSION_FAILED": ConversionFailedError,
119
-
120
- "FILE_OPEN_ERROR": FileOpenErrorError,
121
-
122
- "FILE_REQUIRED": FileRequiredError,
123
-
124
- "INTERNAL_SERVER_ERROR": InternalServerErrorError,
125
-
126
- "INVALID_PARAMETER": InvalidParameterError,
127
-
128
- "INVALID_PARAMS": InvalidParamsError,
129
-
130
- "NOT_FOUND": NotFoundError,
131
-
132
- "NO_MATCH": NoMatchError,
133
-
134
- "NO_TRACKING_DATA": NoTrackingDataError,
135
-
136
- "PHONE_INFO_FAILED": PhoneInfoFailedError,
137
-
138
- "RECOGNITION_FAILED": RecognitionFailedError,
139
-
140
- "REQUEST_ENTITY_TOO_LARGE": RequestEntityTooLargeError,
141
-
142
- "SERVICE_BUSY": ServiceBusyError,
143
-
144
- "TIMEZONE_NOT_FOUND": TimezoneNotFoundError,
145
-
146
- "UNAUTHORIZED": UnauthorizedError,
147
-
148
- "UNSUPPORTED_CARRIER": UnsupportedCarrierError,
149
-
150
- "UNSUPPORTED_FORMAT": UnsupportedFormatError,
151
-
152
- }
153
- return mapping.get(c) or ( {400: InvalidParameterError, 401: UnauthorizedError, 404: NotFoundError, 429: ServiceBusyError, 500: InternalServerErrorError}.get(status) or UapiError )