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,627 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
2
|
+
#
|
|
3
|
+
# BSD 3-Clause License
|
|
4
|
+
|
|
5
|
+
"""Datalayer Runtime-based sandbox implementation.
|
|
6
|
+
|
|
7
|
+
This sandbox uses the Datalayer platform for cloud-based code execution,
|
|
8
|
+
providing full isolation and scalable compute resources.
|
|
9
|
+
|
|
10
|
+
Inspired by E2B and Modal sandbox APIs.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import Any, Iterator, Optional
|
|
16
|
+
|
|
17
|
+
from ..base import Sandbox
|
|
18
|
+
from ..exceptions import (
|
|
19
|
+
SandboxConfigurationError,
|
|
20
|
+
SandboxConnectionError,
|
|
21
|
+
SandboxNotStartedError,
|
|
22
|
+
SandboxSnapshotError,
|
|
23
|
+
)
|
|
24
|
+
from ..models import (
|
|
25
|
+
Context,
|
|
26
|
+
Execution,
|
|
27
|
+
ExecutionError,
|
|
28
|
+
Logs,
|
|
29
|
+
OutputHandler,
|
|
30
|
+
OutputMessage,
|
|
31
|
+
ResourceConfig,
|
|
32
|
+
Result,
|
|
33
|
+
SandboxConfig,
|
|
34
|
+
SandboxInfo,
|
|
35
|
+
SandboxStatus,
|
|
36
|
+
SnapshotInfo,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DatalayerSandbox(Sandbox):
|
|
41
|
+
"""A sandbox using Datalayer Runtime for cloud-based code execution.
|
|
42
|
+
|
|
43
|
+
This sandbox provides full isolation, scalable compute (CPU/GPU),
|
|
44
|
+
and supports snapshots for state persistence.
|
|
45
|
+
|
|
46
|
+
Inspired by E2B Code Interpreter and Modal Sandbox APIs:
|
|
47
|
+
- E2B-like: Simple creation, timeout management, file operations
|
|
48
|
+
- Modal-like: GPU support, exec, snapshots, tagging
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
from code_sandboxes import Sandbox
|
|
52
|
+
|
|
53
|
+
# Simple E2B-style usage
|
|
54
|
+
with Sandbox.create(timeout=60) as sandbox:
|
|
55
|
+
result = sandbox.run_code("print('Hello!')")
|
|
56
|
+
files = sandbox.files.list("/")
|
|
57
|
+
|
|
58
|
+
# Modal-style with GPU
|
|
59
|
+
with Sandbox.create(gpu="T4", environment="python-gpu-env") as sandbox:
|
|
60
|
+
sandbox.run_code("import torch; print(torch.cuda.is_available())")
|
|
61
|
+
|
|
62
|
+
# With explicit API key
|
|
63
|
+
with Sandbox.create(token="your-api-key") as sandbox:
|
|
64
|
+
sandbox.run_code("x = 1 + 1")
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
client: The Datalayer client instance.
|
|
68
|
+
runtime: The runtime service instance.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
config: Optional[SandboxConfig] = None,
|
|
74
|
+
token: Optional[str] = None,
|
|
75
|
+
run_url: Optional[str] = None,
|
|
76
|
+
snapshot_name: Optional[str] = None,
|
|
77
|
+
**kwargs,
|
|
78
|
+
):
|
|
79
|
+
"""Initialize the Datalayer sandbox.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
config: Sandbox configuration.
|
|
83
|
+
token: Datalayer API token. If not provided, uses DATALAYER_API_KEY
|
|
84
|
+
environment variable.
|
|
85
|
+
run_url: Datalayer server URL. If not provided, uses default.
|
|
86
|
+
snapshot_name: Name of snapshot to restore from (optional).
|
|
87
|
+
**kwargs: Additional arguments passed to DatalayerClient.
|
|
88
|
+
"""
|
|
89
|
+
super().__init__(config)
|
|
90
|
+
self._token = token
|
|
91
|
+
self._run_url = run_url
|
|
92
|
+
self._snapshot_name = snapshot_name
|
|
93
|
+
self._client = None
|
|
94
|
+
self._runtime = None
|
|
95
|
+
self._sandbox_id = str(uuid.uuid4())
|
|
96
|
+
self._extra_kwargs = kwargs
|
|
97
|
+
self._end_at: Optional[float] = None
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def client(self):
|
|
101
|
+
"""Get the Datalayer client instance."""
|
|
102
|
+
return self._client
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def runtime(self):
|
|
106
|
+
"""Get the runtime service instance."""
|
|
107
|
+
return self._runtime
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def object_id(self) -> str:
|
|
111
|
+
"""Get the sandbox object ID (Modal-style)."""
|
|
112
|
+
return self._sandbox_id
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_id(cls, sandbox_id: str, **kwargs) -> "DatalayerSandbox":
|
|
116
|
+
"""Retrieve an existing sandbox by its ID.
|
|
117
|
+
|
|
118
|
+
Similar to Modal's Sandbox.from_id() method.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
sandbox_id: The unique identifier of the sandbox.
|
|
122
|
+
**kwargs: Additional arguments (token, run_url).
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
A DatalayerSandbox instance connected to the existing runtime.
|
|
126
|
+
"""
|
|
127
|
+
# Create a new sandbox instance with the existing ID
|
|
128
|
+
sandbox = cls(**kwargs)
|
|
129
|
+
sandbox._sandbox_id = sandbox_id
|
|
130
|
+
# Connect to existing runtime - this would need runtime lookup
|
|
131
|
+
# For now, this is a placeholder that would need datalayer_core support
|
|
132
|
+
return sandbox
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def list_all(
|
|
136
|
+
cls,
|
|
137
|
+
tags: Optional[dict[str, str]] = None,
|
|
138
|
+
token: Optional[str] = None,
|
|
139
|
+
run_url: Optional[str] = None,
|
|
140
|
+
) -> Iterator["DatalayerSandbox"]:
|
|
141
|
+
"""List all running sandboxes.
|
|
142
|
+
|
|
143
|
+
Similar to Modal's Sandbox.list() method.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
tags: Filter sandboxes by tags.
|
|
147
|
+
token: API token for authentication.
|
|
148
|
+
run_url: Datalayer server URL.
|
|
149
|
+
|
|
150
|
+
Yields:
|
|
151
|
+
DatalayerSandbox instances.
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
from datalayer_core import DatalayerClient
|
|
155
|
+
from datalayer_core.utils.urls import DatalayerURLs
|
|
156
|
+
except ImportError:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
if run_url:
|
|
161
|
+
urls = DatalayerURLs.from_run_url(run_url)
|
|
162
|
+
client = DatalayerClient(urls=urls, token=token)
|
|
163
|
+
else:
|
|
164
|
+
client = DatalayerClient(token=token)
|
|
165
|
+
|
|
166
|
+
runtimes = client.list_runtimes()
|
|
167
|
+
|
|
168
|
+
for runtime in runtimes:
|
|
169
|
+
sandbox = cls(token=token, run_url=run_url)
|
|
170
|
+
sandbox._client = client
|
|
171
|
+
sandbox._runtime = runtime
|
|
172
|
+
sandbox._sandbox_id = runtime.uid or str(uuid.uuid4())
|
|
173
|
+
sandbox._started = True
|
|
174
|
+
sandbox._info = SandboxInfo(
|
|
175
|
+
id=sandbox._sandbox_id,
|
|
176
|
+
variant="datalayer-runtime",
|
|
177
|
+
status=SandboxStatus.RUNNING,
|
|
178
|
+
created_at=time.time(),
|
|
179
|
+
name=runtime.name,
|
|
180
|
+
)
|
|
181
|
+
yield sandbox
|
|
182
|
+
except Exception:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
def start(self) -> None:
|
|
186
|
+
"""Start the sandbox by creating a Datalayer runtime.
|
|
187
|
+
|
|
188
|
+
Similar to E2B's sandbox creation with timeout support.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
SandboxConfigurationError: If configuration is invalid.
|
|
192
|
+
SandboxConnectionError: If connection to Datalayer fails.
|
|
193
|
+
"""
|
|
194
|
+
if self._started:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
# Import here to avoid hard dependency
|
|
199
|
+
from datalayer_core import DatalayerClient
|
|
200
|
+
from datalayer_core.utils.urls import DatalayerURLs
|
|
201
|
+
except ImportError as e:
|
|
202
|
+
raise SandboxConfigurationError(
|
|
203
|
+
"datalayer_core package is required for DatalayerSandbox. "
|
|
204
|
+
"Install it with: pip install datalayer_core"
|
|
205
|
+
) from e
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
# Create client with optional custom URL
|
|
209
|
+
if self._run_url:
|
|
210
|
+
urls = DatalayerURLs.from_run_url(self._run_url)
|
|
211
|
+
self._client = DatalayerClient(urls=urls, token=self._token)
|
|
212
|
+
else:
|
|
213
|
+
self._client = DatalayerClient(token=self._token)
|
|
214
|
+
|
|
215
|
+
# Calculate time reservation from max_lifetime config
|
|
216
|
+
# Convert seconds to minutes, minimum 10 minutes
|
|
217
|
+
lifetime_minutes = int(self.config.max_lifetime / 60)
|
|
218
|
+
time_reservation = max(10, min(lifetime_minutes, 1440)) # Max 24 hours
|
|
219
|
+
|
|
220
|
+
# Determine environment based on GPU config
|
|
221
|
+
environment = self.config.environment
|
|
222
|
+
if self.config.gpu and "gpu" not in environment.lower():
|
|
223
|
+
# Try to use a GPU environment if GPU is requested
|
|
224
|
+
environment = "python-gpu-env"
|
|
225
|
+
|
|
226
|
+
# Build sandbox name
|
|
227
|
+
sandbox_name = self.config.name or f"sandbox-{self._sandbox_id[:8]}"
|
|
228
|
+
|
|
229
|
+
# Create the runtime (optionally from snapshot)
|
|
230
|
+
if self._snapshot_name:
|
|
231
|
+
self._runtime = self._client.create_runtime(
|
|
232
|
+
name=sandbox_name,
|
|
233
|
+
environment=environment,
|
|
234
|
+
time_reservation=time_reservation,
|
|
235
|
+
snapshot_name=self._snapshot_name,
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
self._runtime = self._client.create_runtime(
|
|
239
|
+
name=sandbox_name,
|
|
240
|
+
environment=environment,
|
|
241
|
+
time_reservation=time_reservation,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Start the runtime
|
|
245
|
+
self._runtime._start()
|
|
246
|
+
|
|
247
|
+
self._default_context = self.create_context("default")
|
|
248
|
+
|
|
249
|
+
# Calculate end time
|
|
250
|
+
self._created_at = time.time()
|
|
251
|
+
self._end_at = self._created_at + self.config.max_lifetime
|
|
252
|
+
|
|
253
|
+
# Build resource config
|
|
254
|
+
resources = None
|
|
255
|
+
if self.config.gpu or self.config.cpu_limit or self.config.memory_limit:
|
|
256
|
+
resources = ResourceConfig(
|
|
257
|
+
cpu=self.config.cpu_limit,
|
|
258
|
+
memory=self.config.memory_limit // (1024 * 1024) if self.config.memory_limit else None,
|
|
259
|
+
gpu=self.config.gpu,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
self._info = SandboxInfo(
|
|
263
|
+
id=self._sandbox_id,
|
|
264
|
+
variant="datalayer-runtime",
|
|
265
|
+
status=SandboxStatus.RUNNING,
|
|
266
|
+
created_at=self._created_at,
|
|
267
|
+
end_at=self._end_at,
|
|
268
|
+
config=self.config,
|
|
269
|
+
name=sandbox_name,
|
|
270
|
+
resources=resources,
|
|
271
|
+
)
|
|
272
|
+
self._started = True
|
|
273
|
+
|
|
274
|
+
except Exception as e:
|
|
275
|
+
url = self._run_url or "default"
|
|
276
|
+
raise SandboxConnectionError(url, str(e)) from e
|
|
277
|
+
|
|
278
|
+
def stop(self) -> None:
|
|
279
|
+
"""Stop the sandbox and release the Datalayer runtime.
|
|
280
|
+
|
|
281
|
+
Similar to E2B's kill() and Modal's terminate().
|
|
282
|
+
"""
|
|
283
|
+
if not self._started:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
if self._runtime:
|
|
288
|
+
self._runtime._stop()
|
|
289
|
+
except Exception:
|
|
290
|
+
pass # Best effort cleanup
|
|
291
|
+
|
|
292
|
+
self._runtime = None
|
|
293
|
+
self._client = None
|
|
294
|
+
self._started = False
|
|
295
|
+
if self._info:
|
|
296
|
+
self._info.status = SandboxStatus.STOPPED
|
|
297
|
+
|
|
298
|
+
# Alias for Modal compatibility
|
|
299
|
+
def terminate(self) -> None:
|
|
300
|
+
"""Terminate the sandbox. Alias for stop()."""
|
|
301
|
+
self.stop()
|
|
302
|
+
|
|
303
|
+
# Alias for E2B compatibility
|
|
304
|
+
def kill(self) -> None:
|
|
305
|
+
"""Kill the sandbox. Alias for stop()."""
|
|
306
|
+
self.stop()
|
|
307
|
+
|
|
308
|
+
def set_timeout(self, timeout_seconds: float) -> None:
|
|
309
|
+
"""Change the sandbox timeout during runtime.
|
|
310
|
+
|
|
311
|
+
Similar to E2B's set_timeout method. Resets the timeout to the new value.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
timeout_seconds: New timeout in seconds from now.
|
|
315
|
+
"""
|
|
316
|
+
if not self._started:
|
|
317
|
+
raise SandboxNotStartedError()
|
|
318
|
+
|
|
319
|
+
self._end_at = time.time() + timeout_seconds
|
|
320
|
+
if self._info:
|
|
321
|
+
self._info.end_at = self._end_at
|
|
322
|
+
|
|
323
|
+
def get_info(self) -> SandboxInfo:
|
|
324
|
+
"""Retrieve sandbox information.
|
|
325
|
+
|
|
326
|
+
Similar to E2B's getInfo() method.
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
SandboxInfo object with current sandbox state.
|
|
330
|
+
"""
|
|
331
|
+
if self._info:
|
|
332
|
+
return self._info
|
|
333
|
+
return SandboxInfo(
|
|
334
|
+
id=self._sandbox_id,
|
|
335
|
+
variant="datalayer-runtime",
|
|
336
|
+
status=SandboxStatus.PENDING if not self._started else SandboxStatus.RUNNING,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def wait(self, raise_on_termination: bool = True) -> None:
|
|
340
|
+
"""Wait for the sandbox to finish.
|
|
341
|
+
|
|
342
|
+
Similar to Modal's wait() method.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
raise_on_termination: Whether to raise if sandbox terminates with error.
|
|
346
|
+
"""
|
|
347
|
+
# For cloud sandboxes, this would wait for the runtime to complete
|
|
348
|
+
# Currently just a placeholder
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
def poll(self) -> Optional[int]:
|
|
352
|
+
"""Check if the sandbox has finished running.
|
|
353
|
+
|
|
354
|
+
Similar to Modal's poll() method.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
None if still running, exit code otherwise.
|
|
358
|
+
"""
|
|
359
|
+
if self._started:
|
|
360
|
+
return None
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
def run_code(
|
|
364
|
+
self,
|
|
365
|
+
code: str,
|
|
366
|
+
language: str = "python",
|
|
367
|
+
context: Optional[Context] = None,
|
|
368
|
+
on_stdout: Optional[OutputHandler[OutputMessage]] = None,
|
|
369
|
+
on_stderr: Optional[OutputHandler[OutputMessage]] = None,
|
|
370
|
+
on_result: Optional[OutputHandler[Result]] = None,
|
|
371
|
+
on_error: Optional[OutputHandler[ExecutionError]] = None,
|
|
372
|
+
envs: Optional[dict[str, str]] = None,
|
|
373
|
+
timeout: Optional[float] = None,
|
|
374
|
+
) -> Execution:
|
|
375
|
+
"""Execute code in the Datalayer runtime.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
code: The code to execute.
|
|
379
|
+
language: Programming language (default: "python").
|
|
380
|
+
context: Execution context (currently not used, runtime maintains state).
|
|
381
|
+
on_stdout: Callback for stdout messages.
|
|
382
|
+
on_stderr: Callback for stderr messages.
|
|
383
|
+
on_result: Callback for results.
|
|
384
|
+
on_error: Callback for errors.
|
|
385
|
+
envs: Environment variables (set before execution).
|
|
386
|
+
timeout: Timeout in seconds.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Execution result.
|
|
390
|
+
|
|
391
|
+
Raises:
|
|
392
|
+
SandboxNotStartedError: If the sandbox hasn't been started.
|
|
393
|
+
"""
|
|
394
|
+
if not self._started or not self._runtime:
|
|
395
|
+
raise SandboxNotStartedError()
|
|
396
|
+
|
|
397
|
+
# Set environment variables if provided
|
|
398
|
+
if envs:
|
|
399
|
+
env_code = "\n".join(
|
|
400
|
+
f"import os; os.environ[{k!r}] = {v!r}" for k, v in envs.items()
|
|
401
|
+
)
|
|
402
|
+
self._runtime.execute(env_code)
|
|
403
|
+
|
|
404
|
+
# Execute the code
|
|
405
|
+
execution_timeout = timeout or self.config.timeout
|
|
406
|
+
try:
|
|
407
|
+
response = self._runtime.execute(code, timeout=execution_timeout)
|
|
408
|
+
except Exception as e:
|
|
409
|
+
error = ExecutionError(
|
|
410
|
+
name=type(e).__name__,
|
|
411
|
+
value=str(e),
|
|
412
|
+
traceback="",
|
|
413
|
+
)
|
|
414
|
+
if on_error:
|
|
415
|
+
on_error(error)
|
|
416
|
+
return Execution(
|
|
417
|
+
results=[],
|
|
418
|
+
logs=Logs(),
|
|
419
|
+
error=error,
|
|
420
|
+
execution_count=0,
|
|
421
|
+
context_id=context.id if context else "default",
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# Parse the response
|
|
425
|
+
stdout_messages: list[OutputMessage] = []
|
|
426
|
+
stderr_messages: list[OutputMessage] = []
|
|
427
|
+
results: list[Result] = []
|
|
428
|
+
error: Optional[ExecutionError] = None
|
|
429
|
+
|
|
430
|
+
current_time = time.time()
|
|
431
|
+
|
|
432
|
+
# Process stdout
|
|
433
|
+
if hasattr(response, "stdout") and response.stdout:
|
|
434
|
+
for line in response.stdout.splitlines():
|
|
435
|
+
msg = OutputMessage(line=line, timestamp=current_time, error=False)
|
|
436
|
+
stdout_messages.append(msg)
|
|
437
|
+
if on_stdout:
|
|
438
|
+
on_stdout(msg)
|
|
439
|
+
|
|
440
|
+
# Process stderr
|
|
441
|
+
if hasattr(response, "stderr") and response.stderr:
|
|
442
|
+
for line in response.stderr.splitlines():
|
|
443
|
+
msg = OutputMessage(line=line, timestamp=current_time, error=True)
|
|
444
|
+
stderr_messages.append(msg)
|
|
445
|
+
if on_stderr:
|
|
446
|
+
on_stderr(msg)
|
|
447
|
+
|
|
448
|
+
# Process results
|
|
449
|
+
if hasattr(response, "result") and response.result is not None:
|
|
450
|
+
result = Result(
|
|
451
|
+
data={"text/plain": str(response.result)},
|
|
452
|
+
is_main_result=True,
|
|
453
|
+
)
|
|
454
|
+
results.append(result)
|
|
455
|
+
if on_result:
|
|
456
|
+
on_result(result)
|
|
457
|
+
|
|
458
|
+
# Process display data (rich output)
|
|
459
|
+
if hasattr(response, "display_data") and response.display_data:
|
|
460
|
+
for display in response.display_data:
|
|
461
|
+
result = Result(
|
|
462
|
+
data=display.get("data", {}),
|
|
463
|
+
is_main_result=False,
|
|
464
|
+
extra=display.get("metadata", {}),
|
|
465
|
+
)
|
|
466
|
+
results.append(result)
|
|
467
|
+
if on_result:
|
|
468
|
+
on_result(result)
|
|
469
|
+
|
|
470
|
+
# Process errors
|
|
471
|
+
if hasattr(response, "error") and response.error:
|
|
472
|
+
error = ExecutionError(
|
|
473
|
+
name=response.error.get("ename", "Error"),
|
|
474
|
+
value=response.error.get("evalue", ""),
|
|
475
|
+
traceback="\n".join(response.error.get("traceback", [])),
|
|
476
|
+
)
|
|
477
|
+
if on_error:
|
|
478
|
+
on_error(error)
|
|
479
|
+
|
|
480
|
+
return Execution(
|
|
481
|
+
results=results,
|
|
482
|
+
logs=Logs(stdout=stdout_messages, stderr=stderr_messages),
|
|
483
|
+
error=error,
|
|
484
|
+
execution_count=getattr(response, "execution_count", 0),
|
|
485
|
+
context_id=context.id if context else "default",
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
def _get_internal_variable(self, name: str, context: Optional[Context] = None) -> Any:
|
|
489
|
+
"""Get a variable from the runtime.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
name: Variable name.
|
|
493
|
+
context: Context (not used, runtime maintains single namespace).
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
The variable value.
|
|
497
|
+
"""
|
|
498
|
+
if not self._started or not self._runtime:
|
|
499
|
+
raise SandboxNotStartedError()
|
|
500
|
+
|
|
501
|
+
return self._runtime.get_variable(name)
|
|
502
|
+
|
|
503
|
+
def _set_internal_variable(
|
|
504
|
+
self, name: str, value: Any, context: Optional[Context] = None
|
|
505
|
+
) -> None:
|
|
506
|
+
"""Set a variable in the runtime.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
name: Variable name.
|
|
510
|
+
value: Value to set.
|
|
511
|
+
context: Context (not used, runtime maintains single namespace).
|
|
512
|
+
"""
|
|
513
|
+
if not self._started or not self._runtime:
|
|
514
|
+
raise SandboxNotStartedError()
|
|
515
|
+
|
|
516
|
+
self._runtime.set_variable(name, value)
|
|
517
|
+
|
|
518
|
+
def create_snapshot(
|
|
519
|
+
self,
|
|
520
|
+
name: str,
|
|
521
|
+
description: str = "",
|
|
522
|
+
) -> SnapshotInfo:
|
|
523
|
+
"""Create a snapshot of the current runtime state.
|
|
524
|
+
|
|
525
|
+
Similar to Modal's snapshot_filesystem feature. This allows saving
|
|
526
|
+
the current state of the sandbox for later restoration.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
name: Name for the snapshot.
|
|
530
|
+
description: Optional description.
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
SnapshotInfo with the snapshot details.
|
|
534
|
+
|
|
535
|
+
Raises:
|
|
536
|
+
SandboxNotStartedError: If sandbox is not running.
|
|
537
|
+
SandboxSnapshotError: If snapshot creation fails.
|
|
538
|
+
"""
|
|
539
|
+
if not self._started or not self._runtime:
|
|
540
|
+
raise SandboxNotStartedError()
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
snapshot = self._runtime.create_snapshot(name=name, description=description)
|
|
544
|
+
return SnapshotInfo(
|
|
545
|
+
id=snapshot.uid,
|
|
546
|
+
name=name,
|
|
547
|
+
sandbox_id=self._sandbox_id,
|
|
548
|
+
created_at=time.time(),
|
|
549
|
+
description=description,
|
|
550
|
+
)
|
|
551
|
+
except Exception as e:
|
|
552
|
+
raise SandboxSnapshotError("create", str(e)) from e
|
|
553
|
+
|
|
554
|
+
def list_snapshots(self) -> list[SnapshotInfo]:
|
|
555
|
+
"""List all snapshots.
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
List of SnapshotInfo objects.
|
|
559
|
+
"""
|
|
560
|
+
if not self._client:
|
|
561
|
+
return []
|
|
562
|
+
|
|
563
|
+
try:
|
|
564
|
+
snapshots = self._client.list_snapshots()
|
|
565
|
+
return [
|
|
566
|
+
SnapshotInfo(
|
|
567
|
+
id=s.uid,
|
|
568
|
+
name=s.name,
|
|
569
|
+
sandbox_id="",
|
|
570
|
+
created_at=getattr(s, "created_at", 0),
|
|
571
|
+
description=getattr(s, "description", ""),
|
|
572
|
+
)
|
|
573
|
+
for s in snapshots
|
|
574
|
+
]
|
|
575
|
+
except Exception:
|
|
576
|
+
return []
|
|
577
|
+
|
|
578
|
+
def install_packages(
|
|
579
|
+
self, packages: list[str], timeout: Optional[float] = None
|
|
580
|
+
) -> Execution:
|
|
581
|
+
"""Install Python packages in the runtime.
|
|
582
|
+
|
|
583
|
+
Uses pip to install packages. Similar to E2B's package installation.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
packages: List of package names to install.
|
|
587
|
+
timeout: Timeout in seconds.
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Execution result from the installation.
|
|
591
|
+
|
|
592
|
+
Example:
|
|
593
|
+
sandbox.install_packages(["pandas", "numpy", "matplotlib"])
|
|
594
|
+
"""
|
|
595
|
+
# Use %pip magic for better Jupyter integration
|
|
596
|
+
pip_cmd = f"%pip install {' '.join(packages)}"
|
|
597
|
+
return self.run_code(pip_cmd, timeout=timeout or 300)
|
|
598
|
+
|
|
599
|
+
def install_requirements(
|
|
600
|
+
self, requirements_path: str, timeout: Optional[float] = None
|
|
601
|
+
) -> Execution:
|
|
602
|
+
"""Install packages from a requirements file.
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
requirements_path: Path to requirements.txt file in the sandbox.
|
|
606
|
+
timeout: Timeout in seconds.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Execution result from the installation.
|
|
610
|
+
"""
|
|
611
|
+
pip_cmd = f"%pip install -r {requirements_path}"
|
|
612
|
+
return self.run_code(pip_cmd, timeout=timeout or 300)
|
|
613
|
+
|
|
614
|
+
def open_file(self, path: str, mode: str = "r") -> "SandboxFileHandle":
|
|
615
|
+
"""Open a file in the sandbox.
|
|
616
|
+
|
|
617
|
+
Similar to Modal's sandbox.open() method.
|
|
618
|
+
|
|
619
|
+
Args:
|
|
620
|
+
path: Path to the file.
|
|
621
|
+
mode: File mode ('r', 'w', 'rb', 'wb', 'a').
|
|
622
|
+
|
|
623
|
+
Returns:
|
|
624
|
+
SandboxFileHandle for file operations.
|
|
625
|
+
"""
|
|
626
|
+
from ..filesystem import SandboxFileHandle
|
|
627
|
+
return SandboxFileHandle(self, path, mode)
|