scalebox-sdk 0.1.25__py3-none-any.whl → 1.0.1__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.
Files changed (70) hide show
  1. scalebox/__init__.py +2 -2
  2. scalebox/api/__init__.py +3 -1
  3. scalebox/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
  4. scalebox/api/client/models/connect_sandbox.py +59 -0
  5. scalebox/api/client/models/error.py +2 -2
  6. scalebox/api/client/models/listed_sandbox.py +19 -1
  7. scalebox/api/client/models/new_sandbox.py +10 -0
  8. scalebox/api/client/models/sandbox.py +13 -0
  9. scalebox/api/client/models/sandbox_detail.py +24 -0
  10. scalebox/cli.py +125 -125
  11. scalebox/client/aclient.py +57 -57
  12. scalebox/client/client.py +102 -102
  13. scalebox/code_interpreter/__init__.py +12 -12
  14. scalebox/code_interpreter/charts.py +230 -230
  15. scalebox/code_interpreter/constants.py +3 -3
  16. scalebox/code_interpreter/exceptions.py +13 -13
  17. scalebox/code_interpreter/models.py +485 -485
  18. scalebox/connection_config.py +34 -1
  19. scalebox/csx_connect/__init__.py +1 -1
  20. scalebox/csx_connect/client.py +485 -485
  21. scalebox/csx_desktop/main.py +651 -651
  22. scalebox/exceptions.py +83 -83
  23. scalebox/generated/api.py +61 -61
  24. scalebox/generated/api_pb2.py +203 -203
  25. scalebox/generated/api_pb2.pyi +956 -956
  26. scalebox/generated/api_pb2_connect.py +1407 -1407
  27. scalebox/generated/rpc.py +50 -50
  28. scalebox/sandbox/main.py +146 -139
  29. scalebox/sandbox/sandbox_api.py +105 -91
  30. scalebox/sandbox/signature.py +40 -40
  31. scalebox/sandbox/utils.py +34 -34
  32. scalebox/sandbox_async/main.py +226 -44
  33. scalebox/sandbox_async/sandbox_api.py +124 -3
  34. scalebox/sandbox_sync/main.py +205 -130
  35. scalebox/sandbox_sync/sandbox_api.py +119 -3
  36. scalebox/test/CODE_INTERPRETER_TESTS_READY.md +323 -323
  37. scalebox/test/README.md +329 -329
  38. scalebox/test/bedrock_openai_adapter.py +67 -0
  39. scalebox/test/code_interpreter_test.py +34 -34
  40. scalebox/test/code_interpreter_test_sync.py +34 -34
  41. scalebox/test/run_stress_code_interpreter_sync.py +166 -0
  42. scalebox/test/simple_upload_example.py +123 -0
  43. scalebox/test/stabitiy_test.py +310 -0
  44. scalebox/test/test_browser_use.py +25 -0
  45. scalebox/test/test_browser_use_scalebox.py +61 -0
  46. scalebox/test/test_code_interpreter_sync_comprehensive.py +115 -65
  47. scalebox/test/test_connect_pause_async.py +277 -0
  48. scalebox/test/test_connect_pause_sync.py +267 -0
  49. scalebox/test/test_desktop_sandbox_sf.py +117 -0
  50. scalebox/test/test_download_url.py +49 -0
  51. scalebox/test/test_sandbox_async_comprehensive.py +1 -1
  52. scalebox/test/test_sandbox_object_storage_example.py +146 -0
  53. scalebox/test/test_sandbox_object_storage_example_async.py +156 -0
  54. scalebox/test/test_sf.py +137 -0
  55. scalebox/test/test_watch_dir_async.py +56 -0
  56. scalebox/test/testacreate.py +1 -1
  57. scalebox/test/testagetinfo.py +1 -1
  58. scalebox/test/testcomputeuse.py +243 -243
  59. scalebox/test/testsandbox_api.py +1 -3
  60. scalebox/test/testsandbox_sync.py +1 -1
  61. scalebox/test/upload_100mb_example.py +355 -0
  62. scalebox/utils/httpcoreclient.py +297 -297
  63. scalebox/utils/httpxclient.py +403 -403
  64. scalebox/version.py +2 -2
  65. {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.1.dist-info}/METADATA +1 -1
  66. {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.1.dist-info}/RECORD +70 -53
  67. {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.1.dist-info}/WHEEL +1 -1
  68. {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.1.dist-info}/entry_points.txt +0 -0
  69. {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.1.dist-info}/licenses/LICENSE +0 -0
  70. {scalebox_sdk-0.1.25.dist-info → scalebox_sdk-1.0.1.dist-info}/top_level.txt +0 -0
