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,452 @@
1
+ # Copyright (c) 2025-2026 Datalayer, Inc.
2
+ #
3
+ # BSD 3-Clause License
4
+
5
+ """Command execution for sandboxes.
6
+
7
+ Inspired by E2B Commands and Modal exec APIs.
8
+ """
9
+
10
+ import time
11
+ from dataclasses import dataclass, field
12
+ from typing import TYPE_CHECKING, Iterator, Optional, Union
13
+
14
+ if TYPE_CHECKING:
15
+ from .base import Sandbox
16
+
17
+
18
+ @dataclass
19
+ class CommandResult:
20
+ """Result of a command execution.
21
+
22
+ Attributes:
23
+ exit_code: The command's exit code.
24
+ stdout: Standard output content.
25
+ stderr: Standard error content.
26
+ duration: Execution duration in seconds.
27
+ """
28
+
29
+ exit_code: int = 0
30
+ stdout: str = ""
31
+ stderr: str = ""
32
+ duration: float = 0.0
33
+
34
+ @property
35
+ def success(self) -> bool:
36
+ """Whether the command succeeded (exit code 0)."""
37
+ return self.exit_code == 0
38
+
39
+ def __repr__(self) -> str:
40
+ return f"CommandResult(exit_code={self.exit_code}, stdout_len={len(self.stdout)}, stderr_len={len(self.stderr)})"
41
+
42
+
43
+ @dataclass
44
+ class ProcessHandle:
45
+ """Handle for a running process.
46
+
47
+ Similar to Modal's ContainerProcess interface.
48
+
49
+ Attributes:
50
+ pid: Process ID (if available).
51
+ command: The command being executed.
52
+ """
53
+
54
+ sandbox: "Sandbox"
55
+ command: str
56
+ pid: Optional[int] = None
57
+ _process_var: str = ""
58
+ _completed: bool = False
59
+ _exit_code: Optional[int] = None
60
+ _stdout_buffer: list[str] = field(default_factory=list)
61
+ _stderr_buffer: list[str] = field(default_factory=list)
62
+
63
+ def __post_init__(self):
64
+ self._process_var = f"__proc_{id(self)}__"
65
+
66
+ @property
67
+ def stdout(self) -> Iterator[str]:
68
+ """Stream stdout lines.
69
+
70
+ Yields:
71
+ Lines from stdout as they become available.
72
+ """
73
+ # For synchronous implementation, return buffered output
74
+ for line in self._stdout_buffer:
75
+ yield line
76
+
77
+ @property
78
+ def stderr(self) -> Iterator[str]:
79
+ """Stream stderr lines.
80
+
81
+ Yields:
82
+ Lines from stderr as they become available.
83
+ """
84
+ for line in self._stderr_buffer:
85
+ yield line
86
+
87
+ def read_stdout(self) -> str:
88
+ """Read all stdout content.
89
+
90
+ Returns:
91
+ All stdout content as a string.
92
+ """
93
+ return "\n".join(self._stdout_buffer)
94
+
95
+ def read_stderr(self) -> str:
96
+ """Read all stderr content.
97
+
98
+ Returns:
99
+ All stderr content as a string.
100
+ """
101
+ return "\n".join(self._stderr_buffer)
102
+
103
+ def wait(self, timeout: Optional[float] = None) -> int:
104
+ """Wait for the process to complete.
105
+
106
+ Args:
107
+ timeout: Maximum time to wait in seconds.
108
+
109
+ Returns:
110
+ The exit code.
111
+ """
112
+ if self._completed:
113
+ return self._exit_code or 0
114
+
115
+ self.sandbox.run_code(f"""
116
+ {self._process_var}.wait()
117
+ __exit_code__ = {self._process_var}.returncode
118
+ """)
119
+ self._exit_code = self.sandbox.get_variable("__exit_code__")
120
+ self._completed = True
121
+ return self._exit_code
122
+
123
+ def poll(self) -> Optional[int]:
124
+ """Check if the process has completed.
125
+
126
+ Returns:
127
+ Exit code if completed, None otherwise.
128
+ """
129
+ if self._completed:
130
+ return self._exit_code
131
+
132
+ self.sandbox.run_code(f"""
133
+ __poll_result__ = {self._process_var}.poll()
134
+ """)
135
+ result = self.sandbox.get_variable("__poll_result__")
136
+ if result is not None:
137
+ self._exit_code = result
138
+ self._completed = True
139
+ return result
140
+
141
+ def terminate(self) -> None:
142
+ """Terminate the process."""
143
+ self.sandbox.run_code(f"""
144
+ try:
145
+ {self._process_var}.terminate()
146
+ except:
147
+ pass
148
+ """)
149
+ self._completed = True
150
+
151
+ def kill(self) -> None:
152
+ """Kill the process forcefully."""
153
+ self.sandbox.run_code(f"""
154
+ try:
155
+ {self._process_var}.kill()
156
+ except:
157
+ pass
158
+ """)
159
+ self._completed = True
160
+
161
+ @property
162
+ def returncode(self) -> Optional[int]:
163
+ """Get the return code if process has completed."""
164
+ if self._completed:
165
+ return self._exit_code
166
+ return self.poll()
167
+
168
+
169
+ class SandboxCommands:
170
+ """Command execution for a sandbox.
171
+
172
+ Provides terminal command execution similar to E2B and Modal.
173
+
174
+ Example:
175
+ with Sandbox.create() as sandbox:
176
+ # Run a simple command
177
+ result = sandbox.commands.run("ls -la")
178
+ print(result.stdout)
179
+
180
+ # Run with streaming
181
+ process = sandbox.commands.exec("python", "-c", "print('hello')")
182
+ for line in process.stdout:
183
+ print(line)
184
+ """
185
+
186
+ def __init__(self, sandbox: "Sandbox"):
187
+ """Initialize command operations for a sandbox.
188
+
189
+ Args:
190
+ sandbox: The sandbox instance.
191
+ """
192
+ self._sandbox = sandbox
193
+
194
+ def run(
195
+ self,
196
+ command: str,
197
+ cwd: Optional[str] = None,
198
+ env: Optional[dict[str, str]] = None,
199
+ timeout: Optional[float] = None,
200
+ shell: bool = True,
201
+ ) -> CommandResult:
202
+ """Run a command and wait for completion.
203
+
204
+ Args:
205
+ command: The command to run.
206
+ cwd: Working directory.
207
+ env: Environment variables.
208
+ timeout: Timeout in seconds.
209
+ shell: Whether to run through shell.
210
+
211
+ Returns:
212
+ CommandResult with exit code, stdout, stderr.
213
+ """
214
+ start_time = time.time()
215
+
216
+ # Build the subprocess code
217
+ env_str = f", env={{**os.environ, **{env!r}}}" if env else ""
218
+ cwd_str = f", cwd={cwd!r}" if cwd else ""
219
+ timeout_str = f", timeout={timeout}" if timeout else ""
220
+
221
+ code = f"""
222
+ import subprocess
223
+ import os
224
+
225
+ try:
226
+ __cmd_result__ = subprocess.run(
227
+ {command!r},
228
+ shell={shell},
229
+ capture_output=True,
230
+ text=True{cwd_str}{env_str}{timeout_str}
231
+ )
232
+ __cmd_output__ = {{
233
+ 'exit_code': __cmd_result__.returncode,
234
+ 'stdout': __cmd_result__.stdout,
235
+ 'stderr': __cmd_result__.stderr,
236
+ }}
237
+ except subprocess.TimeoutExpired:
238
+ __cmd_output__ = {{
239
+ 'exit_code': -1,
240
+ 'stdout': '',
241
+ 'stderr': 'Command timed out',
242
+ }}
243
+ except Exception as e:
244
+ __cmd_output__ = {{
245
+ 'exit_code': -1,
246
+ 'stdout': '',
247
+ 'stderr': str(e),
248
+ }}
249
+ """
250
+
251
+ execution = self._sandbox.run_code(code, timeout=timeout)
252
+
253
+ if execution.error:
254
+ return CommandResult(
255
+ exit_code=-1,
256
+ stdout="",
257
+ stderr=str(execution.error),
258
+ duration=time.time() - start_time,
259
+ )
260
+
261
+ try:
262
+ result = self._sandbox.get_variable("__cmd_output__")
263
+ return CommandResult(
264
+ exit_code=result["exit_code"],
265
+ stdout=result["stdout"],
266
+ stderr=result["stderr"],
267
+ duration=time.time() - start_time,
268
+ )
269
+ except Exception as e:
270
+ return CommandResult(
271
+ exit_code=-1,
272
+ stdout="",
273
+ stderr=str(e),
274
+ duration=time.time() - start_time,
275
+ )
276
+
277
+ def exec(
278
+ self,
279
+ *args: str,
280
+ cwd: Optional[str] = None,
281
+ env: Optional[dict[str, str]] = None,
282
+ timeout: Optional[float] = None,
283
+ ) -> ProcessHandle:
284
+ """Execute a command with streaming output.
285
+
286
+ Unlike `run()`, this returns immediately with a ProcessHandle
287
+ that can be used to stream output and wait for completion.
288
+
289
+ Args:
290
+ *args: Command and arguments.
291
+ cwd: Working directory.
292
+ env: Environment variables.
293
+ timeout: Timeout in seconds.
294
+
295
+ Returns:
296
+ ProcessHandle for the running process.
297
+ """
298
+ command = " ".join(args)
299
+ process = ProcessHandle(sandbox=self._sandbox, command=command)
300
+
301
+ # Start the subprocess
302
+ env_str = f", env={{**os.environ, **{env!r}}}" if env else ""
303
+ cwd_str = f", cwd={cwd!r}" if cwd else ""
304
+
305
+ code = f"""
306
+ import subprocess
307
+ import os
308
+
309
+ {process._process_var} = subprocess.Popen(
310
+ {list(args)!r},
311
+ stdout=subprocess.PIPE,
312
+ stderr=subprocess.PIPE,
313
+ text=True{cwd_str}{env_str}
314
+ )
315
+ __proc_pid__ = {process._process_var}.pid
316
+ """
317
+
318
+ self._sandbox.run_code(code)
319
+ try:
320
+ process.pid = self._sandbox.get_variable("__proc_pid__")
321
+ except Exception:
322
+ pass
323
+
324
+ return process
325
+
326
+ def spawn(
327
+ self,
328
+ command: str,
329
+ cwd: Optional[str] = None,
330
+ env: Optional[dict[str, str]] = None,
331
+ ) -> ProcessHandle:
332
+ """Spawn a background process.
333
+
334
+ The process runs in the background and doesn't block.
335
+
336
+ Args:
337
+ command: The command to run.
338
+ cwd: Working directory.
339
+ env: Environment variables.
340
+
341
+ Returns:
342
+ ProcessHandle for the background process.
343
+ """
344
+ return self.exec(*command.split(), cwd=cwd, env=env)
345
+
346
+ def run_script(
347
+ self,
348
+ script: str,
349
+ interpreter: str = "/bin/bash",
350
+ cwd: Optional[str] = None,
351
+ env: Optional[dict[str, str]] = None,
352
+ timeout: Optional[float] = None,
353
+ ) -> CommandResult:
354
+ """Run a script with the specified interpreter.
355
+
356
+ Args:
357
+ script: The script content.
358
+ interpreter: Script interpreter path.
359
+ cwd: Working directory.
360
+ env: Environment variables.
361
+ timeout: Timeout in seconds.
362
+
363
+ Returns:
364
+ CommandResult with exit code, stdout, stderr.
365
+ """
366
+ start_time = time.time()
367
+
368
+ env_str = f", env={{**os.environ, **{env!r}}}" if env else ""
369
+ cwd_str = f", cwd={cwd!r}" if cwd else ""
370
+ timeout_str = f", timeout={timeout}" if timeout else ""
371
+
372
+ code = f"""
373
+ import subprocess
374
+ import os
375
+
376
+ try:
377
+ __script_result__ = subprocess.run(
378
+ [{interpreter!r}, '-c', {script!r}],
379
+ capture_output=True,
380
+ text=True{cwd_str}{env_str}{timeout_str}
381
+ )
382
+ __script_output__ = {{
383
+ 'exit_code': __script_result__.returncode,
384
+ 'stdout': __script_result__.stdout,
385
+ 'stderr': __script_result__.stderr,
386
+ }}
387
+ except subprocess.TimeoutExpired:
388
+ __script_output__ = {{
389
+ 'exit_code': -1,
390
+ 'stdout': '',
391
+ 'stderr': 'Script execution timed out',
392
+ }}
393
+ except Exception as e:
394
+ __script_output__ = {{
395
+ 'exit_code': -1,
396
+ 'stdout': '',
397
+ 'stderr': str(e),
398
+ }}
399
+ """
400
+
401
+ execution = self._sandbox.run_code(code, timeout=timeout)
402
+
403
+ if execution.error:
404
+ return CommandResult(
405
+ exit_code=-1,
406
+ stdout="",
407
+ stderr=str(execution.error),
408
+ duration=time.time() - start_time,
409
+ )
410
+
411
+ try:
412
+ result = self._sandbox.get_variable("__script_output__")
413
+ return CommandResult(
414
+ exit_code=result["exit_code"],
415
+ stdout=result["stdout"],
416
+ stderr=result["stderr"],
417
+ duration=time.time() - start_time,
418
+ )
419
+ except Exception as e:
420
+ return CommandResult(
421
+ exit_code=-1,
422
+ stdout="",
423
+ stderr=str(e),
424
+ duration=time.time() - start_time,
425
+ )
426
+
427
+ def install_system_packages(
428
+ self,
429
+ packages: list[str],
430
+ package_manager: str = "apt-get",
431
+ timeout: float = 300,
432
+ ) -> CommandResult:
433
+ """Install system packages.
434
+
435
+ Args:
436
+ packages: List of package names.
437
+ package_manager: Package manager to use (apt-get, yum, etc.).
438
+ timeout: Timeout in seconds.
439
+
440
+ Returns:
441
+ CommandResult from the installation.
442
+ """
443
+ if package_manager == "apt-get":
444
+ cmd = f"apt-get update && apt-get install -y {' '.join(packages)}"
445
+ elif package_manager == "yum":
446
+ cmd = f"yum install -y {' '.join(packages)}"
447
+ elif package_manager == "apk":
448
+ cmd = f"apk add {' '.join(packages)}"
449
+ else:
450
+ cmd = f"{package_manager} install {' '.join(packages)}"
451
+
452
+ return self.run(cmd, timeout=timeout)
@@ -0,0 +1,101 @@
1
+ # Copyright (c) 2025-2026 Datalayer, Inc.
2
+ #
3
+ # BSD 3-Clause License
4
+
5
+ """Custom exceptions for code sandboxes."""
6
+
7
+
8
+ class SandboxError(Exception):
9
+ """Base exception for sandbox errors."""
10
+
11
+ pass
12
+
13
+
14
+ class SandboxTimeoutError(SandboxError):
15
+ """Raised when code execution times out."""
16
+
17
+ def __init__(self, timeout: float, message: str = None):
18
+ self.timeout = timeout
19
+ super().__init__(message or f"Code execution timed out after {timeout} seconds")
20
+
21
+
22
+ class SandboxExecutionError(SandboxError):
23
+ """Raised when code execution fails."""
24
+
25
+ def __init__(self, error_name: str, error_value: str, traceback: str = ""):
26
+ self.error_name = error_name
27
+ self.error_value = error_value
28
+ self.traceback = traceback
29
+ super().__init__(f"{error_name}: {error_value}")
30
+
31
+
32
+ class SandboxNotStartedError(SandboxError):
33
+ """Raised when trying to use a sandbox that hasn't been started."""
34
+
35
+ def __init__(self):
36
+ super().__init__("Sandbox has not been started. Use 'with' context or call start()")
37
+
38
+
39
+ class SandboxConnectionError(SandboxError):
40
+ """Raised when connection to remote sandbox fails."""
41
+
42
+ def __init__(self, url: str, message: str = None):
43
+ self.url = url
44
+ super().__init__(message or f"Failed to connect to sandbox at {url}")
45
+
46
+
47
+ class SandboxConfigurationError(SandboxError):
48
+ """Raised when sandbox configuration is invalid."""
49
+
50
+ pass
51
+
52
+
53
+ class ContextNotFoundError(SandboxError):
54
+ """Raised when a requested context does not exist."""
55
+
56
+ def __init__(self, context_id: str):
57
+ self.context_id = context_id
58
+ super().__init__(f"Context '{context_id}' not found")
59
+
60
+
61
+ class VariableNotFoundError(SandboxError):
62
+ """Raised when a requested variable does not exist."""
63
+
64
+ def __init__(self, variable_name: str):
65
+ self.variable_name = variable_name
66
+ super().__init__(f"Variable '{variable_name}' not found in sandbox")
67
+
68
+
69
+ class SandboxSnapshotError(SandboxError):
70
+ """Raised when snapshot operations fail."""
71
+
72
+ def __init__(self, operation: str, message: str = None):
73
+ self.operation = operation
74
+ super().__init__(message or f"Snapshot operation '{operation}' failed")
75
+
76
+
77
+ class SandboxResourceError(SandboxError):
78
+ """Raised when resource allocation fails (CPU, GPU, memory)."""
79
+
80
+ def __init__(self, resource_type: str, message: str = None):
81
+ self.resource_type = resource_type
82
+ super().__init__(message or f"Failed to allocate resource: {resource_type}")
83
+
84
+
85
+ class SandboxAuthenticationError(SandboxError):
86
+ """Raised when authentication with the sandbox provider fails."""
87
+
88
+ def __init__(self, message: str = None):
89
+ super().__init__(message or "Authentication failed")
90
+
91
+
92
+ class SandboxQuotaExceededError(SandboxError):
93
+ """Raised when sandbox quota or limits are exceeded."""
94
+
95
+ def __init__(self, limit_type: str, limit_value: str = None):
96
+ self.limit_type = limit_type
97
+ self.limit_value = limit_value
98
+ msg = f"Quota exceeded for {limit_type}"
99
+ if limit_value:
100
+ msg += f": {limit_value}"
101
+ super().__init__(msg)