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.
@@ -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
@@ -0,0 +1,9 @@
1
+ # Copyright (c) 2025-2026 Datalayer, Inc.
2
+ #
3
+ # BSD 3-Clause License
4
+
5
+ """Local sandbox implementations."""
6
+
7
+ from .eval_sandbox import LocalEvalSandbox
8
+
9
+ __all__ = ["LocalEvalSandbox"]