python-sandbox-sdk 1.0.1__tar.gz → 1.0.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-sandbox-sdk
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Python SDK for Python Sandbox API - Run Python code, shell commands, and manage packages in Docker containers
5
5
  Home-page: https://github.com/zjwan461/python-sandbox
6
6
  Author-email: Jerry <826935261@qq.com>
@@ -45,7 +45,8 @@ Python Sandbox 官方 Python SDK — 在隔离的 Docker 容器中执行 Python
45
45
  - 🔌 **上下文管理器**:自动管理 HTTP 连接与会话生命周期
46
46
  - 🛡️ **异常体系**:独立的 `ApiRequestError` / `SandboxError` 类型
47
47
  - 🧵 **线程安全**:`requests.Session` 复用,支持 `ThreadPoolExecutor` 并发
48
- - 🐍 **Python 3.8**:仅依赖 `requests>=2.31.0`
48
+ - **异步支持**:提供 `AsyncSandboxClient`,基于 `aiohttp` 实现完整异步 API
49
+ - 🐍 **Python ≥ 3.8**:同步客户端仅依赖 `requests>=2.31.0`
49
50
 
50
51
  ## 📦 安装
51
52
 
@@ -57,6 +58,9 @@ pip install python-sandbox-sdk
57
58
  git clone https://github.com/zjwan461/python-sandbox.git
58
59
  cd python-sandbox/sdk/python-sandbox-sdk
59
60
  pip install -e .
61
+
62
+ # 安装异步支持(可选)
63
+ pip install python-sandbox-sdk[async]
60
64
  ```
61
65
 
62
66
  ## 🚀 快速开始
@@ -112,7 +116,6 @@ content = client.read_file(session_id, "/tmp/config.json")
112
116
  client.upload_file(session_id, "/tmp/image.png", b"\\x89PNG...")
113
117
  data = client.download_file(session_id, "/tmp/image.png")
114
118
  ```
115
-
116
119
  ## 📚 使用样例(覆盖全部 API 与典型场景)
117
120
 
118
121
  完整可运行样例见 [`usage/`](usage/) 目录:
@@ -130,6 +133,8 @@ data = client.download_file(session_id, "/tmp/image.png")
130
133
  | [`09_long_running.py`](usage/09_long_running.py) | 长任务:进度回报 + 异常安全 | `exec_python`, `exec_shell` |
131
134
  | [`10_concurrent_sessions.py`](usage/10_concurrent_sessions.py) | 并发:`ThreadPoolExecutor` + 上限约束 | 多会话并行 |
132
135
  | [`11_error_handling.py`](usage/11_error_handling.py) | 错误全景:鉴权 / 会话 / 黑名单 / 网络 | 全 API 错误路径 |
136
+ | [`12_async_client.py`](usage/12_async_client.py) | 异步客户端:完整异步操作示例 | 全 API 异步版本 |
137
+ | [`11_error_handling.py`](usage/11_error_handling.py) | 错误全景:鉴权 / 会话 / 黑名单 / 网络 | 全 API 错误路径 |
133
138
 
134
139
  运行样例:
135
140
 
@@ -160,6 +165,8 @@ for f in 0[1-9]_*.py 1[0-1]_*.py; do python "$f"; done
160
165
 
161
166
  `CommandResult` 字段:`exit_code`, `stdout`, `stderr`, `success`(property), `combined_output`(property)。
162
167
 
168
+ > 💡 **异步版本**:`AsyncSandboxClient` 提供完全相同的 API,所有方法均为 `async`,支持 `await` 调用和 `async with` 上下文管理器。
169
+
163
170
  ## ⚙️ 环境变量
164
171
 
165
172
  为避免硬编码,样例通过环境变量注入连接信息:
@@ -189,6 +196,8 @@ except OSError as e:
189
196
 
190
197
  ## 🧵 并发使用
191
198
 
199
+ ### 同步并发(ThreadPoolExecutor)
200
+
192
201
  ```python
193
202
  from concurrent.futures import ThreadPoolExecutor
194
203
  # 每个任务使用独立的 client + session(线程安全)
@@ -198,6 +207,33 @@ with ThreadPoolExecutor(max_workers=5) as pool:
198
207
  print(f.result())
199
208
  ```
