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,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)