scalebox-sdk 0.1.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.
- scalebox/__init__.py +80 -0
- scalebox/api/__init__.py +128 -0
- scalebox/api/client/__init__.py +8 -0
- scalebox/api/client/api/__init__.py +1 -0
- scalebox/api/client/api/sandboxes/__init__.py +0 -0
- scalebox/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- scalebox/api/client/api/sandboxes/get_sandboxes.py +176 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +214 -0
- scalebox/api/client/api/sandboxes/get_v2_sandboxes.py +229 -0
- scalebox/api/client/api/sandboxes/post_sandboxes.py +174 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +182 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +190 -0
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +194 -0
- scalebox/api/client/client.py +288 -0
- scalebox/api/client/errors.py +16 -0
- scalebox/api/client/models/__init__.py +81 -0
- scalebox/api/client/models/build_log_entry.py +79 -0
- scalebox/api/client/models/created_access_token.py +100 -0
- scalebox/api/client/models/created_team_api_key.py +166 -0
- scalebox/api/client/models/error.py +67 -0
- scalebox/api/client/models/identifier_masking_details.py +83 -0
- scalebox/api/client/models/listed_sandbox.py +138 -0
- scalebox/api/client/models/log_level.py +11 -0
- scalebox/api/client/models/new_access_token.py +59 -0
- scalebox/api/client/models/new_sandbox.py +125 -0
- scalebox/api/client/models/new_team_api_key.py +59 -0
- scalebox/api/client/models/node.py +154 -0
- scalebox/api/client/models/node_detail.py +152 -0
- scalebox/api/client/models/node_status.py +11 -0
- scalebox/api/client/models/node_status_change.py +61 -0
- scalebox/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- scalebox/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- scalebox/api/client/models/resumed_sandbox.py +68 -0
- scalebox/api/client/models/sandbox.py +125 -0
- scalebox/api/client/models/sandbox_detail.py +178 -0
- scalebox/api/client/models/sandbox_log.py +70 -0
- scalebox/api/client/models/sandbox_logs.py +73 -0
- scalebox/api/client/models/sandbox_metric.py +110 -0
- scalebox/api/client/models/sandbox_state.py +9 -0
- scalebox/api/client/models/sandboxes_with_metrics.py +59 -0
- scalebox/api/client/models/team.py +83 -0
- scalebox/api/client/models/team_api_key.py +158 -0
- scalebox/api/client/models/team_user.py +68 -0
- scalebox/api/client/models/template.py +179 -0
- scalebox/api/client/models/template_build.py +117 -0
- scalebox/api/client/models/template_build_file_upload.py +70 -0
- scalebox/api/client/models/template_build_request.py +115 -0
- scalebox/api/client/models/template_build_request_v2.py +88 -0
- scalebox/api/client/models/template_build_start_v2.py +114 -0
- scalebox/api/client/models/template_build_status.py +11 -0
- scalebox/api/client/models/template_step.py +91 -0
- scalebox/api/client/models/template_update_request.py +59 -0
- scalebox/api/client/models/update_team_api_key.py +59 -0
- scalebox/api/client/py.typed +1 -0
- scalebox/api/client/types.py +46 -0
- scalebox/api/metadata.py +19 -0
- scalebox/cli.py +125 -0
- scalebox/client/__init__.py +0 -0
- scalebox/client/aclient.py +57 -0
- scalebox/client/api.proto +460 -0
- scalebox/client/buf.gen.yaml +8 -0
- scalebox/client/client.py +102 -0
- scalebox/client/requirements.txt +5 -0
- scalebox/code_interpreter/__init__.py +12 -0
- scalebox/code_interpreter/charts.py +230 -0
- scalebox/code_interpreter/code_interpreter_async.py +369 -0
- scalebox/code_interpreter/code_interpreter_sync.py +317 -0
- scalebox/code_interpreter/constants.py +3 -0
- scalebox/code_interpreter/exceptions.py +13 -0
- scalebox/code_interpreter/models.py +485 -0
- scalebox/connection_config.py +92 -0
- scalebox/csx_connect/__init__.py +1 -0
- scalebox/csx_connect/client.py +485 -0
- scalebox/csx_desktop/__init__.py +0 -0
- scalebox/csx_desktop/main.py +651 -0
- scalebox/exceptions.py +83 -0
- scalebox/generated/__init__.py +0 -0
- scalebox/generated/api.py +61 -0
- scalebox/generated/api_pb2.py +203 -0
- scalebox/generated/api_pb2.pyi +956 -0
- scalebox/generated/api_pb2_connect.py +1456 -0
- scalebox/generated/rpc.py +50 -0
- scalebox/generated/versions.py +3 -0
- scalebox/requirements.txt +36 -0
- scalebox/sandbox/__init__.py +0 -0
- scalebox/sandbox/commands/__init__.py +0 -0
- scalebox/sandbox/commands/command_handle.py +69 -0
- scalebox/sandbox/commands/main.py +39 -0
- scalebox/sandbox/filesystem/__init__.py +0 -0
- scalebox/sandbox/filesystem/filesystem.py +95 -0
- scalebox/sandbox/filesystem/watch_handle.py +60 -0
- scalebox/sandbox/main.py +139 -0
- scalebox/sandbox/sandbox_api.py +91 -0
- scalebox/sandbox/signature.py +40 -0
- scalebox/sandbox/utils.py +34 -0
- scalebox/sandbox_async/__init__.py +1 -0
- scalebox/sandbox_async/commands/command.py +307 -0
- scalebox/sandbox_async/commands/command_handle.py +187 -0
- scalebox/sandbox_async/commands/pty.py +187 -0
- scalebox/sandbox_async/filesystem/filesystem.py +557 -0
- scalebox/sandbox_async/filesystem/watch_handle.py +61 -0
- scalebox/sandbox_async/main.py +646 -0
- scalebox/sandbox_async/sandbox_api.py +365 -0
- scalebox/sandbox_async/utils.py +7 -0
- scalebox/sandbox_sync/__init__.py +2 -0
- scalebox/sandbox_sync/commands/__init__.py +0 -0
- scalebox/sandbox_sync/commands/command.py +300 -0
- scalebox/sandbox_sync/commands/command_handle.py +150 -0
- scalebox/sandbox_sync/commands/pty.py +181 -0
- scalebox/sandbox_sync/filesystem/__init__.py +0 -0
- scalebox/sandbox_sync/filesystem/filesystem.py +543 -0
- scalebox/sandbox_sync/filesystem/watch_handle.py +66 -0
- scalebox/sandbox_sync/main.py +790 -0
- scalebox/sandbox_sync/sandbox_api.py +356 -0
- scalebox/test/CODE_INTERPRETER_TESTS_READY.md +323 -0
- scalebox/test/README.md +329 -0
- scalebox/test/__init__.py +0 -0
- scalebox/test/aclient.py +72 -0
- scalebox/test/code_interpreter_centext.py +21 -0
- scalebox/test/code_interpreter_centext_sync.py +21 -0
- scalebox/test/code_interpreter_test.py +34 -0
- scalebox/test/code_interpreter_test_sync.py +34 -0
- scalebox/test/run_all_validation_tests.py +334 -0
- scalebox/test/run_code_interpreter_tests.sh +67 -0
- scalebox/test/run_tests.sh +230 -0
- scalebox/test/test_basic.py +78 -0
- scalebox/test/test_code_interpreter_async_comprehensive.py +2653 -0
- scalebox/test/test_code_interpreter_e2basync_comprehensive.py +2655 -0
- scalebox/test/test_code_interpreter_e2bsync_comprehensive.py +3416 -0
- scalebox/test/test_code_interpreter_sync_comprehensive.py +3412 -0
- scalebox/test/test_e2b_first.py +11 -0
- scalebox/test/test_sandbox_async_comprehensive.py +738 -0
- scalebox/test/test_sandbox_stress_and_edge_cases.py +778 -0
- scalebox/test/test_sandbox_sync_comprehensive.py +770 -0
- scalebox/test/test_sandbox_usage_examples.py +987 -0
- scalebox/test/testacreate.py +24 -0
- scalebox/test/testagetinfo.py +18 -0
- scalebox/test/testcodeinterpreter_async.py +508 -0
- scalebox/test/testcodeinterpreter_sync.py +239 -0
- scalebox/test/testcomputeuse.py +243 -0
- scalebox/test/testnovnc.py +12 -0
- scalebox/test/testsandbox_async.py +118 -0
- scalebox/test/testsandbox_sync.py +38 -0
- scalebox/utils/__init__.py +0 -0
- scalebox/utils/httpcoreclient.py +297 -0
- scalebox/utils/httpxclient.py +403 -0
- scalebox/version.py +16 -0
- scalebox_sdk-0.1.0.dist-info/METADATA +292 -0
- scalebox_sdk-0.1.0.dist-info/RECORD +157 -0
- scalebox_sdk-0.1.0.dist-info/WHEEL +5 -0
- scalebox_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- scalebox_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- scalebox_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
|
4
|
+
|
|
5
|
+
import httpcore
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HTTPXCoreTool:
|
|
9
|
+
"""
|
|
10
|
+
基于 httpcore 的高级 HTTP 工具类
|
|
11
|
+
支持同步/异步请求、连接池管理、流式传输等
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
base_url: str = "",
|
|
17
|
+
timeout: float = 10.0,
|
|
18
|
+
max_connections: int = 100,
|
|
19
|
+
max_keepalive_connections: int = 50,
|
|
20
|
+
keepalive_expiry: float = 5.0,
|
|
21
|
+
http2: bool = False,
|
|
22
|
+
ssl_verify: bool = True,
|
|
23
|
+
headers: Optional[Dict[str, str]] = None,
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
初始化 HTTP 客户端
|
|
27
|
+
|
|
28
|
+
:param base_url: 基础 URL 前缀
|
|
29
|
+
:param timeout: 请求超时时间(秒)
|
|
30
|
+
:param max_connections: 最大连接数
|
|
31
|
+
:param max_keepalive_connections: 最大空闲连接数
|
|
32
|
+
:param keepalive_expiry: 空闲连接超时时间(秒)
|
|
33
|
+
:param http2: 是否启用 HTTP/2
|
|
34
|
+
:param ssl_verify: 是否验证 SSL 证书
|
|
35
|
+
:param headers: 默认请求头
|
|
36
|
+
"""
|
|
37
|
+
self.base_url = base_url.rstrip("/")
|
|
38
|
+
self.timeout = timeout
|
|
39
|
+
self.http2 = http2
|
|
40
|
+
self.ssl_verify = ssl_verify
|
|
41
|
+
self.default_headers = headers or {}
|
|
42
|
+
|
|
43
|
+
# 创建同步连接池
|
|
44
|
+
self.sync_pool = httpcore.ConnectionPool(
|
|
45
|
+
max_connections=max_connections,
|
|
46
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
47
|
+
keepalive_expiry=keepalive_expiry,
|
|
48
|
+
http2=http2,
|
|
49
|
+
ssl_context=(
|
|
50
|
+
None if ssl_verify else httpcore._ssl.create_untrusted_context()
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 创建异步连接池
|
|
55
|
+
self.async_pool = httpcore.AsyncConnectionPool(
|
|
56
|
+
max_connections=max_connections,
|
|
57
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
58
|
+
keepalive_expiry=keepalive_expiry,
|
|
59
|
+
http2=http2,
|
|
60
|
+
ssl_context=(
|
|
61
|
+
None if ssl_verify else httpcore._ssl.create_untrusted_context()
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def close(self) -> None:
|
|
66
|
+
"""关闭同步连接池"""
|
|
67
|
+
self.sync_pool.close()
|
|
68
|
+
|
|
69
|
+
async def aclose(self) -> None:
|
|
70
|
+
"""关闭异步连接池"""
|
|
71
|
+
await self.async_pool.aclose()
|
|
72
|
+
|
|
73
|
+
@contextlib.contextmanager
|
|
74
|
+
def stream_context(
|
|
75
|
+
self,
|
|
76
|
+
method: str,
|
|
77
|
+
url: str,
|
|
78
|
+
params: Optional[Dict[str, Any]] = None,
|
|
79
|
+
headers: Optional[Dict[str, str]] = None,
|
|
80
|
+
content: Optional[Union[bytes, Iterable[bytes]]] = None,
|
|
81
|
+
) -> Iterable[httpcore.Response]:
|
|
82
|
+
"""
|
|
83
|
+
同步流式请求上下文管理器
|
|
84
|
+
|
|
85
|
+
使用示例:
|
|
86
|
+
with tool.stream_context("GET", "https://example.com") as response:
|
|
87
|
+
for chunk in response.iter_bytes():
|
|
88
|
+
print(chunk)
|
|
89
|
+
"""
|
|
90
|
+
full_url = self._build_url(url, params)
|
|
91
|
+
req_headers = self._build_headers(headers)
|
|
92
|
+
|
|
93
|
+
response = self.sync_pool.request(
|
|
94
|
+
method=method.encode(),
|
|
95
|
+
url=self._parse_url(full_url),
|
|
96
|
+
headers=req_headers,
|
|
97
|
+
content=content,
|
|
98
|
+
timeout=self.timeout,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
yield response
|
|
103
|
+
finally:
|
|
104
|
+
# 确保流关闭
|
|
105
|
+
for _ in response.stream:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
@contextlib.asynccontextmanager
|
|
109
|
+
async def async_stream_context(
|
|
110
|
+
self,
|
|
111
|
+
method: str,
|
|
112
|
+
url: str,
|
|
113
|
+
params: Optional[Dict[str, Any]] = None,
|
|
114
|
+
headers: Optional[Dict[str, str]] = None,
|
|
115
|
+
content: Optional[Union[bytes, Iterable[bytes]]] = None,
|
|
116
|
+
) -> Iterable[httpcore.Response]:
|
|
117
|
+
"""
|
|
118
|
+
异步流式请求上下文管理器
|
|
119
|
+
|
|
120
|
+
使用示例:
|
|
121
|
+
async with tool.async_stream_context("GET", "https://example.com") as response:
|
|
122
|
+
async for chunk in response.astream_bytes():
|
|
123
|
+
print(chunk)
|
|
124
|
+
"""
|
|
125
|
+
full_url = self._build_url(url, params)
|
|
126
|
+
req_headers = self._build_headers(headers)
|
|
127
|
+
|
|
128
|
+
response = await self.async_pool.request(
|
|
129
|
+
method=method.encode(),
|
|
130
|
+
url=self._parse_url(full_url),
|
|
131
|
+
headers=req_headers,
|
|
132
|
+
content=content,
|
|
133
|
+
timeout=self.timeout,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
yield response
|
|
138
|
+
finally:
|
|
139
|
+
# 确保流关闭
|
|
140
|
+
async for _ in response.stream:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
def request(
|
|
144
|
+
self,
|
|
145
|
+
method: str,
|
|
146
|
+
url: str,
|
|
147
|
+
params: Optional[Dict[str, Any]] = None,
|
|
148
|
+
headers: Optional[Dict[str, str]] = None,
|
|
149
|
+
json_data: Optional[Any] = None,
|
|
150
|
+
data: Optional[Union[bytes, Dict[str, Any]]] = None,
|
|
151
|
+
) -> httpcore.Response:
|
|
152
|
+
"""
|
|
153
|
+
同步 HTTP 请求
|
|
154
|
+
"""
|
|
155
|
+
content, headers = self._prepare_content(json_data, data, headers)
|
|
156
|
+
full_url = self._build_url(url, params)
|
|
157
|
+
req_headers = self._build_headers(headers)
|
|
158
|
+
|
|
159
|
+
return self.sync_pool.request(
|
|
160
|
+
method=method.encode(),
|
|
161
|
+
url=self._parse_url(full_url),
|
|
162
|
+
headers=req_headers,
|
|
163
|
+
content=content,
|
|
164
|
+
timeout=self.timeout,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
async def async_request(
|
|
168
|
+
self,
|
|
169
|
+
method: str,
|
|
170
|
+
url: str,
|
|
171
|
+
params: Optional[Dict[str, Any]] = None,
|
|
172
|
+
headers: Optional[Dict[str, str]] = None,
|
|
173
|
+
json_data: Optional[Any] = None,
|
|
174
|
+
data: Optional[Union[bytes, Dict[str, Any]]] = None,
|
|
175
|
+
) -> httpcore.Response:
|
|
176
|
+
"""
|
|
177
|
+
异步 HTTP 请求
|
|
178
|
+
"""
|
|
179
|
+
content, headers = self._prepare_content(json_data, data, headers)
|
|
180
|
+
full_url = self._build_url(url, params)
|
|
181
|
+
req_headers = self._build_headers(headers)
|
|
182
|
+
|
|
183
|
+
return await self.async_pool.request(
|
|
184
|
+
method=method.encode(),
|
|
185
|
+
url=self._parse_url(full_url),
|
|
186
|
+
headers=req_headers,
|
|
187
|
+
content=content,
|
|
188
|
+
timeout=self.timeout,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# 快捷方法
|
|
192
|
+
def get(self, url: str, **kwargs) -> httpcore.Response:
|
|
193
|
+
return self.request("GET", url, **kwargs)
|
|
194
|
+
|
|
195
|
+
async def async_get(self, url: str, **kwargs) -> httpcore.Response:
|
|
196
|
+
return await self.async_request("GET", url, **kwargs)
|
|
197
|
+
|
|
198
|
+
def post(self, url: str, **kwargs) -> httpcore.Response:
|
|
199
|
+
return self.request("POST", url, **kwargs)
|
|
200
|
+
|
|
201
|
+
async def async_post(self, url: str, **kwargs) -> httpcore.Response:
|
|
202
|
+
return await self.async_request("POST", url, **kwargs)
|
|
203
|
+
|
|
204
|
+
def put(self, url: str, **kwargs) -> httpcore.Response:
|
|
205
|
+
return self.request("PUT", url, **kwargs)
|
|
206
|
+
|
|
207
|
+
async def async_put(self, url: str, **kwargs) -> httpcore.Response:
|
|
208
|
+
return await self.async_request("PUT", url, **kwargs)
|
|
209
|
+
|
|
210
|
+
def delete(self, url: str, **kwargs) -> httpcore.Response:
|
|
211
|
+
return self.request("DELETE", url, **kwargs)
|
|
212
|
+
|
|
213
|
+
async def async_delete(self, url: str, **kwargs) -> httpcore.Response:
|
|
214
|
+
return await self.async_request("DELETE", url, **kwargs)
|
|
215
|
+
|
|
216
|
+
# 辅助方法
|
|
217
|
+
def _build_url(self, url: str, params: Optional[Dict[str, Any]]) -> str:
|
|
218
|
+
"""构建完整 URL"""
|
|
219
|
+
if not url.startswith(("http://", "https://")):
|
|
220
|
+
url = f"{self.base_url}/{url.lstrip('/')}"
|
|
221
|
+
|
|
222
|
+
if params:
|
|
223
|
+
from urllib.parse import urlencode
|
|
224
|
+
|
|
225
|
+
query = urlencode(params, doseq=True)
|
|
226
|
+
url = f"{url}?{query}" if "?" not in url else f"{url}&{query}"
|
|
227
|
+
|
|
228
|
+
return url
|
|
229
|
+
|
|
230
|
+
def _parse_url(self, url: str) -> Tuple[bytes, bytes, int, bytes]:
|
|
231
|
+
"""解析 URL 为 httpcore 格式"""
|
|
232
|
+
from urllib.parse import urlparse
|
|
233
|
+
|
|
234
|
+
parsed = urlparse(url)
|
|
235
|
+
scheme = parsed.scheme.encode()
|
|
236
|
+
host = parsed.hostname.encode()
|
|
237
|
+
port = parsed.port or (443 if scheme == b"https" else 80)
|
|
238
|
+
path = parsed.path.encode() or b"/"
|
|
239
|
+
if parsed.query:
|
|
240
|
+
path += b"?" + parsed.query.encode()
|
|
241
|
+
return (scheme, host, port, path)
|
|
242
|
+
|
|
243
|
+
def _build_headers(
|
|
244
|
+
self, headers: Optional[Dict[str, str]]
|
|
245
|
+
) -> List[Tuple[bytes, bytes]]:
|
|
246
|
+
"""构建请求头列表"""
|
|
247
|
+
merged_headers = {**self.default_headers, **(headers or {})}
|
|
248
|
+
return [(k.lower().encode(), v.encode()) for k, v in merged_headers.items()]
|
|
249
|
+
|
|
250
|
+
def _prepare_content(
|
|
251
|
+
self,
|
|
252
|
+
json_data: Optional[Any],
|
|
253
|
+
data: Optional[Union[bytes, Dict[str, Any]]],
|
|
254
|
+
headers: Optional[Dict[str, str]],
|
|
255
|
+
) -> Tuple[Optional[Union[bytes, Iterable[bytes]]], Dict[str, str]]:
|
|
256
|
+
"""准备请求内容和头信息"""
|
|
257
|
+
content = None
|
|
258
|
+
headers = headers or {}
|
|
259
|
+
|
|
260
|
+
if json_data is not None:
|
|
261
|
+
content = json.dumps(json_data).encode("utf-8")
|
|
262
|
+
headers.setdefault("Content-Type", "application/json")
|
|
263
|
+
|
|
264
|
+
elif data is not None:
|
|
265
|
+
if isinstance(data, dict):
|
|
266
|
+
from urllib.parse import urlencode
|
|
267
|
+
|
|
268
|
+
content = urlencode(data, doseq=True).encode("utf-8")
|
|
269
|
+
headers.setdefault("Content-Type", "application/x-www-form-urlencoded")
|
|
270
|
+
else:
|
|
271
|
+
content = data
|
|
272
|
+
|
|
273
|
+
return content, headers
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def read_response(response: httpcore.Response) -> bytes:
|
|
277
|
+
"""读取完整响应内容"""
|
|
278
|
+
return b"".join(response.stream)
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def read_response_json(response: httpcore.Response) -> Any:
|
|
282
|
+
"""读取并解析 JSON 响应"""
|
|
283
|
+
return json.loads(HTTPXCoreTool.read_response(response))
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
async def async_read_response(response: httpcore.Response) -> bytes:
|
|
287
|
+
"""异步读取完整响应内容"""
|
|
288
|
+
content = b""
|
|
289
|
+
async for chunk in response.stream:
|
|
290
|
+
content += chunk
|
|
291
|
+
return content
|
|
292
|
+
|
|
293
|
+
@staticmethod
|
|
294
|
+
async def async_read_response_json(response: httpcore.Response) -> Any:
|
|
295
|
+
"""异步读取并解析 JSON 响应"""
|
|
296
|
+
content = await HTTPXCoreTool.async_read_response(response)
|
|
297
|
+
return json.loads(content)
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, AsyncGenerator, Dict, Generator, Optional, Union
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HTTPXClient:
|
|
10
|
+
"""
|
|
11
|
+
基于 httpx.AsyncClient 的高级 HTTP 工具类
|
|
12
|
+
支持同步/异步请求、连接池管理、流式传输等完整功能
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
base_url: str = "",
|
|
18
|
+
timeout: float = 30.0,
|
|
19
|
+
max_connections: int = 100,
|
|
20
|
+
max_keepalive_connections: int = 50,
|
|
21
|
+
keepalive_expiry: float = 5.0,
|
|
22
|
+
http2: bool = False,
|
|
23
|
+
ssl_verify: bool = True,
|
|
24
|
+
default_headers: Optional[Dict[str, str]] = None,
|
|
25
|
+
follow_redirects: bool = True,
|
|
26
|
+
retries: int = 0,
|
|
27
|
+
backoff_factor: float = 0.1,
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
初始化 HTTP 客户端
|
|
31
|
+
|
|
32
|
+
:param base_url: 基础 URL 前缀
|
|
33
|
+
:param timeout: 请求超时时间(秒)
|
|
34
|
+
:param max_connections: 最大连接数
|
|
35
|
+
:param max_keepalive_connections: 最大空闲连接数
|
|
36
|
+
:param keepalive_expiry: 空闲连接超时时间(秒)
|
|
37
|
+
:param http2: 是否启用 HTTP/2
|
|
38
|
+
:param ssl_verify: 是否验证 SSL 证书
|
|
39
|
+
:param default_headers: 默认请求头
|
|
40
|
+
:param follow_redirects: 是否跟随重定向
|
|
41
|
+
:param retries: 请求失败重试次数
|
|
42
|
+
:param backoff_factor: 重试退避因子
|
|
43
|
+
"""
|
|
44
|
+
self.base_url = base_url.rstrip("/")
|
|
45
|
+
self.timeout = timeout
|
|
46
|
+
self.http2 = http2
|
|
47
|
+
self.ssl_verify = ssl_verify
|
|
48
|
+
self.default_headers = default_headers or {}
|
|
49
|
+
self.follow_redirects = follow_redirects
|
|
50
|
+
self.retries = retries
|
|
51
|
+
self.backoff_factor = backoff_factor
|
|
52
|
+
|
|
53
|
+
# 创建同步客户端
|
|
54
|
+
self.sync_client = httpx.Client(
|
|
55
|
+
base_url=self.base_url,
|
|
56
|
+
timeout=self.timeout,
|
|
57
|
+
limits=httpx.Limits(
|
|
58
|
+
max_connections=max_connections,
|
|
59
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
60
|
+
keepalive_expiry=keepalive_expiry,
|
|
61
|
+
),
|
|
62
|
+
http2=http2,
|
|
63
|
+
verify=ssl_verify,
|
|
64
|
+
headers=self.default_headers,
|
|
65
|
+
follow_redirects=follow_redirects,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# 创建异步客户端
|
|
69
|
+
self.async_client = httpx.AsyncClient(
|
|
70
|
+
base_url=self.base_url,
|
|
71
|
+
timeout=self.timeout,
|
|
72
|
+
limits=httpx.Limits(
|
|
73
|
+
max_connections=max_connections,
|
|
74
|
+
max_keepalive_connections=max_keepalive_connections,
|
|
75
|
+
keepalive_expiry=keepalive_expiry,
|
|
76
|
+
),
|
|
77
|
+
http2=http2,
|
|
78
|
+
verify=ssl_verify,
|
|
79
|
+
headers=self.default_headers,
|
|
80
|
+
follow_redirects=follow_redirects,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# 配置重试
|
|
84
|
+
if retries > 0:
|
|
85
|
+
retry_transport = httpx.HTTPTransport(
|
|
86
|
+
retries=self.retries, backoff_factor=self.backoff_factor
|
|
87
|
+
)
|
|
88
|
+
async_retry_transport = httpx.AsyncHTTPTransport(
|
|
89
|
+
retries=self.retries, backoff_factor=self.backoff_factor
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self.sync_client = httpx.Client(transport=retry_transport)
|
|
93
|
+
self.async_client = httpx.AsyncClient(transport=async_retry_transport)
|
|
94
|
+
|
|
95
|
+
def close(self) -> None:
|
|
96
|
+
"""关闭同步客户端"""
|
|
97
|
+
self.sync_client.close()
|
|
98
|
+
|
|
99
|
+
async def aclose(self) -> None:
|
|
100
|
+
"""关闭异步客户端"""
|
|
101
|
+
await self.async_client.aclose()
|
|
102
|
+
|
|
103
|
+
@contextlib.contextmanager
|
|
104
|
+
def context(self) -> "HTTPXClient":
|
|
105
|
+
"""
|
|
106
|
+
同步上下文管理器 (自动关闭)
|
|
107
|
+
|
|
108
|
+
使用示例:
|
|
109
|
+
with HTTPXClient() as client:
|
|
110
|
+
response = client.get("/api")
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
yield self
|
|
114
|
+
finally:
|
|
115
|
+
self.close()
|
|
116
|
+
|
|
117
|
+
@contextlib.asynccontextmanager
|
|
118
|
+
async def acontext(self) -> "HTTPXClient":
|
|
119
|
+
"""
|
|
120
|
+
异步上下文管理器 (自动关闭)
|
|
121
|
+
|
|
122
|
+
使用示例:
|
|
123
|
+
async with HTTPXClient() as client:
|
|
124
|
+
response = await client.aget("/api")
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
yield self
|
|
128
|
+
finally:
|
|
129
|
+
await self.aclose()
|
|
130
|
+
|
|
131
|
+
@contextlib.contextmanager
|
|
132
|
+
def stream(
|
|
133
|
+
self,
|
|
134
|
+
method: str,
|
|
135
|
+
url: str,
|
|
136
|
+
params: Optional[Dict[str, Any]] = None,
|
|
137
|
+
headers: Optional[Dict[str, str]] = None,
|
|
138
|
+
json_data: Optional[Any] = None,
|
|
139
|
+
data: Optional[Union[bytes, Dict[str, Any]]] = None,
|
|
140
|
+
) -> Generator[httpx.Response, None, None]:
|
|
141
|
+
"""
|
|
142
|
+
同步流式请求上下文管理器
|
|
143
|
+
|
|
144
|
+
使用示例:
|
|
145
|
+
with client.stream("GET", "/large-file") as response:
|
|
146
|
+
for chunk in response.iter_bytes():
|
|
147
|
+
process_chunk(chunk)
|
|
148
|
+
"""
|
|
149
|
+
headers = headers or {}
|
|
150
|
+
with self.sync_client.stream(
|
|
151
|
+
method,
|
|
152
|
+
url,
|
|
153
|
+
params=params,
|
|
154
|
+
headers={**self.default_headers, **headers},
|
|
155
|
+
json=json_data,
|
|
156
|
+
data=data,
|
|
157
|
+
) as response:
|
|
158
|
+
yield response
|
|
159
|
+
|
|
160
|
+
@contextlib.asynccontextmanager
|
|
161
|
+
async def astream(
|
|
162
|
+
self,
|
|
163
|
+
method: str,
|
|
164
|
+
url: str,
|
|
165
|
+
params: Optional[Dict[str, Any]] = None,
|
|
166
|
+
headers: Optional[Dict[str, str]] = None,
|
|
167
|
+
json_data: Optional[Any] = None,
|
|
168
|
+
data: Optional[Union[bytes, Dict[str, Any]]] = None,
|
|
169
|
+
) -> AsyncGenerator[httpx.Response, None]:
|
|
170
|
+
"""
|
|
171
|
+
异步流式请求上下文管理器
|
|
172
|
+
|
|
173
|
+
使用示例:
|
|
174
|
+
async with client.astream("GET", "/large-file") as response:
|
|
175
|
+
async for chunk in response.aiter_bytes():
|
|
176
|
+
process_chunk(chunk)
|
|
177
|
+
"""
|
|
178
|
+
headers = headers or {}
|
|
179
|
+
async with self.async_client.stream(
|
|
180
|
+
method,
|
|
181
|
+
url,
|
|
182
|
+
params=params,
|
|
183
|
+
headers={**self.default_headers, **headers},
|
|
184
|
+
json=json_data,
|
|
185
|
+
data=data,
|
|
186
|
+
) as response:
|
|
187
|
+
yield response
|
|
188
|
+
|
|
189
|
+
# ================ 同步请求方法 ================
|
|
190
|
+
def request(
|
|
191
|
+
self,
|
|
192
|
+
method: str,
|
|
193
|
+
url: str,
|
|
194
|
+
params: Optional[Dict[str, Any]] = None,
|
|
195
|
+
headers: Optional[Dict[str, str]] = None,
|
|
196
|
+
json_data: Optional[Any] = None,
|
|
197
|
+
data: Optional[Union[bytes, Dict[str, Any]]] = None,
|
|
198
|
+
) -> httpx.Response:
|
|
199
|
+
"""同步 HTTP 请求"""
|
|
200
|
+
headers = headers or {}
|
|
201
|
+
return self.sync_client.request(
|
|
202
|
+
method,
|
|
203
|
+
url,
|
|
204
|
+
params=params,
|
|
205
|
+
headers={**self.default_headers, **headers},
|
|
206
|
+
json=json_data,
|
|
207
|
+
data=data,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def get(
|
|
211
|
+
self,
|
|
212
|
+
url: str,
|
|
213
|
+
params: Optional[Dict[str, Any]] = None,
|
|
214
|
+
headers: Optional[Dict[str, str]] = None,
|
|
215
|
+
) -> httpx.Response:
|
|
216
|
+
"""同步 GET 请求"""
|
|
217
|
+
return self.request("GET", url, params=params, headers=headers)
|
|
218
|
+
|
|
219
|
+
def post(
|
|
220
|
+
self,
|
|
221
|
+
url: str,
|
|
222
|
+
json_data: Optional[Any] = None,
|
|
223
|
+
data: Optional[Union[bytes, Dict[str, Any]]] = None,
|
|
224
|
+
params: Optional[Dict[str, Any]] = None,
|
|
225
|
+
headers: Optional[Dict[str, str]] = None,
|
|
226
|
+
) -> httpx.Response:
|
|
227
|
+
"""同步 POST 请求"""
|
|
228
|
+
return self.request(
|
|
229
|
+
"POST", url, params=params, headers=headers, json_data=json_data, data=data
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def put(
|
|
233
|
+
self,
|
|
234
|
+
url: str,
|
|
235
|
+
json_data: Optional[Any] = None,
|
|
236
|
+
data: Optional[Union[bytes, Dict[str, Any]]] = None,
|
|
237
|
+
params: Optional[Dict[str, Any]] = None,
|
|
238
|
+
headers: Optional[Dict[str, str]] = None,
|
|
239
|
+
) -> httpx.Response:
|
|
240
|
+
"""同步 PUT 请求"""
|
|
241
|
+
return self.request(
|
|
242
|
+
"PUT", url, params=params, headers=headers, json_data=json_data, data=data
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def delete(
|
|
246
|
+
self,
|
|
247
|
+
url: str,
|
|
248
|
+
params: Optional[Dict[str, Any]] = None,
|
|
249
|
+
headers: Optional[Dict[str, str]] = None,
|
|
250
|
+
) -> httpx.Response:
|
|
251
|
+
"""同步 DELETE 请求"""
|
|
252
|
+
return self.request("DELETE", url, params=params, headers=headers)
|
|
253
|
+
|
|
254
|
+
# ================ 异步请求方法 ================
|
|
255
|
+
async def arequest(
|
|
256
|
+
self,
|
|
257
|
+
method: str,
|
|
258
|
+
url: str,
|
|
259
|
+
params: Optional[Dict[str, Any]] = None,
|
|
260
|
+
headers: Optional[Dict[str, str]] = None,
|
|
261
|
+
json_data: Optional[Any] = None,
|
|
262
|
+
data: Optional[Union[bytes, Dict[str, Any]]] = None,
|
|
263
|
+
) -> httpx.Response:
|
|
264
|
+
"""异步 HTTP 请求"""
|
|
265
|
+
headers = headers or {}
|
|
266
|
+
return await self.async_client.request(
|
|
267
|
+
method,
|
|
268
|
+
url,
|
|
269
|
+
params=params,
|
|
270
|
+
headers={**self.default_headers, **headers},
|
|
271
|
+
json=json_data,
|
|
272
|
+
data=data,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
async def aget(
|
|
276
|
+
self,
|
|
277
|
+
url: str,
|
|
278
|
+
params: Optional[Dict[str, Any]] = None,
|
|
279
|
+
headers: Optional[Dict[str, str]] = None,
|
|
280
|
+
) -> httpx.Response:
|
|
281
|
+
"""异步 GET 请求"""
|
|
282
|
+
return await self.arequest("GET", url, params=params, headers=headers)
|
|
283
|
+
|
|
284
|
+
async def apost(
|
|
285
|
+
self,
|
|
286
|
+
url: str,
|
|
287
|
+
json_data: Optional[Any] = None,
|
|
288
|
+
data: Optional[Union[bytes, Dict[str, Any]]] = None,
|
|
289
|
+
params: Optional[Dict[str, Any]] = None,
|
|
290
|
+
headers: Optional[Dict[str, str]] = None,
|
|
291
|
+
) -> httpx.Response:
|
|
292
|
+
"""异步 POST 请求"""
|
|
293
|
+
return await self.arequest(
|
|
294
|
+
"POST", url, params=params, headers=headers, json_data=json_data, data=data
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
async def aput(
|
|
298
|
+
self,
|
|
299
|
+
url: str,
|
|
300
|
+
json_data: Optional[Any] = None,
|
|
301
|
+
data: Optional[Union[bytes, Dict[str, Any]]] = None,
|
|
302
|
+
params: Optional[Dict[str, Any]] = None,
|
|
303
|
+
headers: Optional[Dict[str, str]] = None,
|
|
304
|
+
) -> httpx.Response:
|
|
305
|
+
"""异步 PUT 请求"""
|
|
306
|
+
return await self.arequest(
|
|
307
|
+
"PUT", url, params=params, headers=headers, json_data=json_data, data=data
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
async def adelete(
|
|
311
|
+
self,
|
|
312
|
+
url: str,
|
|
313
|
+
params: Optional[Dict[str, Any]] = None,
|
|
314
|
+
headers: Optional[Dict[str, str]] = None,
|
|
315
|
+
) -> httpx.Response:
|
|
316
|
+
"""异步 DELETE 请求"""
|
|
317
|
+
return await self.arequest("DELETE", url, params=params, headers=headers)
|
|
318
|
+
|
|
319
|
+
# ================ 响应处理工具 ================
|
|
320
|
+
@staticmethod
|
|
321
|
+
def read_response(response: httpx.Response) -> bytes:
|
|
322
|
+
"""读取完整响应内容 (同步)"""
|
|
323
|
+
return response.content
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def read_response_text(response: httpx.Response) -> str:
|
|
327
|
+
"""读取响应文本 (同步)"""
|
|
328
|
+
return response.text
|
|
329
|
+
|
|
330
|
+
@staticmethod
|
|
331
|
+
def read_response_json(response: httpx.Response) -> Any:
|
|
332
|
+
"""解析 JSON 响应 (同步)"""
|
|
333
|
+
return response.json()
|
|
334
|
+
|
|
335
|
+
@staticmethod
|
|
336
|
+
async def aread_response(response: httpx.Response) -> bytes:
|
|
337
|
+
"""异步读取完整响应内容"""
|
|
338
|
+
return await response.aread()
|
|
339
|
+
|
|
340
|
+
@staticmethod
|
|
341
|
+
async def aread_response_text(response: httpx.Response) -> str:
|
|
342
|
+
"""异步读取响应文本"""
|
|
343
|
+
return await response.atext()
|
|
344
|
+
|
|
345
|
+
@staticmethod
|
|
346
|
+
async def aread_response_json(response: httpx.Response) -> Any:
|
|
347
|
+
"""异步解析 JSON 响应"""
|
|
348
|
+
return await response.ajson()
|
|
349
|
+
|
|
350
|
+
# ================ 连接池状态 ================
|
|
351
|
+
def connection_pool_status(self) -> Dict[str, Any]:
|
|
352
|
+
"""获取同步连接池状态"""
|
|
353
|
+
transport = self.sync_client._transport
|
|
354
|
+
return {
|
|
355
|
+
"total_connections": transport._pool.num_connections,
|
|
356
|
+
"active_connections": transport._pool.num_active_connections,
|
|
357
|
+
"idle_connections": transport._pool.num_idle_connections,
|
|
358
|
+
"max_connections": transport._pool.max_connections,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async def aconnection_pool_status(self) -> Dict[str, Any]:
|
|
362
|
+
"""获取异步连接池状态"""
|
|
363
|
+
transport = self.async_client._transport
|
|
364
|
+
return {
|
|
365
|
+
"total_connections": transport._pool.num_connections,
|
|
366
|
+
"active_connections": transport._pool.num_active_connections,
|
|
367
|
+
"idle_connections": transport._pool.num_idle_connections,
|
|
368
|
+
"max_connections": transport._pool.max_connections,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# ================ 高级功能 ================
|
|
372
|
+
def set_proxy(self, proxy_url: str, proxy_auth: Optional[tuple] = None) -> None:
|
|
373
|
+
"""设置代理 (同步和异步)"""
|
|
374
|
+
self.sync_client = httpx.Client(proxies=proxy_url, auth=proxy_auth)
|
|
375
|
+
self.async_client = httpx.AsyncClient(proxies=proxy_url, auth=proxy_auth)
|
|
376
|
+
|
|
377
|
+
def set_cookies(self, cookies: Dict[str, str]) -> None:
|
|
378
|
+
"""设置 Cookies (同步和异步)"""
|
|
379
|
+
self.sync_client.cookies = httpx.Cookies(cookies)
|
|
380
|
+
self.async_client.cookies = httpx.Cookies(cookies)
|
|
381
|
+
|
|
382
|
+
def add_event_hook(self, event: str, hook: callable) -> None:
|
|
383
|
+
"""添加事件钩子 (同步和异步)"""
|
|
384
|
+
self.sync_client.event_hooks[event].append(hook)
|
|
385
|
+
self.async_client.event_hooks[event].append(hook)
|
|
386
|
+
|
|
387
|
+
def reset_client(self) -> None:
|
|
388
|
+
"""重置客户端 (清除所有状态)"""
|
|
389
|
+
self.close()
|
|
390
|
+
self.async_client = httpx.AsyncClient(
|
|
391
|
+
base_url=self.base_url,
|
|
392
|
+
timeout=self.timeout,
|
|
393
|
+
http2=self.http2,
|
|
394
|
+
verify=self.ssl_verify,
|
|
395
|
+
headers=self.default_headers,
|
|
396
|
+
)
|
|
397
|
+
self.sync_client = httpx.Client(
|
|
398
|
+
base_url=self.base_url,
|
|
399
|
+
timeout=self.timeout,
|
|
400
|
+
http2=self.http2,
|
|
401
|
+
verify=self.ssl_verify,
|
|
402
|
+
headers=self.default_headers,
|
|
403
|
+
)
|