200
209
 
210
+ ### 异步并发(AsyncSandboxClient)
211
+
212
+ ```python
213
+ import asyncio
214
+ from python_sandbox_sdk import AsyncSandboxClient
215
+
216
+ async def run_task(task_id):
217
+ async with AsyncSandboxClient("http://localhost:8080", "sandbox-secret-key") as client:
218
+ session_id = await client.create_session()
219
+ try:
220
+ result = await client.exec_python(
221
+ session_id, f"print('Task {task_id} done')"
222
+ )
223
+ return result.stdout
224
+ finally:
225
+ await client.delete_session(session_id)
226
+
227
+ async def main():
228
+ # 并发执行 5 个任务
229
+ tasks = [run_task(i) for i in range(5)]
230
+ results = await asyncio.gather(*tasks)
231
+ for r in results:
232
+ print(r)
233
+
234
+ asyncio.run(main())
235
+ ```
236
+
201
237
  > ⚠️ 并发数受后端 `sandbox.max-containers`(默认 10)约束。
202
238
 
203
239
  ## 📦 打包发布
@@ -14,7 +14,8 @@ Python Sandbox 官方 Python SDK — 在隔离的 Docker 容器中执行 Python
14
14
  - 🔌 **上下文管理器**:自动管理 HTTP 连接与会话生命周期
15
15
  - 🛡️ **异常体系**:独立的 `ApiRequestError` / `SandboxError` 类型
16
16
  - 🧵 **线程安全**:`requests.Session` 复用,支持 `ThreadPoolExecutor` 并发
17
- - 🐍 **Python 3.8**:仅依赖 `requests>=2.31.0`
17
+ - **异步支持**:提供 `AsyncSandboxClient`,基于 `aiohttp` 实现完整异步 API
18
+ - 🐍 **Python ≥ 3.8**:同步客户端仅依赖 `requests>=2.31.0`
18
19
 
19
20
  ## 📦 安装
20
21
 
@@ -26,6 +27,9 @@ pip install python-sandbox-sdk
26
27
  git clone https://github.com/zjwan461/python-sandbox.git
27
28
  cd python-sandbox/sdk/python-sandbox-sdk
28
29
  pip install -e .
30
+
31
+ # 安装异步支持(可选)
32
+ pip install python-sandbox-sdk[async]
29
33
  ```
30
34
 
31
35
  ## 🚀 快速开始
@@ -81,7 +85,6 @@ content = client.read_file(session_id, "/tmp/config.json")
81
85
  client.upload_file(session_id, "/tmp/image.png", b"\\x89PNG...")
82
86
  data = client.download_file(session_id, "/tmp/image.png")
83
87
  ```
84
-
85
88
  ## 📚 使用样例(覆盖全部 API 与典型场景)
86
89
 
87
90
  完整可运行样例见 [`usage/`](usage/) 目录:
@@ -99,6 +102,8 @@ data = client.download_file(session_id, "/tmp/image.png")
99
102
  | [`09_long_running.py`](usage/09_long_running.py) | 长任务:进度回报 + 异常安全 | `exec_python`, `exec_shell` |
100
103
  | [`10_concurrent_sessions.py`](usage/10_concurrent_sessions.py) | 并发:`ThreadPoolExecutor` + 上限约束 | 多会话并行 |
101
104
  | [`11_error_handling.py`](usage/11_error_handling.py) | 错误全景:鉴权 / 会话 / 黑名单 / 网络 | 全 API 错误路径 |
105
+ | [`12_async_client.py`](usage/12_async_client.py) | 异步客户端:完整异步操作示例 | 全 API 异步版本 |
106
+ | [`11_error_handling.py`](usage/11_error_handling.py) | 错误全景:鉴权 / 会话 / 黑名单 / 网络 | 全 API 错误路径 |
102
107
 
103
108
  运行样例:
104
109
 
@@ -129,6 +134,8 @@ for f in 0[1-9]_*.py 1[0-1]_*.py; do python "$f"; done
129
134
 
130
135
  `CommandResult` 字段:`exit_code`, `stdout`, `stderr`, `success`(property), `combined_output`(property)。
131
136
 
