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.
Files changed (157) hide show
  1. scalebox/__init__.py +80 -0
  2. scalebox/api/__init__.py +128 -0
  3. scalebox/api/client/__init__.py +8 -0
  4. scalebox/api/client/api/__init__.py +1 -0
  5. scalebox/api/client/api/sandboxes/__init__.py +0 -0
  6. scalebox/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
  7. scalebox/api/client/api/sandboxes/get_sandboxes.py +176 -0
  8. scalebox/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
  9. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
  10. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
  11. scalebox/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +214 -0
  12. scalebox/api/client/api/sandboxes/get_v2_sandboxes.py +229 -0
  13. scalebox/api/client/api/sandboxes/post_sandboxes.py +174 -0
  14. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +165 -0
  15. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +182 -0
  16. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +190 -0
  17. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +194 -0
  18. scalebox/api/client/client.py +288 -0
  19. scalebox/api/client/errors.py +16 -0
  20. scalebox/api/client/models/__init__.py +81 -0
  21. scalebox/api/client/models/build_log_entry.py +79 -0
  22. scalebox/api/client/models/created_access_token.py +100 -0
  23. scalebox/api/client/models/created_team_api_key.py +166 -0
  24. scalebox/api/client/models/error.py +67 -0
  25. scalebox/api/client/models/identifier_masking_details.py +83 -0
  26. scalebox/api/client/models/listed_sandbox.py +138 -0
  27. scalebox/api/client/models/log_level.py +11 -0
  28. scalebox/api/client/models/new_access_token.py +59 -0
  29. scalebox/api/client/models/new_sandbox.py +125 -0
  30. scalebox/api/client/models/new_team_api_key.py +59 -0
  31. scalebox/api/client/models/node.py +154 -0
  32. scalebox/api/client/models/node_detail.py +152 -0
  33. scalebox/api/client/models/node_status.py +11 -0
  34. scalebox/api/client/models/node_status_change.py +61 -0
  35. scalebox/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
  36. scalebox/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
  37. scalebox/api/client/models/resumed_sandbox.py +68 -0
  38. scalebox/api/client/models/sandbox.py +125 -0
  39. scalebox/api/client/models/sandbox_detail.py +178 -0
  40. scalebox/api/client/models/sandbox_log.py +70 -0
  41. scalebox/api/client/models/sandbox_logs.py +73 -0
  42. scalebox/api/client/models/sandbox_metric.py +110 -0
  43. scalebox/api/client/models/sandbox_state.py +9 -0
  44. scalebox/api/client/models/sandboxes_with_metrics.py +59 -0
  45. scalebox/api/client/models/team.py +83 -0
  46. scalebox/api/client/models/team_api_key.py +158 -0
  47. scalebox/api/client/models/team_user.py +68 -0
  48. scalebox/api/client/models/template.py +179 -0
  49. scalebox/api/client/models/template_build.py +117 -0
  50. scalebox/api/client/models/template_build_file_upload.py +70 -0
  51. scalebox/api/client/models/template_build_request.py +115 -0
  52. scalebox/api/client/models/template_build_request_v2.py +88 -0
  53. scalebox/api/client/models/template_build_start_v2.py +114 -0
  54. scalebox/api/client/models/template_build_status.py +11 -0
  55. scalebox/api/client/models/template_step.py +91 -0
  56. scalebox/api/client/models/template_update_request.py +59 -0
  57. scalebox/api/client/models/update_team_api_key.py +59 -0
  58. scalebox/api/client/py.typed +1 -0
  59. scalebox/api/client/types.py +46 -0
  60. scalebox/api/metadata.py +19 -0
  61. scalebox/cli.py +125 -0
  62. scalebox/client/__init__.py +0 -0
  63. scalebox/client/aclient.py +57 -0
  64. scalebox/client/api.proto +460 -0
  65. scalebox/client/buf.gen.yaml +8 -0
  66. scalebox/client/client.py +102 -0
  67. scalebox/client/requirements.txt +5 -0
  68. scalebox/code_interpreter/__init__.py +12 -0
  69. scalebox/code_interpreter/charts.py +230 -0
  70. scalebox/code_interpreter/code_interpreter_async.py +369 -0
  71. scalebox/code_interpreter/code_interpreter_sync.py +317 -0
  72. scalebox/code_interpreter/constants.py +3 -0
  73. scalebox/code_interpreter/exceptions.py +13 -0
  74. scalebox/code_interpreter/models.py +485 -0
  75. scalebox/connection_config.py +92 -0
  76. scalebox/csx_connect/__init__.py +1 -0
  77. scalebox/csx_connect/client.py +485 -0
  78. scalebox/csx_desktop/__init__.py +0 -0
  79. scalebox/csx_desktop/main.py +651 -0
  80. scalebox/exceptions.py +83 -0
  81. scalebox/generated/__init__.py +0 -0
  82. scalebox/generated/api.py +61 -0
  83. scalebox/generated/api_pb2.py +203 -0
  84. scalebox/generated/api_pb2.pyi +956 -0
  85. scalebox/generated/api_pb2_connect.py +1456 -0
  86. scalebox/generated/rpc.py +50 -0
  87. scalebox/generated/versions.py +3 -0
  88. scalebox/requirements.txt +36 -0
  89. scalebox/sandbox/__init__.py +0 -0
  90. scalebox/sandbox/commands/__init__.py +0 -0
  91. scalebox/sandbox/commands/command_handle.py +69 -0
  92. scalebox/sandbox/commands/main.py +39 -0
  93. scalebox/sandbox/filesystem/__init__.py +0 -0
  94. scalebox/sandbox/filesystem/filesystem.py +95 -0
  95. scalebox/sandbox/filesystem/watch_handle.py +60 -0
  96. scalebox/sandbox/main.py +139 -0
  97. scalebox/sandbox/sandbox_api.py +91 -0
  98. scalebox/sandbox/signature.py +40 -0
  99. scalebox/sandbox/utils.py +34 -0
  100. scalebox/sandbox_async/__init__.py +1 -0
  101. scalebox/sandbox_async/commands/command.py +307 -0
  102. scalebox/sandbox_async/commands/command_handle.py +187 -0
  103. scalebox/sandbox_async/commands/pty.py +187 -0
  104. scalebox/sandbox_async/filesystem/filesystem.py +557 -0
  105. scalebox/sandbox_async/filesystem/watch_handle.py +61 -0
  106. scalebox/sandbox_async/main.py +646 -0
  107. scalebox/sandbox_async/sandbox_api.py +365 -0
  108. scalebox/sandbox_async/utils.py +7 -0
  109. scalebox/sandbox_sync/__init__.py +2 -0
  110. scalebox/sandbox_sync/commands/__init__.py +0 -0
  111. scalebox/sandbox_sync/commands/command.py +300 -0
  112. scalebox/sandbox_sync/commands/command_handle.py +150 -0
  113. scalebox/sandbox_sync/commands/pty.py +181 -0
  114. scalebox/sandbox_sync/filesystem/__init__.py +0 -0
  115. scalebox/sandbox_sync/filesystem/filesystem.py +543 -0
  116. scalebox/sandbox_sync/filesystem/watch_handle.py +66 -0
  117. scalebox/sandbox_sync/main.py +790 -0
  118. scalebox/sandbox_sync/sandbox_api.py +356 -0
  119. scalebox/test/CODE_INTERPRETER_TESTS_READY.md +323 -0
  120. scalebox/test/README.md +329 -0
  121. scalebox/test/__init__.py +0 -0
  122. scalebox/test/aclient.py +72 -0
  123. scalebox/test/code_interpreter_centext.py +21 -0
  124. scalebox/test/code_interpreter_centext_sync.py +21 -0
  125. scalebox/test/code_interpreter_test.py +34 -0
  126. scalebox/test/code_interpreter_test_sync.py +34 -0
  127. scalebox/test/run_all_validation_tests.py +334 -0
  128. scalebox/test/run_code_interpreter_tests.sh +67 -0
  129. scalebox/test/run_tests.sh +230 -0
  130. scalebox/test/test_basic.py +78 -0
  131. scalebox/test/test_code_interpreter_async_comprehensive.py +2653 -0
  132. scalebox/test/test_code_interpreter_e2basync_comprehensive.py +2655 -0
  133. scalebox/test/test_code_interpreter_e2bsync_comprehensive.py +3416 -0
  134. scalebox/test/test_code_interpreter_sync_comprehensive.py +3412 -0
  135. scalebox/test/test_e2b_first.py +11 -0
  136. scalebox/test/test_sandbox_async_comprehensive.py +738 -0
  137. scalebox/test/test_sandbox_stress_and_edge_cases.py +778 -0
  138. scalebox/test/test_sandbox_sync_comprehensive.py +770 -0
  139. scalebox/test/test_sandbox_usage_examples.py +987 -0
  140. scalebox/test/testacreate.py +24 -0
  141. scalebox/test/testagetinfo.py +18 -0
  142. scalebox/test/testcodeinterpreter_async.py +508 -0
  143. scalebox/test/testcodeinterpreter_sync.py +239 -0
  144. scalebox/test/testcomputeuse.py +243 -0
  145. scalebox/test/testnovnc.py +12 -0
  146. scalebox/test/testsandbox_async.py +118 -0
  147. scalebox/test/testsandbox_sync.py +38 -0
  148. scalebox/utils/__init__.py +0 -0
  149. scalebox/utils/httpcoreclient.py +297 -0
  150. scalebox/utils/httpxclient.py +403 -0
  151. scalebox/version.py +16 -0
  152. scalebox_sdk-0.1.0.dist-info/METADATA +292 -0
  153. scalebox_sdk-0.1.0.dist-info/RECORD +157 -0
  154. scalebox_sdk-0.1.0.dist-info/WHEEL +5 -0
  155. scalebox_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  156. scalebox_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
  157. 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
+ )