code-sandboxes 0.0.2__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.
- code_sandboxes/__init__.py +141 -0
- code_sandboxes/__version__.py +6 -0
- code_sandboxes/base.py +572 -0
- code_sandboxes/commands.py +452 -0
- code_sandboxes/exceptions.py +101 -0
- code_sandboxes/filesystem.py +500 -0
- code_sandboxes/local/__init__.py +9 -0
- code_sandboxes/local/eval_sandbox.py +309 -0
- code_sandboxes/models.py +392 -0
- code_sandboxes/remote/__init__.py +9 -0
- code_sandboxes/remote/datalayer_sandbox.py +627 -0
- code_sandboxes-0.0.2.dist-info/METADATA +299 -0
- code_sandboxes-0.0.2.dist-info/RECORD +15 -0
- code_sandboxes-0.0.2.dist-info/WHEEL +4 -0
- code_sandboxes-0.0.2.dist-info/licenses/LICENSE +29 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
2
|
+
#
|
|
3
|
+
# BSD 3-Clause License
|
|
4
|
+
|
|
5
|
+
"""Filesystem operations for sandboxes.
|
|
6
|
+
|
|
7
|
+
Inspired by E2B Filesystem and Modal file operations APIs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Iterator, Optional, Union
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .base import Sandbox
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FileType(str, Enum):
|
|
21
|
+
"""Type of file in the sandbox filesystem."""
|
|
22
|
+
|
|
23
|
+
FILE = "file"
|
|
24
|
+
DIRECTORY = "directory"
|
|
25
|
+
SYMLINK = "symlink"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FileWatchEventType(str, Enum):
|
|
29
|
+
"""Types of file watch events."""
|
|
30
|
+
|
|
31
|
+
CREATE = "create"
|
|
32
|
+
MODIFY = "modify"
|
|
33
|
+
DELETE = "delete"
|
|
34
|
+
RENAME = "rename"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class FileInfo:
|
|
39
|
+
"""Information about a file or directory.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
name: Name of the file or directory.
|
|
43
|
+
path: Full path to the file.
|
|
44
|
+
type: Type of the entry (file, directory, symlink).
|
|
45
|
+
size: Size in bytes (for files).
|
|
46
|
+
modified: Last modification time (Unix timestamp).
|
|
47
|
+
permissions: File permissions.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
name: str
|
|
51
|
+
path: str
|
|
52
|
+
type: FileType = FileType.FILE
|
|
53
|
+
size: int = 0
|
|
54
|
+
modified: float = 0.0
|
|
55
|
+
permissions: str = ""
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_file(self) -> bool:
|
|
59
|
+
"""Check if this is a file."""
|
|
60
|
+
return self.type == FileType.FILE
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_directory(self) -> bool:
|
|
64
|
+
"""Check if this is a directory."""
|
|
65
|
+
return self.type == FileType.DIRECTORY
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
return f"FileInfo(name={self.name!r}, type={self.type.value}, size={self.size})"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class FileWatchEvent:
|
|
73
|
+
"""Event from watching a file or directory.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
event_type: The type of event.
|
|
77
|
+
path: Path to the affected file.
|
|
78
|
+
timestamp: When the event occurred.
|
|
79
|
+
new_path: For rename events, the new path.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
event_type: FileWatchEventType
|
|
83
|
+
path: str
|
|
84
|
+
timestamp: float = 0.0
|
|
85
|
+
new_path: Optional[str] = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SandboxFilesystem:
|
|
89
|
+
"""Filesystem operations for a sandbox.
|
|
90
|
+
|
|
91
|
+
Provides file and directory operations similar to E2B and Modal.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
with Sandbox.create() as sandbox:
|
|
95
|
+
# Write a file
|
|
96
|
+
sandbox.files.write("/data/test.txt", "Hello World")
|
|
97
|
+
|
|
98
|
+
# Read a file
|
|
99
|
+
content = sandbox.files.read("/data/test.txt")
|
|
100
|
+
|
|
101
|
+
# List directory
|
|
102
|
+
for f in sandbox.files.list("/data"):
|
|
103
|
+
print(f.name, f.size)
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, sandbox: "Sandbox"):
|
|
107
|
+
"""Initialize filesystem operations for a sandbox.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
sandbox: The sandbox instance.
|
|
111
|
+
"""
|
|
112
|
+
self._sandbox = sandbox
|
|
113
|
+
|
|
114
|
+
def read(self, path: str) -> str:
|
|
115
|
+
"""Read a text file from the sandbox.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
path: Path to the file.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
File contents as a string.
|
|
122
|
+
"""
|
|
123
|
+
execution = self._sandbox.run_code(f"""
|
|
124
|
+
with open({path!r}, 'r') as f:
|
|
125
|
+
__file_content__ = f.read()
|
|
126
|
+
""")
|
|
127
|
+
if execution.error:
|
|
128
|
+
raise FileNotFoundError(f"Could not read file: {path}")
|
|
129
|
+
return self._sandbox.get_variable("__file_content__")
|
|
130
|
+
|
|
131
|
+
def read_bytes(self, path: str) -> bytes:
|
|
132
|
+
"""Read a binary file from the sandbox.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
path: Path to the file.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
File contents as bytes.
|
|
139
|
+
"""
|
|
140
|
+
return self._sandbox._read_file(path)
|
|
141
|
+
|
|
142
|
+
def write(self, path: str, content: str, make_dirs: bool = True) -> None:
|
|
143
|
+
"""Write a text file to the sandbox.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
path: Path to the file.
|
|
147
|
+
content: Content to write.
|
|
148
|
+
make_dirs: Whether to create parent directories.
|
|
149
|
+
"""
|
|
150
|
+
if make_dirs:
|
|
151
|
+
dir_path = str(Path(path).parent)
|
|
152
|
+
self._sandbox.run_code(f"""
|
|
153
|
+
import os
|
|
154
|
+
os.makedirs({dir_path!r}, exist_ok=True)
|
|
155
|
+
""")
|
|
156
|
+
|
|
157
|
+
self._sandbox.run_code(f"""
|
|
158
|
+
with open({path!r}, 'w') as f:
|
|
159
|
+
f.write({content!r})
|
|
160
|
+
""")
|
|
161
|
+
|
|
162
|
+
def write_bytes(self, path: str, content: bytes, make_dirs: bool = True) -> None:
|
|
163
|
+
"""Write a binary file to the sandbox.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
path: Path to the file.
|
|
167
|
+
content: Content to write.
|
|
168
|
+
make_dirs: Whether to create parent directories.
|
|
169
|
+
"""
|
|
170
|
+
if make_dirs:
|
|
171
|
+
dir_path = str(Path(path).parent)
|
|
172
|
+
self._sandbox.run_code(f"""
|
|
173
|
+
import os
|
|
174
|
+
os.makedirs({dir_path!r}, exist_ok=True)
|
|
175
|
+
""")
|
|
176
|
+
|
|
177
|
+
self._sandbox._write_file(path, content)
|
|
178
|
+
|
|
179
|
+
def list(self, path: str = "/") -> list[FileInfo]:
|
|
180
|
+
"""List contents of a directory.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
path: Directory path to list.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
List of FileInfo objects.
|
|
187
|
+
"""
|
|
188
|
+
execution = self._sandbox.run_code(f"""
|
|
189
|
+
import os
|
|
190
|
+
import stat
|
|
191
|
+
|
|
192
|
+
__dir_contents__ = []
|
|
193
|
+
for name in os.listdir({path!r}):
|
|
194
|
+
full_path = os.path.join({path!r}, name)
|
|
195
|
+
try:
|
|
196
|
+
st = os.stat(full_path)
|
|
197
|
+
file_type = 'directory' if stat.S_ISDIR(st.st_mode) else 'file'
|
|
198
|
+
if stat.S_ISLNK(st.st_mode):
|
|
199
|
+
file_type = 'symlink'
|
|
200
|
+
__dir_contents__.append({{
|
|
201
|
+
'name': name,
|
|
202
|
+
'path': full_path,
|
|
203
|
+
'type': file_type,
|
|
204
|
+
'size': st.st_size,
|
|
205
|
+
'modified': st.st_mtime,
|
|
206
|
+
'permissions': oct(st.st_mode)[-3:],
|
|
207
|
+
}})
|
|
208
|
+
except OSError:
|
|
209
|
+
pass
|
|
210
|
+
""")
|
|
211
|
+
if execution.error:
|
|
212
|
+
raise FileNotFoundError(f"Could not list directory: {path}")
|
|
213
|
+
|
|
214
|
+
contents = self._sandbox.get_variable("__dir_contents__")
|
|
215
|
+
return [
|
|
216
|
+
FileInfo(
|
|
217
|
+
name=f["name"],
|
|
218
|
+
path=f["path"],
|
|
219
|
+
type=FileType(f["type"]),
|
|
220
|
+
size=f["size"],
|
|
221
|
+
modified=f["modified"],
|
|
222
|
+
permissions=f["permissions"],
|
|
223
|
+
)
|
|
224
|
+
for f in contents
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
def exists(self, path: str) -> bool:
|
|
228
|
+
"""Check if a file or directory exists.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
path: Path to check.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
True if the path exists.
|
|
235
|
+
"""
|
|
236
|
+
execution = self._sandbox.run_code(f"""
|
|
237
|
+
import os
|
|
238
|
+
__path_exists__ = os.path.exists({path!r})
|
|
239
|
+
""")
|
|
240
|
+
return self._sandbox.get_variable("__path_exists__")
|
|
241
|
+
|
|
242
|
+
def is_file(self, path: str) -> bool:
|
|
243
|
+
"""Check if path is a file.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
path: Path to check.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
True if the path is a file.
|
|
250
|
+
"""
|
|
251
|
+
execution = self._sandbox.run_code(f"""
|
|
252
|
+
import os
|
|
253
|
+
__is_file__ = os.path.isfile({path!r})
|
|
254
|
+
""")
|
|
255
|
+
return self._sandbox.get_variable("__is_file__")
|
|
256
|
+
|
|
257
|
+
def is_dir(self, path: str) -> bool:
|
|
258
|
+
"""Check if path is a directory.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
path: Path to check.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
True if the path is a directory.
|
|
265
|
+
"""
|
|
266
|
+
execution = self._sandbox.run_code(f"""
|
|
267
|
+
import os
|
|
268
|
+
__is_dir__ = os.path.isdir({path!r})
|
|
269
|
+
""")
|
|
270
|
+
return self._sandbox.get_variable("__is_dir__")
|
|
271
|
+
|
|
272
|
+
def mkdir(self, path: str, parents: bool = True) -> None:
|
|
273
|
+
"""Create a directory.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
path: Path to create.
|
|
277
|
+
parents: Whether to create parent directories.
|
|
278
|
+
"""
|
|
279
|
+
if parents:
|
|
280
|
+
self._sandbox.run_code(f"""
|
|
281
|
+
import os
|
|
282
|
+
os.makedirs({path!r}, exist_ok=True)
|
|
283
|
+
""")
|
|
284
|
+
else:
|
|
285
|
+
self._sandbox.run_code(f"""
|
|
286
|
+
import os
|
|
287
|
+
os.mkdir({path!r})
|
|
288
|
+
""")
|
|
289
|
+
|
|
290
|
+
def rm(self, path: str, recursive: bool = False) -> None:
|
|
291
|
+
"""Remove a file or directory.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
path: Path to remove.
|
|
295
|
+
recursive: Whether to remove directories recursively.
|
|
296
|
+
"""
|
|
297
|
+
if recursive:
|
|
298
|
+
self._sandbox.run_code(f"""
|
|
299
|
+
import shutil
|
|
300
|
+
shutil.rmtree({path!r}, ignore_errors=True)
|
|
301
|
+
""")
|
|
302
|
+
else:
|
|
303
|
+
self._sandbox.run_code(f"""
|
|
304
|
+
import os
|
|
305
|
+
if os.path.isdir({path!r}):
|
|
306
|
+
os.rmdir({path!r})
|
|
307
|
+
else:
|
|
308
|
+
os.remove({path!r})
|
|
309
|
+
""")
|
|
310
|
+
|
|
311
|
+
def copy(self, src: str, dst: str) -> None:
|
|
312
|
+
"""Copy a file or directory.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
src: Source path.
|
|
316
|
+
dst: Destination path.
|
|
317
|
+
"""
|
|
318
|
+
self._sandbox.run_code(f"""
|
|
319
|
+
import shutil
|
|
320
|
+
import os
|
|
321
|
+
if os.path.isdir({src!r}):
|
|
322
|
+
shutil.copytree({src!r}, {dst!r})
|
|
323
|
+
else:
|
|
324
|
+
shutil.copy2({src!r}, {dst!r})
|
|
325
|
+
""")
|
|
326
|
+
|
|
327
|
+
def move(self, src: str, dst: str) -> None:
|
|
328
|
+
"""Move a file or directory.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
src: Source path.
|
|
332
|
+
dst: Destination path.
|
|
333
|
+
"""
|
|
334
|
+
self._sandbox.run_code(f"""
|
|
335
|
+
import shutil
|
|
336
|
+
shutil.move({src!r}, {dst!r})
|
|
337
|
+
""")
|
|
338
|
+
|
|
339
|
+
def get_info(self, path: str) -> FileInfo:
|
|
340
|
+
"""Get information about a file or directory.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
path: Path to get info for.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
FileInfo object.
|
|
347
|
+
"""
|
|
348
|
+
execution = self._sandbox.run_code(f"""
|
|
349
|
+
import os
|
|
350
|
+
import stat
|
|
351
|
+
|
|
352
|
+
st = os.stat({path!r})
|
|
353
|
+
file_type = 'directory' if stat.S_ISDIR(st.st_mode) else 'file'
|
|
354
|
+
if stat.S_ISLNK(st.st_mode):
|
|
355
|
+
file_type = 'symlink'
|
|
356
|
+
__file_info__ = {{
|
|
357
|
+
'name': os.path.basename({path!r}),
|
|
358
|
+
'path': {path!r},
|
|
359
|
+
'type': file_type,
|
|
360
|
+
'size': st.st_size,
|
|
361
|
+
'modified': st.st_mtime,
|
|
362
|
+
'permissions': oct(st.st_mode)[-3:],
|
|
363
|
+
}}
|
|
364
|
+
""")
|
|
365
|
+
if execution.error:
|
|
366
|
+
raise FileNotFoundError(f"Could not get info for: {path}")
|
|
367
|
+
|
|
368
|
+
info = self._sandbox.get_variable("__file_info__")
|
|
369
|
+
return FileInfo(
|
|
370
|
+
name=info["name"],
|
|
371
|
+
path=info["path"],
|
|
372
|
+
type=FileType(info["type"]),
|
|
373
|
+
size=info["size"],
|
|
374
|
+
modified=info["modified"],
|
|
375
|
+
permissions=info["permissions"],
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def upload(self, local_path: str, remote_path: str) -> None:
|
|
379
|
+
"""Upload a file from local filesystem to sandbox.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
local_path: Local file path.
|
|
383
|
+
remote_path: Destination path in sandbox.
|
|
384
|
+
"""
|
|
385
|
+
self._sandbox.upload_file(local_path, remote_path)
|
|
386
|
+
|
|
387
|
+
def download(self, remote_path: str, local_path: str) -> None:
|
|
388
|
+
"""Download a file from sandbox to local filesystem.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
remote_path: Path in sandbox.
|
|
392
|
+
local_path: Local destination path.
|
|
393
|
+
"""
|
|
394
|
+
self._sandbox.download_file(remote_path, local_path)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
class SandboxFileHandle:
|
|
398
|
+
"""File handle for streaming file operations.
|
|
399
|
+
|
|
400
|
+
Similar to Modal's FileIO interface.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
def __init__(
|
|
404
|
+
self,
|
|
405
|
+
sandbox: "Sandbox",
|
|
406
|
+
path: str,
|
|
407
|
+
mode: str = "r",
|
|
408
|
+
):
|
|
409
|
+
"""Initialize a file handle.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
sandbox: The sandbox instance.
|
|
413
|
+
path: Path to the file.
|
|
414
|
+
mode: File mode ('r', 'w', 'rb', 'wb', 'a').
|
|
415
|
+
"""
|
|
416
|
+
self._sandbox = sandbox
|
|
417
|
+
self._path = path
|
|
418
|
+
self._mode = mode
|
|
419
|
+
self._closed = False
|
|
420
|
+
self._buffer = ""
|
|
421
|
+
|
|
422
|
+
# Create the file handle in the sandbox
|
|
423
|
+
self._handle_id = f"__fh_{id(self)}__"
|
|
424
|
+
self._sandbox.run_code(f"""
|
|
425
|
+
{self._handle_id} = open({path!r}, {mode!r})
|
|
426
|
+
""")
|
|
427
|
+
|
|
428
|
+
def write(self, content: Union[str, bytes]) -> int:
|
|
429
|
+
"""Write content to the file.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
content: Content to write.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Number of bytes/characters written.
|
|
436
|
+
"""
|
|
437
|
+
if self._closed:
|
|
438
|
+
raise ValueError("I/O operation on closed file")
|
|
439
|
+
|
|
440
|
+
self._sandbox.run_code(f"""
|
|
441
|
+
__write_count__ = {self._handle_id}.write({content!r})
|
|
442
|
+
""")
|
|
443
|
+
return self._sandbox.get_variable("__write_count__")
|
|
444
|
+
|
|
445
|
+
def read(self, size: int = -1) -> Union[str, bytes]:
|
|
446
|
+
"""Read from the file.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
size: Number of bytes/characters to read. -1 for all.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
File content.
|
|
453
|
+
"""
|
|
454
|
+
if self._closed:
|
|
455
|
+
raise ValueError("I/O operation on closed file")
|
|
456
|
+
|
|
457
|
+
self._sandbox.run_code(f"""
|
|
458
|
+
__read_content__ = {self._handle_id}.read({size})
|
|
459
|
+
""")
|
|
460
|
+
return self._sandbox.get_variable("__read_content__")
|
|
461
|
+
|
|
462
|
+
def readline(self) -> Union[str, bytes]:
|
|
463
|
+
"""Read a single line from the file.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
A line from the file.
|
|
467
|
+
"""
|
|
468
|
+
if self._closed:
|
|
469
|
+
raise ValueError("I/O operation on closed file")
|
|
470
|
+
|
|
471
|
+
self._sandbox.run_code(f"""
|
|
472
|
+
__read_line__ = {self._handle_id}.readline()
|
|
473
|
+
""")
|
|
474
|
+
return self._sandbox.get_variable("__read_line__")
|
|
475
|
+
|
|
476
|
+
def flush(self) -> None:
|
|
477
|
+
"""Flush the file buffer."""
|
|
478
|
+
if self._closed:
|
|
479
|
+
raise ValueError("I/O operation on closed file")
|
|
480
|
+
|
|
481
|
+
self._sandbox.run_code(f"{self._handle_id}.flush()")
|
|
482
|
+
|
|
483
|
+
def close(self) -> None:
|
|
484
|
+
"""Close the file handle."""
|
|
485
|
+
if not self._closed:
|
|
486
|
+
self._sandbox.run_code(f"{self._handle_id}.close()")
|
|
487
|
+
self._closed = True
|
|
488
|
+
|
|
489
|
+
def __enter__(self) -> "SandboxFileHandle":
|
|
490
|
+
return self
|
|
491
|
+
|
|
492
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
493
|
+
self.close()
|
|
494
|
+
|
|
495
|
+
def __del__(self):
|
|
496
|
+
if not self._closed:
|
|
497
|
+
try:
|
|
498
|
+
self.close()
|
|
499
|
+
except Exception:
|
|
500
|
+
pass
|