137
+ > 💡 **异步版本**:`AsyncSandboxClient` 提供完全相同的 API,所有方法均为 `async`,支持 `await` 调用和 `async with` 上下文管理器。
138
+
132
139
  ## ⚙️ 环境变量
133
140
 
134
141
  为避免硬编码,样例通过环境变量注入连接信息:
@@ -158,6 +165,8 @@ except OSError as e:
158
165
 
159
166
  ## 🧵 并发使用
160
167
 
168
+ ### 同步并发(ThreadPoolExecutor)
169
+
161
170
  ```python
162
171
  from concurrent.futures import ThreadPoolExecutor
163
172
  # 每个任务使用独立的 client + session(线程安全)
@@ -167,6 +176,33 @@ with ThreadPoolExecutor(max_workers=5) as pool:
167
176
  print(f.result())
168
177
  ```
169
178
 
179
+ ### 异步并发(AsyncSandboxClient)
180
+
181
+ ```python
182
+ import asyncio
183
+ from python_sandbox_sdk import AsyncSandboxClient
184
+
185
+ async def run_task(task_id):
186
+ async with AsyncSandboxClient("http://localhost:8080", "sandbox-secret-key") as client:
187
+ session_id = await client.create_session()
188
+ try:
189
+ result = await client.exec_python(
190
+ session_id, f"print('Task {task_id} done')"
191
+ )
192
+ return result.stdout
193
+ finally:
194
+ await client.delete_session(session_id)
195
+
196
+ async def main():
197
+ # 并发执行 5 个任务
198
+ tasks = [run_task(i) for i in range(5)]
199
+ results = await asyncio.gather(*tasks)
200
+ for r in results:
201
+ print(r)
202
+
203
+ asyncio.run(main())
204
+ ```
205
+
170
206
  > ⚠️ 并发数受后端 `sandbox.max-containers`(默认 10)约束。
171
207
 
172
208
  ## 📦 打包发布
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-sandbox-sdk"
7
- version = "1.0.1"
7
+ version = "1.0.3"
8
8
  description = "Python SDK for Python Sandbox API - Run Python code, shell commands, and manage packages in Docker containers"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -15,4 +15,4 @@ __all__ = [
15
15
  'ApiKeyMissingError',
16
16
  'ApiRequestError',
17
17
  ]
18
- __version__ = '1.0.1'
18
+ __version__ = '1.0.2'
@@ -15,8 +15,7 @@
15
15
  Hello!
16
16
  """
17
17
 
18
- import os
19
- import tempfile
18
+ import io
20
19
  from typing import Optional
21
20
 
22
21
  import aiohttp
@@ -86,6 +85,19 @@ class AsyncSandboxClient:
86
85
  data = await resp.json()
87
86
  return data["sessionId"]
88
87
 
88
+ async def create_default_session(self) -> str:
89
+ """
90
+ 创建新的沙箱会话
91
+
92
+ Returns:
93
+ 会话 ID
94
+ """
95
+ session = await self._get_session()
96
+ async with session.post(f"{self.base_url}/api/sandbox/session-default") as resp:
97
+ await self._raise_if_error(resp)
98
+ data = await resp.json()
99
+ return data["sessionId"]
100
+
89
101
  async def delete_session(self, session_id: str) -> None:
90
102
  """
91
103
  删除会话并清理容器
@@ -168,9 +180,7 @@ class AsyncSandboxClient:
168
180
  data["exitCode"], data.get("stdout", ""), data.get("stderr", "")
169
181
  )
170
182
 
171
- async def pip_uninstall(
172
- self, session_id: str, package_name: str
173
- ) -> CommandResult:
183
+ async def pip_uninstall(self, session_id: str, package_name: str) -> CommandResult:
174
184
  """
175
185
  卸载 Python 包
176
186
 
@@ -266,31 +276,27 @@ class AsyncSandboxClient:
266
276
  container_path: 容器内目标路径
267
277
  data: 文件字节数据
268
278
  """
