scalebox-sdk 0.1.25__py3-none-any.whl → 1.0.2__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 +2 -2
- scalebox/api/__init__.py +3 -1
- scalebox/api/client/api/sandboxes/get_sandboxes.py +1 -1
- scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
- scalebox/api/client/models/connect_sandbox.py +59 -0
- scalebox/api/client/models/error.py +2 -2
- scalebox/api/client/models/listed_sandbox.py +24 -3
- scalebox/api/client/models/new_sandbox.py +10 -0
- scalebox/api/client/models/sandbox.py +13 -0
- scalebox/api/client/models/sandbox_detail.py +24 -0
- scalebox/cli.py +125 -125
- scalebox/client/aclient.py +57 -57
- scalebox/client/client.py +102 -102
- scalebox/code_interpreter/__init__.py +12 -12
- scalebox/code_interpreter/charts.py +230 -230
- scalebox/code_interpreter/code_interpreter_async.py +3 -1
- scalebox/code_interpreter/code_interpreter_sync.py +3 -1
- scalebox/code_interpreter/constants.py +3 -3
- scalebox/code_interpreter/exceptions.py +13 -13
- scalebox/code_interpreter/models.py +485 -485
- scalebox/connection_config.py +36 -1
- scalebox/csx_connect/__init__.py +1 -1
- scalebox/csx_connect/client.py +485 -485
- scalebox/csx_desktop/main.py +651 -651
- scalebox/exceptions.py +83 -83
- scalebox/generated/api.py +61 -61
- scalebox/generated/api_pb2.py +203 -203
- scalebox/generated/api_pb2.pyi +956 -956
- scalebox/generated/api_pb2_connect.py +1407 -1407
- scalebox/generated/rpc.py +50 -50
- scalebox/sandbox/main.py +146 -139
- scalebox/sandbox/sandbox_api.py +105 -91
- scalebox/sandbox/signature.py +40 -40
- scalebox/sandbox/utils.py +34 -34
- scalebox/sandbox_async/main.py +226 -44
- scalebox/sandbox_async/sandbox_api.py +124 -3
- scalebox/sandbox_sync/main.py +205 -130
- scalebox/sandbox_sync/sandbox_api.py +119 -3
- scalebox/test/CODE_INTERPRETER_TESTS_READY.md +323 -323
- scalebox/test/README.md +329 -329
- scalebox/test/bedrock_openai_adapter.py +73 -0
- scalebox/test/code_interpreter_test.py +34 -34
- scalebox/test/code_interpreter_test_sync.py +34 -34
- scalebox/test/run_stress_code_interpreter_sync.py +178 -0
- scalebox/test/simple_upload_example.py +131 -0
- scalebox/test/stabitiy_test.py +323 -0
- scalebox/test/test_browser_use.py +27 -0
- scalebox/test/test_browser_use_scalebox.py +62 -0
- scalebox/test/test_code_interpreter_execcode.py +289 -211
- scalebox/test/test_code_interpreter_sync_comprehensive.py +116 -69
- scalebox/test/test_connect_pause_async.py +300 -0
- scalebox/test/test_connect_pause_sync.py +300 -0
- scalebox/test/test_csx_desktop_examples.py +3 -3
- scalebox/test/test_desktop_sandbox_sf.py +112 -0
- scalebox/test/test_download_url.py +41 -0
- scalebox/test/test_existing_sandbox.py +1037 -0
- scalebox/test/test_sandbox_async_comprehensive.py +5 -3
- scalebox/test/test_sandbox_object_storage_example.py +151 -0
- scalebox/test/test_sandbox_object_storage_example_async.py +159 -0
- scalebox/test/test_sandbox_sync_comprehensive.py +1 -1
- scalebox/test/test_sf.py +141 -0
- scalebox/test/test_watch_dir_async.py +58 -0
- scalebox/test/testacreate.py +1 -1
- scalebox/test/testagetinfo.py +1 -3
- scalebox/test/testcomputeuse.py +243 -243
- scalebox/test/testsandbox_api.py +5 -5
- scalebox/test/testsandbox_async.py +17 -47
- scalebox/test/testsandbox_sync.py +19 -15
- scalebox/test/upload_100mb_example.py +377 -0
- scalebox/utils/httpcoreclient.py +297 -297
- scalebox/utils/httpxclient.py +403 -403
- scalebox/version.py +2 -2
- {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.2.dist-info}/METADATA +1 -1
- {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.2.dist-info}/RECORD +78 -60
- {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.2.dist-info}/WHEEL +1 -1
- {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.2.dist-info}/entry_points.txt +0 -0
- {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.2.dist-info}/top_level.txt +0 -0
scalebox/utils/httpcoreclient.py
CHANGED
|
@@ -1,297 +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)
|
|
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)
|