python-sandbox-sdk 1.0.2__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.2
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>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-sandbox-sdk"
7
- version = "1.0.2"
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"
@@ -85,6 +85,19 @@ class AsyncSandboxClient:
85
85
  data = await resp.json()
86
86
  return data["sessionId"]
87
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
+
88
101
  async def delete_session(self, session_id: str) -> None:
89
102
  """
90
103
  删除会话并清理容器
@@ -167,9 +180,7 @@ class AsyncSandboxClient:
167
180
  data["exitCode"], data.get("stdout", ""), data.get("stderr", "")
168
181
  )
169
182
 
170
- async def pip_uninstall(
171
- self, session_id: str, package_name: str
172
- ) -> CommandResult:
183
+ async def pip_uninstall(self, session_id: str, package_name: str) -> CommandResult:
173
184
  """
174
185
  卸载 Python 包
175
186
 
@@ -268,7 +279,7 @@ class AsyncSandboxClient:
268
279
  import os
269
280
 
270
281
  url = f"{self.base_url}/api/sandbox/file/upload"
271
-
282
+
272
283
  # 使用 BytesIO 避免临时文件和阻塞 I/O
273
284
  file_obj = io.BytesIO(data)
274
285
  form = aiohttp.FormData()
@@ -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.2
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>
@@ -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",