269
- # 写入临时文件
270
- fd, tmp_path = tempfile.mkstemp(prefix="sandbox_upload_")
271
- try:
272
- with os.fdopen(fd, "wb") as f:
273
- f.write(data)
274
-
275
- url = f"{self.base_url}/api/sandbox/file/upload"
276
- form = aiohttp.FormData()
277
- form.add_field(
278
- "file",
279
- open(tmp_path, "rb"),
280
- filename=os.path.basename(container_path),
281
- )
282
- form.add_field("sessionId", session_id)
283
- form.add_field("path", container_path)
284
-
285
- # 上传使用独立的 session,避免继承主 session 的 Content-Type
286
- upload_timeout = aiohttp.ClientTimeout(total=120)
287
- async with aiohttp.ClientSession(timeout=upload_timeout) as upload_session:
288
- upload_session.headers.update({"X-Api-Key": self.api_key})
289
- async with upload_session.post(url, data=form) as resp:
290
- await self._raise_if_error(resp)
291
- finally:
292
- if os.path.exists(tmp_path):
293
- os.unlink(tmp_path)
279
+ import os
280
+
281
+ url = f"{self.base_url}/api/sandbox/file/upload"
282
+
283
+ # 使用 BytesIO 避免临时文件和阻塞 I/O
284
+ file_obj = io.BytesIO(data)
285
+ form = aiohttp.FormData()
286
+ form.add_field(
287
+ "file",
288
+ file_obj,
289
+ filename=os.path.basename(container_path),
290
+ )
291
+ form.add_field("sessionId", session_id)
292
+ form.add_field("path", container_path)
293
+
294
+ # 上传使用独立的 session,避免继承主 session 的 Content-Type
295
+ upload_timeout = aiohttp.ClientTimeout(total=120)
296
+ async with aiohttp.ClientSession(timeout=upload_timeout) as upload_session:
297
+ upload_session.headers.update({"X-Api-Key": self.api_key})
298
+ async with upload_session.post(url, data=form) as resp:
299
+ await self._raise_if_error(resp)
294
300
 
295
301
  async def download_file(self, session_id: str, container_path: str) -> bytes:
296
302
  """
@@ -20,16 +20,19 @@ from .dto import CommandResult
20
20
 
21
21
  class SandboxError(Exception):
22
22
  """SDK 异常基类"""
23
+
23
24
  pass
24
25
 
25
26
 
26
27
  class ApiKeyMissingError(SandboxError):
27
28
  """API Key 缺失错误"""
29
+
28
30
  pass
29
31
 
30
32
 
31
33
  class ApiRequestError(SandboxError):
32
34
  """API 请求错误"""
35
+
33
36
  def __init__(self, status_code: int, message: str):
34
37
  self.status_code = status_code
35
38
  super().__init__(f"HTTP {status_code}: {message}")
@@ -38,16 +41,16 @@ class ApiRequestError(SandboxError):
38
41
  class SandboxClient:
39
42
  """
40
43
  Python Sandbox 客户端
41
-
44
+
42
45
  Attributes:
