opensandbox-code-interpreter 0.1.2.dev0__tar.gz → 1.1.0__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.
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/.gitignore +31 -0
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/PKG-INFO +21 -14
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/README.md +13 -6
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/pyproject.toml +10 -10
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/__init__.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/__init__.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/code_adapter.py +60 -14
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/converter/__init__.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/converter/code_execution_converter.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/adapters/factory.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/code_interpreter.py +139 -6
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/models/__init__.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/models/code.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/models/code_sync.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/services/__init__.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/services/code.py +15 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/__init__.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/adapters/__init__.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/adapters/code_adapter.py +78 -15
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/adapters/factory.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/code_interpreter.py +131 -6
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/services/__init__.py +1 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/sync/services/code.py +15 -1
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/LICENSE +0 -0
- {opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/src/code_interpreter/py.typed +0 -0
|
@@ -133,6 +133,8 @@ dist
|
|
|
133
133
|
# Gatsby files
|
|
134
134
|
.cache/
|
|
135
135
|
public
|
|
136
|
+
!docs/public/
|
|
137
|
+
!docs/public/CNAME
|
|
136
138
|
|
|
137
139
|
# Serverless directories
|
|
138
140
|
.serverless/
|
|
@@ -222,6 +224,9 @@ secrets/
|
|
|
222
224
|
|
|
223
225
|
# Generated API documentation
|
|
224
226
|
docs/generated/
|
|
227
|
+
docs/.vitepress/generated/
|
|
228
|
+
docs/.vitepress/dist/
|
|
229
|
+
docs/.vitepress/cache/
|
|
225
230
|
apidocs/
|
|
226
231
|
|
|
227
232
|
# Test results
|
|
@@ -263,3 +268,29 @@ nbdist/
|
|
|
263
268
|
# Generated files
|
|
264
269
|
generated/
|
|
265
270
|
**/generated/**
|
|
271
|
+
|
|
272
|
+
# Fast Sandbox FastPath gRPC stubs are committed intentionally (deterministic protoc
|
|
273
|
+
# output; the server package needs them at install time, so they are checked
|
|
274
|
+
# in rather than regenerated).
|
|
275
|
+
!server/opensandbox_server/services/fast_sandbox/generated/
|
|
276
|
+
!server/opensandbox_server/services/fast_sandbox/generated/**
|
|
277
|
+
server/opensandbox_server/services/fast_sandbox/generated/__pycache__/
|
|
278
|
+
|
|
279
|
+
# execd eBPF kernel BTF dumps (bpf2go build-time only, never committed —
|
|
280
|
+
# see components/execd/pkg/ebpf/prog/audit.bpf.c; regenerate with:
|
|
281
|
+
# bpftool btf dump file /sys/kernel/btf/vmlinux format c > .../vmlinux.h)
|
|
282
|
+
components/execd/pkg/ebpf/prog/vmlinux.h
|
|
283
|
+
components/execd/pkg/ebpf/prog/vmlinux_*.h
|
|
284
|
+
|
|
285
|
+
# gVisor runtime binaries (downloaded dynamically)
|
|
286
|
+
kubernetes/test/kind/gvisor/runsc
|
|
287
|
+
kubernetes/test/kind/gvisor/containerd-shim-runsc-v1
|
|
288
|
+
bin/
|
|
289
|
+
obj/
|
|
290
|
+
|
|
291
|
+
.qoder/
|
|
292
|
+
/.vs
|
|
293
|
+
|
|
294
|
+
# fast-sandbox pinned source checkout (materialized by
|
|
295
|
+
# manifests/release/build-fast-sandbox.sh from manifests/third-party/fast-sandbox.commit)
|
|
296
|
+
.fast-sandbox/
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: opensandbox-code-interpreter
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: OpenSandbox Code Interpreter Python SDK - Advanced code execution with persistent contexts
|
|
5
|
-
Project-URL: Homepage, https://
|
|
6
|
-
Project-URL: Repository, https://github.com/
|
|
7
|
-
Project-URL: Documentation, https://
|
|
8
|
-
Project-URL: Issues, https://github.com/
|
|
5
|
+
Project-URL: Homepage, https://open-sandbox.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/opensandbox-group/OpenSandbox
|
|
7
|
+
Project-URL: Documentation, https://open-sandbox.ai
|
|
8
|
+
Project-URL: Issues, https://github.com/opensandbox-group/OpenSandbox/issues
|
|
9
9
|
Author-email: OpenSandbox Team <ninan.nn@alibaba-inc.com>
|
|
10
10
|
License: Apache License
|
|
11
11
|
Version 2.0, January 2004
|
|
@@ -223,13 +223,12 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
223
223
|
Classifier: Topic :: Software Development :: Libraries
|
|
224
224
|
Classifier: Typing :: Typed
|
|
225
225
|
Requires-Python: >=3.10
|
|
226
|
-
Requires-Dist: opensandbox<
|
|
227
|
-
Requires-Dist: pydantic<3.0,>=2.
|
|
226
|
+
Requires-Dist: opensandbox<2.0.0,>=1.1.0
|
|
227
|
+
Requires-Dist: pydantic<3.0,>=2.4.2
|
|
228
228
|
Description-Content-Type: text/markdown
|
|
229
229
|
|
|
230
230
|
# OpenSandbox Code Interpreter SDK for Python
|
|
231
231
|
|
|
232
|
-
English | [中文](README_zh.md)
|
|
233
232
|
|
|
234
233
|
A Python SDK for executing code in secure, isolated sandboxes. It provides a high-level API for running Python, Java,
|
|
235
234
|
Go, TypeScript, and other languages safely, with support for code execution contexts.
|
|
@@ -241,7 +240,7 @@ This SDK requires a Docker image containing the Code Interpreter runtime environ
|
|
|
241
240
|
Node.js, etc.
|
|
242
241
|
|
|
243
242
|
For detailed information about supported languages and versions, refer to the
|
|
244
|
-
[Environment Documentation](
|
|
243
|
+
[Environment Documentation](https://github.com/opensandbox-group/sandbox-images).
|
|
245
244
|
|
|
246
245
|
## Installation
|
|
247
246
|
|
|
@@ -281,9 +280,9 @@ async def main() -> None:
|
|
|
281
280
|
|
|
282
281
|
# 2. Create a Sandbox with the code-interpreter image + runtime versions
|
|
283
282
|
sandbox = await Sandbox.create(
|
|
284
|
-
"opensandbox/code-interpreter:
|
|
283
|
+
"opensandbox/code-interpreter:v1.1.0",
|
|
285
284
|
connection_config=config,
|
|
286
|
-
entrypoint=["/opt/
|
|
285
|
+
entrypoint=["/opt/code-interpreter/code-interpreter.sh"],
|
|
287
286
|
env={
|
|
288
287
|
"PYTHON_VERSION": "3.11",
|
|
289
288
|
"JAVA_VERSION": "17",
|
|
@@ -342,9 +341,9 @@ config = ConnectionConfigSync(
|
|
|
342
341
|
)
|
|
343
342
|
|
|
344
343
|
sandbox = SandboxSync.create(
|
|
345
|
-
"opensandbox/code-interpreter:
|
|
344
|
+
"opensandbox/code-interpreter:v1.1.0",
|
|
346
345
|
connection_config=config,
|
|
347
|
-
entrypoint=["/opt/
|
|
346
|
+
entrypoint=["/opt/code-interpreter/code-interpreter.sh"],
|
|
348
347
|
env={"PYTHON_VERSION": "3.11"},
|
|
349
348
|
)
|
|
350
349
|
with sandbox:
|
|
@@ -355,6 +354,14 @@ with sandbox:
|
|
|
355
354
|
sandbox.kill()
|
|
356
355
|
```
|
|
357
356
|
|
|
357
|
+
### Installing Python packages at runtime
|
|
358
|
+
|
|
359
|
+
You can install packages directly via `sandbox.commands.run(...)`:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
execution = await sandbox.commands.run("pip install pandas numpy")
|
|
363
|
+
```
|
|
364
|
+
|
|
358
365
|
## Runtime Configuration
|
|
359
366
|
|
|
360
367
|
### Docker Image
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# OpenSandbox Code Interpreter SDK for Python
|
|
2
2
|
|
|
3
|
-
English | [中文](README_zh.md)
|
|
4
3
|
|
|
5
4
|
A Python SDK for executing code in secure, isolated sandboxes. It provides a high-level API for running Python, Java,
|
|
6
5
|
Go, TypeScript, and other languages safely, with support for code execution contexts.
|
|
@@ -12,7 +11,7 @@ This SDK requires a Docker image containing the Code Interpreter runtime environ
|
|
|
12
11
|
Node.js, etc.
|
|
13
12
|
|
|
14
13
|
For detailed information about supported languages and versions, refer to the
|
|
15
|
-
[Environment Documentation](
|
|
14
|
+
[Environment Documentation](https://github.com/opensandbox-group/sandbox-images).
|
|
16
15
|
|
|
17
16
|
## Installation
|
|
18
17
|
|
|
@@ -52,9 +51,9 @@ async def main() -> None:
|
|
|
52
51
|
|
|
53
52
|
# 2. Create a Sandbox with the code-interpreter image + runtime versions
|
|
54
53
|
sandbox = await Sandbox.create(
|
|
55
|
-
"opensandbox/code-interpreter:
|
|
54
|
+
"opensandbox/code-interpreter:v1.1.0",
|
|
56
55
|
connection_config=config,
|
|
57
|
-
entrypoint=["/opt/
|
|
56
|
+
entrypoint=["/opt/code-interpreter/code-interpreter.sh"],
|
|
58
57
|
env={
|
|
59
58
|
"PYTHON_VERSION": "3.11",
|
|
60
59
|
"JAVA_VERSION": "17",
|
|
@@ -113,9 +112,9 @@ config = ConnectionConfigSync(
|
|
|
113
112
|
)
|
|
114
113
|
|
|
115
114
|
sandbox = SandboxSync.create(
|
|
116
|
-
"opensandbox/code-interpreter:
|
|
115
|
+
"opensandbox/code-interpreter:v1.1.0",
|
|
117
116
|
connection_config=config,
|
|
118
|
-
entrypoint=["/opt/
|
|
117
|
+
entrypoint=["/opt/code-interpreter/code-interpreter.sh"],
|
|
119
118
|
env={"PYTHON_VERSION": "3.11"},
|
|
120
119
|
)
|
|
121
120
|
with sandbox:
|
|
@@ -126,6 +125,14 @@ with sandbox:
|
|
|
126
125
|
sandbox.kill()
|
|
127
126
|
```
|
|
128
127
|
|
|
128
|
+
### Installing Python packages at runtime
|
|
129
|
+
|
|
130
|
+
You can install packages directly via `sandbox.commands.run(...)`:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
execution = await sandbox.commands.run("pip install pandas numpy")
|
|
134
|
+
```
|
|
135
|
+
|
|
129
136
|
## Runtime Configuration
|
|
130
137
|
|
|
131
138
|
### Docker Image
|
{opensandbox_code_interpreter-0.1.2.dev0 → opensandbox_code_interpreter-1.1.0}/pyproject.toml
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2025
|
|
1
|
+
# Copyright 2025 The OpenSandbox Authors
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -42,15 +42,15 @@ classifiers = [
|
|
|
42
42
|
"Typing :: Typed",
|
|
43
43
|
]
|
|
44
44
|
dependencies = [
|
|
45
|
-
"pydantic>=2.
|
|
46
|
-
"opensandbox>=
|
|
45
|
+
"pydantic>=2.4.2,<3.0",
|
|
46
|
+
"opensandbox>=1.1.0,<2.0.0",
|
|
47
47
|
]
|
|
48
48
|
|
|
49
49
|
[project.urls]
|
|
50
|
-
Homepage = "https://
|
|
51
|
-
Repository = "https://github.com/
|
|
52
|
-
Documentation = "https://
|
|
53
|
-
Issues = "https://github.com/
|
|
50
|
+
Homepage = "https://open-sandbox.ai"
|
|
51
|
+
Repository = "https://github.com/opensandbox-group/OpenSandbox"
|
|
52
|
+
Documentation = "https://open-sandbox.ai"
|
|
53
|
+
Issues = "https://github.com/opensandbox-group/OpenSandbox/issues"
|
|
54
54
|
|
|
55
55
|
[tool.hatch.version]
|
|
56
56
|
source = "vcs"
|
|
@@ -58,9 +58,9 @@ source = "vcs"
|
|
|
58
58
|
[tool.hatch.version.raw-options]
|
|
59
59
|
# This package is in a subdirectory; explicitly point setuptools-scm at the git root.
|
|
60
60
|
root = "../../.."
|
|
61
|
-
tag_regex = "^
|
|
62
|
-
git_describe_command = 'git describe --dirty --tags --long --match "
|
|
63
|
-
fallback_version = "0.1.
|
|
61
|
+
tag_regex = "^release-(?P<version>\\d+\\.\\d+\\.\\d+(?:[\\.\\w\\+\\-]*)?)$"
|
|
62
|
+
git_describe_command = 'git describe --dirty --tags --long --match "release-*"'
|
|
63
|
+
fallback_version = "0.1.3"
|
|
64
64
|
|
|
65
65
|
[tool.hatch.build]
|
|
66
66
|
include = [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025
|
|
2
|
+
# Copyright 2025 The OpenSandbox Authors
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
# you may not use this file except in compliance with the License.
|
|
@@ -22,6 +22,7 @@ API clients and handling SSE streaming for real-time code execution.
|
|
|
22
22
|
|
|
23
23
|
import json
|
|
24
24
|
import logging
|
|
25
|
+
import time
|
|
25
26
|
|
|
26
27
|
import httpx
|
|
27
28
|
from opensandbox.adapters.converter.event_node import EventNode
|
|
@@ -32,6 +33,7 @@ from opensandbox.adapters.converter.execution_event_dispatcher import (
|
|
|
32
33
|
ExecutionEventDispatcher,
|
|
33
34
|
)
|
|
34
35
|
from opensandbox.adapters.converter.response_handler import (
|
|
36
|
+
extract_request_id,
|
|
35
37
|
handle_api_error,
|
|
36
38
|
require_parsed,
|
|
37
39
|
)
|
|
@@ -39,6 +41,7 @@ from opensandbox.config import ConnectionConfig
|
|
|
39
41
|
from opensandbox.exceptions import InvalidArgumentException, SandboxApiException
|
|
40
42
|
from opensandbox.models.execd import Execution, ExecutionHandlers
|
|
41
43
|
from opensandbox.models.sandboxes import SandboxEndpoint
|
|
44
|
+
from opensandbox.transport import unwrap_retry_transport
|
|
42
45
|
|
|
43
46
|
from code_interpreter.adapters.converter.code_execution_converter import (
|
|
44
47
|
CodeExecutionConverter,
|
|
@@ -49,6 +52,22 @@ from code_interpreter.services.code import Codes
|
|
|
49
52
|
logger = logging.getLogger(__name__)
|
|
50
53
|
|
|
51
54
|
|
|
55
|
+
def _normalize_sse_event(event_dict: dict) -> dict:
|
|
56
|
+
if "type" in event_dict and "timestamp" in event_dict:
|
|
57
|
+
return event_dict
|
|
58
|
+
if "code" in event_dict and "message" in event_dict:
|
|
59
|
+
return {
|
|
60
|
+
"type": "error",
|
|
61
|
+
"timestamp": int(time.time() * 1000),
|
|
62
|
+
"error": {
|
|
63
|
+
"ename": str(event_dict["code"]),
|
|
64
|
+
"evalue": str(event_dict["message"]),
|
|
65
|
+
"traceback": [],
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
return event_dict
|
|
69
|
+
|
|
70
|
+
|
|
52
71
|
class CodesAdapter(Codes):
|
|
53
72
|
"""
|
|
54
73
|
Adapter implementation for code execution service.
|
|
@@ -88,6 +107,7 @@ class CodesAdapter(Codes):
|
|
|
88
107
|
headers = {
|
|
89
108
|
"User-Agent": self.connection_config.user_agent,
|
|
90
109
|
**self.connection_config.headers,
|
|
110
|
+
**self.execd_endpoint.headers,
|
|
91
111
|
}
|
|
92
112
|
|
|
93
113
|
# Execd API does not require authentication
|
|
@@ -111,6 +131,9 @@ class CodesAdapter(Codes):
|
|
|
111
131
|
"Accept": "text/event-stream",
|
|
112
132
|
"Cache-Control": "no-cache",
|
|
113
133
|
}
|
|
134
|
+
# SSE bootstraps bypass the retry wrapper: request bodies are
|
|
135
|
+
# not replayable and a non-idempotent status opt-in would cause
|
|
136
|
+
# duplicate execution on a resent SSE POST.
|
|
114
137
|
self._sse_client = httpx.AsyncClient(
|
|
115
138
|
headers=sse_headers,
|
|
116
139
|
timeout=httpx.Timeout(
|
|
@@ -119,7 +142,7 @@ class CodesAdapter(Codes):
|
|
|
119
142
|
write=timeout_seconds,
|
|
120
143
|
pool=None,
|
|
121
144
|
),
|
|
122
|
-
transport=self.connection_config.transport,
|
|
145
|
+
transport=unwrap_retry_transport(self.connection_config.transport),
|
|
123
146
|
)
|
|
124
147
|
|
|
125
148
|
async def _get_client(self):
|
|
@@ -197,7 +220,9 @@ class CodesAdapter(Codes):
|
|
|
197
220
|
)
|
|
198
221
|
handle_api_error(response_obj, "List code contexts")
|
|
199
222
|
parsed_list = require_parsed(response_obj, list, "List code contexts")
|
|
200
|
-
return [
|
|
223
|
+
return [
|
|
224
|
+
CodeExecutionConverter.from_api_code_context(c) for c in parsed_list
|
|
225
|
+
]
|
|
201
226
|
except Exception as e:
|
|
202
227
|
logger.error("Failed to list contexts", exc_info=e)
|
|
203
228
|
raise ExceptionConverter.to_sandbox_exception(e) from e
|
|
@@ -250,7 +275,11 @@ class CodesAdapter(Codes):
|
|
|
250
275
|
raise InvalidArgumentException("Code cannot be empty")
|
|
251
276
|
|
|
252
277
|
try:
|
|
253
|
-
if
|
|
278
|
+
if (
|
|
279
|
+
context is not None
|
|
280
|
+
and language is not None
|
|
281
|
+
and context.language != language
|
|
282
|
+
):
|
|
254
283
|
raise InvalidArgumentException(
|
|
255
284
|
f"language '{language}' must match context.language '{context.language}'"
|
|
256
285
|
)
|
|
@@ -280,13 +309,12 @@ class CodesAdapter(Codes):
|
|
|
280
309
|
await response.aread()
|
|
281
310
|
error_body = response.text
|
|
282
311
|
logger.error(
|
|
283
|
-
"Failed to run code. Status:
|
|
284
|
-
response.status_code,
|
|
285
|
-
error_body,
|
|
312
|
+
f"Failed to run code. Status: {response.status_code}, Body: {error_body}"
|
|
286
313
|
)
|
|
287
314
|
raise SandboxApiException(
|
|
288
|
-
message=f"Failed to run code. Status code: {response.status_code}",
|
|
315
|
+
message=f"Failed to run code. Status code: {response.status_code}, Body: {error_body}",
|
|
289
316
|
status_code=response.status_code,
|
|
317
|
+
request_id=extract_request_id(response.headers),
|
|
290
318
|
)
|
|
291
319
|
|
|
292
320
|
dispatcher = ExecutionEventDispatcher(execution, handlers)
|
|
@@ -301,22 +329,20 @@ class CodesAdapter(Codes):
|
|
|
301
329
|
data = data[5:].strip()
|
|
302
330
|
|
|
303
331
|
try:
|
|
304
|
-
event_dict = json.loads(data)
|
|
332
|
+
event_dict = _normalize_sse_event(json.loads(data))
|
|
305
333
|
event_node = EventNode(**event_dict)
|
|
306
334
|
await dispatcher.dispatch(event_node)
|
|
307
335
|
except json.JSONDecodeError:
|
|
308
|
-
logger.debug("Failed to parse SSE line:
|
|
336
|
+
logger.debug(f"Failed to parse SSE line: {line}")
|
|
309
337
|
continue
|
|
310
338
|
except Exception as e:
|
|
311
|
-
logger.error("Error processing event:
|
|
339
|
+
logger.error(f"Error processing event: {data}", exc_info=e)
|
|
312
340
|
continue
|
|
313
341
|
|
|
314
342
|
return execution
|
|
315
343
|
|
|
316
344
|
except Exception as e:
|
|
317
|
-
logger.error(
|
|
318
|
-
"Failed to run code (length: %s)", len(code), exc_info=e
|
|
319
|
-
)
|
|
345
|
+
logger.error(f"Failed to run code (length: {len(code)})", exc_info=e)
|
|
320
346
|
raise ExceptionConverter.to_sandbox_exception(e) from e
|
|
321
347
|
|
|
322
348
|
async def interrupt(self, execution_id: str) -> None:
|
|
@@ -339,3 +365,23 @@ class CodesAdapter(Codes):
|
|
|
339
365
|
except Exception as e:
|
|
340
366
|
logger.error("Failed to interrupt code execution", exc_info=e)
|
|
341
367
|
raise ExceptionConverter.to_sandbox_exception(e) from e
|
|
368
|
+
|
|
369
|
+
async def ping(self) -> bool:
|
|
370
|
+
"""
|
|
371
|
+
Check if the execd code execution service is alive.
|
|
372
|
+
|
|
373
|
+
Uses the shared generated API client so the ping exercises the same
|
|
374
|
+
HTTP client and endpoint that serve code execution requests.
|
|
375
|
+
"""
|
|
376
|
+
try:
|
|
377
|
+
from opensandbox.api.execd.api.health import ping as ping_api
|
|
378
|
+
|
|
379
|
+
client = await self._get_client()
|
|
380
|
+
response_obj = await ping_api.asyncio_detailed(client=client)
|
|
381
|
+
|
|
382
|
+
handle_api_error(response_obj, "Ping code interpreter")
|
|
383
|
+
return True
|
|
384
|
+
|
|
385
|
+
except Exception as e:
|
|
386
|
+
logger.debug(f"Code interpreter ping failed: {e}")
|
|
387
|
+
return False
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025
|
|
2
|
+
# Copyright 2025 The OpenSandbox Authors
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
# you may not use this file except in compliance with the License.
|
|
@@ -21,12 +21,16 @@ functionality with specialized code execution features, including multi-language
|
|
|
21
21
|
support, session management, and variable persistence.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
import asyncio
|
|
24
25
|
import logging
|
|
26
|
+
import time
|
|
27
|
+
from datetime import timedelta
|
|
25
28
|
|
|
26
29
|
from opensandbox.exceptions import (
|
|
27
30
|
InvalidArgumentException,
|
|
28
31
|
SandboxException,
|
|
29
32
|
SandboxInternalException,
|
|
33
|
+
SandboxReadyTimeoutException,
|
|
30
34
|
)
|
|
31
35
|
from opensandbox.sandbox import Sandbox
|
|
32
36
|
|
|
@@ -35,6 +39,21 @@ from code_interpreter.services.code import Codes
|
|
|
35
39
|
|
|
36
40
|
logger = logging.getLogger(__name__)
|
|
37
41
|
|
|
42
|
+
DEFAULT_READY_TIMEOUT = timedelta(seconds=30)
|
|
43
|
+
DEFAULT_HEALTH_CHECK_POLLING_INTERVAL = timedelta(milliseconds=200)
|
|
44
|
+
|
|
45
|
+
# Strict health check script: verifies the code interpreter runtime (Jupyter
|
|
46
|
+
# kernel gateway) is actually serving inside the sandbox. execd starts serving
|
|
47
|
+
# /ping before the entrypoint launches Jupyter, and the setup stage may run
|
|
48
|
+
# short-lived "jupyter kernelspec" helpers, so a daemon ping or a process-name
|
|
49
|
+
# grep cannot prove the runtime is ready. Probing the Jupyter listen port
|
|
50
|
+
# (127.0.0.1:${JUPYTER_PORT:-44771}, same default as the entrypoint) only
|
|
51
|
+
# passes once the server accepts connections.
|
|
52
|
+
RUNTIME_PROCESS_CHECK_COMMAND = (
|
|
53
|
+
"bash -c 'exec 3<>/dev/tcp/127.0.0.1/${JUPYTER_PORT:-44771}' "
|
|
54
|
+
"&& exit 0 || exit 1"
|
|
55
|
+
)
|
|
56
|
+
|
|
38
57
|
|
|
39
58
|
class CodeInterpreter:
|
|
40
59
|
"""
|
|
@@ -169,14 +188,114 @@ class CodeInterpreter:
|
|
|
169
188
|
"""
|
|
170
189
|
return self._code_service
|
|
171
190
|
|
|
191
|
+
async def ping(self) -> bool:
|
|
192
|
+
"""
|
|
193
|
+
Check if the code execution service (execd) is responsive.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
True if the code execution service is responsive, False otherwise
|
|
197
|
+
"""
|
|
198
|
+
return await self._code_service.ping()
|
|
199
|
+
|
|
200
|
+
async def is_healthy(self) -> bool:
|
|
201
|
+
"""
|
|
202
|
+
Check if the code interpreter is healthy (strict check).
|
|
203
|
+
|
|
204
|
+
Healthy means both:
|
|
205
|
+
|
|
206
|
+
- the code execution service (execd) answers ``GET /ping``; and
|
|
207
|
+
- the code interpreter runtime (Jupyter kernel gateway) is serving
|
|
208
|
+
inside the sandbox, verified by probing its listen port through the
|
|
209
|
+
execd command API.
|
|
210
|
+
|
|
211
|
+
Exceptions raised by either leg are treated as unhealthy.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
True if healthy, False otherwise
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
return await self.ping() and await self._is_runtime_process_alive()
|
|
218
|
+
except Exception:
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
async def _is_runtime_process_alive(self) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Check if the code interpreter runtime (Jupyter) is serving.
|
|
224
|
+
|
|
225
|
+
Probes the runtime listen port through the sandbox command service and
|
|
226
|
+
treats a failed command (non-zero exit surfaced as an execution error)
|
|
227
|
+
as an unhealthy runtime.
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
execution = await self._sandbox.commands.run(
|
|
231
|
+
RUNTIME_PROCESS_CHECK_COMMAND
|
|
232
|
+
)
|
|
233
|
+
return execution.error is None
|
|
234
|
+
except Exception:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
async def check_ready(
|
|
238
|
+
self,
|
|
239
|
+
timeout: timedelta,
|
|
240
|
+
polling_interval: timedelta,
|
|
241
|
+
) -> None:
|
|
242
|
+
"""
|
|
243
|
+
Wait for the code interpreter to pass the strict health check with polling.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
SandboxReadyTimeoutException: if the health check doesn't pass within timeout
|
|
247
|
+
"""
|
|
248
|
+
logger.info(
|
|
249
|
+
f"Waiting for code interpreter {self.id} to pass health check "
|
|
250
|
+
f"(timeout: {timeout.total_seconds()}s)"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
deadline = time.monotonic() + timeout.total_seconds()
|
|
254
|
+
attempt = 0
|
|
255
|
+
|
|
256
|
+
while time.monotonic() < deadline:
|
|
257
|
+
attempt += 1
|
|
258
|
+
if await self.is_healthy():
|
|
259
|
+
logger.info(
|
|
260
|
+
f"Code interpreter {self.id} passed health check "
|
|
261
|
+
f"after {attempt} attempts"
|
|
262
|
+
)
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
remaining = deadline - time.monotonic()
|
|
266
|
+
if remaining <= 0:
|
|
267
|
+
break
|
|
268
|
+
await asyncio.sleep(min(polling_interval.total_seconds(), remaining))
|
|
269
|
+
|
|
270
|
+
raise SandboxReadyTimeoutException(
|
|
271
|
+
f"Code interpreter {self.id} health check timed out after "
|
|
272
|
+
f"{timeout.total_seconds()}s ({attempt} attempts). The code execution "
|
|
273
|
+
f"service (execd) or the interpreter runtime (Jupyter) did not become "
|
|
274
|
+
f"ready. Pass skip_health_check=True to skip this check."
|
|
275
|
+
)
|
|
276
|
+
|
|
172
277
|
@classmethod
|
|
173
|
-
async def create(
|
|
278
|
+
async def create(
|
|
279
|
+
cls,
|
|
280
|
+
sandbox: Sandbox,
|
|
281
|
+
*,
|
|
282
|
+
ready_timeout: timedelta = DEFAULT_READY_TIMEOUT,
|
|
283
|
+
health_check_polling_interval: timedelta = DEFAULT_HEALTH_CHECK_POLLING_INTERVAL,
|
|
284
|
+
skip_health_check: bool = False,
|
|
285
|
+
) -> "CodeInterpreter":
|
|
174
286
|
"""
|
|
175
287
|
Creates a CodeInterpreter from an existing Sandbox instance.
|
|
176
288
|
|
|
177
289
|
This factory method handles the creation and initialization of CodeInterpreter
|
|
178
290
|
services, including the code execution service and language configuration.
|
|
179
291
|
|
|
292
|
+
By default a strict health check runs before the interpreter is returned:
|
|
293
|
+
the code execution service (execd) must answer ``GET /ping`` AND the
|
|
294
|
+
code interpreter runtime (Jupyter kernel gateway) must be serving,
|
|
295
|
+
both within ``ready_timeout``. execd starts serving before the runtime
|
|
296
|
+
launches, so the daemon ping alone is not enough. Set
|
|
297
|
+
``skip_health_check=True`` to opt out.
|
|
298
|
+
|
|
180
299
|
CodeInterpreter must be created by wrapping an existing Sandbox instance with
|
|
181
300
|
code execution capabilities. This design ensures clear separation of concerns:
|
|
182
301
|
- Sandbox handles infrastructure (containers, resources, networking)
|
|
@@ -184,6 +303,10 @@ class CodeInterpreter:
|
|
|
184
303
|
|
|
185
304
|
Args:
|
|
186
305
|
sandbox: Existing sandbox instance to wrap with code execution capabilities
|
|
306
|
+
ready_timeout: Maximum time to wait for the code execution service health check
|
|
307
|
+
health_check_polling_interval: Time between health check attempts
|
|
308
|
+
skip_health_check: If True, do not wait for the code execution service
|
|
309
|
+
to become ready; the returned interpreter may fail on first use
|
|
187
310
|
|
|
188
311
|
Returns:
|
|
189
312
|
CodeInterpreter instance wrapping the sandbox
|
|
@@ -191,24 +314,34 @@ class CodeInterpreter:
|
|
|
191
314
|
Raises:
|
|
192
315
|
InvalidArgumentException: If sandbox is not provided
|
|
193
316
|
SandboxException: If creation fails
|
|
317
|
+
SandboxReadyTimeoutException: If the code execution service health check
|
|
318
|
+
times out and ``skip_health_check`` is False
|
|
194
319
|
SandboxInternalException: If internal service initialization fails
|
|
195
320
|
"""
|
|
196
321
|
if sandbox is None:
|
|
197
322
|
raise InvalidArgumentException("Sandbox instance must be provided")
|
|
198
323
|
|
|
199
|
-
logger.info("Creating code interpreter from sandbox:
|
|
324
|
+
logger.info(f"Creating code interpreter from sandbox: {sandbox.id}")
|
|
200
325
|
|
|
201
326
|
factory = AdapterFactory(sandbox.connection_config)
|
|
202
327
|
|
|
203
328
|
try:
|
|
204
329
|
# Connect to the execd daemon endpoint for code execution services
|
|
205
330
|
from opensandbox.constants import DEFAULT_EXECD_PORT
|
|
331
|
+
|
|
206
332
|
code_interpreter_endpoint = await sandbox.get_endpoint(DEFAULT_EXECD_PORT)
|
|
207
|
-
code_execution_service = factory.create_code_execution_service(
|
|
333
|
+
code_execution_service = factory.create_code_execution_service(
|
|
334
|
+
code_interpreter_endpoint
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
interpreter = cls(sandbox, code_execution_service)
|
|
338
|
+
|
|
339
|
+
if not skip_health_check:
|
|
340
|
+
await interpreter.check_ready(ready_timeout, health_check_polling_interval)
|
|
208
341
|
|
|
209
|
-
logger.info("Code interpreter
|
|
342
|
+
logger.info(f"Code interpreter {sandbox.id} created successfully")
|
|
210
343
|
|
|
211
|
-
return
|
|
344
|
+
return interpreter
|
|
212
345
|
except Exception as e:
|
|
213
346
|
if isinstance(e, SandboxException):
|
|
214
347
|
raise
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025
|
|
2
|
+
# Copyright 2025 The OpenSandbox Authors
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
# you may not use this file except in compliance with the License.
|
|
@@ -212,3 +212,17 @@ class Codes(Protocol):
|
|
|
212
212
|
SandboxException: If interruption fails
|
|
213
213
|
"""
|
|
214
214
|
...
|
|
215
|
+
|
|
216
|
+
async def ping(self) -> bool:
|
|
217
|
+
"""
|
|
218
|
+
Check if the code execution service (execd) is alive.
|
|
219
|
+
|
|
220
|
+
The ping targets the execd daemon endpoint used by this service
|
|
221
|
+
(``GET /ping``). It does not verify that a specific language runtime
|
|
222
|
+
is ready, only that the daemon serving code execution requests is
|
|
223
|
+
responsive.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
True if the code execution service is responsive, False otherwise
|
|
227
|
+
"""
|
|
228
|
+
...
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025
|
|
2
|
+
# Copyright 2025 The OpenSandbox Authors
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
# you may not use this file except in compliance with the License.
|
|
@@ -19,6 +19,7 @@ Synchronous adapter for code execution service (including SSE streaming).
|
|
|
19
19
|
|
|
20
20
|
import json
|
|
21
21
|
import logging
|
|
22
|
+
import time
|
|
22
23
|
|
|
23
24
|
import httpx
|
|
24
25
|
from opensandbox.adapters.converter.event_node import EventNode
|
|
@@ -26,6 +27,7 @@ from opensandbox.adapters.converter.exception_converter import (
|
|
|
26
27
|
ExceptionConverter,
|
|
27
28
|
)
|
|
28
29
|
from opensandbox.adapters.converter.response_handler import (
|
|
30
|
+
extract_request_id,
|
|
29
31
|
handle_api_error,
|
|
30
32
|
require_parsed,
|
|
31
33
|
)
|
|
@@ -37,6 +39,7 @@ from opensandbox.models.sandboxes import SandboxEndpoint
|
|
|
37
39
|
from opensandbox.sync.adapters.converter.execution_event_dispatcher import (
|
|
38
40
|
ExecutionEventDispatcherSync,
|
|
39
41
|
)
|
|
42
|
+
from opensandbox.transport import unwrap_retry_transport
|
|
40
43
|
|
|
41
44
|
from code_interpreter.models.code_sync import CodeContextSync, SupportedLanguageSync
|
|
42
45
|
from code_interpreter.sync.services.code import CodesSync
|
|
@@ -44,6 +47,22 @@ from code_interpreter.sync.services.code import CodesSync
|
|
|
44
47
|
logger = logging.getLogger(__name__)
|
|
45
48
|
|
|
46
49
|
|
|
50
|
+
def _normalize_sse_event(event_dict: dict) -> dict:
|
|
51
|
+
if "type" in event_dict and "timestamp" in event_dict:
|
|
52
|
+
return event_dict
|
|
53
|
+
if "code" in event_dict and "message" in event_dict:
|
|
54
|
+
return {
|
|
55
|
+
"type": "error",
|
|
56
|
+
"timestamp": int(time.time() * 1000),
|
|
57
|
+
"error": {
|
|
58
|
+
"ename": str(event_dict["code"]),
|
|
59
|
+
"evalue": str(event_dict["message"]),
|
|
60
|
+
"traceback": [],
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
return event_dict
|
|
64
|
+
|
|
65
|
+
|
|
47
66
|
class CodesAdapterSync(CodesSync):
|
|
48
67
|
"""
|
|
49
68
|
Synchronous adapter for code execution service.
|
|
@@ -63,7 +82,9 @@ class CodesAdapterSync(CodesSync):
|
|
|
63
82
|
RUN_CODE_PATH = "/code"
|
|
64
83
|
CREATE_CONTEXT_PATH = "/code/context"
|
|
65
84
|
|
|
66
|
-
def __init__(
|
|
85
|
+
def __init__(
|
|
86
|
+
self, execd_endpoint: SandboxEndpoint, connection_config: ConnectionConfigSync
|
|
87
|
+
) -> None:
|
|
67
88
|
"""
|
|
68
89
|
Initialize the code service adapter (sync).
|
|
69
90
|
|
|
@@ -78,7 +99,11 @@ class CodesAdapterSync(CodesSync):
|
|
|
78
99
|
base_url = f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}"
|
|
79
100
|
timeout_seconds = self.connection_config.request_timeout.total_seconds()
|
|
80
101
|
timeout = httpx.Timeout(timeout_seconds)
|
|
81
|
-
headers = {
|
|
102
|
+
headers = {
|
|
103
|
+
"User-Agent": self.connection_config.user_agent,
|
|
104
|
+
**self.connection_config.headers,
|
|
105
|
+
**(self.execd_endpoint.headers or {}),
|
|
106
|
+
}
|
|
82
107
|
|
|
83
108
|
self._client = Client(base_url=base_url, timeout=timeout)
|
|
84
109
|
self._httpx_client = httpx.Client(
|
|
@@ -89,16 +114,27 @@ class CodesAdapterSync(CodesSync):
|
|
|
89
114
|
)
|
|
90
115
|
self._client.set_httpx_client(self._httpx_client)
|
|
91
116
|
|
|
92
|
-
sse_headers = {
|
|
117
|
+
sse_headers = {
|
|
118
|
+
**headers,
|
|
119
|
+
"Accept": "text/event-stream",
|
|
120
|
+
"Cache-Control": "no-cache",
|
|
121
|
+
}
|
|
122
|
+
# SSE bootstraps bypass the retry wrapper: request bodies are
|
|
123
|
+
# not replayable and a non-idempotent status opt-in would cause
|
|
124
|
+
# duplicate execution on a resent SSE POST.
|
|
93
125
|
self._sse_client = httpx.Client(
|
|
94
126
|
headers=sse_headers,
|
|
95
|
-
timeout=httpx.Timeout(
|
|
96
|
-
|
|
127
|
+
timeout=httpx.Timeout(
|
|
128
|
+
connect=timeout_seconds, read=None, write=timeout_seconds, pool=None
|
|
129
|
+
),
|
|
130
|
+
transport=unwrap_retry_transport(self.connection_config.transport),
|
|
97
131
|
)
|
|
98
132
|
|
|
99
133
|
def _get_execd_url(self, path: str) -> str:
|
|
100
134
|
"""Build URL for execd endpoint."""
|
|
101
|
-
return
|
|
135
|
+
return (
|
|
136
|
+
f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}{path}"
|
|
137
|
+
)
|
|
102
138
|
|
|
103
139
|
def create_context(self, language: str) -> CodeContextSync:
|
|
104
140
|
"""
|
|
@@ -225,7 +261,11 @@ class CodesAdapterSync(CodesSync):
|
|
|
225
261
|
raise InvalidArgumentException("Code cannot be empty")
|
|
226
262
|
|
|
227
263
|
try:
|
|
228
|
-
if
|
|
264
|
+
if (
|
|
265
|
+
context is not None
|
|
266
|
+
and language is not None
|
|
267
|
+
and context.language != language
|
|
268
|
+
):
|
|
229
269
|
raise InvalidArgumentException(
|
|
230
270
|
f"language '{language}' must match context.language '{context.language}'"
|
|
231
271
|
)
|
|
@@ -233,7 +273,9 @@ class CodesAdapterSync(CodesSync):
|
|
|
233
273
|
if context is None:
|
|
234
274
|
# Default context: language default context (server-side behavior).
|
|
235
275
|
# When context.id is omitted, execd will create/reuse a default session per language.
|
|
236
|
-
context = CodeContextSync(
|
|
276
|
+
context = CodeContextSync(
|
|
277
|
+
language=language or SupportedLanguageSync.PYTHON
|
|
278
|
+
)
|
|
237
279
|
api_request = {
|
|
238
280
|
"code": code,
|
|
239
281
|
"context": {
|
|
@@ -249,9 +291,11 @@ class CodesAdapterSync(CodesSync):
|
|
|
249
291
|
with self._sse_client.stream("POST", url, json=api_request) as response:
|
|
250
292
|
if response.status_code != 200:
|
|
251
293
|
response.read()
|
|
294
|
+
error_body = response.text
|
|
252
295
|
raise SandboxApiException(
|
|
253
|
-
message=f"Failed to run code. Status code: {response.status_code}",
|
|
296
|
+
message=f"Failed to run code. Status code: {response.status_code}, Body: {error_body}",
|
|
254
297
|
status_code=response.status_code,
|
|
298
|
+
request_id=extract_request_id(response.headers),
|
|
255
299
|
)
|
|
256
300
|
|
|
257
301
|
for line in response.iter_lines():
|
|
@@ -261,19 +305,19 @@ class CodesAdapterSync(CodesSync):
|
|
|
261
305
|
if data.startswith("data:"):
|
|
262
306
|
data = data[5:].strip()
|
|
263
307
|
try:
|
|
264
|
-
event_dict = json.loads(data)
|
|
308
|
+
event_dict = _normalize_sse_event(json.loads(data))
|
|
265
309
|
event_node = EventNode(**event_dict)
|
|
266
310
|
dispatcher.dispatch(event_node)
|
|
267
311
|
except json.JSONDecodeError:
|
|
268
|
-
logger.debug("Failed to parse SSE line:
|
|
312
|
+
logger.debug(f"Failed to parse SSE line: {line}")
|
|
269
313
|
continue
|
|
270
314
|
except Exception as e:
|
|
271
|
-
logger.error("Error processing event:
|
|
315
|
+
logger.error(f"Error processing event: {data}", exc_info=e)
|
|
272
316
|
continue
|
|
273
317
|
|
|
274
318
|
return execution
|
|
275
319
|
except Exception as e:
|
|
276
|
-
logger.error("Failed to run code (length:
|
|
320
|
+
logger.error(f"Failed to run code (length: {len(code)})", exc_info=e)
|
|
277
321
|
raise ExceptionConverter.to_sandbox_exception(e) from e
|
|
278
322
|
|
|
279
323
|
def interrupt(self, execution_id: str) -> None:
|
|
@@ -286,8 +330,27 @@ class CodesAdapterSync(CodesSync):
|
|
|
286
330
|
try:
|
|
287
331
|
from opensandbox.api.execd.api.code_interpreting import interrupt_code
|
|
288
332
|
|
|
289
|
-
response_obj = interrupt_code.sync_detailed(
|
|
333
|
+
response_obj = interrupt_code.sync_detailed(
|
|
334
|
+
client=self._client, id=execution_id
|
|
335
|
+
)
|
|
290
336
|
handle_api_error(response_obj, "Interrupt code execution")
|
|
291
337
|
except Exception as e:
|
|
292
338
|
logger.error("Failed to interrupt code execution", exc_info=e)
|
|
293
339
|
raise ExceptionConverter.to_sandbox_exception(e) from e
|
|
340
|
+
|
|
341
|
+
def ping(self) -> bool:
|
|
342
|
+
"""
|
|
343
|
+
Check if the execd code execution service is alive (blocking).
|
|
344
|
+
|
|
345
|
+
Uses the shared generated API client so the ping exercises the same
|
|
346
|
+
HTTP client and endpoint that serve code execution requests.
|
|
347
|
+
"""
|
|
348
|
+
try:
|
|
349
|
+
from opensandbox.api.execd.api.health import ping as ping_api
|
|
350
|
+
|
|
351
|
+
response_obj = ping_api.sync_detailed(client=self._client)
|
|
352
|
+
handle_api_error(response_obj, "Ping code interpreter")
|
|
353
|
+
return True
|
|
354
|
+
except Exception as e:
|
|
355
|
+
logger.debug(f"Code interpreter ping failed: {e}")
|
|
356
|
+
return False
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025
|
|
2
|
+
# Copyright 2025 The OpenSandbox Authors
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
# you may not use this file except in compliance with the License.
|
|
@@ -18,12 +18,15 @@ Synchronous Code Interpreter SDK.
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
+
import time
|
|
22
|
+
from datetime import timedelta
|
|
21
23
|
|
|
22
24
|
from opensandbox.constants import DEFAULT_EXECD_PORT
|
|
23
25
|
from opensandbox.exceptions import (
|
|
24
26
|
InvalidArgumentException,
|
|
25
27
|
SandboxException,
|
|
26
28
|
SandboxInternalException,
|
|
29
|
+
SandboxReadyTimeoutException,
|
|
27
30
|
)
|
|
28
31
|
from opensandbox.sync.sandbox import SandboxSync
|
|
29
32
|
|
|
@@ -32,6 +35,21 @@ from code_interpreter.sync.services.code import CodesSync
|
|
|
32
35
|
|
|
33
36
|
logger = logging.getLogger(__name__)
|
|
34
37
|
|
|
38
|
+
DEFAULT_READY_TIMEOUT = timedelta(seconds=30)
|
|
39
|
+
DEFAULT_HEALTH_CHECK_POLLING_INTERVAL = timedelta(milliseconds=200)
|
|
40
|
+
|
|
41
|
+
# Strict health check script: verifies the code interpreter runtime (Jupyter
|
|
42
|
+
# kernel gateway) is actually serving inside the sandbox. execd starts serving
|
|
43
|
+
# /ping before the entrypoint launches Jupyter, and the setup stage may run
|
|
44
|
+
# short-lived "jupyter kernelspec" helpers, so a daemon ping or a process-name
|
|
45
|
+
# grep cannot prove the runtime is ready. Probing the Jupyter listen port
|
|
46
|
+
# (127.0.0.1:${JUPYTER_PORT:-44771}, same default as the entrypoint) only
|
|
47
|
+
# passes once the server accepts connections.
|
|
48
|
+
RUNTIME_PROCESS_CHECK_COMMAND = (
|
|
49
|
+
"bash -c 'exec 3<>/dev/tcp/127.0.0.1/${JUPYTER_PORT:-44771}' "
|
|
50
|
+
"&& exit 0 || exit 1"
|
|
51
|
+
)
|
|
52
|
+
|
|
35
53
|
|
|
36
54
|
class CodeInterpreterSync:
|
|
37
55
|
"""
|
|
@@ -146,13 +164,110 @@ class CodeInterpreterSync:
|
|
|
146
164
|
"""
|
|
147
165
|
return self._code_service
|
|
148
166
|
|
|
167
|
+
def ping(self) -> bool:
|
|
168
|
+
"""
|
|
169
|
+
Check if the code execution service (execd) is responsive.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if the code execution service is responsive, False otherwise
|
|
173
|
+
"""
|
|
174
|
+
return self._code_service.ping()
|
|
175
|
+
|
|
176
|
+
def is_healthy(self) -> bool:
|
|
177
|
+
"""
|
|
178
|
+
Check if the code interpreter is healthy (strict check).
|
|
179
|
+
|
|
180
|
+
Healthy means both:
|
|
181
|
+
|
|
182
|
+
- the code execution service (execd) answers ``GET /ping``; and
|
|
183
|
+
- the code interpreter runtime (Jupyter kernel gateway) is serving
|
|
184
|
+
inside the sandbox, verified by probing its listen port through the
|
|
185
|
+
execd command API.
|
|
186
|
+
|
|
187
|
+
Exceptions raised by either leg are treated as unhealthy.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
True if healthy, False otherwise
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
return self.ping() and self._is_runtime_process_alive()
|
|
194
|
+
except Exception:
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
def _is_runtime_process_alive(self) -> bool:
|
|
198
|
+
"""
|
|
199
|
+
Check if the code interpreter runtime (Jupyter) is serving.
|
|
200
|
+
"""
|
|
201
|
+
try:
|
|
202
|
+
execution = self._sandbox.commands.run(RUNTIME_PROCESS_CHECK_COMMAND)
|
|
203
|
+
return execution.error is None
|
|
204
|
+
except Exception:
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
def check_ready(
|
|
208
|
+
self,
|
|
209
|
+
timeout: timedelta,
|
|
210
|
+
polling_interval: timedelta,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""
|
|
213
|
+
Wait for the code interpreter to pass the strict health check with polling (blocking).
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
SandboxReadyTimeoutException: if the health check doesn't pass within timeout
|
|
217
|
+
"""
|
|
218
|
+
logger.info(
|
|
219
|
+
f"Waiting for code interpreter {self.id} to pass health check "
|
|
220
|
+
f"(timeout: {timeout.total_seconds()}s)"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
deadline = time.monotonic() + timeout.total_seconds()
|
|
224
|
+
attempt = 0
|
|
225
|
+
|
|
226
|
+
while time.monotonic() < deadline:
|
|
227
|
+
attempt += 1
|
|
228
|
+
if self.is_healthy():
|
|
229
|
+
logger.info(
|
|
230
|
+
f"Code interpreter {self.id} passed health check "
|
|
231
|
+
f"after {attempt} attempts"
|
|
232
|
+
)
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
remaining = deadline - time.monotonic()
|
|
236
|
+
if remaining <= 0:
|
|
237
|
+
break
|
|
238
|
+
time.sleep(min(polling_interval.total_seconds(), remaining))
|
|
239
|
+
|
|
240
|
+
raise SandboxReadyTimeoutException(
|
|
241
|
+
f"Code interpreter {self.id} health check timed out after "
|
|
242
|
+
f"{timeout.total_seconds()}s ({attempt} attempts). The code execution "
|
|
243
|
+
f"service (execd) or the interpreter runtime (Jupyter) did not become "
|
|
244
|
+
f"ready. Pass skip_health_check=True to skip this check."
|
|
245
|
+
)
|
|
246
|
+
|
|
149
247
|
@classmethod
|
|
150
|
-
def create(
|
|
248
|
+
def create(
|
|
249
|
+
cls,
|
|
250
|
+
sandbox: SandboxSync,
|
|
251
|
+
*,
|
|
252
|
+
ready_timeout: timedelta = DEFAULT_READY_TIMEOUT,
|
|
253
|
+
health_check_polling_interval: timedelta = DEFAULT_HEALTH_CHECK_POLLING_INTERVAL,
|
|
254
|
+
skip_health_check: bool = False,
|
|
255
|
+
) -> "CodeInterpreterSync":
|
|
151
256
|
"""
|
|
152
257
|
Create a CodeInterpreterSync from an existing SandboxSync instance (blocking).
|
|
153
258
|
|
|
259
|
+
By default a strict health check runs before the interpreter is returned:
|
|
260
|
+
the code execution service (execd) must answer ``GET /ping`` AND the
|
|
261
|
+
code interpreter runtime process (Jupyter kernel gateway) must be
|
|
262
|
+
running inside the sandbox, both within ``ready_timeout``. Set
|
|
263
|
+
``skip_health_check=True`` to opt out.
|
|
264
|
+
|
|
154
265
|
Args:
|
|
155
266
|
sandbox: Existing sandbox instance to wrap with code execution capabilities
|
|
267
|
+
ready_timeout: Maximum time to wait for the code execution service health check
|
|
268
|
+
health_check_polling_interval: Time between health check attempts
|
|
269
|
+
skip_health_check: If True, do not wait for the code execution service
|
|
270
|
+
to become ready; the returned interpreter may fail on first use
|
|
156
271
|
|
|
157
272
|
Returns:
|
|
158
273
|
CodeInterpreterSync instance wrapping the sandbox
|
|
@@ -160,19 +275,29 @@ class CodeInterpreterSync:
|
|
|
160
275
|
Raises:
|
|
161
276
|
InvalidArgumentException: If sandbox is not provided
|
|
162
277
|
SandboxException: If creation fails
|
|
278
|
+
SandboxReadyTimeoutException: If the code execution service health check
|
|
279
|
+
times out and ``skip_health_check`` is False
|
|
163
280
|
SandboxInternalException: If internal service initialization fails
|
|
164
281
|
"""
|
|
165
282
|
if sandbox is None:
|
|
166
283
|
raise InvalidArgumentException("Sandbox instance must be provided")
|
|
167
284
|
|
|
168
|
-
logger.info("Creating code interpreter from sandbox:
|
|
285
|
+
logger.info(f"Creating code interpreter from sandbox: {sandbox.id}")
|
|
169
286
|
factory = AdapterFactorySync(sandbox.connection_config)
|
|
170
287
|
try:
|
|
171
288
|
endpoint = sandbox.get_endpoint(DEFAULT_EXECD_PORT)
|
|
172
289
|
code_service = factory.create_code_execution_service(endpoint)
|
|
173
|
-
|
|
174
|
-
|
|
290
|
+
|
|
291
|
+
interpreter = cls(sandbox, code_service)
|
|
292
|
+
|
|
293
|
+
if not skip_health_check:
|
|
294
|
+
interpreter.check_ready(ready_timeout, health_check_polling_interval)
|
|
295
|
+
|
|
296
|
+
logger.info(f"Code interpreter {sandbox.id} created successfully")
|
|
297
|
+
return interpreter
|
|
175
298
|
except Exception as e:
|
|
176
299
|
if isinstance(e, SandboxException):
|
|
177
300
|
raise
|
|
178
|
-
raise SandboxInternalException(
|
|
301
|
+
raise SandboxInternalException(
|
|
302
|
+
f"Failed to create code interpreter: {e}", cause=e
|
|
303
|
+
) from e
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright 2025
|
|
2
|
+
# Copyright 2025 The OpenSandbox Authors
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
5
|
# you may not use this file except in compliance with the License.
|
|
@@ -159,3 +159,17 @@ class CodesSync(Protocol):
|
|
|
159
159
|
SandboxException: If interruption fails.
|
|
160
160
|
"""
|
|
161
161
|
...
|
|
162
|
+
|
|
163
|
+
def ping(self) -> bool:
|
|
164
|
+
"""
|
|
165
|
+
Check if the code execution service (execd) is alive (blocking).
|
|
166
|
+
|
|
167
|
+
The ping targets the execd daemon endpoint used by this service
|
|
168
|
+
(``GET /ping``). It does not verify that a specific language runtime
|
|
169
|
+
is ready, only that the daemon serving code execution requests is
|
|
170
|
+
responsive.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if the code execution service is responsive, False otherwise.
|
|
174
|
+
"""
|
|
175
|
+
...
|
|
File without changes
|
|
File without changes
|