scalebox-sdk 0.1.24__py3-none-any.whl → 1.0.1__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.
Files changed (87) hide show
  1. scalebox/__init__.py +2 -2
  2. scalebox/api/__init__.py +130 -128
  3. scalebox/api/client/__init__.py +8 -8
  4. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +2 -2
  5. scalebox/api/client/api/sandboxes/post_sandboxes.py +2 -2
  6. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
  7. scalebox/api/client/client.py +288 -288
  8. scalebox/api/client/models/connect_sandbox.py +59 -0
  9. scalebox/api/client/models/error.py +2 -2
  10. scalebox/api/client/models/listed_sandbox.py +19 -1
  11. scalebox/api/client/models/new_sandbox.py +10 -0
  12. scalebox/api/client/models/sandbox.py +138 -125
  13. scalebox/api/client/models/sandbox_detail.py +24 -0
  14. scalebox/api/client/types.py +46 -46
  15. scalebox/cli.py +125 -125
  16. scalebox/client/aclient.py +57 -57
  17. scalebox/client/client.py +102 -102
  18. scalebox/code_interpreter/__init__.py +12 -12
  19. scalebox/code_interpreter/charts.py +230 -230
  20. scalebox/code_interpreter/constants.py +3 -3
  21. scalebox/code_interpreter/exceptions.py +13 -13
  22. scalebox/code_interpreter/models.py +485 -485
  23. scalebox/connection_config.py +34 -1
  24. scalebox/csx_connect/__init__.py +1 -1
  25. scalebox/csx_connect/client.py +485 -485
  26. scalebox/csx_desktop/main.py +651 -651
  27. scalebox/exceptions.py +83 -83
  28. scalebox/generated/api.py +61 -61
  29. scalebox/generated/api_pb2.py +203 -203
  30. scalebox/generated/api_pb2.pyi +956 -956
  31. scalebox/generated/api_pb2_connect.py +1407 -1407
  32. scalebox/generated/rpc.py +50 -50
  33. scalebox/sandbox/main.py +146 -139
  34. scalebox/sandbox/sandbox_api.py +105 -91
  35. scalebox/sandbox/signature.py +40 -40
  36. scalebox/sandbox/utils.py +34 -34
  37. scalebox/sandbox_async/commands/command.py +307 -307
  38. scalebox/sandbox_async/commands/command_handle.py +187 -187
  39. scalebox/sandbox_async/commands/pty.py +187 -187
  40. scalebox/sandbox_async/filesystem/filesystem.py +557 -557
  41. scalebox/sandbox_async/filesystem/watch_handle.py +61 -61
  42. scalebox/sandbox_async/main.py +228 -46
  43. scalebox/sandbox_async/sandbox_api.py +124 -3
  44. scalebox/sandbox_async/utils.py +7 -7
  45. scalebox/sandbox_sync/__init__.py +2 -2
  46. scalebox/sandbox_sync/commands/command.py +300 -300
  47. scalebox/sandbox_sync/commands/command_handle.py +150 -150
  48. scalebox/sandbox_sync/commands/pty.py +181 -181
  49. scalebox/sandbox_sync/filesystem/filesystem.py +3 -3
  50. scalebox/sandbox_sync/filesystem/watch_handle.py +66 -66
  51. scalebox/sandbox_sync/main.py +208 -133
  52. scalebox/sandbox_sync/sandbox_api.py +119 -3
  53. scalebox/test/CODE_INTERPRETER_TESTS_READY.md +323 -323
  54. scalebox/test/README.md +329 -329
  55. scalebox/test/bedrock_openai_adapter.py +67 -0
  56. scalebox/test/code_interpreter_test.py +34 -34
  57. scalebox/test/code_interpreter_test_sync.py +34 -34
  58. scalebox/test/run_stress_code_interpreter_sync.py +166 -0
  59. scalebox/test/simple_upload_example.py +123 -0
  60. scalebox/test/stabitiy_test.py +310 -0
  61. scalebox/test/test_browser_use.py +25 -0
  62. scalebox/test/test_browser_use_scalebox.py +61 -0
  63. scalebox/test/test_code_interpreter_sync_comprehensive.py +115 -65
  64. scalebox/test/test_connect_pause_async.py +277 -0
  65. scalebox/test/test_connect_pause_sync.py +267 -0
  66. scalebox/test/test_desktop_sandbox_sf.py +117 -0
  67. scalebox/test/test_download_url.py +49 -0
  68. scalebox/test/test_sandbox_async_comprehensive.py +1 -1
  69. scalebox/test/test_sandbox_object_storage_example.py +146 -0
  70. scalebox/test/test_sandbox_object_storage_example_async.py +156 -0
  71. scalebox/test/test_sf.py +137 -0
  72. scalebox/test/test_watch_dir_async.py +56 -0
  73. scalebox/test/testacreate.py +1 -1
  74. scalebox/test/testagetinfo.py +1 -1
  75. scalebox/test/testcomputeuse.py +243 -243
  76. scalebox/test/testsandbox_api.py +13 -0
  77. scalebox/test/testsandbox_sync.py +1 -1
  78. scalebox/test/upload_100mb_example.py +355 -0
  79. scalebox/utils/httpcoreclient.py +297 -297
  80. scalebox/utils/httpxclient.py +403 -403
  81. scalebox/version.py +2 -2
  82. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/METADATA +1 -1
  83. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/RECORD +87 -69
  84. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/WHEEL +1 -1
  85. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/entry_points.txt +0 -0
  86. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {scalebox_sdk-0.1.24.dist-info → scalebox_sdk-1.0.1.dist-info}/top_level.txt +0 -0
@@ -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)