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
code_sandboxes/base.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
2
|
+
#
|
|
3
|
+
# BSD 3-Clause License
|
|
4
|
+
|
|
5
|
+
"""Abstract base class for code sandboxes.
|
|
6
|
+
|
|
7
|
+
Inspired by E2B Code Interpreter and Modal Sandbox APIs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import Any, AsyncIterator, Iterator, Literal, Optional, Union
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
from .commands import CommandResult, ProcessHandle, SandboxCommands
|
|
18
|
+
from .filesystem import FileInfo, SandboxFileHandle, SandboxFilesystem
|
|
19
|
+
from .models import (
|
|
20
|
+
Context,
|
|
21
|
+
Execution,
|
|
22
|
+
ExecutionError,
|
|
23
|
+
OutputHandler,
|
|
24
|
+
OutputMessage,
|
|
25
|
+
ResourceConfig,
|
|
26
|
+
Result,
|
|
27
|
+
SandboxConfig,
|
|
28
|
+
SandboxInfo,
|
|
29
|
+
SandboxStatus,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
SandboxVariant = Literal["local-eval", "local-docker", "datalayer-runtime"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Sandbox(ABC):
|
|
38
|
+
"""Abstract base class for code execution sandboxes.
|
|
39
|
+
|
|
40
|
+
A sandbox provides a safe, isolated environment for executing code.
|
|
41
|
+
Different implementations provide different isolation levels:
|
|
42
|
+
- local-eval: Simple Python exec() based, minimal isolation
|
|
43
|
+
- local-docker: Docker container based, good isolation
|
|
44
|
+
- datalayer-runtime: Cloud-based Datalayer runtime, full isolation
|
|
45
|
+
|
|
46
|
+
Features inspired by E2B and Modal:
|
|
47
|
+
- Code execution with result streaming
|
|
48
|
+
- Filesystem operations (read, write, list, upload, download)
|
|
49
|
+
- Command execution (run, exec, spawn)
|
|
50
|
+
- Context management for state persistence
|
|
51
|
+
- Snapshot support (for datalayer-runtime)
|
|
52
|
+
- GPU/resource configuration (for datalayer-runtime)
|
|
53
|
+
- Timeout and lifecycle management
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
with Sandbox.create(variant="datalayer-runtime") as sandbox:
|
|
57
|
+
# Execute code
|
|
58
|
+
result = sandbox.run_code("x = 1 + 1")
|
|
59
|
+
result = sandbox.run_code("print(x)") # prints 2
|
|
60
|
+
|
|
61
|
+
# Use filesystem
|
|
62
|
+
sandbox.files.write("/data/test.txt", "Hello")
|
|
63
|
+
content = sandbox.files.read("/data/test.txt")
|
|
64
|
+
|
|
65
|
+
# Run commands
|
|
66
|
+
result = sandbox.commands.run("ls -la")
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
config: The sandbox configuration.
|
|
70
|
+
info: Information about the running sandbox.
|
|
71
|
+
files: Filesystem operations.
|
|
72
|
+
commands: Command execution operations.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, config: Optional[SandboxConfig] = None):
|
|
76
|
+
"""Initialize sandbox with configuration.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
config: Sandbox configuration. Uses defaults if not provided.
|
|
80
|
+
"""
|
|
81
|
+
self.config = config or SandboxConfig()
|
|
82
|
+
self._info: Optional[SandboxInfo] = None
|
|
83
|
+
self._started = False
|
|
84
|
+
self._default_context: Optional[Context] = None
|
|
85
|
+
self._files: Optional[SandboxFilesystem] = None
|
|
86
|
+
self._commands: Optional[SandboxCommands] = None
|
|
87
|
+
self._tags: dict[str, str] = {}
|
|
88
|
+
self._created_at: float = 0.0
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def info(self) -> Optional[SandboxInfo]:
|
|
92
|
+
"""Get information about this sandbox."""
|
|
93
|
+
return self._info
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def is_started(self) -> bool:
|
|
97
|
+
"""Check if sandbox has been started."""
|
|
98
|
+
return self._started
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def sandbox_id(self) -> Optional[str]:
|
|
102
|
+
"""Get the sandbox ID."""
|
|
103
|
+
return self._info.id if self._info else None
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def files(self) -> SandboxFilesystem:
|
|
107
|
+
"""Get filesystem operations interface.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
SandboxFilesystem for file operations.
|
|
111
|
+
"""
|
|
112
|
+
if self._files is None:
|
|
113
|
+
self._files = SandboxFilesystem(self)
|
|
114
|
+
return self._files
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def commands(self) -> SandboxCommands:
|
|
118
|
+
"""Get command execution interface.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
SandboxCommands for running terminal commands.
|
|
122
|
+
"""
|
|
123
|
+
if self._commands is None:
|
|
124
|
+
self._commands = SandboxCommands(self)
|
|
125
|
+
return self._commands
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def tags(self) -> dict[str, str]:
|
|
129
|
+
"""Get sandbox tags (key-value pairs for metadata)."""
|
|
130
|
+
return self._tags.copy()
|
|
131
|
+
|
|
132
|
+
def set_tags(self, tags: dict[str, str]) -> None:
|
|
133
|
+
"""Set sandbox tags.
|
|
134
|
+
|
|
135
|
+
Tags can be used to filter and organize sandboxes.
|
|
136
|
+
Similar to Modal's sandbox tagging feature.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
tags: Dictionary of tag names to values.
|
|
140
|
+
"""
|
|
141
|
+
self._tags.update(tags)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def create(
|
|
146
|
+
cls,
|
|
147
|
+
variant: SandboxVariant = "datalayer-runtime",
|
|
148
|
+
config: Optional[SandboxConfig] = None,
|
|
149
|
+
timeout: Optional[float] = None,
|
|
150
|
+
name: Optional[str] = None,
|
|
151
|
+
environment: Optional[str] = None,
|
|
152
|
+
gpu: Optional[str] = None,
|
|
153
|
+
cpu: Optional[float] = None,
|
|
154
|
+
memory: Optional[int] = None,
|
|
155
|
+
env: Optional[dict[str, str]] = None,
|
|
156
|
+
tags: Optional[dict[str, str]] = None,
|
|
157
|
+
**kwargs,
|
|
158
|
+
) -> "Sandbox":
|
|
159
|
+
"""Factory method to create a sandbox of the specified variant.
|
|
160
|
+
|
|
161
|
+
This method provides a simple interface similar to E2B and Modal:
|
|
162
|
+
- E2B: Sandbox.create(timeout=60_000)
|
|
163
|
+
- Modal: Sandbox.create(gpu="T4", timeout=300)
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
variant: The type of sandbox to create.
|
|
167
|
+
- "local-eval": Simple Python exec() based, minimal isolation
|
|
168
|
+
- "local-docker": Docker container based (requires Docker)
|
|
169
|
+
- "datalayer-runtime": Cloud-based Datalayer runtime (default)
|
|
170
|
+
config: Optional full configuration object (overrides individual params).
|
|
171
|
+
timeout: Default timeout for code execution in seconds.
|
|
172
|
+
name: Optional name for the sandbox.
|
|
173
|
+
environment: Runtime environment (e.g., "python-simple-env", "python-gpu-env").
|
|
174
|
+
gpu: GPU type to use (e.g., "T4", "A100", "H100"). Only for datalayer-runtime.
|
|
175
|
+
cpu: CPU cores to allocate.
|
|
176
|
+
memory: Memory limit in MB.
|
|
177
|
+
env: Environment variables to set in the sandbox.
|
|
178
|
+
tags: Metadata tags for the sandbox.
|
|
179
|
+
**kwargs: Additional arguments passed to the sandbox constructor.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A Sandbox instance of the specified variant.
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
ValueError: If the variant is not supported.
|
|
186
|
+
|
|
187
|
+
Example:
|
|
188
|
+
# Simple usage
|
|
189
|
+
sandbox = Sandbox.create()
|
|
190
|
+
|
|
191
|
+
# With timeout (like E2B)
|
|
192
|
+
sandbox = Sandbox.create(timeout=60)
|
|
193
|
+
|
|
194
|
+
# With GPU (like Modal)
|
|
195
|
+
sandbox = Sandbox.create(gpu="T4", environment="python-gpu-env")
|
|
196
|
+
|
|
197
|
+
# Local development
|
|
198
|
+
sandbox = Sandbox.create(variant="local-eval")
|
|
199
|
+
"""
|
|
200
|
+
# Build config from individual parameters if not provided
|
|
201
|
+
if config is None:
|
|
202
|
+
config = SandboxConfig(
|
|
203
|
+
timeout=timeout or 30.0,
|
|
204
|
+
environment=environment or "python-simple-env",
|
|
205
|
+
memory_limit=memory * 1024 * 1024 if memory else None,
|
|
206
|
+
cpu_limit=cpu,
|
|
207
|
+
env_vars=env or {},
|
|
208
|
+
gpu=gpu,
|
|
209
|
+
name=name,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
from .local.eval_sandbox import LocalEvalSandbox
|
|
213
|
+
|
|
214
|
+
if variant == "local-eval":
|
|
215
|
+
sandbox = LocalEvalSandbox(config=config, **kwargs)
|
|
216
|
+
elif variant == "local-docker":
|
|
217
|
+
# Import here to avoid circular imports
|
|
218
|
+
from .local.docker_sandbox import LocalDockerSandbox
|
|
219
|
+
|
|
220
|
+
sandbox = LocalDockerSandbox(config=config, **kwargs)
|
|
221
|
+
elif variant == "datalayer-runtime":
|
|
222
|
+
from .remote.datalayer_sandbox import DatalayerSandbox
|
|
223
|
+
|
|
224
|
+
sandbox = DatalayerSandbox(config=config, **kwargs)
|
|
225
|
+
else:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"Unknown sandbox variant: {variant}. "
|
|
228
|
+
f"Supported variants: local-eval, local-docker, datalayer-runtime"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Set tags if provided
|
|
232
|
+
if tags:
|
|
233
|
+
sandbox.set_tags(tags)
|
|
234
|
+
|
|
235
|
+
return sandbox
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def from_id(cls, sandbox_id: str, **kwargs) -> "Sandbox":
|
|
239
|
+
"""Retrieve an existing sandbox by its ID.
|
|
240
|
+
|
|
241
|
+
Similar to Modal's Sandbox.from_id() method.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
sandbox_id: The unique identifier of the sandbox.
|
|
245
|
+
**kwargs: Additional arguments.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
A Sandbox instance connected to the existing sandbox.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
SandboxNotFoundError: If no sandbox with the given ID exists.
|
|
252
|
+
"""
|
|
253
|
+
# This is primarily for datalayer-runtime
|
|
254
|
+
from .remote.datalayer_sandbox import DatalayerSandbox
|
|
255
|
+
|
|
256
|
+
return DatalayerSandbox.from_id(sandbox_id, **kwargs)
|
|
257
|
+
|
|
258
|
+
@classmethod
|
|
259
|
+
def list(
|
|
260
|
+
cls,
|
|
261
|
+
tags: Optional[dict[str, str]] = None,
|
|
262
|
+
**kwargs,
|
|
263
|
+
) -> Iterator["Sandbox"]:
|
|
264
|
+
"""List all running sandboxes.
|
|
265
|
+
|
|
266
|
+
Similar to Modal's Sandbox.list() method.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
tags: Filter sandboxes by tags.
|
|
270
|
+
**kwargs: Additional filter arguments.
|
|
271
|
+
|
|
272
|
+
Yields:
|
|
273
|
+
Sandbox instances.
|
|
274
|
+
"""
|
|
275
|
+
from .remote.datalayer_sandbox import DatalayerSandbox
|
|
276
|
+
|
|
277
|
+
yield from DatalayerSandbox.list_all(tags=tags, **kwargs)
|
|
278
|
+
|
|
279
|
+
def __enter__(self) -> "Sandbox":
|
|
280
|
+
"""Context manager entry - starts the sandbox."""
|
|
281
|
+
self.start()
|
|
282
|
+
return self
|
|
283
|
+
|
|
284
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
285
|
+
"""Context manager exit - stops the sandbox."""
|
|
286
|
+
self.stop()
|
|
287
|
+
|
|
288
|
+
async def __aenter__(self) -> "Sandbox":
|
|
289
|
+
"""Async context manager entry."""
|
|
290
|
+
await self.start_async()
|
|
291
|
+
return self
|
|
292
|
+
|
|
293
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
294
|
+
"""Async context manager exit."""
|
|
295
|
+
await self.stop_async()
|
|
296
|
+
|
|
297
|
+
@abstractmethod
|
|
298
|
+
def start(self) -> None:
|
|
299
|
+
"""Start the sandbox.
|
|
300
|
+
|
|
301
|
+
Must be called before any code execution. Called automatically
|
|
302
|
+
when using the sandbox as a context manager.
|
|
303
|
+
"""
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
@abstractmethod
|
|
307
|
+
def stop(self) -> None:
|
|
308
|
+
"""Stop the sandbox and release resources.
|
|
309
|
+
|
|
310
|
+
Called automatically when exiting the context manager.
|
|
311
|
+
"""
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
async def start_async(self) -> None:
|
|
315
|
+
"""Async version of start(). Default implementation calls sync version."""
|
|
316
|
+
self.start()
|
|
317
|
+
|
|
318
|
+
async def stop_async(self) -> None:
|
|
319
|
+
"""Async version of stop(). Default implementation calls sync version."""
|
|
320
|
+
self.stop()
|
|
321
|
+
|
|
322
|
+
@abstractmethod
|
|
323
|
+
def run_code(
|
|
324
|
+
self,
|
|
325
|
+
code: str,
|
|
326
|
+
language: str = "python",
|
|
327
|
+
context: Optional[Context] = None,
|
|
328
|
+
on_stdout: Optional[OutputHandler[OutputMessage]] = None,
|
|
329
|
+
on_stderr: Optional[OutputHandler[OutputMessage]] = None,
|
|
330
|
+
on_result: Optional[OutputHandler[Result]] = None,
|
|
331
|
+
on_error: Optional[OutputHandler[ExecutionError]] = None,
|
|
332
|
+
envs: Optional[dict[str, str]] = None,
|
|
333
|
+
timeout: Optional[float] = None,
|
|
334
|
+
) -> Execution:
|
|
335
|
+
"""Execute code in the sandbox.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
code: The code to execute.
|
|
339
|
+
language: Programming language (default: "python").
|
|
340
|
+
context: Execution context for maintaining state. If not provided,
|
|
341
|
+
uses the default context.
|
|
342
|
+
on_stdout: Callback for stdout messages.
|
|
343
|
+
on_stderr: Callback for stderr messages.
|
|
344
|
+
on_result: Callback for results.
|
|
345
|
+
on_error: Callback for errors.
|
|
346
|
+
envs: Additional environment variables for this execution.
|
|
347
|
+
timeout: Timeout in seconds. Uses config default if not provided.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Execution result containing output, results, and any errors.
|
|
351
|
+
"""
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
async def run_code_async(
|
|
355
|
+
self,
|
|
356
|
+
code: str,
|
|
357
|
+
language: str = "python",
|
|
358
|
+
context: Optional[Context] = None,
|
|
359
|
+
on_stdout: Optional[OutputHandler[OutputMessage]] = None,
|
|
360
|
+
on_stderr: Optional[OutputHandler[OutputMessage]] = None,
|
|
361
|
+
on_result: Optional[OutputHandler[Result]] = None,
|
|
362
|
+
on_error: Optional[OutputHandler[ExecutionError]] = None,
|
|
363
|
+
envs: Optional[dict[str, str]] = None,
|
|
364
|
+
timeout: Optional[float] = None,
|
|
365
|
+
) -> Execution:
|
|
366
|
+
"""Async version of run_code(). Default implementation calls sync version."""
|
|
367
|
+
return self.run_code(
|
|
368
|
+
code=code,
|
|
369
|
+
language=language,
|
|
370
|
+
context=context,
|
|
371
|
+
on_stdout=on_stdout,
|
|
372
|
+
on_stderr=on_stderr,
|
|
373
|
+
on_result=on_result,
|
|
374
|
+
on_error=on_error,
|
|
375
|
+
envs=envs,
|
|
376
|
+
timeout=timeout,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def run_code_streaming(
|
|
380
|
+
self,
|
|
381
|
+
code: str,
|
|
382
|
+
language: str = "python",
|
|
383
|
+
context: Optional[Context] = None,
|
|
384
|
+
envs: Optional[dict[str, str]] = None,
|
|
385
|
+
timeout: Optional[float] = None,
|
|
386
|
+
) -> Iterator[Union[OutputMessage, Result, ExecutionError]]:
|
|
387
|
+
"""Execute code with streaming output.
|
|
388
|
+
|
|
389
|
+
Yields output messages, results, and errors as they are produced.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
code: The code to execute.
|
|
393
|
+
language: Programming language (default: "python").
|
|
394
|
+
context: Execution context for maintaining state.
|
|
395
|
+
envs: Additional environment variables.
|
|
396
|
+
timeout: Timeout in seconds.
|
|
397
|
+
|
|
398
|
+
Yields:
|
|
399
|
+
OutputMessage, Result, or ExecutionError objects.
|
|
400
|
+
"""
|
|
401
|
+
# Default implementation: run and yield all at once
|
|
402
|
+
execution = self.run_code(
|
|
403
|
+
code=code,
|
|
404
|
+
language=language,
|
|
405
|
+
context=context,
|
|
406
|
+
envs=envs,
|
|
407
|
+
timeout=timeout,
|
|
408
|
+
)
|
|
409
|
+
for msg in execution.logs.stdout:
|
|
410
|
+
yield msg
|
|
411
|
+
for msg in execution.logs.stderr:
|
|
412
|
+
yield msg
|
|
413
|
+
for result in execution.results:
|
|
414
|
+
yield result
|
|
415
|
+
if execution.error:
|
|
416
|
+
yield execution.error
|
|
417
|
+
|
|
418
|
+
async def run_code_streaming_async(
|
|
419
|
+
self,
|
|
420
|
+
code: str,
|
|
421
|
+
language: str = "python",
|
|
422
|
+
context: Optional[Context] = None,
|
|
423
|
+
envs: Optional[dict[str, str]] = None,
|
|
424
|
+
timeout: Optional[float] = None,
|
|
425
|
+
) -> AsyncIterator[Union[OutputMessage, Result, ExecutionError]]:
|
|
426
|
+
"""Async version of run_code_streaming()."""
|
|
427
|
+
execution = await self.run_code_async(
|
|
428
|
+
code=code,
|
|
429
|
+
language=language,
|
|
430
|
+
context=context,
|
|
431
|
+
envs=envs,
|
|
432
|
+
timeout=timeout,
|
|
433
|
+
)
|
|
434
|
+
for msg in execution.logs.stdout:
|
|
435
|
+
yield msg
|
|
436
|
+
for msg in execution.logs.stderr:
|
|
437
|
+
yield msg
|
|
438
|
+
for result in execution.results:
|
|
439
|
+
yield result
|
|
440
|
+
if execution.error:
|
|
441
|
+
yield execution.error
|
|
442
|
+
|
|
443
|
+
def create_context(self, name: Optional[str] = None) -> Context:
|
|
444
|
+
"""Create a new execution context.
|
|
445
|
+
|
|
446
|
+
A context maintains state (variables, imports, etc.) between executions.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
name: Optional name for the context. Auto-generated if not provided.
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
A new Context object.
|
|
453
|
+
"""
|
|
454
|
+
context_id = name or str(uuid.uuid4())
|
|
455
|
+
return Context(id=context_id, language="python", cwd=self.config.working_dir)
|
|
456
|
+
|
|
457
|
+
def get_variable(self, name: str, context: Optional[Context] = None) -> Any:
|
|
458
|
+
"""Get a variable from the sandbox.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
name: Name of the variable to retrieve.
|
|
462
|
+
context: Context to get the variable from. Uses default if not provided.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
The value of the variable.
|
|
466
|
+
|
|
467
|
+
Raises:
|
|
468
|
+
VariableNotFoundError: If the variable doesn't exist.
|
|
469
|
+
"""
|
|
470
|
+
# Default implementation using code execution
|
|
471
|
+
execution = self.run_code(f"__result__ = {name}", context=context)
|
|
472
|
+
if execution.error:
|
|
473
|
+
from .exceptions import VariableNotFoundError
|
|
474
|
+
|
|
475
|
+
raise VariableNotFoundError(name)
|
|
476
|
+
return self._get_internal_variable("__result__", context)
|
|
477
|
+
|
|
478
|
+
def set_variable(self, name: str, value: Any, context: Optional[Context] = None) -> None:
|
|
479
|
+
"""Set a variable in the sandbox.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
name: Name of the variable to set.
|
|
483
|
+
value: Value to assign.
|
|
484
|
+
context: Context to set the variable in. Uses default if not provided.
|
|
485
|
+
"""
|
|
486
|
+
self._set_internal_variable(name, value, context)
|
|
487
|
+
|
|
488
|
+
def set_variables(
|
|
489
|
+
self, variables: dict[str, Any], context: Optional[Context] = None
|
|
490
|
+
) -> None:
|
|
491
|
+
"""Set multiple variables in the sandbox.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
variables: Dictionary of variable names to values.
|
|
495
|
+
context: Context to set variables in. Uses default if not provided.
|
|
496
|
+
"""
|
|
497
|
+
for name, value in variables.items():
|
|
498
|
+
self.set_variable(name, value, context)
|
|
499
|
+
|
|
500
|
+
@abstractmethod
|
|
501
|
+
def _get_internal_variable(self, name: str, context: Optional[Context] = None) -> Any:
|
|
502
|
+
"""Internal method to get a variable. Must be implemented by subclasses."""
|
|
503
|
+
pass
|
|
504
|
+
|
|
505
|
+
@abstractmethod
|
|
506
|
+
def _set_internal_variable(
|
|
507
|
+
self, name: str, value: Any, context: Optional[Context] = None
|
|
508
|
+
) -> None:
|
|
509
|
+
"""Internal method to set a variable. Must be implemented by subclasses."""
|
|
510
|
+
pass
|
|
511
|
+
|
|
512
|
+
def install_packages(
|
|
513
|
+
self, packages: list[str], timeout: Optional[float] = None
|
|
514
|
+
) -> Execution:
|
|
515
|
+
"""Install Python packages in the sandbox.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
packages: List of package names to install.
|
|
519
|
+
timeout: Timeout in seconds.
|
|
520
|
+
|
|
521
|
+
Returns:
|
|
522
|
+
Execution result from the installation.
|
|
523
|
+
"""
|
|
524
|
+
install_cmd = f"import subprocess; subprocess.run(['pip', 'install'] + {packages!r}, check=True)"
|
|
525
|
+
return self.run_code(install_cmd, timeout=timeout or 300)
|
|
526
|
+
|
|
527
|
+
def upload_file(self, local_path: str, remote_path: str) -> None:
|
|
528
|
+
"""Upload a file to the sandbox.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
local_path: Path to the local file.
|
|
532
|
+
remote_path: Destination path in the sandbox.
|
|
533
|
+
"""
|
|
534
|
+
with open(local_path, "rb") as f:
|
|
535
|
+
content = f.read()
|
|
536
|
+
self._write_file(remote_path, content)
|
|
537
|
+
|
|
538
|
+
def download_file(self, remote_path: str, local_path: str) -> None:
|
|
539
|
+
"""Download a file from the sandbox.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
remote_path: Path to the file in the sandbox.
|
|
543
|
+
local_path: Destination path on the local filesystem.
|
|
544
|
+
"""
|
|
545
|
+
content = self._read_file(remote_path)
|
|
546
|
+
with open(local_path, "wb") as f:
|
|
547
|
+
f.write(content)
|
|
548
|
+
|
|
549
|
+
def _write_file(self, path: str, content: bytes) -> None:
|
|
550
|
+
"""Write a file in the sandbox. Override in subclasses for better performance."""
|
|
551
|
+
import base64
|
|
552
|
+
|
|
553
|
+
encoded = base64.b64encode(content).decode("utf-8")
|
|
554
|
+
code = f"""
|
|
555
|
+
import base64
|
|
556
|
+
with open({path!r}, 'wb') as f:
|
|
557
|
+
f.write(base64.b64decode({encoded!r}))
|
|
558
|
+
"""
|
|
559
|
+
self.run_code(code)
|
|
560
|
+
|
|
561
|
+
def _read_file(self, path: str) -> bytes:
|
|
562
|
+
"""Read a file from the sandbox. Override in subclasses for better performance."""
|
|
563
|
+
import base64
|
|
564
|
+
|
|
565
|
+
code = f"""
|
|
566
|
+
import base64
|
|
567
|
+
with open({path!r}, 'rb') as f:
|
|
568
|
+
__file_content__ = base64.b64encode(f.read()).decode('utf-8')
|
|
569
|
+
"""
|
|
570
|
+
self.run_code(code)
|
|
571
|
+
encoded = self.get_variable("__file_content__")
|
|
572
|
+
return base64.b64decode(encoded)
|