watasu-code-interpreter 0.1.47__tar.gz

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,10 @@
1
+ .pytest_cache/
2
+ **/__pycache__/
3
+ *.py[cod]
4
+ dist/
5
+ *.egg-info/
6
+
7
+ ts/node_modules/
8
+ ts/dist/
9
+
10
+ rust/target/
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: watasu-code-interpreter
3
+ Version: 0.1.47
4
+ Summary: Code Interpreter SDK for Watasu
5
+ Project-URL: Repository, https://github.com/watasuio/sdk
6
+ License-Expression: MIT OR Apache-2.0
7
+ Requires-Python: >=3.9
8
+ Requires-Dist: watasu<0.2,>=0.1.47
9
+ Description-Content-Type: text/markdown
10
+
11
+ # Watasu Code Interpreter Python SDK
12
+
13
+ Python package for Watasu code interpreter sandboxes.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install watasu-code-interpreter
19
+ ```
20
+
21
+ Set `WATASU_API_KEY` before using the SDK.
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ from watasu_code_interpreter import Sandbox
27
+
28
+ with Sandbox.create() as sbx:
29
+ context = sbx.create_code_context()
30
+ execution = sbx.run_code(
31
+ "print('hello')\n2 + 3",
32
+ context=context,
33
+ on_stdout=lambda message: print(message.line),
34
+ )
35
+
36
+ print(execution.text)
37
+ sbx.remove_code_context(context)
38
+ ```
39
+
40
+ `Sandbox.create()` starts the `code-interpreter` template by default. Code runs
41
+ in persistent contexts and returns structured `results`, `logs`, and `error`
42
+ fields for each execution.
43
+
44
+ The package installs `watasu` and exposes the `watasu_code_interpreter` import
45
+ path.
@@ -0,0 +1,35 @@
1
+ # Watasu Code Interpreter Python SDK
2
+
3
+ Python package for Watasu code interpreter sandboxes.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install watasu-code-interpreter
9
+ ```
10
+
11
+ Set `WATASU_API_KEY` before using the SDK.
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from watasu_code_interpreter import Sandbox
17
+
18
+ with Sandbox.create() as sbx:
19
+ context = sbx.create_code_context()
20
+ execution = sbx.run_code(
21
+ "print('hello')\n2 + 3",
22
+ context=context,
23
+ on_stdout=lambda message: print(message.line),
24
+ )
25
+
26
+ print(execution.text)
27
+ sbx.remove_code_context(context)
28
+ ```
29
+
30
+ `Sandbox.create()` starts the `code-interpreter` template by default. Code runs
31
+ in persistent contexts and returns structured `results`, `logs`, and `error`
32
+ fields for each execution.
33
+
34
+ The package installs `watasu` and exposes the `watasu_code_interpreter` import
35
+ path.
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "watasu-code-interpreter"
7
+ version = "0.1.47"
8
+ description = "Code Interpreter SDK for Watasu"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT OR Apache-2.0"
12
+ dependencies = [
13
+ "watasu>=0.1.47,<0.2",
14
+ ]
15
+
16
+ [project.urls]
17
+ Repository = "https://github.com/watasuio/sdk"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/watasu_code_interpreter"]
@@ -0,0 +1,36 @@
1
+ """Code execution helpers for the Watasu Python SDK."""
2
+
3
+ from watasu import * # noqa: F403
4
+ from watasu import __all__ as _watasu_all
5
+
6
+ from . import code_interpreter_async, code_interpreter_sync, constants, exceptions
7
+ from .main import AsyncSandbox, Sandbox
8
+ from .models import (
9
+ Context,
10
+ Execution,
11
+ ExecutionError,
12
+ Logs,
13
+ MIMEType,
14
+ OutputHandler,
15
+ OutputMessage,
16
+ Result,
17
+ )
18
+
19
+ _code_interpreter_all = [
20
+ "AsyncSandbox",
21
+ "Context",
22
+ "code_interpreter_async",
23
+ "code_interpreter_sync",
24
+ "constants",
25
+ "Execution",
26
+ "ExecutionError",
27
+ "exceptions",
28
+ "Logs",
29
+ "MIMEType",
30
+ "OutputHandler",
31
+ "OutputMessage",
32
+ "Result",
33
+ "Sandbox",
34
+ ]
35
+
36
+ __all__ = sorted(set(_watasu_all) | set(_code_interpreter_all))
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ from typing import Any, List, Optional, Tuple, Union
5
+
6
+
7
+ class ChartType(str, enum.Enum):
8
+ """Supported chart kinds returned by code execution."""
9
+
10
+ LINE = "line"
11
+ SCATTER = "scatter"
12
+ BAR = "bar"
13
+ PIE = "pie"
14
+ BOX_AND_WHISKER = "box_and_whisker"
15
+ SUPERCHART = "superchart"
16
+ UNKNOWN = "unknown"
17
+
18
+
19
+ class ScaleType(str, enum.Enum):
20
+ """Supported chart axis scale kinds."""
21
+
22
+ LINEAR = "linear"
23
+ DATETIME = "datetime"
24
+ CATEGORICAL = "categorical"
25
+ LOG = "log"
26
+ SYMLOG = "symlog"
27
+ LOGIT = "logit"
28
+ FUNCTION = "function"
29
+ FUNCTIONLOG = "functionlog"
30
+ ASINH = "asinh"
31
+ UNKNOWN = "unknown"
32
+
33
+
34
+ class Chart:
35
+ """Extracted chart data for custom visualizations."""
36
+
37
+ type: ChartType
38
+ title: str
39
+ elements: List[Any]
40
+
41
+ def __init__(self, **kwargs: Any) -> None:
42
+ self._raw_data = dict(kwargs)
43
+ self.type = _chart_type(kwargs.get("type"))
44
+ self.title = str(kwargs.get("title") or "")
45
+ self.elements = list(kwargs.get("elements") or [])
46
+
47
+ def to_dict(self) -> dict:
48
+ return self._raw_data
49
+
50
+
51
+ class Chart2D(Chart):
52
+ x_label: Optional[str]
53
+ y_label: Optional[str]
54
+ x_unit: Optional[str]
55
+ y_unit: Optional[str]
56
+
57
+ def __init__(self, **kwargs: Any) -> None:
58
+ super().__init__(**kwargs)
59
+ self.x_label = kwargs.get("x_label")
60
+ self.y_label = kwargs.get("y_label")
61
+ self.x_unit = kwargs.get("x_unit")
62
+ self.y_unit = kwargs.get("y_unit")
63
+
64
+
65
+ class PointData:
66
+ label: str
67
+ points: List[Tuple[Union[str, float], Union[str, float]]]
68
+
69
+ def __init__(self, **kwargs: Any) -> None:
70
+ self.label = str(kwargs.get("label") or "")
71
+ self.points = [(x, y) for x, y in kwargs.get("points") or []]
72
+
73
+
74
+ class PointChart(Chart2D):
75
+ x_ticks: List[Union[str, float]]
76
+ x_tick_labels: List[str]
77
+ x_scale: ScaleType
78
+ y_ticks: List[Union[str, float]]
79
+ y_tick_labels: List[str]
80
+ y_scale: ScaleType
81
+ elements: List[PointData]
82
+
83
+ def __init__(self, **kwargs: Any) -> None:
84
+ super().__init__(**kwargs)
85
+ self.x_ticks = list(kwargs.get("x_ticks") or [])
86
+ self.x_tick_labels = list(kwargs.get("x_tick_labels") or [])
87
+ self.x_scale = _scale_type(kwargs.get("x_scale"))
88
+ self.y_ticks = list(kwargs.get("y_ticks") or [])
89
+ self.y_tick_labels = list(kwargs.get("y_tick_labels") or [])
90
+ self.y_scale = _scale_type(kwargs.get("y_scale"))
91
+ self.elements = [PointData(**item) for item in kwargs.get("elements") or []]
92
+
93
+
94
+ class LineChart(PointChart):
95
+ type = ChartType.LINE
96
+
97
+
98
+ class ScatterChart(PointChart):
99
+ type = ChartType.SCATTER
100
+
101
+
102
+ class BarData:
103
+ label: str
104
+ group: str
105
+ value: str
106
+
107
+ def __init__(self, **kwargs: Any) -> None:
108
+ self.label = str(kwargs.get("label") or "")
109
+ self.value = str(kwargs.get("value") or "")
110
+ self.group = str(kwargs.get("group") or "")
111
+
112
+
113
+ class BarChart(Chart2D):
114
+ type = ChartType.BAR
115
+ elements: List[BarData]
116
+
117
+ def __init__(self, **kwargs: Any) -> None:
118
+ super().__init__(**kwargs)
119
+ self.elements = [BarData(**item) for item in kwargs.get("elements") or []]
120
+
121
+
122
+ class PieData:
123
+ label: str
124
+ angle: float
125
+ radius: float
126
+
127
+ def __init__(self, **kwargs: Any) -> None:
128
+ self.label = str(kwargs.get("label") or "")
129
+ self.angle = float(kwargs.get("angle") or 0)
130
+ self.radius = float(kwargs.get("radius") or 0)
131
+
132
+
133
+ class PieChart(Chart):
134
+ type = ChartType.PIE
135
+ elements: List[PieData]
136
+
137
+ def __init__(self, **kwargs: Any) -> None:
138
+ super().__init__(**kwargs)
139
+ self.elements = [PieData(**item) for item in kwargs.get("elements") or []]
140
+
141
+
142
+ class BoxAndWhiskerData:
143
+ label: str
144
+ min: float
145
+ first_quartile: float
146
+ median: float
147
+ third_quartile: float
148
+ max: float
149
+ outliers: List[float]
150
+
151
+ def __init__(self, **kwargs: Any) -> None:
152
+ self.label = str(kwargs.get("label") or "")
153
+ self.min = float(kwargs.get("min") or 0)
154
+ self.first_quartile = float(kwargs.get("first_quartile") or 0)
155
+ self.median = float(kwargs.get("median") or 0)
156
+ self.third_quartile = float(kwargs.get("third_quartile") or 0)
157
+ self.max = float(kwargs.get("max") or 0)
158
+ self.outliers = [float(item) for item in kwargs.get("outliers") or []]
159
+
160
+
161
+ class BoxAndWhiskerChart(Chart2D):
162
+ type = ChartType.BOX_AND_WHISKER
163
+ elements: List[BoxAndWhiskerData]
164
+
165
+ def __init__(self, **kwargs: Any) -> None:
166
+ super().__init__(**kwargs)
167
+ self.elements = [
168
+ BoxAndWhiskerData(**item) for item in kwargs.get("elements") or []
169
+ ]
170
+
171
+
172
+ class SuperChart(Chart):
173
+ type = ChartType.SUPERCHART
174
+ elements: List[
175
+ Union[LineChart, ScatterChart, BarChart, PieChart, BoxAndWhiskerChart, Chart]
176
+ ]
177
+
178
+ def __init__(self, **kwargs: Any) -> None:
179
+ super().__init__(**kwargs)
180
+ self.elements = [
181
+ _deserialize_chart(item) or Chart(**item)
182
+ for item in kwargs.get("elements") or []
183
+ if isinstance(item, dict)
184
+ ]
185
+
186
+
187
+ ChartTypes = Union[
188
+ LineChart, ScatterChart, BarChart, PieChart, BoxAndWhiskerChart, SuperChart
189
+ ]
190
+
191
+
192
+ def _deserialize_chart(data: Optional[dict]) -> Optional[ChartTypes]:
193
+ if not data:
194
+ return None
195
+
196
+ chart_type = _chart_type(data.get("type"))
197
+ if chart_type == ChartType.LINE:
198
+ return LineChart(**data)
199
+ if chart_type == ChartType.SCATTER:
200
+ return ScatterChart(**data)
201
+ if chart_type == ChartType.BAR:
202
+ return BarChart(**data)
203
+ if chart_type == ChartType.PIE:
204
+ return PieChart(**data)
205
+ if chart_type == ChartType.BOX_AND_WHISKER:
206
+ return BoxAndWhiskerChart(**data)
207
+ if chart_type == ChartType.SUPERCHART:
208
+ return SuperChart(**data)
209
+ return Chart(**data)
210
+
211
+
212
+ def _chart_type(value: Any) -> ChartType:
213
+ try:
214
+ return ChartType(value)
215
+ except ValueError:
216
+ return ChartType.UNKNOWN
217
+
218
+
219
+ def _scale_type(value: Any) -> ScaleType:
220
+ try:
221
+ return ScaleType(value)
222
+ except ValueError:
223
+ return ScaleType.UNKNOWN
@@ -0,0 +1,3 @@
1
+ from .main import AsyncSandbox
2
+
3
+ __all__ = ["AsyncSandbox"]
@@ -0,0 +1,3 @@
1
+ from .main import Sandbox
2
+
3
+ __all__ = ["Sandbox"]
@@ -0,0 +1,5 @@
1
+ DEFAULT_TEMPLATE = "code-interpreter"
2
+ JUPYTER_PORT = 49999
3
+ DEFAULT_TIMEOUT = 300
4
+
5
+ __all__ = ["DEFAULT_TEMPLATE", "DEFAULT_TIMEOUT", "JUPYTER_PORT"]
@@ -0,0 +1,18 @@
1
+ from watasu import TimeoutException
2
+
3
+
4
+ def format_request_timeout_error() -> Exception:
5
+ """Build the standard request-timeout exception."""
6
+ return TimeoutException(
7
+ "Request timed out - the 'request_timeout' option can be used to increase this timeout"
8
+ )
9
+
10
+
11
+ def format_execution_timeout_error() -> Exception:
12
+ """Build the standard code-execution-timeout exception."""
13
+ return TimeoutException(
14
+ "Execution timed out - the 'timeout' option can be used to increase this timeout"
15
+ )
16
+
17
+
18
+ __all__ = ["format_execution_timeout_error", "format_request_timeout_error"]
@@ -0,0 +1,289 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, Callable, Dict, List, Optional, Union
5
+ from urllib.parse import quote
6
+
7
+ from watasu.connection_config import ApiParams
8
+ from watasu.exceptions import InvalidArgumentException
9
+ from watasu.sandbox_async.main import AsyncSandbox as BaseAsyncSandbox
10
+ from watasu.sandbox_async.main import _AsyncDualMethod, _thread_callback
11
+ from watasu.sandbox_sync.main import Sandbox as BaseSandbox
12
+
13
+ from .constants import DEFAULT_TEMPLATE
14
+ from .models import Context, Execution, ExecutionError, OutputMessage, Result
15
+ from .models import context_from_api
16
+ from .models import execution_from_api
17
+
18
+
19
+ class Sandbox(BaseSandbox):
20
+ """Sandbox specialized for running Python code."""
21
+
22
+ default_template = DEFAULT_TEMPLATE
23
+
24
+ def run_code(
25
+ self,
26
+ code: str,
27
+ language: Optional[str] = None,
28
+ context: Optional[Union[Context, str]] = None,
29
+ on_stdout: Optional[Callable[[OutputMessage], None]] = None,
30
+ on_stderr: Optional[Callable[[OutputMessage], None]] = None,
31
+ on_result: Optional[Callable[[Result], None]] = None,
32
+ on_error: Optional[Callable[[ExecutionError], None]] = None,
33
+ envs: Optional[Dict[str, str]] = None,
34
+ timeout: Optional[int] = None,
35
+ request_timeout: Optional[float] = None,
36
+ ) -> Execution:
37
+ """Run Python code in the sandbox and return structured execution output."""
38
+
39
+ if not isinstance(code, str):
40
+ raise InvalidArgumentException("code must be a string")
41
+ if language is not None and context is not None:
42
+ raise InvalidArgumentException("language and context cannot both be set")
43
+
44
+ payload = _compact(
45
+ {
46
+ "code": code,
47
+ "language": language,
48
+ "context_id": _context_id(context),
49
+ "env_vars": envs,
50
+ "timeout_seconds": timeout,
51
+ }
52
+ )
53
+ response = self._require_data_plane().post_json(
54
+ "/runtime/v1/code/run",
55
+ json=payload,
56
+ request_timeout=request_timeout,
57
+ )
58
+ execution = execution_from_api(response)
59
+ _emit_callbacks(execution, on_stdout, on_stderr, on_result, on_error)
60
+ return execution
61
+
62
+ def create_code_context(
63
+ self,
64
+ cwd: Optional[str] = None,
65
+ language: Optional[str] = None,
66
+ request_timeout: Optional[float] = None,
67
+ ) -> Context:
68
+ """Create a persistent code context."""
69
+
70
+ payload = _compact({"cwd": cwd, "language": language})
71
+ response = self._require_data_plane().post_json(
72
+ "/runtime/v1/code/contexts",
73
+ json=payload,
74
+ request_timeout=request_timeout,
75
+ )
76
+ return context_from_api(response)
77
+
78
+ def remove_code_context(
79
+ self,
80
+ context: Union[Context, str],
81
+ request_timeout: Optional[float] = None,
82
+ ) -> None:
83
+ """Remove a persistent code context."""
84
+
85
+ self._require_data_plane().delete_json(
86
+ f"/runtime/v1/code/contexts/{_context_path_id(context)}",
87
+ request_timeout=request_timeout,
88
+ )
89
+
90
+ def list_code_contexts(self, request_timeout: Optional[float] = None) -> List[Context]:
91
+ """List persistent code contexts."""
92
+
93
+ response = self._require_data_plane().get_json(
94
+ "/runtime/v1/code/contexts",
95
+ request_timeout=request_timeout,
96
+ )
97
+ contexts = response if isinstance(response, list) else response.get("contexts", [])
98
+ return [context_from_api(item) for item in contexts]
99
+
100
+ def restart_code_context(
101
+ self,
102
+ context: Union[Context, str],
103
+ request_timeout: Optional[float] = None,
104
+ ) -> None:
105
+ """Restart a persistent code context."""
106
+
107
+ self._require_data_plane().post_json(
108
+ f"/runtime/v1/code/contexts/{_context_path_id(context)}/restart",
109
+ json={},
110
+ request_timeout=request_timeout,
111
+ )
112
+
113
+
114
+ class AsyncSandbox(BaseAsyncSandbox):
115
+ """Async sandbox specialized for running Python code."""
116
+
117
+ default_template = Sandbox.default_template
118
+
119
+ @classmethod
120
+ async def create(cls, *args: Any, **kwargs: Any) -> "AsyncSandbox":
121
+ """Create a code-interpreter sandbox and return async helpers."""
122
+
123
+ return cls(sync_sandbox=await asyncio.to_thread(Sandbox.create, *args, **kwargs))
124
+
125
+ beta_create = create
126
+
127
+ async def _connect_instance(
128
+ self, timeout: Optional[int] = None, **opts: ApiParams
129
+ ) -> "AsyncSandbox":
130
+ """Reconnect this sandbox and refresh its data-plane session."""
131
+
132
+ await asyncio.to_thread(self._sync.connect, timeout=timeout, **opts)
133
+ self._set_sync(self._sync)
134
+ return self
135
+
136
+ @classmethod
137
+ async def _connect_class(
138
+ cls, sandbox_id: str, timeout: Optional[int] = None, **opts: ApiParams
139
+ ) -> "AsyncSandbox":
140
+ """Connect to an existing code-interpreter sandbox by id."""
141
+
142
+ return cls(
143
+ sync_sandbox=await asyncio.to_thread(
144
+ Sandbox.connect, sandbox_id, timeout=timeout, **opts
145
+ )
146
+ )
147
+
148
+ connect = _AsyncDualMethod(_connect_instance, _connect_class)
149
+
150
+ async def run_code(
151
+ self,
152
+ code: str,
153
+ language: Optional[str] = None,
154
+ context: Optional[Union[Context, str]] = None,
155
+ on_stdout: Optional[Callable[[OutputMessage], None]] = None,
156
+ on_stderr: Optional[Callable[[OutputMessage], None]] = None,
157
+ on_result: Optional[Callable[[Result], None]] = None,
158
+ on_error: Optional[Callable[[ExecutionError], None]] = None,
159
+ envs: Optional[Dict[str, str]] = None,
160
+ timeout: Optional[int] = None,
161
+ request_timeout: Optional[float] = None,
162
+ ) -> Execution:
163
+ """Run Python code in the sandbox and return structured execution output."""
164
+
165
+ loop = asyncio.get_running_loop()
166
+ return await asyncio.to_thread(
167
+ self._sync.run_code,
168
+ code,
169
+ language=language,
170
+ context=context,
171
+ on_stdout=_thread_callback(loop, on_stdout),
172
+ on_stderr=_thread_callback(loop, on_stderr),
173
+ on_result=_thread_callback(loop, on_result),
174
+ on_error=_thread_callback(loop, on_error),
175
+ envs=envs,
176
+ timeout=timeout,
177
+ request_timeout=request_timeout,
178
+ )
179
+
180
+ async def create_code_context(
181
+ self,
182
+ cwd: Optional[str] = None,
183
+ language: Optional[str] = None,
184
+ request_timeout: Optional[float] = None,
185
+ ) -> Context:
186
+ """Create a persistent code context."""
187
+
188
+ return await asyncio.to_thread(
189
+ self._sync.create_code_context,
190
+ cwd=cwd,
191
+ language=language,
192
+ request_timeout=request_timeout,
193
+ )
194
+
195
+ async def remove_code_context(
196
+ self,
197
+ context: Union[Context, str],
198
+ request_timeout: Optional[float] = None,
199
+ ) -> None:
200
+ """Remove a persistent code context."""
201
+
202
+ await asyncio.to_thread(
203
+ self._sync.remove_code_context,
204
+ context,
205
+ request_timeout=request_timeout,
206
+ )
207
+
208
+ async def list_code_contexts(
209
+ self, request_timeout: Optional[float] = None
210
+ ) -> List[Context]:
211
+ """List persistent code contexts."""
212
+
213
+ return await asyncio.to_thread(
214
+ self._sync.list_code_contexts,
215
+ request_timeout=request_timeout,
216
+ )
217
+
218
+ async def restart_code_context(
219
+ self,
220
+ context: Union[Context, str],
221
+ request_timeout: Optional[float] = None,
222
+ ) -> None:
223
+ """Restart a persistent code context."""
224
+
225
+ await asyncio.to_thread(
226
+ self._sync.restart_code_context,
227
+ context,
228
+ request_timeout=request_timeout,
229
+ )
230
+
231
+
232
+ def _emit_callbacks(
233
+ execution: Execution,
234
+ on_stdout: Optional[Callable[[OutputMessage], None]],
235
+ on_stderr: Optional[Callable[[OutputMessage], None]],
236
+ on_result: Optional[Callable[[Result], None]],
237
+ on_error: Optional[Callable[[ExecutionError], None]],
238
+ ) -> None:
239
+ for message in execution.logs.stdout:
240
+ if on_stdout is not None:
241
+ on_stdout(message)
242
+ for message in execution.logs.stderr:
243
+ if on_stderr is not None:
244
+ on_stderr(message)
245
+ for result in execution.results:
246
+ if on_result is not None:
247
+ on_result(result)
248
+ if execution.error is not None and on_error is not None:
249
+ on_error(execution.error)
250
+
251
+
252
+ def _context_id(context: Optional[Union[Context, str]]) -> Optional[str]:
253
+ if context is None:
254
+ return None
255
+ return _context_id_required(context)
256
+
257
+
258
+ def _context_id_required(context: Union[Context, str]) -> str:
259
+ if isinstance(context, str):
260
+ context_id = context
261
+ elif isinstance(context, dict):
262
+ value = context.get("id")
263
+ context_id = str(value) if value is not None else ""
264
+ else:
265
+ value = getattr(context, "id", None)
266
+ context_id = str(value) if value is not None else ""
267
+
268
+ if not context_id:
269
+ raise InvalidArgumentException("context id is required")
270
+ return context_id
271
+
272
+
273
+ def _context_path_id(context: Union[Context, str]) -> str:
274
+ return quote(_context_id_required(context), safe="")
275
+
276
+
277
+ def _compact(payload: Dict[str, Any]) -> Dict[str, Any]:
278
+ return {key: value for key, value in payload.items() if value is not None}
279
+
280
+
281
+ __all__ = [
282
+ "AsyncSandbox",
283
+ "Context",
284
+ "Execution",
285
+ "ExecutionError",
286
+ "OutputMessage",
287
+ "Result",
288
+ "Sandbox",
289
+ ]
@@ -0,0 +1,303 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Callable, Dict, List, Optional, TypeVar
6
+
7
+ from .charts import ChartTypes, _deserialize_chart
8
+
9
+ T = TypeVar("T")
10
+ OutputHandler = Callable[[T], Any]
11
+
12
+
13
+ class MIMEType(str):
14
+ """MIME type marker used by code execution results."""
15
+
16
+
17
+ @dataclass
18
+ class OutputMessage:
19
+ """One stdout or stderr line emitted by code execution."""
20
+
21
+ line: str
22
+ timestamp: float = field(default_factory=time.time)
23
+ error: bool = False
24
+
25
+ def __str__(self) -> str:
26
+ return self.line
27
+
28
+ def to_json(self) -> Dict[str, Any]:
29
+ return {
30
+ "line": self.line,
31
+ "timestamp": self.timestamp,
32
+ "error": self.error,
33
+ }
34
+
35
+
36
+ @dataclass
37
+ class Logs:
38
+ """Captured stdout and stderr output for an execution."""
39
+
40
+ stdout: List[OutputMessage] = field(default_factory=list)
41
+ stderr: List[OutputMessage] = field(default_factory=list)
42
+
43
+ def to_json(self) -> Dict[str, Any]:
44
+ return {
45
+ "stdout": [message.to_json() for message in self.stdout],
46
+ "stderr": [message.to_json() for message in self.stderr],
47
+ }
48
+
49
+
50
+ @dataclass
51
+ class ExecutionError:
52
+ """Structured exception raised by user code inside the sandbox."""
53
+
54
+ name: str
55
+ value: str
56
+ traceback: str
57
+
58
+ def to_json(self) -> Dict[str, Any]:
59
+ return {
60
+ "name": self.name,
61
+ "value": self.value,
62
+ "traceback": self.traceback,
63
+ }
64
+
65
+
66
+ @dataclass
67
+ class Result:
68
+ """Rich result produced by the last expression of a code execution."""
69
+
70
+ text: Optional[str] = None
71
+ html: Optional[str] = None
72
+ markdown: Optional[str] = None
73
+ svg: Optional[str] = None
74
+ png: Optional[str] = None
75
+ jpeg: Optional[str] = None
76
+ pdf: Optional[str] = None
77
+ latex: Optional[str] = None
78
+ json: Any = None
79
+ javascript: Optional[str] = None
80
+ data: Any = None
81
+ chart: Optional[ChartTypes] = None
82
+ extra: Dict[str, Any] = field(default_factory=dict)
83
+ is_main_result: bool = False
84
+
85
+ def __post_init__(self) -> None:
86
+ if isinstance(self.chart, dict):
87
+ self.chart = _deserialize_chart(self.chart)
88
+
89
+ def __getitem__(self, item):
90
+ return getattr(self, item)
91
+
92
+ def formats(self) -> List[str]:
93
+ """Return available display formats for this result."""
94
+
95
+ names = [
96
+ "text",
97
+ "html",
98
+ "markdown",
99
+ "svg",
100
+ "png",
101
+ "jpeg",
102
+ "pdf",
103
+ "latex",
104
+ "json",
105
+ "javascript",
106
+ "data",
107
+ "chart",
108
+ ]
109
+ formats = [name for name in names if getattr(self, name) is not None]
110
+ if self.extra:
111
+ formats.extend(self.extra)
112
+ return formats
113
+
114
+ def to_json(self) -> Dict[str, Any]:
115
+ payload = {
116
+ "text": self.text,
117
+ "html": self.html,
118
+ "markdown": self.markdown,
119
+ "svg": self.svg,
120
+ "png": self.png,
121
+ "jpeg": self.jpeg,
122
+ "pdf": self.pdf,
123
+ "latex": self.latex,
124
+ "json": self.json,
125
+ "javascript": self.javascript,
126
+ "data": self.data,
127
+ "chart": self.chart,
128
+ "extra": self.extra,
129
+ "is_main_result": self.is_main_result,
130
+ }
131
+ return {key: value for key, value in payload.items() if value is not None}
132
+
133
+ def __repr__(self) -> str:
134
+ if self.text:
135
+ return f"Result({self.text})"
136
+ return "Result(Formats: " + ", ".join(self.formats()) + ")"
137
+
138
+ def __str__(self) -> str:
139
+ return self.__repr__()
140
+
141
+ def _repr_html_(self) -> Optional[str]:
142
+ return self.html
143
+
144
+ def _repr_markdown_(self) -> Optional[str]:
145
+ return self.markdown
146
+
147
+ def _repr_svg_(self) -> Optional[str]:
148
+ return self.svg
149
+
150
+ def _repr_png_(self) -> Optional[str]:
151
+ return self.png
152
+
153
+ def _repr_jpeg_(self) -> Optional[str]:
154
+ return self.jpeg
155
+
156
+ def _repr_pdf_(self) -> Optional[str]:
157
+ return self.pdf
158
+
159
+ def _repr_latex_(self) -> Optional[str]:
160
+ return self.latex
161
+
162
+
163
+ @dataclass
164
+ class Execution:
165
+ """Complete result of a sandbox code execution."""
166
+
167
+ results: List[Result] = field(default_factory=list)
168
+ logs: Logs = field(default_factory=Logs)
169
+ error: Optional[ExecutionError] = None
170
+ execution_count: Optional[int] = None
171
+
172
+ @property
173
+ def text(self) -> Optional[str]:
174
+ """Text for the main result, when code produced one."""
175
+
176
+ for result in self.results:
177
+ if result.is_main_result and result.text is not None:
178
+ return result.text
179
+ for result in self.results:
180
+ if result.text is not None:
181
+ return result.text
182
+ return None
183
+
184
+ def to_json(self) -> Dict[str, Any]:
185
+ return {
186
+ "results": [result.to_json() for result in self.results],
187
+ "logs": self.logs.to_json(),
188
+ "error": self.error.to_json() if self.error else None,
189
+ "execution_count": self.execution_count,
190
+ }
191
+
192
+
193
+ @dataclass(init=False)
194
+ class Context:
195
+ """Code execution context metadata."""
196
+
197
+ id: str
198
+ language: Optional[str] = None
199
+ cwd: Optional[str] = None
200
+
201
+ def __init__(
202
+ self,
203
+ id: Optional[str] = None,
204
+ language: Optional[str] = None,
205
+ cwd: Optional[str] = None,
206
+ context_id: Optional[str] = None,
207
+ **_: Any,
208
+ ) -> None:
209
+ self.id = str(context_id if context_id is not None else id)
210
+ self.language = language
211
+ self.cwd = cwd
212
+
213
+ def to_json(self) -> Dict[str, Any]:
214
+ return {
215
+ "id": self.id,
216
+ "language": self.language,
217
+ "cwd": self.cwd,
218
+ }
219
+
220
+
221
+ def execution_from_api(payload: Dict[str, Any]) -> Execution:
222
+ execution = payload.get("execution") or payload
223
+ logs = execution.get("logs") or {}
224
+ return Execution(
225
+ results=[result_from_api(item) for item in execution.get("results") or []],
226
+ logs=Logs(
227
+ stdout=[
228
+ output_message_from_api(item, error=False)
229
+ for item in logs.get("stdout") or []
230
+ ],
231
+ stderr=[
232
+ output_message_from_api(item, error=True)
233
+ for item in logs.get("stderr") or []
234
+ ],
235
+ ),
236
+ error=error_from_api(execution.get("error")),
237
+ execution_count=execution.get("execution_count"),
238
+ )
239
+
240
+
241
+ def context_from_api(payload: Dict[str, Any]) -> Context:
242
+ return Context(
243
+ id=payload.get("id"),
244
+ language=payload.get("language"),
245
+ cwd=payload.get("cwd"),
246
+ )
247
+
248
+
249
+ def result_from_api(payload: Dict[str, Any]) -> Result:
250
+ known = {
251
+ "text",
252
+ "html",
253
+ "markdown",
254
+ "svg",
255
+ "png",
256
+ "jpeg",
257
+ "pdf",
258
+ "latex",
259
+ "json",
260
+ "javascript",
261
+ "data",
262
+ "chart",
263
+ "extra",
264
+ "is_main_result",
265
+ }
266
+ return Result(
267
+ text=payload.get("text"),
268
+ html=payload.get("html"),
269
+ markdown=payload.get("markdown"),
270
+ svg=payload.get("svg"),
271
+ png=payload.get("png"),
272
+ jpeg=payload.get("jpeg"),
273
+ pdf=payload.get("pdf"),
274
+ latex=payload.get("latex"),
275
+ json=payload.get("json"),
276
+ javascript=payload.get("javascript"),
277
+ data=payload.get("data"),
278
+ chart=_deserialize_chart(payload.get("chart")),
279
+ extra=payload.get("extra") or {
280
+ key: value for key, value in payload.items() if key not in known
281
+ },
282
+ is_main_result=bool(payload.get("is_main_result")),
283
+ )
284
+
285
+
286
+ def output_message_from_api(payload: Any, error: bool) -> OutputMessage:
287
+ if isinstance(payload, dict):
288
+ return OutputMessage(
289
+ line=str(payload.get("line", "")),
290
+ timestamp=float(payload.get("timestamp") or time.time()),
291
+ error=bool(payload.get("error", error)),
292
+ )
293
+ return OutputMessage(line=str(payload), error=error)
294
+
295
+
296
+ def error_from_api(payload: Any) -> Optional[ExecutionError]:
297
+ if not isinstance(payload, dict):
298
+ return None
299
+ return ExecutionError(
300
+ name=str(payload.get("name") or ""),
301
+ value=str(payload.get("value") or ""),
302
+ traceback=str(payload.get("traceback") or ""),
303
+ )