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,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)
@@ -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})"
@@ -0,0 +1,9 @@
1
+ # Copyright (c) 2025-2026 Datalayer, Inc.
2
+ #
3
+ # BSD 3-Clause License
4
+
5
+ """Remote sandbox implementations."""
6
+
7
+ from .datalayer_sandbox import DatalayerSandbox
8
+
9
+ __all__ = ["DatalayerSandbox"]