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,309 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
2
|
+
#
|
|
3
|
+
# BSD 3-Clause License
|
|
4
|
+
|
|
5
|
+
"""Local eval-based sandbox implementation.
|
|
6
|
+
|
|
7
|
+
This is a simple sandbox that uses Python's exec() for code execution.
|
|
8
|
+
It provides minimal isolation and is suitable for development and testing.
|
|
9
|
+
|
|
10
|
+
WARNING: This sandbox does NOT provide security isolation. Do not use
|
|
11
|
+
for executing untrusted code.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import io
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
import traceback
|
|
18
|
+
import uuid
|
|
19
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
20
|
+
from typing import Any, Optional
|
|
21
|
+
|
|
22
|
+
from ..base import Sandbox
|
|
23
|
+
from ..exceptions import SandboxNotStartedError, SandboxTimeoutError
|
|
24
|
+
from ..models import (
|
|
25
|
+
Context,
|
|
26
|
+
Execution,
|
|
27
|
+
ExecutionError,
|
|
28
|
+
Logs,
|
|
29
|
+
OutputHandler,
|
|
30
|
+
OutputMessage,
|
|
31
|
+
Result,
|
|
32
|
+
SandboxConfig,
|
|
33
|
+
SandboxInfo,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LocalEvalSandbox(Sandbox):
|
|
38
|
+
"""A simple sandbox using Python's exec() for code execution.
|
|
39
|
+
|
|
40
|
+
This sandbox maintains separate namespaces for each context, allowing
|
|
41
|
+
variable persistence between executions within the same context.
|
|
42
|
+
|
|
43
|
+
WARNING: This provides NO security isolation. Only use for trusted code.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
with LocalEvalSandbox() as sandbox:
|
|
47
|
+
sandbox.run_code("x = 42")
|
|
48
|
+
result = sandbox.run_code("print(x * 2)") # prints 84
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, config: Optional[SandboxConfig] = None, **kwargs):
|
|
52
|
+
"""Initialize the local eval sandbox.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
config: Sandbox configuration.
|
|
56
|
+
**kwargs: Additional arguments (ignored).
|
|
57
|
+
"""
|
|
58
|
+
super().__init__(config)
|
|
59
|
+
self._namespaces: dict[str, dict[str, Any]] = {}
|
|
60
|
+
self._execution_count: dict[str, int] = {}
|
|
61
|
+
self._sandbox_id = str(uuid.uuid4())
|
|
62
|
+
|
|
63
|
+
def start(self) -> None:
|
|
64
|
+
"""Start the sandbox (initializes the default namespace)."""
|
|
65
|
+
if self._started:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
self._default_context = self.create_context("default")
|
|
69
|
+
self._namespaces[self._default_context.id] = {"__builtins__": __builtins__}
|
|
70
|
+
self._execution_count[self._default_context.id] = 0
|
|
71
|
+
|
|
72
|
+
self._info = SandboxInfo(
|
|
73
|
+
id=self._sandbox_id,
|
|
74
|
+
variant="local-eval",
|
|
75
|
+
status="running",
|
|
76
|
+
created_at=time.time(),
|
|
77
|
+
config=self.config,
|
|
78
|
+
)
|
|
79
|
+
self._started = True
|
|
80
|
+
|
|
81
|
+
def stop(self) -> None:
|
|
82
|
+
"""Stop the sandbox (clears all namespaces)."""
|
|
83
|
+
if not self._started:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
self._namespaces.clear()
|
|
87
|
+
self._execution_count.clear()
|
|
88
|
+
self._started = False
|
|
89
|
+
if self._info:
|
|
90
|
+
self._info.status = "stopped"
|
|
91
|
+
|
|
92
|
+
def create_context(self, name: Optional[str] = None) -> Context:
|
|
93
|
+
"""Create a new execution context with its own namespace.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
name: Optional name for the context.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
A new Context object.
|
|
100
|
+
"""
|
|
101
|
+
context = super().create_context(name)
|
|
102
|
+
if context.id not in self._namespaces:
|
|
103
|
+
self._namespaces[context.id] = {"__builtins__": __builtins__}
|
|
104
|
+
self._execution_count[context.id] = 0
|
|
105
|
+
return context
|
|
106
|
+
|
|
107
|
+
def run_code(
|
|
108
|
+
self,
|
|
109
|
+
code: str,
|
|
110
|
+
language: str = "python",
|
|
111
|
+
context: Optional[Context] = None,
|
|
112
|
+
on_stdout: Optional[OutputHandler[OutputMessage]] = None,
|
|
113
|
+
on_stderr: Optional[OutputHandler[OutputMessage]] = None,
|
|
114
|
+
on_result: Optional[OutputHandler[Result]] = None,
|
|
115
|
+
on_error: Optional[OutputHandler[ExecutionError]] = None,
|
|
116
|
+
envs: Optional[dict[str, str]] = None,
|
|
117
|
+
timeout: Optional[float] = None,
|
|
118
|
+
) -> Execution:
|
|
119
|
+
"""Execute Python code using exec().
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
code: The Python code to execute.
|
|
123
|
+
language: Must be "python" for this sandbox.
|
|
124
|
+
context: Execution context (uses default if not provided).
|
|
125
|
+
on_stdout: Callback for stdout messages.
|
|
126
|
+
on_stderr: Callback for stderr messages.
|
|
127
|
+
on_result: Callback for results.
|
|
128
|
+
on_error: Callback for errors.
|
|
129
|
+
envs: Environment variables (applied to os.environ temporarily).
|
|
130
|
+
timeout: Timeout in seconds (not enforced in this simple implementation).
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Execution result.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
SandboxNotStartedError: If the sandbox hasn't been started.
|
|
137
|
+
ValueError: If language is not "python".
|
|
138
|
+
"""
|
|
139
|
+
if not self._started:
|
|
140
|
+
raise SandboxNotStartedError()
|
|
141
|
+
|
|
142
|
+
if language != "python":
|
|
143
|
+
raise ValueError(f"LocalEvalSandbox only supports Python, got: {language}")
|
|
144
|
+
|
|
145
|
+
# Get or create context
|
|
146
|
+
ctx = context or self._default_context
|
|
147
|
+
if ctx.id not in self._namespaces:
|
|
148
|
+
self._namespaces[ctx.id] = {"__builtins__": __builtins__}
|
|
149
|
+
self._execution_count[ctx.id] = 0
|
|
150
|
+
|
|
151
|
+
namespace = self._namespaces[ctx.id]
|
|
152
|
+
self._execution_count[ctx.id] += 1
|
|
153
|
+
execution_count = self._execution_count[ctx.id]
|
|
154
|
+
|
|
155
|
+
# Set up environment variables temporarily
|
|
156
|
+
old_env = {}
|
|
157
|
+
if envs:
|
|
158
|
+
import os
|
|
159
|
+
|
|
160
|
+
for key, value in envs.items():
|
|
161
|
+
old_env[key] = os.environ.get(key)
|
|
162
|
+
os.environ[key] = value
|
|
163
|
+
|
|
164
|
+
# Capture stdout and stderr
|
|
165
|
+
stdout_buffer = io.StringIO()
|
|
166
|
+
stderr_buffer = io.StringIO()
|
|
167
|
+
stdout_messages: list[OutputMessage] = []
|
|
168
|
+
stderr_messages: list[OutputMessage] = []
|
|
169
|
+
results: list[Result] = []
|
|
170
|
+
error: Optional[ExecutionError] = None
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
|
|
174
|
+
# Try to evaluate as an expression first
|
|
175
|
+
try:
|
|
176
|
+
compiled = compile(code, "<sandbox>", "eval")
|
|
177
|
+
result_value = eval(compiled, namespace)
|
|
178
|
+
if result_value is not None:
|
|
179
|
+
result = Result(
|
|
180
|
+
data={"text/plain": repr(result_value)},
|
|
181
|
+
is_main_result=True,
|
|
182
|
+
)
|
|
183
|
+
results.append(result)
|
|
184
|
+
if on_result:
|
|
185
|
+
on_result(result)
|
|
186
|
+
except SyntaxError:
|
|
187
|
+
# Not an expression, execute as statements
|
|
188
|
+
exec(code, namespace)
|
|
189
|
+
|
|
190
|
+
except Exception as e:
|
|
191
|
+
# Capture the error
|
|
192
|
+
tb = traceback.format_exc()
|
|
193
|
+
error = ExecutionError(
|
|
194
|
+
name=type(e).__name__,
|
|
195
|
+
value=str(e),
|
|
196
|
+
traceback=tb,
|
|
197
|
+
)
|
|
198
|
+
if on_error:
|
|
199
|
+
on_error(error)
|
|
200
|
+
|
|
201
|
+
finally:
|
|
202
|
+
# Restore environment variables
|
|
203
|
+
if envs:
|
|
204
|
+
import os
|
|
205
|
+
|
|
206
|
+
for key, old_value in old_env.items():
|
|
207
|
+
if old_value is None:
|
|
208
|
+
os.environ.pop(key, None)
|
|
209
|
+
else:
|
|
210
|
+
os.environ[key] = old_value
|
|
211
|
+
|
|
212
|
+
# Process stdout
|
|
213
|
+
stdout_content = stdout_buffer.getvalue()
|
|
214
|
+
if stdout_content:
|
|
215
|
+
current_time = time.time()
|
|
216
|
+
for line in stdout_content.splitlines():
|
|
217
|
+
msg = OutputMessage(line=line, timestamp=current_time, error=False)
|
|
218
|
+
stdout_messages.append(msg)
|
|
219
|
+
if on_stdout:
|
|
220
|
+
on_stdout(msg)
|
|
221
|
+
|
|
222
|
+
# Process stderr
|
|
223
|
+
stderr_content = stderr_buffer.getvalue()
|
|
224
|
+
if stderr_content:
|
|
225
|
+
current_time = time.time()
|
|
226
|
+
for line in stderr_content.splitlines():
|
|
227
|
+
msg = OutputMessage(line=line, timestamp=current_time, error=True)
|
|
228
|
+
stderr_messages.append(msg)
|
|
229
|
+
if on_stderr:
|
|
230
|
+
on_stderr(msg)
|
|
231
|
+
|
|
232
|
+
return Execution(
|
|
233
|
+
results=results,
|
|
234
|
+
logs=Logs(stdout=stdout_messages, stderr=stderr_messages),
|
|
235
|
+
error=error,
|
|
236
|
+
execution_count=execution_count,
|
|
237
|
+
context_id=ctx.id,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _get_internal_variable(self, name: str, context: Optional[Context] = None) -> Any:
|
|
241
|
+
"""Get a variable from the namespace.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
name: Variable name.
|
|
245
|
+
context: Context to get from.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The variable value.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
VariableNotFoundError: If variable doesn't exist.
|
|
252
|
+
"""
|
|
253
|
+
ctx = context or self._default_context
|
|
254
|
+
if ctx.id not in self._namespaces:
|
|
255
|
+
from ..exceptions import VariableNotFoundError
|
|
256
|
+
|
|
257
|
+
raise VariableNotFoundError(name)
|
|
258
|
+
|
|
259
|
+
namespace = self._namespaces[ctx.id]
|
|
260
|
+
if name not in namespace:
|
|
261
|
+
from ..exceptions import VariableNotFoundError
|
|
262
|
+
|
|
263
|
+
raise VariableNotFoundError(name)
|
|
264
|
+
|
|
265
|
+
return namespace[name]
|
|
266
|
+
|
|
267
|
+
def _set_internal_variable(
|
|
268
|
+
self, name: str, value: Any, context: Optional[Context] = None
|
|
269
|
+
) -> None:
|
|
270
|
+
"""Set a variable in the namespace.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
name: Variable name.
|
|
274
|
+
value: Value to set.
|
|
275
|
+
context: Context to set in.
|
|
276
|
+
"""
|
|
277
|
+
ctx = context or self._default_context
|
|
278
|
+
if ctx.id not in self._namespaces:
|
|
279
|
+
self._namespaces[ctx.id] = {"__builtins__": __builtins__}
|
|
280
|
+
|
|
281
|
+
self._namespaces[ctx.id][name] = value
|
|
282
|
+
|
|
283
|
+
def get_variable(self, name: str, context: Optional[Context] = None) -> Any:
|
|
284
|
+
"""Get a variable from the sandbox.
|
|
285
|
+
|
|
286
|
+
This is more efficient than the base class implementation as it
|
|
287
|
+
directly accesses the namespace.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
name: Variable name.
|
|
291
|
+
context: Context to get from.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
The variable value.
|
|
295
|
+
"""
|
|
296
|
+
return self._get_internal_variable(name, context)
|
|
297
|
+
|
|
298
|
+
def set_variable(self, name: str, value: Any, context: Optional[Context] = None) -> None:
|
|
299
|
+
"""Set a variable in the sandbox.
|
|
300
|
+
|
|
301
|
+
This is more efficient than the base class implementation as it
|
|
302
|
+
directly accesses the namespace.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
name: Variable name.
|
|
306
|
+
value: Value to set.
|
|
307
|
+
context: Context to set in.
|
|
308
|
+
"""
|
|
309
|
+
self._set_internal_variable(name, value, context)
|
code_sandboxes/models.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
2
|
+
#
|
|
3
|
+
# BSD 3-Clause License
|
|
4
|
+
|
|
5
|
+
"""Models for code execution results and contexts.
|
|
6
|
+
|
|
7
|
+
Inspired by E2B Code Interpreter and Modal Sandbox models.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any, Callable, Optional, TypeVar
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MIMEType(str, Enum):
|
|
18
|
+
"""Common MIME types for execution results."""
|
|
19
|
+
|
|
20
|
+
TEXT_PLAIN = "text/plain"
|
|
21
|
+
TEXT_HTML = "text/html"
|
|
22
|
+
TEXT_MARKDOWN = "text/markdown"
|
|
23
|
+
APPLICATION_JSON = "application/json"
|
|
24
|
+
IMAGE_PNG = "image/png"
|
|
25
|
+
IMAGE_JPEG = "image/jpeg"
|
|
26
|
+
IMAGE_SVG = "image/svg+xml"
|
|
27
|
+
IMAGE_GIF = "image/gif"
|
|
28
|
+
APPLICATION_PDF = "application/pdf"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SandboxStatus(str, Enum):
|
|
32
|
+
"""Status of a sandbox."""
|
|
33
|
+
|
|
34
|
+
PENDING = "pending"
|
|
35
|
+
STARTING = "starting"
|
|
36
|
+
RUNNING = "running"
|
|
37
|
+
STOPPING = "stopping"
|
|
38
|
+
STOPPED = "stopped"
|
|
39
|
+
ERROR = "error"
|
|
40
|
+
TERMINATED = "terminated"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class GPUType(str, Enum):
|
|
44
|
+
"""Available GPU types for cloud sandboxes."""
|
|
45
|
+
|
|
46
|
+
T4 = "T4"
|
|
47
|
+
A10G = "A10G"
|
|
48
|
+
A100 = "A100"
|
|
49
|
+
A100_80GB = "A100-80GB"
|
|
50
|
+
H100 = "H100"
|
|
51
|
+
L4 = "L4"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ResourceConfig:
|
|
56
|
+
"""Resource configuration for sandbox.
|
|
57
|
+
|
|
58
|
+
Similar to Modal's resource specification.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
cpu: CPU cores to allocate.
|
|
62
|
+
memory: Memory limit in MB.
|
|
63
|
+
gpu: GPU type to use.
|
|
64
|
+
gpu_count: Number of GPUs.
|
|
65
|
+
disk: Disk size in GB.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
cpu: Optional[float] = None
|
|
69
|
+
memory: Optional[int] = None # MB
|
|
70
|
+
gpu: Optional[str] = None
|
|
71
|
+
gpu_count: int = 1
|
|
72
|
+
disk: Optional[int] = None # GB
|
|
73
|
+
|
|
74
|
+
def __repr__(self) -> str:
|
|
75
|
+
parts = []
|
|
76
|
+
if self.cpu:
|
|
77
|
+
parts.append(f"cpu={self.cpu}")
|
|
78
|
+
if self.memory:
|
|
79
|
+
parts.append(f"memory={self.memory}MB")
|
|
80
|
+
if self.gpu:
|
|
81
|
+
parts.append(f"gpu={self.gpu}x{self.gpu_count}")
|
|
82
|
+
return f"ResourceConfig({', '.join(parts) or 'default'})"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class OutputMessage:
|
|
88
|
+
"""A single output message from code execution.
|
|
89
|
+
|
|
90
|
+
Attributes:
|
|
91
|
+
line: The content of the output line.
|
|
92
|
+
timestamp: Unix timestamp when the output was produced.
|
|
93
|
+
error: Whether this is an error output (stderr).
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
line: str
|
|
97
|
+
timestamp: float = 0.0
|
|
98
|
+
error: bool = False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class Logs:
|
|
103
|
+
"""Container for stdout and stderr logs.
|
|
104
|
+
|
|
105
|
+
Attributes:
|
|
106
|
+
stdout: List of stdout output messages.
|
|
107
|
+
stderr: List of stderr output messages.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
stdout: list[OutputMessage] = field(default_factory=list)
|
|
111
|
+
stderr: list[OutputMessage] = field(default_factory=list)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def stdout_text(self) -> str:
|
|
115
|
+
"""Get stdout as a single string."""
|
|
116
|
+
return "\n".join(msg.line for msg in self.stdout)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def stderr_text(self) -> str:
|
|
120
|
+
"""Get stderr as a single string."""
|
|
121
|
+
return "\n".join(msg.line for msg in self.stderr)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class Result:
|
|
126
|
+
"""A single result from code execution.
|
|
127
|
+
|
|
128
|
+
Can contain multiple representations of the same data (e.g., text, HTML, image).
|
|
129
|
+
|
|
130
|
+
Attributes:
|
|
131
|
+
data: Dictionary mapping MIME types to their content.
|
|
132
|
+
is_main_result: Whether this is the main result of the execution.
|
|
133
|
+
extra: Additional metadata about the result.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
137
|
+
is_main_result: bool = False
|
|
138
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def text(self) -> Optional[str]:
|
|
142
|
+
"""Get text/plain representation if available."""
|
|
143
|
+
return self.data.get(MIMEType.TEXT_PLAIN) or self.data.get("text/plain")
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def html(self) -> Optional[str]:
|
|
147
|
+
"""Get text/html representation if available."""
|
|
148
|
+
return self.data.get(MIMEType.TEXT_HTML) or self.data.get("text/html")
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def markdown(self) -> Optional[str]:
|
|
152
|
+
"""Get text/markdown representation if available."""
|
|
153
|
+
return self.data.get(MIMEType.TEXT_MARKDOWN) or self.data.get("text/markdown")
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def json(self) -> Optional[Any]:
|
|
157
|
+
"""Get application/json representation if available."""
|
|
158
|
+
return self.data.get(MIMEType.APPLICATION_JSON) or self.data.get("application/json")
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def png(self) -> Optional[str]:
|
|
162
|
+
"""Get base64-encoded PNG image if available."""
|
|
163
|
+
return self.data.get(MIMEType.IMAGE_PNG) or self.data.get("image/png")
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def jpeg(self) -> Optional[str]:
|
|
167
|
+
"""Get base64-encoded JPEG image if available."""
|
|
168
|
+
return self.data.get(MIMEType.IMAGE_JPEG) or self.data.get("image/jpeg")
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def svg(self) -> Optional[str]:
|
|
172
|
+
"""Get SVG image if available."""
|
|
173
|
+
return self.data.get(MIMEType.IMAGE_SVG) or self.data.get("image/svg+xml")
|
|
174
|
+
|
|
175
|
+
def __repr__(self) -> str:
|
|
176
|
+
if self.text:
|
|
177
|
+
return f"Result(text={self.text[:50]}...)" if len(self.text) > 50 else f"Result(text={self.text})"
|
|
178
|
+
return f"Result(types={list(self.data.keys())})"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass
|
|
182
|
+
class ExecutionError:
|
|
183
|
+
"""Error information from failed code execution.
|
|
184
|
+
|
|
185
|
+
Attributes:
|
|
186
|
+
name: The error class name (e.g., "ValueError", "SyntaxError").
|
|
187
|
+
value: The error message.
|
|
188
|
+
traceback: Full traceback as a string.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
name: str
|
|
192
|
+
value: str
|
|
193
|
+
traceback: str = ""
|
|
194
|
+
|
|
195
|
+
def __str__(self) -> str:
|
|
196
|
+
if self.traceback:
|
|
197
|
+
return f"{self.traceback}\n{self.name}: {self.value}"
|
|
198
|
+
return f"{self.name}: {self.value}"
|
|
199
|
+
|
|
200
|
+
def __repr__(self) -> str:
|
|
201
|
+
return f"ExecutionError(name={self.name!r}, value={self.value!r})"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class Context:
|
|
206
|
+
"""Execution context for maintaining state across code executions.
|
|
207
|
+
|
|
208
|
+
A context represents an isolated execution environment where variables,
|
|
209
|
+
imports, and function definitions persist between executions.
|
|
210
|
+
|
|
211
|
+
Attributes:
|
|
212
|
+
id: Unique identifier for this context.
|
|
213
|
+
language: Programming language for this context.
|
|
214
|
+
cwd: Current working directory for file operations.
|
|
215
|
+
env: Environment variables for this context.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
id: str
|
|
219
|
+
language: str = "python"
|
|
220
|
+
cwd: Optional[str] = None
|
|
221
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
222
|
+
|
|
223
|
+
def __repr__(self) -> str:
|
|
224
|
+
return f"Context(id={self.id!r}, language={self.language!r})"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass
|
|
228
|
+
class Execution:
|
|
229
|
+
"""Complete result of a code execution.
|
|
230
|
+
|
|
231
|
+
Attributes:
|
|
232
|
+
results: List of results produced by the execution.
|
|
233
|
+
logs: Stdout and stderr logs.
|
|
234
|
+
error: Error information if execution failed.
|
|
235
|
+
execution_count: The execution counter (like Jupyter's In[n]).
|
|
236
|
+
context_id: ID of the context where this was executed.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
results: list[Result] = field(default_factory=list)
|
|
240
|
+
logs: Logs = field(default_factory=Logs)
|
|
241
|
+
error: Optional[ExecutionError] = None
|
|
242
|
+
execution_count: int = 0
|
|
243
|
+
context_id: Optional[str] = None
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def text(self) -> Optional[str]:
|
|
247
|
+
"""Get the main text result if available."""
|
|
248
|
+
for result in self.results:
|
|
249
|
+
if result.text:
|
|
250
|
+
return result.text
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def success(self) -> bool:
|
|
255
|
+
"""Whether the execution completed without errors."""
|
|
256
|
+
return self.error is None
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def stdout(self) -> str:
|
|
260
|
+
"""Get stdout as a single string."""
|
|
261
|
+
return self.logs.stdout_text
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def stderr(self) -> str:
|
|
265
|
+
"""Get stderr as a single string."""
|
|
266
|
+
return self.logs.stderr_text
|
|
267
|
+
|
|
268
|
+
def __repr__(self) -> str:
|
|
269
|
+
status = "success" if self.success else f"error={self.error.name}"
|
|
270
|
+
return f"Execution({status}, results={len(self.results)}, execution_count={self.execution_count})"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# Type alias for output handlers (callbacks)
|
|
274
|
+
OutputHandler = Callable[[T], None]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@dataclass
|
|
278
|
+
class SandboxConfig:
|
|
279
|
+
"""Configuration for sandbox creation.
|
|
280
|
+
|
|
281
|
+
Inspired by E2B and Modal configuration options.
|
|
282
|
+
|
|
283
|
+
Attributes:
|
|
284
|
+
timeout: Default timeout for code execution in seconds.
|
|
285
|
+
memory_limit: Memory limit in bytes (for Docker/Datalayer sandboxes).
|
|
286
|
+
cpu_limit: CPU limit (for Docker/Datalayer sandboxes).
|
|
287
|
+
environment: Environment name for Datalayer sandboxes.
|
|
288
|
+
working_dir: Default working directory.
|
|
289
|
+
env_vars: Default environment variables.
|
|
290
|
+
gpu: GPU type to use (e.g., "T4", "A100").
|
|
291
|
+
name: Optional name for the sandbox.
|
|
292
|
+
idle_timeout: Time in seconds before idle sandbox is terminated.
|
|
293
|
+
max_lifetime: Maximum lifetime in seconds.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
timeout: float = 30.0
|
|
297
|
+
memory_limit: Optional[int] = None
|
|
298
|
+
cpu_limit: Optional[float] = None
|
|
299
|
+
environment: str = "python-simple-env"
|
|
300
|
+
working_dir: Optional[str] = None
|
|
301
|
+
env_vars: dict[str, str] = field(default_factory=dict)
|
|
302
|
+
gpu: Optional[str] = None
|
|
303
|
+
name: Optional[str] = None
|
|
304
|
+
idle_timeout: Optional[float] = None
|
|
305
|
+
max_lifetime: float = 86400.0 # 24 hours default like Modal
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@dataclass
|
|
309
|
+
class SandboxInfo:
|
|
310
|
+
"""Information about a running sandbox.
|
|
311
|
+
|
|
312
|
+
Inspired by E2B's getInfo() and Modal's sandbox info.
|
|
313
|
+
|
|
314
|
+
Attributes:
|
|
315
|
+
id: Unique identifier for the sandbox.
|
|
316
|
+
variant: The sandbox variant (local-eval, local-docker, datalayer-runtime).
|
|
317
|
+
status: Current status of the sandbox.
|
|
318
|
+
created_at: Unix timestamp when the sandbox was created.
|
|
319
|
+
end_at: Unix timestamp when the sandbox will be terminated.
|
|
320
|
+
config: The configuration used to create this sandbox.
|
|
321
|
+
name: Name of the sandbox if set.
|
|
322
|
+
metadata: Additional metadata about the sandbox.
|
|
323
|
+
resources: Resource configuration for the sandbox.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
id: str
|
|
327
|
+
variant: str
|
|
328
|
+
status: SandboxStatus = SandboxStatus.RUNNING
|
|
329
|
+
created_at: float = 0.0
|
|
330
|
+
end_at: Optional[float] = None
|
|
331
|
+
config: Optional[SandboxConfig] = None
|
|
332
|
+
name: Optional[str] = None
|
|
333
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
334
|
+
resources: Optional[ResourceConfig] = None
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def remaining_time(self) -> Optional[float]:
|
|
338
|
+
"""Get remaining time in seconds before sandbox terminates."""
|
|
339
|
+
import time
|
|
340
|
+
if self.end_at:
|
|
341
|
+
return max(0, self.end_at - time.time())
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
def __repr__(self) -> str:
|
|
345
|
+
return f"SandboxInfo(id={self.id!r}, status={self.status.value}, variant={self.variant!r})"
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@dataclass
|
|
349
|
+
class SnapshotInfo:
|
|
350
|
+
"""Information about a sandbox snapshot.
|
|
351
|
+
|
|
352
|
+
Snapshots allow saving and restoring sandbox state.
|
|
353
|
+
Similar to Modal's snapshot_filesystem feature.
|
|
354
|
+
|
|
355
|
+
Attributes:
|
|
356
|
+
id: Unique identifier for the snapshot.
|
|
357
|
+
name: Name of the snapshot.
|
|
358
|
+
sandbox_id: ID of the sandbox this snapshot was taken from.
|
|
359
|
+
created_at: Unix timestamp when the snapshot was created.
|
|
360
|
+
size: Size of the snapshot in bytes.
|
|
361
|
+
description: Optional description.
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
id: str
|
|
365
|
+
name: str
|
|
366
|
+
sandbox_id: str
|
|
367
|
+
created_at: float = 0.0
|
|
368
|
+
size: int = 0
|
|
369
|
+
description: str = ""
|
|
370
|
+
|
|
371
|
+
def __repr__(self) -> str:
|
|
372
|
+
return f"SnapshotInfo(id={self.id!r}, name={self.name!r})"
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@dataclass
|
|
376
|
+
class TunnelInfo:
|
|
377
|
+
"""Information about a tunnel to a sandbox port.
|
|
378
|
+
|
|
379
|
+
Similar to Modal's Tunnel interface.
|
|
380
|
+
|
|
381
|
+
Attributes:
|
|
382
|
+
port: The port in the sandbox.
|
|
383
|
+
url: The external URL to access the port.
|
|
384
|
+
protocol: The protocol (http, https, tcp).
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
port: int
|
|
388
|
+
url: str
|
|
389
|
+
protocol: str = "https"
|
|
390
|
+
|
|
391
|
+
def __repr__(self) -> str:
|
|
392
|
+
return f"TunnelInfo(port={self.port}, url={self.url!r})"
|