@@ -1,485 +1,485 @@
1
- import asyncio
2
- import json
3
- import logging
4
- from dataclasses import dataclass
5
- from datetime import datetime
6
- from typing import (
7
- Any,
8
- Awaitable,
9
- Callable,
10
- Dict,
11
- Iterable,
12
- List,
13
- Optional,
14
- TypeVar,
15
- Union,
16
- )
17
-
18
- from google.protobuf.timestamp_pb2 import Timestamp
19
- from httpx import Response
20
-
21
- from ..exceptions import NotFoundException, SandboxException, TimeoutException
22
- from ..generated import api_pb2
23
-
24
- T = TypeVar("T")
25
- OutputHandler = Union[
26
- Callable[[T], Any],
27
- Callable[[T], Awaitable[Any]],
28
- ]
29
-
30
- logger = logging.getLogger(__name__)
31
-
32
-
33
- @dataclass
34
- class OutputMessage:
35
- """
36
- Represents an output message from the sandbox code execution.
37
- """
38
-
39
- content: str
40
- """
41
- The output content.
42
- """
43
- timestamp: int
44
- """
45
- Unix epoch in nanoseconds
46
- """
47
- error: bool = False
48
- """
49
- Whether the output is an error.
50
- """
51
-
52
- def __str__(self):
53
- return self.content
54
-
55
-
56
- @dataclass
57
- class ExecutionError:
58
- """
59
- Represents an error that occurred during the execution of a cell.
60
- """
61
-
62
- name: str
63
- """
64
- Name of the error.
65
- """
66
- value: str
67
- """
68
- Value of the error.
69
- """
70
- traceback: str
71
- """
72
- The raw traceback of the error.
73
- """
74
-
75
- def __init__(self, name: str, value: str, traceback: str, **kwargs):
76
- self.name = name
77
- self.value = value
78
- self.traceback = traceback
79
-
80
- def to_json(self) -> str:
81
- """
82
- Returns the JSON representation of the Error object.
83
- """
84
- data = {"name": self.name, "value": self.value, "traceback": self.traceback}
85
- return json.dumps(data)
86
-
87
-
88
- class MIMEType(str):
89
- """
90
- Represents a MIME type.
91
- """
92
-
93
-
94
- @dataclass
95
- class Result:
96
- """
97
- Represents the result of code execution.
98
- """
99
-
100
- exit_code: int = 0
101
- """Process exit code."""
102
- started_at: Optional[datetime] = None
103
- """Start time."""
104
- finished_at: Optional[datetime] = None
105
- """End time."""
106
- text: Optional[str] = None
107
- """Text representation."""
108
- html: Optional[str] = None
109
- """HTML representation."""
110
- markdown: Optional[str] = None
111
- """Markdown representation."""
112
- svg: Optional[str] = None
113
- """SVG representation."""
114
- png: Optional[str] = None
115
- """PNG representation."""
116
- jpeg: Optional[str] = None
117
- """JPEG representation."""
118
- pdf: Optional[str] = None
119
- """PDF representation."""
120
- latex: Optional[str] = None
121
- """LaTeX representation."""
122
- json_data: Optional[dict] = None
123
- """JSON data."""
124
- javascript: Optional[str] = None
125
- """JavaScript representation."""
126
- data: Optional[dict] = None
127
- """Raw data."""
128
- chart: Optional[api_pb2.Chart] = None
129
- """Chart data."""
130
- execution_count: Optional[int] = None
131
- """Execution count."""
132
- is_main_result: bool = False
133
- """Whether this is the main result."""
134
- extra: Optional[dict] = None
135
- """Extra data."""
136
-
137
- def __init__(
138
- self,
139
- exit_code: int = 0,
140
- started_at: Optional[Timestamp] = None,
141
- finished_at: Optional[Timestamp] = None,
142
- text: Optional[str] = None,
143
- html: Optional[str] = None,
144
- markdown: Optional[str] = None,
145
- svg: Optional[str] = None,
146
- png: Optional[str] = None,
147
- jpeg: Optional[str] = None,
148
- pdf: Optional[str] = None,
149
- latex: Optional[str] = None,
150
- json: Optional[str] = None,
151
- javascript: Optional[str] = None,
152
- data: Optional[str] = None,
153
- chart: Optional[api_pb2.Chart] = None,
154
- execution_count: Optional[int] = None,
155
- is_main_result: bool = False,
156
- extra: Optional[Dict[str, str]] = None,
157
- **kwargs,
158
- ):
159
- self.exit_code = exit_code
160
- self.started_at = self._convert_timestamp(started_at) if started_at else None
161
- self.finished_at = self._convert_timestamp(finished_at) if finished_at else None
162
- self.text = text
163
- self.html = html
164
- self.markdown = markdown
165
- self.svg = svg
166
- self.png = png
167
- self.jpeg = jpeg
168
- self.pdf = pdf
169
- self.latex = latex
170
- self.json_data = json.loads(json) if json else None
171
- self.javascript = javascript
172
- self.data = json.loads(data) if data else None
173
- self.chart = chart
174
- self.execution_count = execution_count
175
- self.is_main_result = is_main_result
176
- self.extra = dict(extra) if extra else {}
177
-
178
- def _convert_timestamp(self, timestamp: Timestamp) -> datetime:
179
- """Convert protobuf timestamp to datetime."""
180
- return datetime.fromtimestamp(timestamp.seconds + timestamp.nanos / 1e9)
181
-
182
- def formats(self) -> Iterable[str]:
183
- """
184
- Returns all available formats of the result.
185
-
186
- :return: All available formats of the result.
187
- """
188
- formats = []
189
- if self.text:
190
- formats.append("text")
191
- if self.html:
192
- formats.append("html")
193
- if self.markdown:
194
- formats.append("markdown")
195
- if self.svg:
196
- formats.append("svg")
197
- if self.png:
198
- formats.append("png")
199
- if self.jpeg:
200
- formats.append("jpeg")
201
- if self.pdf:
202
- formats.append("pdf")
203
- if self.latex:
204
- formats.append("latex")
205
- if self.json_data:
206
- formats.append("json")
207
- if self.javascript:
208
- formats.append("javascript")
209
- if self.data:
210
- formats.append("data")
211
- if self.chart:
212
- formats.append("chart")
213
-
214
- if self.extra:
215
- for key in self.extra:
216
- formats.append(key)
217
-
218
- return formats
219
-
220
- def __str__(self) -> str:
221
- """
222
- Returns the text representation of the data.
223
-
224
- :return: The text representation of the data.
225
- """
226
- if self.text:
227
- return f"Result(exit_code={self.exit_code}, text={self.text})"
228
- else:
229
- return f"Result(exit_code={self.exit_code}, formats={list(self.formats())})"
230
-
231
-
232
- @dataclass
233
- class Logs:
234
- """
235
- Data printed to stdout and stderr during execution.
236
- """
237
-
238
- stdout: List[str]
239
- """List of strings printed to stdout."""
240
- stderr: List[str]
241
- """List of strings printed to stderr."""
242
-
243
- def __init__(self, stdout: List[str] = None, stderr: List[str] = None, **kwargs):
244
- self.stdout = stdout or []
245
- self.stderr = stderr or []
246
-
247
- def __repr__(self):
248
- return f"Logs(stdout={self.stdout}, stderr={self.stderr})"
249
-
250
- def to_json(self) -> str:
251
- """
252
- Returns the JSON representation of the Logs object.
253
- """
254
- data = {"stdout": self.stdout, "stderr": self.stderr}
255
- return json.dumps(data)
256
-
257
-
258
- @dataclass
259
- class Execution:
260
- """
261
- Represents the result of a code execution.
262
- """
263
-
264
- results: List[Result]
265
- """List of execution results."""
266
- logs: Logs
267
- """Logs printed during execution."""
268
- error: Optional[ExecutionError]
269
- """Error object if an error occurred."""
270
- execution_count: Optional[int]
271
- """Execution count."""
272
-
273
- def __init__(
274
- self,
275
- results: List[Result] = None,
276
- logs: Logs = None,
277
- error: Optional[ExecutionError] = None,
278
- execution_count: Optional[int] = None,
279
- **kwargs,
280
- ):
281
- self.results = results or []
282
- self.logs = logs or Logs()
283
- self.error = error
284
- self.execution_count = execution_count
285
-
286
- def __repr__(self):
287
- return f"Execution(results={self.results}, logs={self.logs}, error={self.error}, execution_count={self.execution_count})"
288
-
289
- @property
290
- def text(self) -> Optional[str]:
291
- """
292
- Returns the text representation of the main result.
293
-
294
- :return: The text representation of the main result.
295
- """
296
- for result in self.results:
297
- if result.is_main_result:
298
- return result.text
299
- return None
300
-
301
- def to_json(self) -> str:
302
- """
303
- Returns the JSON representation of the Execution object.
304
- """
305
- data = {
306
- "results": [self._serialize_result(result) for result in self.results],
307
- "logs": self.logs.to_json(),
308
- "error": self.error.to_json() if self.error else None,
309
- "execution_count": self.execution_count,
310
- }
311
- return json.dumps(data)
312
-
313
- def _serialize_result(self, result: Result) -> Dict[str, Any]:
314
- """Serialize a single result to JSON-serializable format."""
315
- serialized = {
316
- "exit_code": result.exit_code,
317
- "started_at": result.started_at.isoformat() if result.started_at else None,
318
- "finished_at": (
319
- result.finished_at.isoformat() if result.finished_at else None
320
- ),
321
- "text": result.text,
322
- "html": result.html,
323
- "markdown": result.markdown,
324
- "svg": result.svg,
325
- "png": result.png,
326
- "jpeg": result.jpeg,
327
- "pdf": result.pdf,
328
- "latex": result.latex,
329
- "json": result.json_data,
330
- "javascript": result.javascript,
331
- "data": result.data,
332
- "execution_count": result.execution_count,
333
- "is_main_result": result.is_main_result,
334
- "extra": result.extra,
335
- }
336
-
337
- # Remove None values
338
- return {k: v for k, v in serialized.items() if v is not None}
339
-
340
-
341
- @dataclass
342
- class Context:
343
- """
344
- Represents a context for code execution.
345
- """
346
-
347
- id: str
348
- """The ID of the context."""
349
- language: str
350
- """The language of the context."""
351
- cwd: str
352
- """The working directory of the context."""
353
-
354
- def __init__(self, context_id: str, language: str, cwd: str, **kwargs):
355
- self.id = context_id
356
- self.language = language
357
- self.cwd = cwd
358
-
359
- @classmethod
360
- def from_json(cls, data: Dict[str, str]):
361
- return cls(
362
- context_id=data.get("id"),
363
- language=data.get("language"),
364
- cwd=data.get("cwd"),
365
- )
366
-
367
-
368
- def parse_output(
369
- execution: Execution,
370
- output: api_pb2.ExecuteResponse,
371
- on_stdout: Optional[OutputHandler[OutputMessage]] = None,
372
- on_stderr: Optional[OutputHandler[OutputMessage]] = None,
373
- on_result: Optional[OutputHandler[Result]] = None,
374
- on_error: Optional[OutputHandler[ExecutionError]] = None,
375
- ):
376
- """
377
- Parse the output from the execution service and update the execution object.
378
- """
379
- if output.HasField("stdout"):
380
- content = output.stdout.content
381
- execution.logs.stdout.append(content)
382
- if on_stdout:
383
- message = OutputMessage(
384
- content=content,
385
- timestamp=int(datetime.now().timestamp() * 1e9),
386
- error=False,
387
- )
388
- if asyncio.iscoroutinefunction(on_stdout):
389
- asyncio.create_task(on_stdout(message))
390
- else:
391
- on_stdout(message)
392
-
393
- elif output.HasField("stderr"):
394
- content = output.stderr.content
395
- execution.logs.stderr.append(content)
396
- if on_stderr:
397
- message = OutputMessage(
398
- content=content,
399
- timestamp=int(datetime.now().timestamp() * 1e9),
400
- error=True,
401
- )
402
- if asyncio.iscoroutinefunction(on_stderr):
403
- asyncio.create_task(on_stderr(message))
404
- else:
405
- on_stderr(message)
406
-
407
- elif output.HasField("result"):
408
- result_msg = output.result
409
- result = Result(
410
- exit_code=result_msg.exit_code,
411
- started_at=result_msg.started_at,
412
- finished_at=result_msg.finished_at,
413
- text=result_msg.text,
414
- html=result_msg.html,
415
- markdown=result_msg.markdown,
416
- svg=result_msg.svg,
417
- png=result_msg.png,
418
- jpeg=result_msg.jpeg,
419
- pdf=result_msg.pdf,
420
- latex=result_msg.latex,
421
- json=result_msg.json,
422
- javascript=result_msg.javascript,
423
- data=result_msg.data,
424
- chart=result_msg.chart,
425
- execution_count=result_msg.execution_count,
426
- is_main_result=result_msg.is_main_result,
427
- extra=dict(result_msg.extra),
428
- )
429
- execution.results.append(result)
430
- if on_result:
431
- if asyncio.iscoroutinefunction(on_result):
432
- asyncio.create_task(on_result(result))
433
- else:
434
- on_result(result)
435
-
436
- # Update execution count if this is the main result
437
- if result.is_main_result and result.execution_count is not None:
438
- execution.execution_count = result.execution_count
439
-
440
- elif output.HasField("error"):
441
- error_msg = output.error
442
- error = ExecutionError(
443
- name=error_msg.name,
444
- value=error_msg.value,
445
- traceback=error_msg.traceback,
446
- )
447
- execution.error = error
448
- if on_error:
449
- if asyncio.iscoroutinefunction(on_error):
450
- asyncio.create_task(on_error(error))
451
- else:
452
- on_error(error)
453
-
454
-
455
- async def aextract_exception(res: Response):
456
- """Asynchronously extract exception from response."""
457
- if res.is_success:
458
- return None
459
-
460
- await res.aread()
461
- return extract_exception(res)
462
-
463
-
464
- def extract_exception(res: Response):
465
- """Extract exception from response."""
466
- if res.is_success:
467
- return None
468
-
469
- res.read()
470
- return format_exception(res)
471
-
472
-
473
- def format_exception(res: Response):
474
- """Format exception from response."""
475
- if res.is_success:
476
- return None
477
-
478
- if res.status_code == 404:
479
- return NotFoundException(res.text)
480
- elif res.status_code == 502:
481
- return TimeoutException(
482
- f"{res.text}: This error is likely due to sandbox timeout. You can modify the sandbox timeout by passing 'timeout' when starting the sandbox or calling '.set_timeout' on the sandbox with the desired timeout."
483
- )
484
- else:
485
- return SandboxException(f"{res.status_code}: {res.text}")
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+ from typing import (
7
+ Any,
8
+ Awaitable,
9
+ Callable,
10
+ Dict,
11
+ Iterable,
12
+ List,
13
+ Optional,
14
+ TypeVar,
15
+ Union,
16
+ )
17
+
18
+ from google.protobuf.timestamp_pb2 import Timestamp
19
+ from httpx import Response
20
+
21
+ from ..exceptions import NotFoundException, SandboxException, TimeoutException
22
+ from ..generated import api_pb2
23
+
24
+ T = TypeVar("T")
25
+ OutputHandler = Union[
26
+ Callable[[T], Any],
27
+ Callable[[T], Awaitable[Any]],
28
+ ]
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ @dataclass
34
+ class OutputMessage:
35
+ """
36
+ Represents an output message from the sandbox code execution.
37
+ """
38
+
39
+ content: str
40
+ """
41
+ The output content.
42
+ """
43
+ timestamp: int
44
+ """
45
+ Unix epoch in nanoseconds
46
+ """
47
+ error: bool = False
48
+ """
49
+ Whether the output is an error.
50
+ """
51
+
52
+ def __str__(self):
53
+ return self.content
54
+
55
+
56
+ @dataclass
57
+ class ExecutionError:
58
+ """
59
+ Represents an error that occurred during the execution of a cell.
60
+ """
61
+
62
+ name: str
63
+ """
64
+ Name of the error.
65
+ """
66
+ value: str
67
+ """
68
+ Value of the error.
69
+ """
70
+ traceback: str
71
+ """
72
+ The raw traceback of the error.
73
+ """
74
+
75
+ def __init__(self, name: str, value: str, traceback: str, **kwargs):
76
+ self.name = name
77
+ self.value = value
78
+ self.traceback = traceback
79
+
80
+ def to_json(self) -> str:
81
+ """
82
+ Returns the JSON representation of the Error object.
83
+ """
84
+ data = {"name": self.name, "value": self.value, "traceback": self.traceback}
85
+ return json.dumps(data)
86
+
87
+
88
+ class MIMEType(str):
89
+ """
90
+ Represents a MIME type.
91
+ """
92
+
93
+
94
+ @dataclass
95
+ class Result:
96
+ """
97
+ Represents the result of code execution.
98
+ """
99
+
100
+ exit_code: int = 0
101
+ """Process exit code."""
102
+ started_at: Optional[datetime] = None
103
+ """Start time."""
104
+ finished_at: Optional[datetime] = None
105
+ """End time."""
106
+ text: Optional[str] = None
107
+ """Text representation."""
108
+ html: Optional[str] = None
109
+ """HTML representation."""
110
+ markdown: Optional[str] = None
111
+ """Markdown representation."""
112
+ svg: Optional[str] = None
113
+ """SVG representation."""
114
+ png: Optional[str] = None
115
+ """PNG representation."""
116
+ jpeg: Optional[str] = None
117
+ """JPEG representation."""
118
+ pdf: Optional[str] = None
119
+ """PDF representation."""
120
+ latex: Optional[str] = None
121
+ """LaTeX representation."""
122
+ json_data: Optional[dict] = None
123
+ """JSON data."""
124
+ javascript: Optional[str] = None
125
+ """JavaScript representation."""
126
+ data: Optional[dict] = None
127
+ """Raw data."""
128
+ chart: Optional[api_pb2.Chart] = None
129
+ """Chart data."""
130
+ execution_count: Optional[int] = None
131
+ """Execution count."""
132
+ is_main_result: bool = False
133
+ """Whether this is the main result."""
134
+ extra: Optional[dict] = None
135
+ """Extra data."""
136
+
137
+ def __init__(
138
+ self,
139
+ exit_code: int = 0,
140
+ started_at: Optional[Timestamp] = None,
141
+ finished_at: Optional[Timestamp] = None,
142
+ text: Optional[str] = None,
143
+ html: Optional[str] = None,
144
+ markdown: Optional[str] = None,
145
+ svg: Optional[str] = None,
146
+ png: Optional[str] = None,
147
+ jpeg: Optional[str] = None,
148
+ pdf: Optional[str] = None,
149
+ latex: Optional[str] = None,
150
+ json: Optional[str] = None,
151
+ javascript: Optional[str] = None,
152
+ data: Optional[str] = None,
153
+ chart: Optional[api_pb2.Chart] = None,
154
+ execution_count: Optional[int] = None,
155
+ is_main_result: bool = False,
156
+ extra: Optional[Dict[str, str]] = None,
157
+ **kwargs,
158
+ ):
159
+ self.exit_code = exit_code
160
+ self.started_at = self._convert_timestamp(started_at) if started_at else None
161
+ self.finished_at = self._convert_timestamp(finished_at) if finished_at else None
162
+ self.text = text
163
+ self.html = html
164
+ self.markdown = markdown
165
+ self.svg = svg
166
+ self.png = png
167
+ self.jpeg = jpeg
168
+ self.pdf = pdf
169
+ self.latex = latex
170
+ self.json_data = json.loads(json) if json else None
171
+ self.javascript = javascript
172
+ self.data = json.loads(data) if data else None
173
+ self.chart = chart
174
+ self.execution_count = execution_count
175
+ self.is_main_result = is_main_result
176
+ self.extra = dict(extra) if extra else {}
177
+
178
+ def _convert_timestamp(self, timestamp: Timestamp) -> datetime:
179
+ """Convert protobuf timestamp to datetime."""
180
+ return datetime.fromtimestamp(timestamp.seconds + timestamp.nanos / 1e9)
181
+
182
+ def formats(self) -> Iterable[str]:
183
+ """
184
+ Returns all available formats of the result.
185
+
186
+ :return: All available formats of the result.
187
+ """
188
+ formats = []
189
+ if self.text:
190
+ formats.append("text")
191
+ if self.html:
192
+ formats.append("html")
193
+ if self.markdown:
194
+ formats.append("markdown")
195
+ if self.svg:
196
+ formats.append("svg")
197
+ if self.png:
198
+ formats.append("png")
199
+ if self.jpeg:
200
+ formats.append("jpeg")
201
+ if self.pdf:
202
+ formats.append("pdf")
203
+ if self.latex:
204
+ formats.append("latex")
205
+ if self.json_data:
206
+ formats.append("json")
207
+ if self.javascript:
208
+ formats.append("javascript")
209
+ if self.data:
210
+ formats.append("data")
211
+ if self.chart:
212
+ formats.append("chart")
213
+
214
+ if self.extra:
215
+ for key in self.extra:
216
+ formats.append(key)
217
+
218
+ return formats
219
+
220
+ def __str__(self) -> str:
221
+ """
222
+ Returns the text representation of the data.
223
+
224
+ :return: The text representation of the data.
225
+ """
226
+ if self.text:
227
+ return f"Result(exit_code={self.exit_code}, text={self.text})"
228
+ else:
229
+ return f"Result(exit_code={self.exit_code}, formats={list(self.formats())})"
230
+
231
+
232
+ @dataclass
233
+ class Logs:
234
+ """
235
+ Data printed to stdout and stderr during execution.
236
+ """
237
+
238
+ stdout: List[str]
239
+ """List of strings printed to stdout."""
240
+ stderr: List[str]
241
+ """List of strings printed to stderr."""
242
+
243
+ def __init__(self, stdout: List[str] = None, stderr: List[str] = None, **kwargs):
244
+ self.stdout = stdout or []
245
+ self.stderr = stderr or []
246
+
247
+ def __repr__(self):
248
+ return f"Logs(stdout={self.stdout}, stderr={self.stderr})"
249
+
250
+ def to_json(self) -> str:
251
+ """
252
+ Returns the JSON representation of the Logs object.
253
+ """
254
+ data = {"stdout": self.stdout, "stderr": self.stderr}
255
+ return json.dumps(data)
256
+
257
+
258
+ @dataclass
259
+ class Execution:
260
+ """
261
+ Represents the result of a code execution.
262
+ """
263
+
264
+ results: List[Result]
265
+ """List of execution results."""
266
+ logs: Logs
267
+ """Logs printed during execution."""
268
+ error: Optional[ExecutionError]
269
+ """Error object if an error occurred."""
270
+ execution_count: Optional[int]
271
+ """Execution count."""
272
+
273
+ def __init__(
274
+ self,
275
+ results: List[Result] = None,
276
+ logs: Logs = None,
277
+ error: Optional[ExecutionError] = None,
278
+ execution_count: Optional[int] = None,
279
+ **kwargs,
280
+ ):
281
+ self.results = results or []
282
+ self.logs = logs or Logs()
283
+ self.error = error
284
+ self.execution_count = execution_count
285
+
286
+ def __repr__(self):
287
+ return f"Execution(results={self.results}, logs={self.logs}, error={self.error}, execution_count={self.execution_count})"
288
+
289
+ @property
290
+ def text(self) -> Optional[str]:
291
+ """
292
+ Returns the text representation of the main result.
293
+
294
+ :return: The text representation of the main result.
295
+ """
296
+ for result in self.results:
297
+ if result.is_main_result:
298
+ return result.text
299
+ return None
300
+
301
+ def to_json(self) -> str:
302
+ """
303
+ Returns the JSON representation of the Execution object.
304
+ """
305
+ data = {
306
+ "results": [self._serialize_result(result) for result in self.results],
307
+ "logs": self.logs.to_json(),
308
+ "error": self.error.to_json() if self.error else None,
309
+ "execution_count": self.execution_count,
310
+ }
311
+ return json.dumps(data)
312
+
313
+ def _serialize_result(self, result: Result) -> Dict[str, Any]:
314
+ """Serialize a single result to JSON-serializable format."""
315
+ serialized = {
316
+ "exit_code": result.exit_code,
317
+ "started_at": result.started_at.isoformat() if result.started_at else None,
318
+ "finished_at": (
319
+ result.finished_at.isoformat() if result.finished_at else None
320
+ ),
321
+ "text": result.text,
322
+ "html": result.html,
323
+ "markdown": result.markdown,
324
+ "svg": result.svg,
325
+ "png": result.png,
326
+ "jpeg": result.jpeg,
327
+ "pdf": result.pdf,
328
+ "latex": result.latex,
329
+ "json": result.json_data,
330
+ "javascript": result.javascript,
331
+ "data": result.data,
332
+ "execution_count": result.execution_count,
333
+ "is_main_result": result.is_main_result,
334
+ "extra": result.extra,
335
+ }
336
+
337
+ # Remove None values
338
+ return {k: v for k, v in serialized.items() if v is not None}
339
+
340
+
341
+ @dataclass
342
+ class Context:
343
+ """
344
+ Represents a context for code execution.
345
+ """
346
+
347
+ id: str
348
+ """The ID of the context."""
349
+ language: str
350
+ """The language of the context."""
351
+ cwd: str
352
+ """The working directory of the context."""
353
+
354
+ def __init__(self, context_id: str, language: str, cwd: str, **kwargs):
355
+ self.id = context_id
356
+ self.language = language
357
+ self.cwd = cwd
358
+
359
+ @classmethod
360
+ def from_json(cls, data: Dict[str, str]):
361
+ return cls(
362
+ context_id=data.get("id"),
363
+ language=data.get("language"),
364
+ cwd=data.get("cwd"),
365
+ )
366
+
367
+
368
+ def parse_output(
369
+ execution: Execution,
370
+ output: api_pb2.ExecuteResponse,
371
+ on_stdout: Optional[OutputHandler[OutputMessage]] = None,
372
+ on_stderr: Optional[OutputHandler[OutputMessage]] = None,
373
+ on_result: Optional[OutputHandler[Result]] = None,
374
+ on_error: Optional[OutputHandler[ExecutionError]] = None,
375
+ ):
376
+ """
377
+ Parse the output from the execution service and update the execution object.
378
+ """
379
+ if output.HasField("stdout"):
380
+ content = output.stdout.content
381
+ execution.logs.stdout.append(content)
382
+ if on_stdout:
383
+ message = OutputMessage(
384
+ content=content,
385
+ timestamp=int(datetime.now().timestamp() * 1e9),
386
+ error=False,
387
+ )
388
+ if asyncio.iscoroutinefunction(on_stdout):
389
+ asyncio.create_task(on_stdout(message))
390
+ else:
391
+ on_stdout(message)
392
+
393
+ elif output.HasField("stderr"):
394
+ content = output.stderr.content
395
+ execution.logs.stderr.append(content)
396
+ if on_stderr:
397
+ message = OutputMessage(
398
+ content=content,
399
+ timestamp=int(datetime.now().timestamp() * 1e9),
400
+ error=True,
401
+ )
402
+ if asyncio.iscoroutinefunction(on_stderr):
403
+ asyncio.create_task(on_stderr(message))
404
+ else:
405
+ on_stderr(message)
406
+
407
+ elif output.HasField("result"):
408
+ result_msg = output.result
409
+ result = Result(
410
+ exit_code=result_msg.exit_code,
411
+ started_at=result_msg.started_at,
412
+ finished_at=result_msg.finished_at,
413
+ text=result_msg.text,
414
+ html=result_msg.html,
415
+ markdown=result_msg.markdown,
416
+ svg=result_msg.svg,
417
+ png=result_msg.png,
418
+ jpeg=result_msg.jpeg,
419
+ pdf=result_msg.pdf,
420
+ latex=result_msg.latex,
421
+ json=result_msg.json,
422
+ javascript=result_msg.javascript,
423
+ data=result_msg.data,
424
+ chart=result_msg.chart,
425
+ execution_count=result_msg.execution_count,
426
+ is_main_result=result_msg.is_main_result,
427
+ extra=dict(result_msg.extra),
428
+ )
429
+ execution.results.append(result)
430
+ if on_result:
431
+ if asyncio.iscoroutinefunction(on_result):
432
+ asyncio.create_task(on_result(result))
433
+ else:
434
+ on_result(result)
435
+
436
+ # Update execution count if this is the main result
437
+ if result.is_main_result and result.execution_count is not None:
438
+ execution.execution_count = result.execution_count
439
+
440
+ elif output.HasField("error"):
441
+ error_msg = output.error
442
+ error = ExecutionError(
443
+ name=error_msg.name,
444
+ value=error_msg.value,
445
+ traceback=error_msg.traceback,
446
+ )
447
+ execution.error = error
448
+ if on_error:
449
+ if asyncio.iscoroutinefunction(on_error):
450
+ asyncio.create_task(on_error(error))
451
+ else:
452
+ on_error(error)
453
+
454
+
455
+ async def aextract_exception(res: Response):
456
+ """Asynchronously extract exception from response."""
457
+ if res.is_success:
458
+ return None
459
+
460
+ await res.aread()
461
+ return extract_exception(res)
462
+
463
+
464
+ def extract_exception(res: Response):
465
+ """Extract exception from response."""
466
+ if res.is_success:
467
+ return None
468
+
469
+ res.read()
470
+ return format_exception(res)
471
+
472
+
473
+ def format_exception(res: Response):
474
+ """Format exception from response."""
475
+ if res.is_success:
476
+ return None
477
+
478
+ if res.status_code == 404:
479
+ return NotFoundException(res.text)
480
+ elif res.status_code == 502:
481
+ return TimeoutException(
482
+ f"{res.text}: This error is likely due to sandbox timeout. You can modify the sandbox timeout by passing 'timeout' when starting the sandbox or calling '.set_timeout' on the sandbox with the desired timeout."
483
+ )
484
+ else:
485
+ return SandboxException(f"{res.status_code}: {res.text}")