dify-sandbox 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dify_sandbox/__init__.py +22 -0
- dify_sandbox/client.py +410 -0
- dify_sandbox/models.py +106 -0
- dify_sandbox-1.0.0.dist-info/METADATA +352 -0
- dify_sandbox-1.0.0.dist-info/RECORD +7 -0
- dify_sandbox-1.0.0.dist-info/WHEEL +5 -0
- dify_sandbox-1.0.0.dist-info/top_level.txt +1 -0
dify_sandbox/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dify Sandbox Python SDK
|
|
3
|
+
|
|
4
|
+
A Python SDK for interacting with the Dify Sandbox API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client import DifySandboxClient
|
|
8
|
+
from .models import (
|
|
9
|
+
DifySandboxResponse,
|
|
10
|
+
RunCodeResponse,
|
|
11
|
+
UploadFileResponse,
|
|
12
|
+
DependencyInfo,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0"
|
|
16
|
+
__all__ = [
|
|
17
|
+
"DifySandboxClient",
|
|
18
|
+
"DifySandboxResponse",
|
|
19
|
+
"RunCodeResponse",
|
|
20
|
+
"UploadFileResponse",
|
|
21
|
+
"DependencyInfo",
|
|
22
|
+
]
|
dify_sandbox/client.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dify Sandbox SDK - Main Client
|
|
3
|
+
|
|
4
|
+
This module provides the main client for interacting with the Dify Sandbox API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional, Union, BinaryIO
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .models import (
|
|
12
|
+
DifySandboxResponse,
|
|
13
|
+
RunCodeResponse,
|
|
14
|
+
RunCommandResponse,
|
|
15
|
+
UploadFileResponse,
|
|
16
|
+
DependencyInfo,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DifySandboxClient:
|
|
21
|
+
"""
|
|
22
|
+
Client for interacting with Dify Sandbox API.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
client = DifySandboxClient(
|
|
26
|
+
base_url="http://localhost:8194",
|
|
27
|
+
api_key="dify-sandbox"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Run Python code
|
|
31
|
+
result = client.run_python("print('Hello, World!')")
|
|
32
|
+
print(result.stdout)
|
|
33
|
+
|
|
34
|
+
# Upload a file
|
|
35
|
+
with open("test.txt", "rb") as f:
|
|
36
|
+
upload_result = client.upload_file(f, "test.txt")
|
|
37
|
+
|
|
38
|
+
# Download a file
|
|
39
|
+
content = client.download_file(upload_result.data.filename)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
base_url: str = "http://localhost:8194",
|
|
45
|
+
api_key: str = "dify-sandbox",
|
|
46
|
+
timeout: int = 30,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Initialize the Dify Sandbox client.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
base_url: Base URL of the Dify Sandbox server
|
|
53
|
+
api_key: API key for authentication (X-Api-Key header)
|
|
54
|
+
timeout: Request timeout in seconds
|
|
55
|
+
"""
|
|
56
|
+
self.base_url = base_url.rstrip("/")
|
|
57
|
+
self.api_key = api_key
|
|
58
|
+
self.timeout = timeout
|
|
59
|
+
self.session = requests.Session()
|
|
60
|
+
self.session.headers.update({"X-Api-Key": self.api_key})
|
|
61
|
+
|
|
62
|
+
def _request(
|
|
63
|
+
self,
|
|
64
|
+
method: str,
|
|
65
|
+
path: str,
|
|
66
|
+
json: Optional[dict] = None,
|
|
67
|
+
files: Optional[dict] = None,
|
|
68
|
+
stream: bool = False,
|
|
69
|
+
) -> requests.Response:
|
|
70
|
+
"""
|
|
71
|
+
Make an HTTP request to the API.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
method: HTTP method (GET, POST, etc.)
|
|
75
|
+
path: API path (will be appended to base_url)
|
|
76
|
+
json: JSON data to send
|
|
77
|
+
files: Files to upload
|
|
78
|
+
stream: Whether to stream the response
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
requests.Response object
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
requests.HTTPError: If the request fails
|
|
85
|
+
"""
|
|
86
|
+
url = f"{self.base_url}{path}"
|
|
87
|
+
response = self.session.request(
|
|
88
|
+
method=method,
|
|
89
|
+
url=url,
|
|
90
|
+
json=json,
|
|
91
|
+
files=files,
|
|
92
|
+
timeout=self.timeout,
|
|
93
|
+
stream=stream,
|
|
94
|
+
)
|
|
95
|
+
response.raise_for_status()
|
|
96
|
+
return response
|
|
97
|
+
|
|
98
|
+
def health_check(self) -> bool:
|
|
99
|
+
"""
|
|
100
|
+
Check if the sandbox server is healthy.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if server is healthy, False otherwise
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
response = self._request("GET", "/health")
|
|
107
|
+
return response.status_code == 200
|
|
108
|
+
except requests.RequestException:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def run_python(
|
|
112
|
+
self,
|
|
113
|
+
code: str,
|
|
114
|
+
preload: str = "",
|
|
115
|
+
enable_network: bool = False,
|
|
116
|
+
) -> RunCodeResponse:
|
|
117
|
+
"""
|
|
118
|
+
Run Python code in the sandbox.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
code: Python code to execute
|
|
122
|
+
preload: Optional preload code to run before main code
|
|
123
|
+
enable_network: Whether to enable network access
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
RunCodeResponse with execution results
|
|
127
|
+
"""
|
|
128
|
+
return self._run_code("python3", code, preload, enable_network)
|
|
129
|
+
|
|
130
|
+
def run_nodejs(
|
|
131
|
+
self,
|
|
132
|
+
code: str,
|
|
133
|
+
preload: str = "",
|
|
134
|
+
enable_network: bool = False,
|
|
135
|
+
) -> RunCodeResponse:
|
|
136
|
+
"""
|
|
137
|
+
Run Node.js code in the sandbox.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
code: Node.js code to execute
|
|
141
|
+
preload: Optional preload code to run before main code
|
|
142
|
+
enable_network: Whether to enable network access
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
RunCodeResponse with execution results
|
|
146
|
+
"""
|
|
147
|
+
return self._run_code("nodejs", code, preload, enable_network)
|
|
148
|
+
|
|
149
|
+
def _run_code(
|
|
150
|
+
self,
|
|
151
|
+
language: str,
|
|
152
|
+
code: str,
|
|
153
|
+
preload: str,
|
|
154
|
+
enable_network: bool,
|
|
155
|
+
) -> RunCodeResponse:
|
|
156
|
+
"""
|
|
157
|
+
Internal method to run code in the sandbox.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
language: Programming language ("python3" or "nodejs")
|
|
161
|
+
code: Code to execute
|
|
162
|
+
preload: Preload code
|
|
163
|
+
enable_network: Enable network access
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
RunCodeResponse with execution results
|
|
167
|
+
"""
|
|
168
|
+
payload = {
|
|
169
|
+
"language": language,
|
|
170
|
+
"code": code,
|
|
171
|
+
"preload": preload,
|
|
172
|
+
"enable_network": enable_network,
|
|
173
|
+
}
|
|
174
|
+
response = self._request("POST", "/v1/sandbox/run", json=payload)
|
|
175
|
+
result = DifySandboxResponse.from_dict(response.json())
|
|
176
|
+
|
|
177
|
+
if not result.is_success:
|
|
178
|
+
raise Exception(f"API error: {result.message}")
|
|
179
|
+
|
|
180
|
+
return RunCodeResponse.from_dict(result.data)
|
|
181
|
+
|
|
182
|
+
def get_dependencies(self, language: str = "python3") -> DependencyInfo:
|
|
183
|
+
"""
|
|
184
|
+
Get list of installed dependencies.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
language: Programming language ("python3" or "nodejs")
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
DependencyInfo with list of dependencies
|
|
191
|
+
"""
|
|
192
|
+
response = self._request(
|
|
193
|
+
"GET", "/v1/sandbox/dependencies", json={"language": language}
|
|
194
|
+
)
|
|
195
|
+
result = DifySandboxResponse.from_dict(response.json())
|
|
196
|
+
|
|
197
|
+
if not result.is_success:
|
|
198
|
+
raise Exception(f"API error: {result.message}")
|
|
199
|
+
|
|
200
|
+
return DependencyInfo.from_dict(result.data)
|
|
201
|
+
|
|
202
|
+
def update_dependencies(self, language: str = "python3") -> DifySandboxResponse:
|
|
203
|
+
"""
|
|
204
|
+
Update dependencies for the specified language.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
language: Programming language ("python3" or "nodejs")
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
DifySandboxResponse with update result
|
|
211
|
+
"""
|
|
212
|
+
response = self._request(
|
|
213
|
+
"POST", "/v1/sandbox/dependencies/update", json={"language": language}
|
|
214
|
+
)
|
|
215
|
+
result = DifySandboxResponse.from_dict(response.json())
|
|
216
|
+
|
|
217
|
+
if not result.is_success:
|
|
218
|
+
raise Exception(f"API error: {result.message}")
|
|
219
|
+
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
def refresh_dependencies(self, language: str = "python3") -> DifySandboxResponse:
|
|
223
|
+
"""
|
|
224
|
+
Refresh dependencies for the specified language.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
language: Programming language ("python3" or "nodejs")
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
DifySandboxResponse with refresh result
|
|
231
|
+
"""
|
|
232
|
+
response = self._request(
|
|
233
|
+
"GET", "/v1/sandbox/dependencies/refresh", json={"language": language}
|
|
234
|
+
)
|
|
235
|
+
result = DifySandboxResponse.from_dict(response.json())
|
|
236
|
+
|
|
237
|
+
if not result.is_success:
|
|
238
|
+
raise Exception(f"API error: {result.message}")
|
|
239
|
+
|
|
240
|
+
return result
|
|
241
|
+
|
|
242
|
+
def upload_file(
|
|
243
|
+
self,
|
|
244
|
+
file: Union[BinaryIO, str],
|
|
245
|
+
filename: Optional[str] = None,
|
|
246
|
+
) -> UploadFileResponse:
|
|
247
|
+
"""
|
|
248
|
+
Upload a file to the sandbox.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
file: File object or file path to upload
|
|
252
|
+
filename: Optional filename (required if file is a file object)
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
UploadFileResponse with uploaded file info
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
# Upload from file path
|
|
259
|
+
result = client.upload_file("/path/to/file.txt")
|
|
260
|
+
|
|
261
|
+
# Upload from file object
|
|
262
|
+
with open("file.txt", "rb") as f:
|
|
263
|
+
result = client.upload_file(f, "file.txt")
|
|
264
|
+
"""
|
|
265
|
+
if isinstance(file, str):
|
|
266
|
+
# file is a path
|
|
267
|
+
if not os.path.exists(file):
|
|
268
|
+
raise FileNotFoundError(f"File not found: {file}")
|
|
269
|
+
|
|
270
|
+
if filename is None:
|
|
271
|
+
filename = os.path.basename(file)
|
|
272
|
+
|
|
273
|
+
with open(file, "rb") as f:
|
|
274
|
+
return self._upload_file_object(f, filename)
|
|
275
|
+
else:
|
|
276
|
+
# file is a file object
|
|
277
|
+
if filename is None:
|
|
278
|
+
raise ValueError("filename is required when file is a file object")
|
|
279
|
+
return self._upload_file_object(file, filename)
|
|
280
|
+
|
|
281
|
+
def _upload_file_object(
|
|
282
|
+
self,
|
|
283
|
+
file: BinaryIO,
|
|
284
|
+
filename: str,
|
|
285
|
+
) -> UploadFileResponse:
|
|
286
|
+
"""
|
|
287
|
+
Internal method to upload a file object.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
file: File object to upload
|
|
291
|
+
filename: Name of the file
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
UploadFileResponse with uploaded file info
|
|
295
|
+
"""
|
|
296
|
+
files = {"file": (filename, file)}
|
|
297
|
+
response = self._request("POST", "/v1/sandbox/file/upload", files=files)
|
|
298
|
+
result = DifySandboxResponse.from_dict(response.json())
|
|
299
|
+
|
|
300
|
+
if not result.is_success:
|
|
301
|
+
raise Exception(f"API error: {result.message}")
|
|
302
|
+
|
|
303
|
+
return UploadFileResponse.from_dict(result.data)
|
|
304
|
+
|
|
305
|
+
def download_file(
|
|
306
|
+
self,
|
|
307
|
+
filename: str,
|
|
308
|
+
save_path: Optional[str] = None,
|
|
309
|
+
) -> Union[bytes, str]:
|
|
310
|
+
"""
|
|
311
|
+
Download a file from the sandbox.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
filename: Name of the file to download
|
|
315
|
+
save_path: Optional path to save the file. If not provided, returns bytes.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
File content as bytes if save_path is None, otherwise the save_path
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
# Download to bytes
|
|
322
|
+
content = client.download_file("uploaded_file.txt")
|
|
323
|
+
|
|
324
|
+
# Download to file
|
|
325
|
+
client.download_file("uploaded_file.txt", "downloaded.txt")
|
|
326
|
+
"""
|
|
327
|
+
payload = {"filename": filename}
|
|
328
|
+
response = self._request(
|
|
329
|
+
"POST", "/v1/sandbox/file/download", json=payload, stream=True
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if save_path:
|
|
333
|
+
with open(save_path, "wb") as f:
|
|
334
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
335
|
+
f.write(chunk)
|
|
336
|
+
return save_path
|
|
337
|
+
else:
|
|
338
|
+
return response.content
|
|
339
|
+
|
|
340
|
+
def delete_file(self, filename: str) -> DifySandboxResponse:
|
|
341
|
+
"""
|
|
342
|
+
Delete a file from the sandbox.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
filename: Name of the file to delete
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
DifySandboxResponse with deletion result
|
|
349
|
+
|
|
350
|
+
Example:
|
|
351
|
+
client.delete_file("uploaded_file.txt")
|
|
352
|
+
"""
|
|
353
|
+
payload = {"filename": filename}
|
|
354
|
+
response = self._request("POST", "/v1/sandbox/file/delete", json=payload)
|
|
355
|
+
result = DifySandboxResponse.from_dict(response.json())
|
|
356
|
+
|
|
357
|
+
if not result.is_success:
|
|
358
|
+
raise Exception(f"API error: {result.message}")
|
|
359
|
+
|
|
360
|
+
return result
|
|
361
|
+
|
|
362
|
+
def run_command(
|
|
363
|
+
self,
|
|
364
|
+
command: str,
|
|
365
|
+
args: Optional[list] = None,
|
|
366
|
+
work_dir: str = "",
|
|
367
|
+
timeout: int = 0,
|
|
368
|
+
enable_network: bool = False,
|
|
369
|
+
) -> RunCommandResponse:
|
|
370
|
+
"""
|
|
371
|
+
Run a sandbox-approved binary against a previously uploaded file.
|
|
372
|
+
|
|
373
|
+
The command basename is matched against the deny-list exposed by the
|
|
374
|
+
sandbox (shells, rm, sudo, package managers, ...). Any
|
|
375
|
+
argument that looks like a shell metacharacter is rejected so the
|
|
376
|
+
request cannot accidentally invoke a shell.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
command: Command basename to execute, e.g. "python3".
|
|
380
|
+
args: Arguments to pass to the command (each must be
|
|
381
|
+
shell-metachar free).
|
|
382
|
+
work_dir: Working directory, relative to the sandbox upload
|
|
383
|
+
directory. Empty means the upload directory itself.
|
|
384
|
+
timeout: Optional per-request timeout in seconds. 0 means
|
|
385
|
+
use the sandbox-wide worker timeout.
|
|
386
|
+
enable_network: Forwarded to the sandbox only when the sandbox
|
|
387
|
+
itself was started with network access enabled.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
RunCommandResponse with execution results.
|
|
391
|
+
|
|
392
|
+
Example:
|
|
393
|
+
client.upload_file("hello.py")
|
|
394
|
+
result = client.run_command("python3", ["hello.py"])
|
|
395
|
+
print(result.stdout)
|
|
396
|
+
"""
|
|
397
|
+
payload = {
|
|
398
|
+
"command": command,
|
|
399
|
+
"args": args or [],
|
|
400
|
+
"work_dir": work_dir,
|
|
401
|
+
"timeout": timeout,
|
|
402
|
+
"enable_network": enable_network,
|
|
403
|
+
}
|
|
404
|
+
response = self._request("POST", "/v1/sandbox/run/command", json=payload)
|
|
405
|
+
result = DifySandboxResponse.from_dict(response.json())
|
|
406
|
+
|
|
407
|
+
if not result.is_success:
|
|
408
|
+
raise Exception(f"API error: {result.message}")
|
|
409
|
+
|
|
410
|
+
return RunCommandResponse.from_dict(result.data)
|
dify_sandbox/models.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dify Sandbox SDK - Data Models
|
|
3
|
+
|
|
4
|
+
This module contains data models for the Dify Sandbox SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional, Any, Dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class DifySandboxResponse:
|
|
13
|
+
"""Base response from Dify Sandbox API"""
|
|
14
|
+
code: int
|
|
15
|
+
message: str
|
|
16
|
+
data: Optional[Any] = None
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_dict(cls, data: Dict[str, Any]) -> "DifySandboxResponse":
|
|
20
|
+
return cls(
|
|
21
|
+
code=data.get("code", -1),
|
|
22
|
+
message=data.get("message", ""),
|
|
23
|
+
data=data.get("data")
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def is_success(self) -> bool:
|
|
28
|
+
return self.code == 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class RunCodeResponse:
|
|
33
|
+
"""Response from code execution endpoint"""
|
|
34
|
+
stdout: str
|
|
35
|
+
stderr: str
|
|
36
|
+
error: str
|
|
37
|
+
exit_code: int
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_dict(cls, data: Dict[str, Any]) -> "RunCodeResponse":
|
|
41
|
+
return cls(
|
|
42
|
+
stdout=data.get("stdout", ""),
|
|
43
|
+
stderr=data.get("stderr", ""),
|
|
44
|
+
error=data.get("error", ""),
|
|
45
|
+
exit_code=data.get("exit_code", -1)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def is_success(self) -> bool:
|
|
50
|
+
return self.exit_code == 0 and not self.error
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class RunCommandResponse:
|
|
55
|
+
"""Response from the command execution endpoint.
|
|
56
|
+
|
|
57
|
+
Mirrors ``RunCodeResponse`` because both endpoints share the same
|
|
58
|
+
stdout/stderr/exit_code envelope. ``RunCommandResponse`` exists as a
|
|
59
|
+
separate type so callers can tell which endpoint produced the data
|
|
60
|
+
without inspecting the request URL.
|
|
61
|
+
"""
|
|
62
|
+
stdout: str
|
|
63
|
+
stderr: str
|
|
64
|
+
error: str
|
|
65
|
+
exit_code: int
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_dict(cls, data: Dict[str, Any]) -> "RunCommandResponse":
|
|
69
|
+
return cls(
|
|
70
|
+
stdout=data.get("stdout", ""),
|
|
71
|
+
stderr=data.get("stderr", ""),
|
|
72
|
+
error=data.get("error", ""),
|
|
73
|
+
exit_code=data.get("exit_code", -1),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def is_success(self) -> bool:
|
|
78
|
+
return self.exit_code == 0 and not self.error
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class UploadFileResponse:
|
|
83
|
+
"""Response from file upload endpoint"""
|
|
84
|
+
filename: str
|
|
85
|
+
size: int
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_dict(cls, data: Dict[str, Any]) -> "UploadFileResponse":
|
|
89
|
+
return cls(
|
|
90
|
+
filename=data.get("filename", ""),
|
|
91
|
+
size=data.get("size", 0)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class DependencyInfo:
|
|
97
|
+
"""Information about installed dependencies"""
|
|
98
|
+
language: str
|
|
99
|
+
dependencies: list
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_dict(cls, data: Dict[str, Any]) -> "DependencyInfo":
|
|
103
|
+
return cls(
|
|
104
|
+
language=data.get("language", ""),
|
|
105
|
+
dependencies=data.get("dependencies", [])
|
|
106
|
+
)
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dify-sandbox
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for Dify Sandbox API
|
|
5
|
+
Home-page: https://github.com/langgenius/dify-sandbox
|
|
6
|
+
Author: Dify
|
|
7
|
+
Author-email: support@dify.ai
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Requires-Python: >=3.7
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: requests>=2.25.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
23
|
+
Requires-Dist: pytest-cov>=2.0; extra == "dev"
|
|
24
|
+
Requires-Dist: black>=21.0; extra == "dev"
|
|
25
|
+
Requires-Dist: mypy>=0.900; extra == "dev"
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: author-email
|
|
28
|
+
Dynamic: classifier
|
|
29
|
+
Dynamic: description
|
|
30
|
+
Dynamic: description-content-type
|
|
31
|
+
Dynamic: home-page
|
|
32
|
+
Dynamic: license
|
|
33
|
+
Dynamic: provides-extra
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: requires-python
|
|
36
|
+
Dynamic: summary
|
|
37
|
+
|
|
38
|
+
# Dify Sandbox Python SDK
|
|
39
|
+
|
|
40
|
+
Python SDK for interacting with the Dify Sandbox API.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install -e sdk/
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or install dependencies directly:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install requests
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from dify_sandbox import DifySandboxClient
|
|
58
|
+
|
|
59
|
+
# Initialize client
|
|
60
|
+
client = DifySandboxClient(
|
|
61
|
+
base_url="http://localhost:8194",
|
|
62
|
+
api_key="your-api-key"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Run Python code
|
|
66
|
+
result = client.run_python("print('Hello from sandbox!')")
|
|
67
|
+
print(result.stdout) # Output: Hello from sandbox!
|
|
68
|
+
|
|
69
|
+
# Run Node.js code
|
|
70
|
+
result = client.run_nodejs("console.log('Hello from Node.js!')")
|
|
71
|
+
print(result.stdout) # Output: Hello from Node.js!
|
|
72
|
+
|
|
73
|
+
# Upload a Python script and run it from the sandbox upload directory
|
|
74
|
+
with open("hello.py", "rb") as f:
|
|
75
|
+
uploaded = client.upload_file(f, filename="hello.py")
|
|
76
|
+
|
|
77
|
+
result = client.run_command(
|
|
78
|
+
command="python3",
|
|
79
|
+
args=[uploaded.filename],
|
|
80
|
+
timeout=30,
|
|
81
|
+
)
|
|
82
|
+
print(result.stdout)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Features
|
|
86
|
+
|
|
87
|
+
- **Code Execution**: Run Python and Node.js code in a secure sandbox
|
|
88
|
+
- **Command Execution**: Launch sandbox-approved binaries (e.g. `python3`,
|
|
89
|
+
`node`) against previously uploaded files via the deny-list enforced
|
|
90
|
+
`POST /v1/sandbox/run/command` endpoint
|
|
91
|
+
- **File Operations**: Upload and download files to/from the sandbox
|
|
92
|
+
- **Dependency Management**: View and manage sandbox dependencies
|
|
93
|
+
- **Health Check**: Monitor sandbox server status
|
|
94
|
+
|
|
95
|
+
## API Reference
|
|
96
|
+
|
|
97
|
+
### Client Initialization
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
client = DifySandboxClient(
|
|
101
|
+
base_url="http://localhost:8194", # Sandbox server URL
|
|
102
|
+
api_key="your-api-key", # API key for authentication
|
|
103
|
+
timeout=30 # Request timeout in seconds
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Code Execution
|
|
108
|
+
|
|
109
|
+
#### Run Python Code
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
result = client.run_python(
|
|
113
|
+
code="print('Hello, World!')",
|
|
114
|
+
preload="", # Optional: code to run before main code
|
|
115
|
+
enable_network=False # Optional: enable network access
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
print(result.stdout) # Standard output
|
|
119
|
+
print(result.stderr) # Standard error
|
|
120
|
+
print(result.exit_code) # Exit code (0 = success)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Run Node.js Code
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
result = client.run_nodejs(
|
|
127
|
+
code="console.log('Hello, World!')",
|
|
128
|
+
preload="",
|
|
129
|
+
enable_network=False
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
print(result.stdout)
|
|
133
|
+
print(result.stderr)
|
|
134
|
+
print(result.exit_code)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Command Execution
|
|
138
|
+
|
|
139
|
+
Run a sandbox-approved binary against a file that was previously uploaded
|
|
140
|
+
to the sandbox via `upload_file`. The endpoint enforces a deny-list of
|
|
141
|
+
dangerous commands (shells, `rm`, `sudo`, package managers, …) and rejects
|
|
142
|
+
any argument containing shell metacharacters so the request can never
|
|
143
|
+
accidentally spawn a shell.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# 1. Upload the script you want to execute
|
|
147
|
+
with open("hello.py", "rb") as f:
|
|
148
|
+
uploaded = client.upload_file(f, filename="hello.py")
|
|
149
|
+
|
|
150
|
+
# 2. Invoke python3 with the uploaded file as an argument
|
|
151
|
+
result = client.run_command(
|
|
152
|
+
command="python3", # Command basename (resolved via PATH)
|
|
153
|
+
args=[uploaded.filename], # Arguments; cannot contain shell metachars
|
|
154
|
+
work_dir="", # "" or "." → sandbox upload_dir; or a relative subdir
|
|
155
|
+
timeout=30, # 0 → use the sandbox worker timeout
|
|
156
|
+
enable_network=False, # Must respect global enable_network setting
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
print(result.stdout)
|
|
160
|
+
print(result.stderr)
|
|
161
|
+
print(result.exit_code)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
#### `work_dir` rules
|
|
165
|
+
|
|
166
|
+
| Input | Result |
|
|
167
|
+
|----------------------|-------------------------------------------------------|
|
|
168
|
+
| `""` or `"."` | Resolved to the sandbox `upload_dir` itself |
|
|
169
|
+
| `"scripts"` | Resolved to `<upload_dir>/scripts` |
|
|
170
|
+
| `<upload_dir>` (absolute) | Resolved to `upload_dir` itself |
|
|
171
|
+
| `/etc`, `../etc`, … | Rejected with `work_dir is invalid: ...` |
|
|
172
|
+
|
|
173
|
+
#### Deny-list
|
|
174
|
+
|
|
175
|
+
The deny-list is the union of a built-in default (shells, `rm`, `sudo`,
|
|
176
|
+
`apt`, `pip3`, `npm`, …) and the operator-configured
|
|
177
|
+
`blocked_commands` list. User configuration can only add entries — never
|
|
178
|
+
remove them. Commands that match the deny-list, or arguments containing
|
|
179
|
+
shell metacharacters (`|`, `&`, `;`, `<`, `>`, `` ` ``, `$`, `*`, `?`,
|
|
180
|
+
`{`, `}`, `~`, `!`, `#`, quotes, …) are rejected with a 400 before any
|
|
181
|
+
process is spawned.
|
|
182
|
+
|
|
183
|
+
### File Operations
|
|
184
|
+
|
|
185
|
+
#### Upload File
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
# Upload from file path
|
|
189
|
+
result = client.upload_file("/path/to/file.txt")
|
|
190
|
+
print(result.filename) # Uploaded filename in sandbox
|
|
191
|
+
print(result.size) # File size in bytes
|
|
192
|
+
|
|
193
|
+
# Upload from file object
|
|
194
|
+
with open("local_file.txt", "rb") as f:
|
|
195
|
+
result = client.upload_file(f, filename="custom_name.txt")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### Download File
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
# Download to memory
|
|
202
|
+
content = client.download_file("sandbox_file.txt")
|
|
203
|
+
|
|
204
|
+
# Download to local file
|
|
205
|
+
client.download_file("sandbox_file.txt", save_path="local_copy.txt")
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Dependency Management
|
|
209
|
+
|
|
210
|
+
#### Get Dependencies
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
deps = client.get_dependencies(language="python3")
|
|
214
|
+
print(deps.dependencies) # List of installed packages
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### Update Dependencies
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
response = client.update_dependencies(language="python3")
|
|
221
|
+
print(response.message)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### Refresh Dependencies
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
response = client.refresh_dependencies(language="python3")
|
|
228
|
+
print(response.message)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Health Check
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
if client.health_check():
|
|
235
|
+
print("Sandbox is healthy")
|
|
236
|
+
else:
|
|
237
|
+
print("Sandbox is not responding")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Data Models
|
|
241
|
+
|
|
242
|
+
### RunCodeResponse
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
@dataclass
|
|
246
|
+
class RunCodeResponse:
|
|
247
|
+
stdout: str # Standard output from code execution
|
|
248
|
+
stderr: str # Standard error from code execution
|
|
249
|
+
exit_code: int # Exit code (0 = success)
|
|
250
|
+
error: str # Sandbox-side error message (empty on success)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### RunCommandResponse
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
@dataclass
|
|
257
|
+
class RunCommandResponse:
|
|
258
|
+
stdout: str # Standard output from the executed command
|
|
259
|
+
stderr: str # Standard error from the executed command
|
|
260
|
+
exit_code: int # Exit code (0 = success)
|
|
261
|
+
error: str # Sandbox-side error message (empty on success)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### UploadFileResponse
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
@dataclass
|
|
268
|
+
class UploadFileResponse:
|
|
269
|
+
filename: str # Filename in sandbox
|
|
270
|
+
size: int # File size in bytes
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### DependencyInfo
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
@dataclass
|
|
277
|
+
class DependencyInfo:
|
|
278
|
+
language: str # Language (python3/nodejs)
|
|
279
|
+
dependencies: list # List of dependencies
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### DifySandboxResponse
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
@dataclass
|
|
286
|
+
class DifySandboxResponse:
|
|
287
|
+
code: int # Response code (0 = success)
|
|
288
|
+
message: str # Response message
|
|
289
|
+
data: Any # Response data
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## End-to-end Example: Upload a Script, Then Run It
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from dify_sandbox import DifySandboxClient
|
|
296
|
+
|
|
297
|
+
client = DifySandboxClient(base_url="http://localhost:8194", api_key="dify-sandbox")
|
|
298
|
+
|
|
299
|
+
# Local script we want to run inside the sandbox
|
|
300
|
+
script = b"""
|
|
301
|
+
import sys
|
|
302
|
+
print("hello from the sandbox!")
|
|
303
|
+
print("args:", sys.argv[1:])
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
# 1. Upload the script — the server stores it in upload_dir and returns the
|
|
307
|
+
# filename it used (a UUID is appended to avoid collisions).
|
|
308
|
+
with open("hello.py", "wb") as f:
|
|
309
|
+
f.write(script)
|
|
310
|
+
|
|
311
|
+
uploaded = client.upload_file("hello.py")
|
|
312
|
+
print("uploaded as:", uploaded.filename)
|
|
313
|
+
|
|
314
|
+
# 2. Run python3 with the uploaded file as its argument.
|
|
315
|
+
result = client.run_command(
|
|
316
|
+
command="python3",
|
|
317
|
+
args=[uploaded.filename],
|
|
318
|
+
timeout=10,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
assert result.exit_code == 0, result.stderr
|
|
322
|
+
print(result.stdout)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Examples
|
|
326
|
+
|
|
327
|
+
See the `examples/` directory for complete usage examples:
|
|
328
|
+
|
|
329
|
+
- `basic_usage.py` - Basic code execution examples
|
|
330
|
+
- `file_operations.py` - File upload and download examples
|
|
331
|
+
- `dependency_management.py` - Dependency management examples
|
|
332
|
+
- `command_execution.py` - Upload a script and run it through the deny-list-enforced `run_command` endpoint
|
|
333
|
+
|
|
334
|
+
## Error Handling
|
|
335
|
+
|
|
336
|
+
The SDK raises exceptions for API errors:
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
try:
|
|
340
|
+
result = client.run_python("invalid code")
|
|
341
|
+
except Exception as e:
|
|
342
|
+
print(f"Error: {e}")
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`run_command` raises the same kind of exception when the deny-list rejects
|
|
346
|
+
the command, when an argument contains shell metacharacters, or when the
|
|
347
|
+
work directory is invalid — the exception message is the human-readable
|
|
348
|
+
reason returned by the sandbox.
|
|
349
|
+
|
|
350
|
+
## License
|
|
351
|
+
|
|
352
|
+
MIT License
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
dify_sandbox/__init__.py,sha256=cT-RoMHZVYVSy72gMq9bDzEqM2_qmgAum1vJnxy3-1U,402
|
|
2
|
+
dify_sandbox/client.py,sha256=UiLpaVCV7Lg02xE0xhPBYLXJXv1fQZ7y-RPqcknsfVY,12753
|
|
3
|
+
dify_sandbox/models.py,sha256=Mb-Oi-mkx6El8R_6DF7VD2OiPI8iSvgNA93HSmb7LqA,2658
|
|
4
|
+
dify_sandbox-1.0.0.dist-info/METADATA,sha256=XQNAIG51UZCKbZWc05PIyCnW5X-lji8AOcKiemgbqNM,9415
|
|
5
|
+
dify_sandbox-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
dify_sandbox-1.0.0.dist-info/top_level.txt,sha256=Gkc2twiAhYZSCB2n2dNa68hdwIuzuojtc3DTvMgKB-c,13
|
|
7
|
+
dify_sandbox-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dify_sandbox
|