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