opensandbox-code-interpreter 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ #
2
+ # Copyright 2025 Alibaba Group Holding Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ """
17
+ Adapter implementations for Code Interpreter sync services.
18
+ """
19
+
20
+ from code_interpreter.sync.adapters.code_adapter import CodesAdapterSync
21
+ from code_interpreter.sync.adapters.factory import AdapterFactorySync
22
+
23
+ __all__ = [
24
+ "AdapterFactorySync",
25
+ "CodesAdapterSync",
26
+ ]
@@ -0,0 +1,215 @@
1
+ #
2
+ # Copyright 2025 Alibaba Group Holding Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ """
17
+ Synchronous adapter for code execution service (including SSE streaming).
18
+ """
19
+
20
+ import json
21
+ import logging
22
+
23
+ import httpx
24
+ from opensandbox.adapters.converter.event_node import EventNode
25
+ from opensandbox.adapters.converter.exception_converter import (
26
+ ExceptionConverter,
27
+ )
28
+ from opensandbox.adapters.converter.response_handler import (
29
+ handle_api_error,
30
+ require_parsed,
31
+ )
32
+ from opensandbox.config.connection_sync import ConnectionConfigSync
33
+ from opensandbox.exceptions import InvalidArgumentException, SandboxApiException
34
+ from opensandbox.models.execd import Execution
35
+ from opensandbox.models.execd_sync import ExecutionHandlersSync
36
+ from opensandbox.models.sandboxes import SandboxEndpoint
37
+ from opensandbox.sync.adapters.converter.execution_event_dispatcher import (
38
+ ExecutionEventDispatcherSync,
39
+ )
40
+
41
+ from code_interpreter.models.code_sync import CodeContextSync, SupportedLanguageSync
42
+ from code_interpreter.sync.services.code import CodesSync
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ class CodesAdapterSync(CodesSync):
48
+ """
49
+ Synchronous adapter for code execution service.
50
+
51
+ This adapter is the sync counterpart of :class:`code_interpreter.adapters.code_adapter.CodesAdapter`.
52
+ It wraps the generated execd API client for non-streaming operations and uses direct ``httpx``
53
+ streaming for SSE output while running code.
54
+
55
+ Notes:
56
+
57
+ - ``run`` performs blocking SSE streaming via ``httpx.Client.stream``.
58
+ - Each SSE line is parsed into an :class:`EventNode` and dispatched via
59
+ :class:`ExecutionEventDispatcherSync` to update the shared :class:`Execution` object
60
+ and invoke any user-provided handlers.
61
+ """
62
+
63
+ RUN_CODE_PATH = "/code"
64
+ CREATE_CONTEXT_PATH = "/code/context"
65
+
66
+ def __init__(self, execd_endpoint: SandboxEndpoint, connection_config: ConnectionConfigSync) -> None:
67
+ """
68
+ Initialize the code service adapter (sync).
69
+
70
+ Args:
71
+ execd_endpoint: Endpoint for execd daemon connection
72
+ connection_config: Shared connection configuration (transport, headers, timeouts)
73
+ """
74
+ self.execd_endpoint = execd_endpoint
75
+ self.connection_config = connection_config
76
+ from opensandbox.api.execd import Client
77
+
78
+ base_url = f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}"
79
+ timeout_seconds = self.connection_config.request_timeout.total_seconds()
80
+ timeout = httpx.Timeout(timeout_seconds)
81
+ headers = {"User-Agent": self.connection_config.user_agent, **self.connection_config.headers}
82
+
83
+ self._client = Client(base_url=base_url, timeout=timeout)
84
+ self._httpx_client = httpx.Client(
85
+ base_url=base_url,
86
+ headers=headers,
87
+ timeout=timeout,
88
+ transport=self.connection_config.transport,
89
+ )
90
+ self._client.set_httpx_client(self._httpx_client)
91
+
92
+ sse_headers = {**headers, "Accept": "text/event-stream", "Cache-Control": "no-cache"}
93
+ self._sse_client = httpx.Client(
94
+ headers=sse_headers,
95
+ timeout=httpx.Timeout(connect=timeout_seconds, read=None, write=timeout_seconds, pool=None),
96
+ transport=self.connection_config.transport,
97
+ )
98
+
99
+ def _get_execd_url(self, path: str) -> str:
100
+ """Build URL for execd endpoint."""
101
+ return f"{self.connection_config.protocol}://{self.execd_endpoint.endpoint}{path}"
102
+
103
+ def create_context(self, language: str) -> CodeContextSync:
104
+ """
105
+ Create a new execution context for code interpretation (sync).
106
+
107
+ Uses the generated API client for this non-streaming operation.
108
+ """
109
+ try:
110
+ from opensandbox.api.execd.api.code_interpreting import create_code_context
111
+ from opensandbox.api.execd.models.code_context import (
112
+ CodeContext as ApiCodeContext,
113
+ )
114
+ from opensandbox.api.execd.models.code_context_request import (
115
+ CodeContextRequest,
116
+ )
117
+ from opensandbox.api.execd.types import UNSET
118
+
119
+ response_obj = create_code_context.sync_detailed(
120
+ client=self._client,
121
+ body=CodeContextRequest(language=language),
122
+ )
123
+ handle_api_error(response_obj, "Create code context")
124
+ parsed = require_parsed(response_obj, ApiCodeContext, "Create code context")
125
+ context_id = parsed.id if parsed.id is not UNSET else None
126
+ return CodeContextSync(id=context_id, language=parsed.language)
127
+ except Exception as e:
128
+ logger.error("Failed to create context", exc_info=e)
129
+ raise ExceptionConverter.to_sandbox_exception(e) from e
130
+
131
+ def run(
132
+ self,
133
+ code: str,
134
+ *,
135
+ context: CodeContextSync | None = None,
136
+ handlers: ExecutionHandlersSync | None = None,
137
+ ) -> Execution:
138
+ """
139
+ Execute code within the specified context using SSE streaming (sync).
140
+
141
+ Args:
142
+ code: Source code to execute.
143
+ context: Execution context (language + optional id). If None, a temporary Python context is used.
144
+ handlers: Optional streaming handlers for stdout/stderr/events.
145
+
146
+ Returns:
147
+ Execution result populated incrementally while streaming events
148
+
149
+ Raises:
150
+ InvalidArgumentException: if code is empty
151
+ SandboxApiException: if execd returns a non-200 response
152
+ SandboxException: for other errors converted by :class:`ExceptionConverter`
153
+ """
154
+ if not code.strip():
155
+ raise InvalidArgumentException("Code cannot be empty")
156
+
157
+ try:
158
+ context = context or CodeContextSync(language=SupportedLanguageSync.PYTHON)
159
+ api_request = {
160
+ "code": code,
161
+ "context": {
162
+ "language": context.language,
163
+ **({"id": context.id} if context.id else {}),
164
+ },
165
+ }
166
+
167
+ url = self._get_execd_url(self.RUN_CODE_PATH)
168
+ execution = Execution(id=None, execution_count=None, result=[], error=None)
169
+ dispatcher = ExecutionEventDispatcherSync(execution, handlers)
170
+
171
+ with self._sse_client.stream("POST", url, json=api_request) as response:
172
+ if response.status_code != 200:
173
+ response.read()
174
+ raise SandboxApiException(
175
+ message=f"Failed to run code. Status code: {response.status_code}",
176
+ status_code=response.status_code,
177
+ )
178
+
179
+ for line in response.iter_lines():
180
+ if not line or not line.strip():
181
+ continue
182
+ data = line
183
+ if data.startswith("data:"):
184
+ data = data[5:].strip()
185
+ try:
186
+ event_dict = json.loads(data)
187
+ event_node = EventNode(**event_dict)
188
+ dispatcher.dispatch(event_node)
189
+ except json.JSONDecodeError:
190
+ logger.debug("Failed to parse SSE line: %s", line)
191
+ continue
192
+ except Exception as e:
193
+ logger.error("Error processing event: %s", data, exc_info=e)
194
+ continue
195
+
196
+ return execution
197
+ except Exception as e:
198
+ logger.error("Failed to run code (length: %s)", len(code), exc_info=e)
199
+ raise ExceptionConverter.to_sandbox_exception(e) from e
200
+
201
+ def interrupt(self, execution_id: str) -> None:
202
+ """
203
+ Interrupt a currently running code execution.
204
+
205
+ Args:
206
+ execution_id: Execution id returned by execd for the running code execution
207
+ """
208
+ try:
209
+ from opensandbox.api.execd.api.code_interpreting import interrupt_code
210
+
211
+ response_obj = interrupt_code.sync_detailed(client=self._client, id=execution_id)
212
+ handle_api_error(response_obj, "Interrupt code execution")
213
+ except Exception as e:
214
+ logger.error("Failed to interrupt code execution", exc_info=e)
215
+ raise ExceptionConverter.to_sandbox_exception(e) from e
@@ -0,0 +1,54 @@
1
+ #
2
+ # Copyright 2025 Alibaba Group Holding Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ """
17
+ Factory for creating Code Interpreter sync services.
18
+ """
19
+
20
+ from opensandbox.config.connection_sync import ConnectionConfigSync
21
+ from opensandbox.models.sandboxes import SandboxEndpoint
22
+
23
+ from code_interpreter.sync.adapters.code_adapter import CodesAdapterSync
24
+ from code_interpreter.sync.services.code import CodesSync
25
+
26
+
27
+ class AdapterFactorySync:
28
+ """
29
+ Factory for creating Code Interpreter sync service instances.
30
+
31
+ This factory centralizes construction of sync services so they all share the same
32
+ connection configuration (transport, headers, timeouts).
33
+ """
34
+
35
+ def __init__(self, connection_config: ConnectionConfigSync) -> None:
36
+ """
37
+ Initialize the factory with shared connection configuration (sync).
38
+
39
+ Args:
40
+ connection_config: Shared connection configuration (transport, headers, timeouts).
41
+ """
42
+ self.connection_config = connection_config
43
+
44
+ def create_code_execution_service(self, endpoint: SandboxEndpoint) -> CodesSync:
45
+ """
46
+ Create a code execution service for the specified endpoint (sync).
47
+
48
+ Args:
49
+ endpoint: Sandbox endpoint for code execution services.
50
+
51
+ Returns:
52
+ Configured sync code service instance.
53
+ """
54
+ return CodesAdapterSync(endpoint, self.connection_config)
@@ -0,0 +1,286 @@
1
+ #
2
+ # Copyright 2025 Alibaba Group Holding Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ """
17
+ Synchronous Code Interpreter SDK.
18
+ """
19
+
20
+ import logging
21
+ from datetime import datetime, timedelta, timezone
22
+ from uuid import UUID
23
+
24
+ from opensandbox.constants import DEFAULT_EXECD_PORT
25
+ from opensandbox.exceptions import (
26
+ InvalidArgumentException,
27
+ SandboxException,
28
+ SandboxInternalException,
29
+ )
30
+ from opensandbox.models.sandboxes import (
31
+ SandboxEndpoint,
32
+ SandboxInfo,
33
+ SandboxMetrics,
34
+ )
35
+ from opensandbox.sync.sandbox import SandboxSync
36
+
37
+ from code_interpreter.sync.adapters.factory import AdapterFactorySync
38
+ from code_interpreter.sync.services.code import CodesSync
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class CodeInterpreterSync:
44
+ """
45
+ Synchronous Code Interpreter SDK providing secure, isolated code execution capabilities.
46
+
47
+ This class mirrors the async :class:`code_interpreter.code_interpreter.CodeInterpreter`, but all
48
+ operations are **blocking** and executed in the current thread.
49
+
50
+ It wraps an existing :class:`opensandbox.sync.sandbox.SandboxSync` instance and adds
51
+ code-execution APIs (contexts, run with SSE streaming, interrupts) on top.
52
+
53
+ Notes:
54
+
55
+ - **Blocking**: Do not call these methods directly from an asyncio event loop thread.
56
+ If you need non-blocking behavior, prefer the async :class:`~code_interpreter.code_interpreter.CodeInterpreter`.
57
+ - **Lifecycle**: Remote lifecycle is owned by the underlying sandbox. This class delegates
58
+ pause/resume/kill/renew/metrics to the sandbox.
59
+
60
+ Usage Example:
61
+
62
+ ```python
63
+ from opensandbox.sync.sandbox import SandboxSync
64
+ from code_interpreter.sync.code_interpreter import CodeInterpreterSync
65
+ from code_interpreter.models.code import SupportedLanguage
66
+
67
+ sandbox = SandboxSync.create("python:3.11")
68
+ interpreter = CodeInterpreterSync.create(sandbox=sandbox)
69
+
70
+ ctx = interpreter.codes.create_context(SupportedLanguage.PYTHON)
71
+ result = interpreter.codes.run("print('hi')", context=ctx)
72
+
73
+ sandbox.kill()
74
+ sandbox.close()
75
+ ```
76
+ """
77
+
78
+ def __init__(self, sandbox: SandboxSync, code_service: CodesSync) -> None:
79
+ """
80
+ Initialize CodeInterpreterSync with sandbox and code service.
81
+
82
+ Note: This constructor is for internal use. Use :meth:`create` instead.
83
+
84
+ Args:
85
+ sandbox: Underlying sandbox instance
86
+ code_service: Code execution service implementation (sync)
87
+ """
88
+ self._sandbox = sandbox
89
+ self._code_service = code_service
90
+
91
+ @property
92
+ def sandbox(self) -> SandboxSync:
93
+ """
94
+ Provides access to the underlying sandbox instance.
95
+
96
+ Returns:
97
+ The underlying sandbox instance
98
+ """
99
+ return self._sandbox
100
+
101
+ @property
102
+ def id(self) -> UUID:
103
+ """
104
+ Gets the unique identifier of this code interpreter (same as underlying sandbox ID).
105
+
106
+ Returns:
107
+ UUID of the code interpreter/sandbox
108
+ """
109
+ return self._sandbox.id
110
+
111
+ @property
112
+ def files(self):
113
+ """
114
+ Provides access to file system operations within the sandbox.
115
+
116
+ Returns:
117
+ Service for filesystem manipulation
118
+ """
119
+ return self._sandbox.files
120
+
121
+ @property
122
+ def commands(self):
123
+ """
124
+ Provides access to command execution operations.
125
+
126
+ Returns:
127
+ Service for command execution
128
+ """
129
+ return self._sandbox.commands
130
+
131
+ @property
132
+ def metrics(self):
133
+ """
134
+ Provides access to sandbox metrics and monitoring.
135
+
136
+ Returns:
137
+ Service for metrics retrieval
138
+ """
139
+ return self._sandbox.metrics
140
+
141
+ @property
142
+ def codes(self) -> CodesSync:
143
+ """
144
+ Provides access to code execution operations (sync).
145
+
146
+ This service enables:
147
+ - Multi-language code execution (Python, JavaScript, Bash, etc.)
148
+ - Execution context management with persistent variables
149
+ - Real-time output streaming and interruption capabilities
150
+
151
+ Returns:
152
+ Service for advanced code execution with session support
153
+ """
154
+ return self._code_service
155
+
156
+ def get_endpoint(self, port: int) -> SandboxEndpoint:
157
+ """
158
+ Gets a specific network endpoint for the underlying sandbox.
159
+
160
+ Args:
161
+ port: The port number to get the endpoint for
162
+
163
+ Returns:
164
+ Endpoint information including host, port, and connection details
165
+
166
+ Raises:
167
+ SandboxException: If endpoint cannot be retrieved
168
+ """
169
+ return self._sandbox.get_endpoint(port)
170
+
171
+ def get_info(self) -> SandboxInfo:
172
+ """
173
+ Gets the current status of this sandbox.
174
+
175
+ Returns:
176
+ Current sandbox status including state and metadata
177
+
178
+ Raises:
179
+ SandboxException: If status cannot be retrieved
180
+ """
181
+ return self._sandbox.get_info()
182
+
183
+ def get_metrics(self) -> SandboxMetrics:
184
+ """
185
+ Gets the current resource usage metrics for the underlying sandbox.
186
+
187
+ Returns:
188
+ Current sandbox metrics including CPU, memory, and I/O statistics
189
+
190
+ Raises:
191
+ SandboxException: If metrics cannot be retrieved
192
+ """
193
+ return self._sandbox.get_metrics()
194
+
195
+ def renew(self, timeout: timedelta | int) -> None:
196
+ """
197
+ Renew the sandbox expiration time to delay automatic termination.
198
+
199
+ Args:
200
+ timeout: Duration to add to the current time to set the new expiration.
201
+ Can be timedelta or seconds as int.
202
+
203
+ Raises:
204
+ SandboxException: If the operation fails
205
+ """
206
+ if isinstance(timeout, int):
207
+ timeout = timedelta(seconds=timeout)
208
+ logger.info(
209
+ "Renew code interpreter %s timeout, estimated expiration to %s",
210
+ self.id,
211
+ datetime.now(timezone.utc) + timeout,
212
+ )
213
+ self._sandbox.renew(timeout)
214
+
215
+ def pause(self) -> None:
216
+ """
217
+ Pauses the sandbox while preserving its state.
218
+
219
+ Raises:
220
+ SandboxException: If pause operation fails
221
+ """
222
+ logger.info("Pausing code interpreter: %s", self.id)
223
+ self._sandbox.pause()
224
+
225
+ def resume(self) -> None:
226
+ """
227
+ Resumes a previously paused sandbox.
228
+
229
+ Raises:
230
+ SandboxException: If resume operation fails
231
+ """
232
+ logger.info("Resuming code interpreter: %s", self.id)
233
+ self._sandbox.resume()
234
+
235
+ def kill(self) -> None:
236
+ """
237
+ Terminate the remote sandbox instance (irreversible).
238
+
239
+ Note: This method does NOT close the local `SandboxSync` object resources (like connection pools).
240
+ You should call `sandbox().close()` or use the sync context manager on the sandbox to clean up.
241
+
242
+ Raises:
243
+ SandboxException: If termination fails
244
+ """
245
+ logger.info("Killing code interpreter: %s", self.id)
246
+ self._sandbox.kill()
247
+
248
+ def is_healthy(self) -> bool:
249
+ """
250
+ Checks if the code interpreter and its underlying sandbox are healthy and responsive.
251
+
252
+ Returns:
253
+ True if sandbox is healthy, False otherwise
254
+ """
255
+ return self._sandbox.is_healthy()
256
+
257
+ @classmethod
258
+ def create(cls, sandbox: SandboxSync) -> "CodeInterpreterSync":
259
+ """
260
+ Create a CodeInterpreterSync from an existing SandboxSync instance (blocking).
261
+
262
+ Args:
263
+ sandbox: Existing sandbox instance to wrap with code execution capabilities
264
+
265
+ Returns:
266
+ CodeInterpreterSync instance wrapping the sandbox
267
+
268
+ Raises:
269
+ InvalidArgumentException: If sandbox is not provided
270
+ SandboxException: If creation fails
271
+ SandboxInternalException: If internal service initialization fails
272
+ """
273
+ if sandbox is None:
274
+ raise InvalidArgumentException("Sandbox instance must be provided")
275
+
276
+ logger.info("Creating code interpreter from sandbox: %s", sandbox.id)
277
+ factory = AdapterFactorySync(sandbox.connection_config)
278
+ try:
279
+ endpoint = sandbox.get_endpoint(DEFAULT_EXECD_PORT)
280
+ code_service = factory.create_code_execution_service(endpoint)
281
+ logger.info("Code interpreter %s created successfully", sandbox.id)
282
+ return cls(sandbox, code_service)
283
+ except Exception as e:
284
+ if isinstance(e, SandboxException):
285
+ raise
286
+ raise SandboxInternalException(f"Failed to create code interpreter: {e}", cause=e) from e
@@ -0,0 +1,27 @@
1
+ #
2
+ # Copyright 2025 Alibaba Group Holding Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ """
17
+ Synchronous service interfaces (Protocols) for the Code Interpreter sync SDK.
18
+
19
+ These interfaces mirror the async interfaces under :mod:`code_interpreter.services`,
20
+ but are **blocking** and intended for use with :class:`code_interpreter.sync.code_interpreter.CodeInterpreterSync`.
21
+ """
22
+
23
+ from code_interpreter.sync.services.code import CodesSync
24
+
25
+ __all__ = [
26
+ "CodesSync",
27
+ ]