verhub-sdk 0.2.3__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.
- verhub_sdk/__init__.py +51 -0
- verhub_sdk/_http.py +318 -0
- verhub_sdk/_unset.py +29 -0
- verhub_sdk/_version.py +3 -0
- verhub_sdk/admin_api.py +1016 -0
- verhub_sdk/client.py +118 -0
- verhub_sdk/errors.py +38 -0
- verhub_sdk/models.py +286 -0
- verhub_sdk/public_api.py +235 -0
- verhub_sdk/py.typed +0 -0
- verhub_sdk-0.2.3.dist-info/METADATA +126 -0
- verhub_sdk-0.2.3.dist-info/RECORD +13 -0
- verhub_sdk-0.2.3.dist-info/WHEEL +4 -0
verhub_sdk/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Verhub Python SDK。
|
|
3
|
+
|
|
4
|
+
接口面与 TypeScript / Rust / 纯 JS 版一一对应,只是方法名按各语言习惯改写
|
|
5
|
+
(Python 用 snake_case)。契约以仓库根目录的 ``verhub.openapi.yaml`` 为准。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ._http import (
|
|
9
|
+
PLATFORM_HEADER,
|
|
10
|
+
PLATFORM_VERSION_HEADER,
|
|
11
|
+
VERHUB_SDK_VERSION,
|
|
12
|
+
detect_platform,
|
|
13
|
+
detect_platform_version,
|
|
14
|
+
)
|
|
15
|
+
from ._unset import UNSET, UnsetType
|
|
16
|
+
from .admin_api import AdminApi
|
|
17
|
+
from .client import VerhubClient, VerhubSDK
|
|
18
|
+
from .errors import VerhubApiError, VerhubConnectionError, VerhubError
|
|
19
|
+
from .models import (
|
|
20
|
+
LOG_LEVEL_DEBUG,
|
|
21
|
+
LOG_LEVEL_ERROR,
|
|
22
|
+
LOG_LEVEL_INFO,
|
|
23
|
+
LOG_LEVEL_WARNING,
|
|
24
|
+
PLATFORMS,
|
|
25
|
+
)
|
|
26
|
+
from .public_api import PublicApi
|
|
27
|
+
|
|
28
|
+
__version__ = VERHUB_SDK_VERSION
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"VerhubClient",
|
|
32
|
+
"VerhubSDK",
|
|
33
|
+
"PublicApi",
|
|
34
|
+
"AdminApi",
|
|
35
|
+
"VerhubError",
|
|
36
|
+
"VerhubApiError",
|
|
37
|
+
"VerhubConnectionError",
|
|
38
|
+
"UNSET",
|
|
39
|
+
"UnsetType",
|
|
40
|
+
"PLATFORMS",
|
|
41
|
+
"PLATFORM_HEADER",
|
|
42
|
+
"PLATFORM_VERSION_HEADER",
|
|
43
|
+
"LOG_LEVEL_DEBUG",
|
|
44
|
+
"LOG_LEVEL_INFO",
|
|
45
|
+
"LOG_LEVEL_WARNING",
|
|
46
|
+
"LOG_LEVEL_ERROR",
|
|
47
|
+
"detect_platform",
|
|
48
|
+
"detect_platform_version",
|
|
49
|
+
"VERHUB_SDK_VERSION",
|
|
50
|
+
"__version__",
|
|
51
|
+
]
|
verhub_sdk/_http.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any, Dict, Mapping, Optional
|
|
6
|
+
from urllib.parse import quote, urlencode
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from ._unset import UNSET, UnsetType
|
|
11
|
+
from ._version import VERHUB_SDK_VERSION
|
|
12
|
+
from .errors import VerhubApiError, VerhubConnectionError, VerhubError
|
|
13
|
+
|
|
14
|
+
#: 客户端平台声明头。仅用于服务端请求统计,不影响接口返回内容。
|
|
15
|
+
PLATFORM_HEADER = "x-verhub-platform"
|
|
16
|
+
|
|
17
|
+
#: 客户端系统版本明细头,如 ``11`` / ``ubuntu 24.04``;超过 32 字符会被服务端丢弃。
|
|
18
|
+
PLATFORM_VERSION_HEADER = "x-verhub-platform-version"
|
|
19
|
+
|
|
20
|
+
#: 系统版本明细的长度上限,与服务端一致,超出直接截断。
|
|
21
|
+
MAX_PLATFORM_VERSION_LENGTH = 32
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def detect_platform() -> str:
|
|
25
|
+
"""
|
|
26
|
+
猜测当前运行平台,用于填充 :data:`PLATFORM_HEADER`。
|
|
27
|
+
|
|
28
|
+
只区分契约里的七个取值;认不出时返回 ``others`` 而不是瞎猜,
|
|
29
|
+
服务端拿到 ``others`` 至少知道这是「说不清的平台」。
|
|
30
|
+
|
|
31
|
+
:return: 平台标识
|
|
32
|
+
"""
|
|
33
|
+
name = sys.platform
|
|
34
|
+
if name.startswith("win"):
|
|
35
|
+
return "windows"
|
|
36
|
+
if name == "darwin":
|
|
37
|
+
return "macos"
|
|
38
|
+
if name.startswith("linux"):
|
|
39
|
+
return "linux"
|
|
40
|
+
return "others"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def detect_platform_version() -> str:
|
|
44
|
+
"""
|
|
45
|
+
从系统信息里提取系统版本明细,用于填充 :data:`PLATFORM_VERSION_HEADER`。
|
|
46
|
+
|
|
47
|
+
Windows 按内核构建号还原市场版本号(11 / 10 …),macOS 取产品版本号,
|
|
48
|
+
Linux 读 os-release 拼成 ``发行版 版本号``。取不到就返回空串,交给服务端
|
|
49
|
+
从 User-Agent 兜底推断。
|
|
50
|
+
|
|
51
|
+
:return: 系统版本明细;空串表示无从得知
|
|
52
|
+
"""
|
|
53
|
+
name = sys.platform
|
|
54
|
+
try:
|
|
55
|
+
if name.startswith("win"):
|
|
56
|
+
info = sys.getwindowsversion() # type: ignore[attr-defined]
|
|
57
|
+
# Win11 仍上报内核 10.0,只有构建号 >= 22000 能区分出来。
|
|
58
|
+
if info.major == 10 and info.build >= 22000:
|
|
59
|
+
return "11"
|
|
60
|
+
return str(info.major)
|
|
61
|
+
|
|
62
|
+
if name == "darwin":
|
|
63
|
+
import platform as _platform
|
|
64
|
+
|
|
65
|
+
return _platform.mac_ver()[0] or ""
|
|
66
|
+
|
|
67
|
+
if name.startswith("linux"):
|
|
68
|
+
import platform as _platform
|
|
69
|
+
|
|
70
|
+
read_os_release = getattr(_platform, "freedesktop_os_release", None)
|
|
71
|
+
if read_os_release is not None:
|
|
72
|
+
try:
|
|
73
|
+
data = read_os_release()
|
|
74
|
+
distro = (data.get("ID") or "").strip().lower()
|
|
75
|
+
version = (data.get("VERSION_ID") or "").strip()
|
|
76
|
+
combined = f"{distro} {version}".strip()
|
|
77
|
+
if combined:
|
|
78
|
+
return combined[:MAX_PLATFORM_VERSION_LENGTH]
|
|
79
|
+
except OSError:
|
|
80
|
+
pass
|
|
81
|
+
return ""
|
|
82
|
+
except Exception:
|
|
83
|
+
# 版本探测纯属锦上添花,任何异常都不该阻断请求。
|
|
84
|
+
return ""
|
|
85
|
+
|
|
86
|
+
return ""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def compact(source: Mapping[str, Any]) -> Dict[str, Any]:
|
|
90
|
+
"""
|
|
91
|
+
丢掉值为 :data:`~verhub_sdk._unset.UNSET` 的字段,保留显式的 ``None``。
|
|
92
|
+
|
|
93
|
+
``None`` 会被序列化成 JSON null,是「把这个字段置空」的意思;只有
|
|
94
|
+
完全没提供的字段才该从请求里消失。
|
|
95
|
+
|
|
96
|
+
:param source: 原始字段表
|
|
97
|
+
:return: 过滤后的字段表
|
|
98
|
+
"""
|
|
99
|
+
return {key: value for key, value in source.items() if not isinstance(value, UnsetType)}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class HttpClient:
|
|
103
|
+
"""底层 HTTP 客户端,两个命名空间共用一份连接、凭据与来源声明。"""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
base_url: str,
|
|
108
|
+
project_key: Optional[str] = None,
|
|
109
|
+
token: Optional[str] = None,
|
|
110
|
+
*,
|
|
111
|
+
platform: Any = UNSET,
|
|
112
|
+
platform_version: Any = UNSET,
|
|
113
|
+
timeout: float = 15.0,
|
|
114
|
+
session: Optional[requests.Session] = None,
|
|
115
|
+
user_agent: Optional[str] = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""
|
|
118
|
+
:param base_url: API 根地址,须包含 ``/api/v1`` 前缀
|
|
119
|
+
:param project_key: 绑定的项目标识;项目作用域的方法默认用它
|
|
120
|
+
:param token: 管理员 JWT 或 API Key,仅 admin 接口需要
|
|
121
|
+
:param platform: 平台声明;省略则自动探测,传 ``None`` 则不声明
|
|
122
|
+
:param platform_version: 系统版本明细;省略时若平台也是自动探测则一并
|
|
123
|
+
自动探测,传 ``None`` 则不声明
|
|
124
|
+
:param timeout: 单次请求超时(秒)
|
|
125
|
+
:param session: 自定义 ``requests.Session``,可用于配置代理、重试、证书
|
|
126
|
+
:param user_agent: 覆盖默认 User-Agent
|
|
127
|
+
"""
|
|
128
|
+
self.base_url = base_url.strip().rstrip("/")
|
|
129
|
+
self.project_key = project_key
|
|
130
|
+
self.token = token or ""
|
|
131
|
+
self.timeout = timeout
|
|
132
|
+
self.session = session or requests.Session()
|
|
133
|
+
self.user_agent = user_agent or f"verhub-sdk-python/{VERHUB_SDK_VERSION}"
|
|
134
|
+
|
|
135
|
+
auto_platform = isinstance(platform, UnsetType)
|
|
136
|
+
self.platform = detect_platform() if auto_platform else platform
|
|
137
|
+
|
|
138
|
+
if isinstance(platform_version, UnsetType):
|
|
139
|
+
# 平台是自己探测出来的,才顺带把版本也探测了——用户指定了平台却由我们
|
|
140
|
+
# 猜版本,很容易出现「平台 linux、版本却是 windows 11」的错配。
|
|
141
|
+
self.platform_version = (
|
|
142
|
+
(detect_platform_version() or None) if (auto_platform and self.platform) else None
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
self.platform_version = platform_version
|
|
146
|
+
|
|
147
|
+
def set_token(self, token: str) -> None:
|
|
148
|
+
"""
|
|
149
|
+
:param token: 管理员 JWT 或 API Key
|
|
150
|
+
"""
|
|
151
|
+
self.token = token
|
|
152
|
+
|
|
153
|
+
def clear_token(self) -> None:
|
|
154
|
+
"""清除当前凭据,之后调用 admin 接口会直接抛错。"""
|
|
155
|
+
self.token = ""
|
|
156
|
+
|
|
157
|
+
def set_project_key(self, project_key: str) -> None:
|
|
158
|
+
"""
|
|
159
|
+
:param project_key: 新的绑定项目标识
|
|
160
|
+
"""
|
|
161
|
+
self.project_key = project_key
|
|
162
|
+
|
|
163
|
+
def set_platform(self, platform: Optional[str]) -> None:
|
|
164
|
+
"""
|
|
165
|
+
:param platform: 平台声明;传 ``None`` 则不再声明平台
|
|
166
|
+
"""
|
|
167
|
+
self.platform = platform
|
|
168
|
+
|
|
169
|
+
def set_platform_version(self, platform_version: Optional[str]) -> None:
|
|
170
|
+
"""
|
|
171
|
+
:param platform_version: 系统版本明细;传 ``None`` 则不再声明
|
|
172
|
+
"""
|
|
173
|
+
self.platform_version = platform_version
|
|
174
|
+
|
|
175
|
+
def require_project_key(self) -> str:
|
|
176
|
+
"""
|
|
177
|
+
:return: 绑定的项目标识
|
|
178
|
+
:raises VerhubError: 未绑定 project_key
|
|
179
|
+
"""
|
|
180
|
+
if not self.project_key:
|
|
181
|
+
raise VerhubError("未设置 project_key:请在创建客户端时传入,或调用 set_project_key()")
|
|
182
|
+
return self.project_key
|
|
183
|
+
|
|
184
|
+
def request(
|
|
185
|
+
self,
|
|
186
|
+
method: str,
|
|
187
|
+
path_template: str,
|
|
188
|
+
*,
|
|
189
|
+
path_params: Optional[Mapping[str, str]] = None,
|
|
190
|
+
query: Optional[Mapping[str, Any]] = None,
|
|
191
|
+
body: Optional[Mapping[str, Any]] = None,
|
|
192
|
+
auth: bool = False,
|
|
193
|
+
) -> Any:
|
|
194
|
+
"""
|
|
195
|
+
:param method: HTTP 方法
|
|
196
|
+
:param path_template: 形如 ``/public/{projectKey}`` 的路径模板
|
|
197
|
+
:param path_params: 路径参数,值会被 URL 编码
|
|
198
|
+
:param query: 查询参数,值为 ``None`` 的项会被丢弃
|
|
199
|
+
:param body: JSON 请求体
|
|
200
|
+
:param auth: 是否附带 Bearer 凭据
|
|
201
|
+
:return: 已解析的响应体
|
|
202
|
+
:raises VerhubApiError: 服务端返回非 2xx
|
|
203
|
+
:raises VerhubConnectionError: 请求未能到达服务端
|
|
204
|
+
"""
|
|
205
|
+
url = self._build_url(self._resolve_path(path_template, path_params), query)
|
|
206
|
+
|
|
207
|
+
headers: Dict[str, str] = {
|
|
208
|
+
"Accept": "application/json",
|
|
209
|
+
"User-Agent": self.user_agent,
|
|
210
|
+
}
|
|
211
|
+
if self.platform:
|
|
212
|
+
headers[PLATFORM_HEADER] = self.platform
|
|
213
|
+
if self.platform_version:
|
|
214
|
+
headers[PLATFORM_VERSION_HEADER] = self.platform_version
|
|
215
|
+
|
|
216
|
+
if auth:
|
|
217
|
+
if not self.token:
|
|
218
|
+
raise VerhubApiError("缺少凭据:请先设置 token", 401, None)
|
|
219
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
220
|
+
|
|
221
|
+
payload = None
|
|
222
|
+
if body is not None:
|
|
223
|
+
headers["Content-Type"] = "application/json"
|
|
224
|
+
payload = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
response = self.session.request(
|
|
228
|
+
method=method,
|
|
229
|
+
url=url,
|
|
230
|
+
headers=headers,
|
|
231
|
+
data=payload,
|
|
232
|
+
timeout=self.timeout,
|
|
233
|
+
)
|
|
234
|
+
except requests.RequestException as exc:
|
|
235
|
+
raise VerhubConnectionError(f"请求 {method} {url} 失败:{exc}", exc) from exc
|
|
236
|
+
|
|
237
|
+
data = self._parse_json(response.text)
|
|
238
|
+
if not response.ok:
|
|
239
|
+
message = self._error_message(data) or f"请求失败,HTTP {response.status_code}"
|
|
240
|
+
raise VerhubApiError(message, response.status_code, data)
|
|
241
|
+
|
|
242
|
+
return data
|
|
243
|
+
|
|
244
|
+
def _resolve_path(self, template: str, params: Optional[Mapping[str, str]]) -> str:
|
|
245
|
+
"""
|
|
246
|
+
:param template: 路径模板
|
|
247
|
+
:param params: 路径参数
|
|
248
|
+
:return: 填充后的路径
|
|
249
|
+
"""
|
|
250
|
+
path = template
|
|
251
|
+
while True:
|
|
252
|
+
left = path.find("{")
|
|
253
|
+
if left < 0:
|
|
254
|
+
break
|
|
255
|
+
right = path.find("}", left)
|
|
256
|
+
if right < 0:
|
|
257
|
+
break
|
|
258
|
+
|
|
259
|
+
key = path[left + 1 : right]
|
|
260
|
+
value = (params or {}).get(key)
|
|
261
|
+
if value is None or value == "":
|
|
262
|
+
raise ValueError(f"缺少路径参数:{key}")
|
|
263
|
+
|
|
264
|
+
path = f"{path[:left]}{quote(str(value), safe='')}{path[right + 1 :]}"
|
|
265
|
+
|
|
266
|
+
return path
|
|
267
|
+
|
|
268
|
+
def _build_url(self, path: str, query: Optional[Mapping[str, Any]]) -> str:
|
|
269
|
+
"""
|
|
270
|
+
:param path: 已填充的路径
|
|
271
|
+
:param query: 查询参数
|
|
272
|
+
:return: 完整 URL
|
|
273
|
+
"""
|
|
274
|
+
if not query:
|
|
275
|
+
return f"{self.base_url}{path}"
|
|
276
|
+
|
|
277
|
+
pairs = []
|
|
278
|
+
for key, value in query.items():
|
|
279
|
+
if value is None or isinstance(value, UnsetType):
|
|
280
|
+
continue
|
|
281
|
+
if isinstance(value, bool):
|
|
282
|
+
pairs.append((key, "true" if value else "false"))
|
|
283
|
+
else:
|
|
284
|
+
pairs.append((key, str(value)))
|
|
285
|
+
|
|
286
|
+
if not pairs:
|
|
287
|
+
return f"{self.base_url}{path}"
|
|
288
|
+
|
|
289
|
+
return f"{self.base_url}{path}?{urlencode(pairs)}"
|
|
290
|
+
|
|
291
|
+
def _parse_json(self, raw: str) -> Any:
|
|
292
|
+
"""
|
|
293
|
+
:param raw: 原始响应文本
|
|
294
|
+
:return: 解析结果;不是 JSON 时原样返回文本
|
|
295
|
+
"""
|
|
296
|
+
if not raw:
|
|
297
|
+
return {}
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
return json.loads(raw)
|
|
301
|
+
except json.JSONDecodeError:
|
|
302
|
+
return raw
|
|
303
|
+
|
|
304
|
+
def _error_message(self, body: Any) -> Optional[str]:
|
|
305
|
+
"""
|
|
306
|
+
:param body: 已解析的响应体
|
|
307
|
+
:return: 错误信息;NestJS 校验失败时 message 是字符串数组
|
|
308
|
+
"""
|
|
309
|
+
if not isinstance(body, dict):
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
message = body.get("message")
|
|
313
|
+
if isinstance(message, str):
|
|
314
|
+
return message
|
|
315
|
+
if isinstance(message, list) and message:
|
|
316
|
+
return "; ".join(str(item) for item in message)
|
|
317
|
+
|
|
318
|
+
return None
|
verhub_sdk/_unset.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Final
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UnsetType:
|
|
7
|
+
"""
|
|
8
|
+
「未提供」哨兵。
|
|
9
|
+
|
|
10
|
+
可选字段的默认值必须与显式 ``None`` 区分开:省略字段表示保持原值,
|
|
11
|
+
显式传 ``None`` 表示把该字段置空(服务端收到 JSON null)。若直接用
|
|
12
|
+
``None`` 作默认值,这两种意图就没法表达了。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
_instance: "UnsetType | None" = None
|
|
16
|
+
|
|
17
|
+
def __new__(cls) -> "UnsetType":
|
|
18
|
+
if cls._instance is None:
|
|
19
|
+
cls._instance = super().__new__(cls)
|
|
20
|
+
return cls._instance
|
|
21
|
+
|
|
22
|
+
def __bool__(self) -> bool:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def __repr__(self) -> str:
|
|
26
|
+
return "UNSET"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
UNSET: Final[Any] = UnsetType()
|
verhub_sdk/_version.py
ADDED