43
46
  base_url: Sandbox API 基础 URL (如 http://localhost:8080)
44
47
  api_key: API 认证密钥
45
48
  """
46
-
49
+
47
50
  def __init__(self, base_url: str, api_key: str):
48
51
  """
49
52
  创建客户端实例
50
-
53
+
51
54
  Args:
52
55
  base_url: Sandbox API 基础 URL
53
56
  api_key: API 认证密钥
@@ -55,180 +58,188 @@ class SandboxClient:
55
58
  self.base_url = base_url.rstrip("/")
56
59
  self.api_key = api_key
57
60
  self._session = requests.Session()
58
- self._session.headers.update({
59
- "X-Api-Key": api_key,
60
- "Content-Type": "application/json"
61
- })
62
-
61
+ self._session.headers.update(
62
+ {"X-Api-Key": api_key, "Content-Type": "application/json"}
63
+ )
64
+
63
65
  # ==================== 会话管理 ====================
64
-
66
+
65
67
  def create_session(self) -> str:
66
68
  """
67
69
  创建新的沙箱会话
68
-
70
+
69
71
  Returns:
70
72
  会话 ID
71
73
  """
72
74
  resp = self._session.post(f"{self.base_url}/api/sandbox/session")
73
75
  self._raise_if_error(resp)
74
76
  return resp.json()["sessionId"]
75
-
77
+
78
+ def create_default_session(self) -> str:
79
+ """
80
+ 创建默认沙箱会话
81
+
82
+ Returns:
83
+ 会话 ID
84
+ """
85
+ resp = self._session.post(f"{self.base_url}/api/sandbox/session-default")
86
+ self._raise_if_error(resp)
87
+ return resp.json()["sessionId"]
88
+
76
89
  def delete_session(self, session_id: str) -> None:
77
90
  """
78
91
  删除会话并清理容器
79
-
92
+
80
93
  Args:
81
94
  session_id: 会话 ID
82
95
  """
83
96
  url = f"{self.base_url}/api/sandbox/session/{session_id}"
84
97
  resp = self._session.delete(url)
85
98
  self._raise_if_error(resp)
86
-
99
+
87
100
  # ==================== 代码执行 ====================
88
-
101
+
89
102
  def exec_python(self, session_id: str, code: str) -> CommandResult:
90
103
  """
91
104
  在沙箱中执行 Python 代码
92
-
105
+
93
106
  Args:
94
107
  session_id: 会话 ID
95
108
  code: Python 源代码
96
-
109
+
97
110
  Returns:
98
111
  命令执行结果
99
112
  """
100
113
  payload = {"sessionId": session_id, "code": code}
101
114
  resp = self._session.post(
102
- f"{self.base_url}/api/sandbox/exec/python",
103
- json=payload
115
+ f"{self.base_url}/api/sandbox/exec/python", json=payload
104
116
  )
105
117
  self._raise_if_error(resp)
106
118
  data = resp.json()
107
- return CommandResult(data["exitCode"], data.get("stdout", ""), data.get("stderr", ""))
108
-
119
+ return CommandResult(
120
+ data["exitCode"], data.get("stdout", ""), data.get("stderr", "")
121
+ )
122
+
109
123
  def exec_shell(self, session_id: str, command: str) -> CommandResult:
110
124
  """
111
125
  在沙箱中执行 Shell 命令
112
-
126
+
113
127
  Args:
114
128
  session_id: 会话 ID
115
129
  command: Shell 命令
116
-
130
+
117
131
  Returns:
118
132
  命令执行结果
119
133
  """
120
134
  payload = {"sessionId": session_id, "command": command}
121
135
  resp = self._session.post(
122
- f"{self.base_url}/api/sandbox/exec/shell",
123
- json=payload
136
+ f"{self.base_url}/api/sandbox/exec/shell", json=payload
124
137
  )
125
138
  self._raise_if_error(resp)
126
139
  data = resp.json()
127
- return CommandResult(data["exitCode"], data.get("stdout", ""), data.get("stderr", ""))
128
-
140
+ return CommandResult(
141
+ data["exitCode"], data.get("stdout", ""), data.get("stderr", "")
142
+ )
143
+
129
144
  # ==================== pip 包管理 ====================
130
-
145
+
131
146
  def pip_install(self, session_id: str, package_name: str) -> CommandResult:
132
147
  """
133
148
  安装 Python 包
134
-
149
+
135
150
  Args:
136
151
  session_id: 会话 ID
137
152
  package_name: 包名(支持版本约束)
138
-
153
+
139
154
  Returns:
140
155
  命令执行结果
141
156
  """
142
157
  payload = {"sessionId": session_id, "pkg": package_name}
143
158
  resp = self._session.post(
144
- f"{self.base_url}/api/sandbox/pip/install",
145
- json=payload
159
+ f"{self.base_url}/api/sandbox/pip/install", json=payload
146
160
  )
147
161
  self._raise_if_error(resp)
148
162
  data = resp.json()
149
- return CommandResult(data["exitCode"], data.get("stdout", ""), data.get("stderr", ""))
150
-
163
+ return CommandResult(
164
+ data["exitCode"], data.get("stdout", ""), data.get("stderr", "")
165
+ )
166
+
151
167
  def pip_uninstall(self, session_id: str, package_name: str) -> CommandResult:
152
168
  """
153
169
  卸载 Python 包
154
-
170
+
155
171
  Args:
156
172
  session_id: 会话 ID
157
173
  package_name: 包名
158
-
174
+
159
175
  Returns:
160
176
  命令执行结果
161
177
  """
162
178
  payload = {"sessionId": session_id, "pkg": package_name}
163
179
  resp = self._session.post(
164
- f"{self.base_url}/api/sandbox/pip/uninstall",
165
- json=payload
180
+ f"{self.base_url}/api/sandbox/pip/uninstall", json=payload
166
181
  )
167
182
  self._raise_if_error(resp)
168
183
  data = resp.json()
169
- return CommandResult(data["exitCode"], data.get("stdout", ""), data.get("stderr", ""))
170
-
184
+ return CommandResult(
185
+ data["exitCode"], data.get("stdout", ""), data.get("stderr", "")
186
+ )
187
+
171
188
  def pip_list(self, session_id: str) -> str:
172
189
  """
173
190
  列出已安装的 Python 包
174
-
191
+
175
192
  Args:
176
193
  session_id: 会话 ID
177
-
194
+
178
195
  Returns:
179
196
  安装包列表文本
180
197
  """
181
198
  resp = self._session.get(
182
- f"{self.base_url}/api/sandbox/pip/list",
183
- params={"sessionId": session_id}
199
+ f"{self.base_url}/api/sandbox/pip/list", params={"sessionId": session_id}
184
200
  )
185
201
  self._raise_if_error(resp)
186
202
  return resp.json()["packages"]
187
-
203
+
188
204
  # ==================== 文件操作 ====================
189
-
205
+
190
206
  def write_file(self, session_id: str, container_path: str, content: str) -> None:
191
207
  """
192
208
  向沙箱写入文件内容
193
-
209
+
194
210
  Args:
195
211
  session_id: 会话 ID
196
212
  container_path: 容器内目标路径
197
213
  content: 文件内容
198
214
  """
199
- payload = {
200
- "sessionId": session_id,
201
- "path": container_path,
202
- "content": content
203
- }
215
+ payload = {"sessionId": session_id, "path": container_path, "content": content}
204
216
  resp = self._session.post(
205
- f"{self.base_url}/api/sandbox/file/write",
206
- json=payload
217
+ f"{self.base_url}/api/sandbox/file/write", json=payload
207
218
  )
208
219
  self._raise_if_error(resp)
209
-
220
+
210
221
  def read_file(self, session_id: str, container_path: str) -> str:
211
222
  """
212
223
  读取沙箱中的文件内容
213
-
224
+
214
225
  Args:
215
226
  session_id: 会话 ID
216
227
  container_path: 容器内文件路径
217
-
228
+
218
229
  Returns:
219
230
  文件内容字符串
220
231
  """
221
232
  resp = self._session.get(
222
233
  f"{self.base_url}/api/sandbox/file/read",
223
- params={"sessionId": session_id, "path": container_path}
234
+ params={"sessionId": session_id, "path": container_path},
224
235
  )
225
236
  self._raise_if_error(resp)
226
237
  return resp.json()["content"]
227
-
238
+
228
239
  def upload_file(self, session_id: str, container_path: str, data: bytes) -> None:
229
240
  """
230
241
  上传二进制文件到沙箱
231
-
242
+
232
243
  Args:
233
244
  session_id: 会话 ID
234
245
  container_path: 容器内目标路径
@@ -236,56 +247,58 @@ class SandboxClient:
236
247
  """
237
248
  import tempfile
238
249
  import os
239
-
250
+
240
251
  # 写入临时文件
241
252
  fd, tmp_path = tempfile.mkstemp(prefix="sandbox_upload_")
242
253
  try:
243
254
  # os.fdopen 接管了 fd 的所有权,with 块结束时会自动关闭 fd
244
- with os.fdopen(fd, 'wb') as f:
255
+ with os.fdopen(fd, "wb") as f:
245
256
  f.write(data)
246
-
247
- with open(tmp_path, 'rb') as f:
248
- files = {'file': (os.path.basename(container_path), f)}
249
- data_params = {'sessionId': session_id, 'path': container_path}
250
-
257
+
258
+ with open(tmp_path, "rb") as f:
259
+ files = {"file": (os.path.basename(container_path), f)}
260
+ data_params = {"sessionId": session_id, "path": container_path}
261
+
251
262
  # 发送 multipart 请求(requests 会自动设置正确的 Content-Type)
252
263
  url = f"{self.base_url}/api/sandbox/file/upload"
253
264
  # 使用新的 session 避免继承主 session 的 Content-Type: application/json
254
265
  with requests.Session() as upload_session:
255
266
  upload_session.headers.update({"X-Api-Key": self.api_key})
256
- resp = upload_session.post(url, files=files, data=data_params, timeout=120)
267
+ resp = upload_session.post(
268
+ url, files=files, data=data_params, timeout=120
269
+ )
257
270
  self._raise_if_error(resp)
258
271
  finally:
259
272
  if os.path.exists(tmp_path):
260
273
  os.unlink(tmp_path)
261
-
274
+
262
275
  def download_file(self, session_id: str, container_path: str) -> bytes:
263
276
  """
264
277
  下载沙箱中的文件
265
-
278
+
266
279
  Args:
267
280
  session_id: 会话 ID
268
281
  container_path: 容器内文件路径
269
-
282
+
270
283
  Returns:
271
284
  文件字节数据
272
285
  """
273
286
  url = f"{self.base_url}/api/sandbox/file/download"
274
287
  params = {"sessionId": session_id, "path": container_path}
275
-
288
+
276
289
  # 使用新的 session 避免继承主 session 的 Content-Type: application/json
277
290
  with requests.Session() as s:
278
291
  s.headers.update({"X-Api-Key": self.api_key})
279
292
  resp = s.get(url, params=params, timeout=60)
280
293
  self._raise_if_error(resp)
281
294
  return resp.content
282
-
295
+
283
296
  # ==================== 健康检查 ====================
284
-
297
+
285
298
  def is_health(self) -> bool:
286
299
  """
287
300
  检查沙箱服务是否可用
288
-
301
+
289
302
  Returns:
290
303
  True 如果服务正常运行
291
304
  """
@@ -294,9 +307,9 @@ class SandboxClient:
294
307
  return resp.status_code == 200
295
308
  except Exception:
296
309
  return False
297
-
310
+
298
311
  # ==================== 内部方法 ====================
299
-
312
+
300
313
  @staticmethod
301
314
  def _raise_if_error(resp: requests.Response) -> None:
302
315
  """如果响应状态码表示错误则抛出异常"""
@@ -307,13 +320,13 @@ class SandboxClient:
307
320
  except Exception:
308
321
  message = resp.text
309
322
  raise ApiRequestError(resp.status_code, message)
310
-
323
+
311
324
  def close(self) -> None:
312
325
  """关闭 HTTP 会话"""
313
326
  self._session.close()
314
-
327
+
315
328
  def __enter__(self):
316
329
  return self
317
-
330
+
318
331
  def __exit__(self, exc_type, exc_val, exc_tb):
319
332
  self.close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-sandbox-sdk
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Python SDK for Python Sandbox API - Run Python code, shell commands, and manage packages in Docker containers
5
5
  Home-page: https://github.com/zjwan461/python-sandbox
6
6
  Author-email: Jerry <826935261@qq.com>
@@ -45,7 +45,8 @@ Python Sandbox 官方 Python SDK — 在隔离的 Docker 容器中执行 Python
45
45
  - 🔌 **上下文管理器**:自动管理 HTTP 连接与会话生命周期
46
46
  - 🛡️ **异常体系**:独立的 `ApiRequestError` / `SandboxError` 类型
47
47
  - 🧵 **线程安全**:`requests.Session` 复用,支持 `ThreadPoolExecutor` 并发
48
- - 🐍 **Python 3.8**:仅依赖 `requests>=2.31.0`
48
+ - **异步支持**:提供 `AsyncSandboxClient`,基于 `aiohttp` 实现完整异步 API
49
+ - 🐍 **Python ≥ 3.8**:同步客户端仅依赖 `requests>=2.31.0`
49
50
 
50
51
  ## 📦 安装
51
52
 
@@ -57,6 +58,9 @@ pip install python-sandbox-sdk
57
58
  git clone https://github.com/zjwan461/python-sandbox.git
58
59
  cd python-sandbox/sdk/python-sandbox-sdk
59
60
  pip install -e .
61
+
62
+ # 安装异步支持(可选)
63
+ pip install python-sandbox-sdk[async]
60
64
  ```
61
65
 
62
66
  ## 🚀 快速开始
@@ -112,7 +116,6 @@ content = client.read_file(session_id, "/tmp/config.json")
112
116
  client.upload_file(session_id, "/tmp/image.png", b"\\x89PNG...")
113
117
  data = client.download_file(session_id, "/tmp/image.png")
114
118
  ```
115
-
116
119
  ## 📚 使用样例(覆盖全部 API 与典型场景)
117
120
 
118
121
  完整可运行样例见 [`usage/`](usage/) 目录:
@@ -130,6 +133,8 @@ data = client.download_file(session_id, "/tmp/image.png")
130
133
  | [`09_long_running.py`](usage/09_long_running.py) | 长任务:进度回报 + 异常安全 | `exec_python`, `exec_shell` |
131
134
  | [`10_concurrent_sessions.py`](usage/10_concurrent_sessions.py) | 并发:`ThreadPoolExecutor` + 上限约束 | 多会话并行 |
132
135
  | [`11_error_handling.py`](usage/11_error_handling.py) | 错误全景:鉴权 / 会话 / 黑名单 / 网络 | 全 API 错误路径 |
136
+ | [`12_async_client.py`](usage/12_async_client.py) | 异步客户端:完整异步操作示例 | 全 API 异步版本 |
137
+ | [`11_error_handling.py`](usage/11_error_handling.py) | 错误全景:鉴权 / 会话 / 黑名单 / 网络 | 全 API 错误路径 |
133
138
 
134
139
  运行样例:
135
140
 
@@ -160,6 +165,8 @@ for f in 0[1-9]_*.py 1[0-1]_*.py; do python "$f"; done
160
165
 
161
166
  `CommandResult` 字段:`exit_code`, `stdout`, `stderr`, `success`(property), `combined_output`(property)。
162
167
 
168
+ > 💡 **异步版本**:`AsyncSandboxClient` 提供完全相同的 API,所有方法均为 `async`,支持 `await` 调用和 `async with` 上下文管理器。
169
+
163
170
  ## ⚙️ 环境变量
164
171
 
165
172
  为避免硬编码,样例通过环境变量注入连接信息:
@@ -189,6 +196,8 @@ except OSError as e:
189
196
 
190
197
  ## 🧵 并发使用
191
198
 
199
+ ### 同步并发(ThreadPoolExecutor)
200
+
192
201
  ```python
193
202
  from concurrent.futures import ThreadPoolExecutor
194
203
  # 每个任务使用独立的 client + session(线程安全)
@@ -198,6 +207,33 @@ with ThreadPoolExecutor(max_workers=5) as pool:
198
207
  print(f.result())
199
208
  ```
200
209
 
210
+ ### 异步并发(AsyncSandboxClient)
211
+
212
+ ```python
213
+ import asyncio
214
+ from python_sandbox_sdk import AsyncSandboxClient
215
+
216
+ async def run_task(task_id):
217
+ async with AsyncSandboxClient("http://localhost:8080", "sandbox-secret-key") as client:
218
+ session_id = await client.create_session()
219
+ try:
220
+ result = await client.exec_python(
221
+ session_id, f"print('Task {task_id} done')"
222
+ )
223
+ return result.stdout
224
+ finally:
225
+ await client.delete_session(session_id)
226
+
227
+ async def main():
228
+ # 并发执行 5 个任务
229
+ tasks = [run_task(i) for i in range(5)]
230
+ results = await asyncio.gather(*tasks)
231
+ for r in results:
232
+ print(r)
233
+
234
+ asyncio.run(main())
235
+ ```
236
+
201
237
  > ⚠️ 并发数受后端 `sandbox.max-containers`(默认 10)约束。
202
238
 
203
239
  ## 📦 打包发布
@@ -26,7 +26,7 @@ def _read_readme() -> str:
26
26
 
27
27
  setup(
28
28
  name="python-sandbox-sdk",
29
- version="1.0.0",
29
+ version="1.0.3",
30
30
  description="Python SDK for Python Sandbox API - Run Python code, shell commands, and manage packages in Docker containers",
31
31
  long_description=_read_readme(),
32
32
  long_description_content_type="text/markdown",