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.
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/PKG-INFO +39 -3
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/README.md +38 -2
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/pyproject.toml +1 -1
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk/__init__.py +1 -1
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk/async_client.py +36 -30
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk/client.py +92 -79
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk.egg-info/PKG-INFO +39 -3
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/setup.py +1 -1
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk/dto.py +0 -0
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk.egg-info/SOURCES.txt +0 -0
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk.egg-info/dependency_links.txt +0 -0
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk.egg-info/requires.txt +0 -0
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk.egg-info/top_level.txt +0 -0
- {python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-sandbox-sdk
|
|
3
|
-
Version: 1.0.
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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.
|
|
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,8 +15,7 @@
|
|
|
15
15
|
Hello!
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
import
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
255
|
+
with os.fdopen(fd, "wb") as f:
|
|
245
256
|
f.write(data)
|
|
246
|
-
|
|
247
|
-
with open(tmp_path,
|
|
248
|
-
files = {
|
|
249
|
-
data_params = {
|
|
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(
|
|
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.
|
|
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
|
-
-
|
|
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.
|
|
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",
|
|
File without changes
|
{python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk.egg-info/requires.txt
RENAMED
|
File without changes
|
{python_sandbox_sdk-1.0.1 → python_sandbox_sdk-1.0.3}/python_sandbox_sdk.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|