opensandbox 0.1.2.dev0__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.
- opensandbox/__init__.py +117 -0
- opensandbox/adapters/__init__.py +36 -0
- opensandbox/adapters/command_adapter.py +224 -0
- opensandbox/adapters/converter/__init__.py +52 -0
- opensandbox/adapters/converter/event_node.py +55 -0
- opensandbox/adapters/converter/exception_converter.py +232 -0
- opensandbox/adapters/converter/execution_converter.py +74 -0
- opensandbox/adapters/converter/execution_event_dispatcher.py +129 -0
- opensandbox/adapters/converter/filesystem_model_converter.py +135 -0
- opensandbox/adapters/converter/metrics_model_converter.py +43 -0
- opensandbox/adapters/converter/response_handler.py +112 -0
- opensandbox/adapters/converter/sandbox_model_converter.py +325 -0
- opensandbox/adapters/factory.py +113 -0
- opensandbox/adapters/filesystem_adapter.py +489 -0
- opensandbox/adapters/health_adapter.py +110 -0
- opensandbox/adapters/metrics_adapter.py +123 -0
- opensandbox/adapters/sandboxes_adapter.py +362 -0
- opensandbox/api/__init__.py +17 -0
- opensandbox/api/execd/__init__.py +24 -0
- opensandbox/api/execd/api/__init__.py +17 -0
- opensandbox/api/execd/api/code_interpreting/__init__.py +17 -0
- opensandbox/api/execd/api/code_interpreting/create_code_context.py +203 -0
- opensandbox/api/execd/api/code_interpreting/delete_context.py +192 -0
- opensandbox/api/execd/api/code_interpreting/delete_contexts_by_language.py +192 -0
- opensandbox/api/execd/api/code_interpreting/get_context.py +193 -0
- opensandbox/api/execd/api/code_interpreting/interrupt_code.py +192 -0
- opensandbox/api/execd/api/code_interpreting/list_contexts.py +203 -0
- opensandbox/api/execd/api/code_interpreting/run_code.py +207 -0
- opensandbox/api/execd/api/command/__init__.py +17 -0
- opensandbox/api/execd/api/command/get_background_command_logs.py +233 -0
- opensandbox/api/execd/api/command/get_command_status.py +198 -0
- opensandbox/api/execd/api/command/interrupt_command.py +192 -0
- opensandbox/api/execd/api/command/run_command.py +203 -0
- opensandbox/api/execd/api/filesystem/__init__.py +17 -0
- opensandbox/api/execd/api/filesystem/chmod_files.py +197 -0
- opensandbox/api/execd/api/filesystem/download_file.py +233 -0
- opensandbox/api/execd/api/filesystem/get_files_info.py +204 -0
- opensandbox/api/execd/api/filesystem/make_dirs.py +197 -0
- opensandbox/api/execd/api/filesystem/remove_dirs.py +189 -0
- opensandbox/api/execd/api/filesystem/remove_files.py +189 -0
- opensandbox/api/execd/api/filesystem/rename_files.py +205 -0
- opensandbox/api/execd/api/filesystem/replace_content.py +197 -0
- opensandbox/api/execd/api/filesystem/search_files.py +227 -0
- opensandbox/api/execd/api/filesystem/upload_file.py +199 -0
- opensandbox/api/execd/api/health/__init__.py +17 -0
- opensandbox/api/execd/api/health/ping.py +106 -0
- opensandbox/api/execd/api/metric/__init__.py +17 -0
- opensandbox/api/execd/api/metric/get_metrics.py +165 -0
- opensandbox/api/execd/api/metric/watch_metrics.py +169 -0
- opensandbox/api/execd/client.py +284 -0
- opensandbox/api/execd/errors.py +32 -0
- opensandbox/api/execd/models/__init__.py +63 -0
- opensandbox/api/execd/models/chmod_files_body.py +75 -0
- opensandbox/api/execd/models/code_context.py +89 -0
- opensandbox/api/execd/models/code_context_request.py +78 -0
- opensandbox/api/execd/models/command_status_response.py +174 -0
- opensandbox/api/execd/models/error_response.py +86 -0
- opensandbox/api/execd/models/file_info.py +128 -0
- opensandbox/api/execd/models/file_metadata.py +105 -0
- opensandbox/api/execd/models/get_files_info_response_200.py +75 -0
- opensandbox/api/execd/models/make_dirs_body.py +75 -0
- opensandbox/api/execd/models/metrics.py +110 -0
- opensandbox/api/execd/models/permission.py +98 -0
- opensandbox/api/execd/models/rename_file_item.py +86 -0
- opensandbox/api/execd/models/replace_content_body.py +75 -0
- opensandbox/api/execd/models/replace_file_content_item.py +86 -0
- opensandbox/api/execd/models/run_code_request.py +105 -0
- opensandbox/api/execd/models/run_command_request.py +98 -0
- opensandbox/api/execd/models/server_stream_event.py +164 -0
- opensandbox/api/execd/models/server_stream_event_error.py +99 -0
- opensandbox/api/execd/models/server_stream_event_results.py +67 -0
- opensandbox/api/execd/models/server_stream_event_type.py +32 -0
- opensandbox/api/execd/models/upload_file_body.py +110 -0
- opensandbox/api/execd/py.typed +1 -0
- opensandbox/api/execd/types.py +70 -0
- opensandbox/api/lifecycle/__init__.py +24 -0
- opensandbox/api/lifecycle/api/__init__.py +17 -0
- opensandbox/api/lifecycle/api/sandboxes/__init__.py +17 -0
- opensandbox/api/lifecycle/api/sandboxes/delete_sandboxes_sandbox_id.py +202 -0
- opensandbox/api/lifecycle/api/sandboxes/get_sandboxes.py +252 -0
- opensandbox/api/lifecycle/api/sandboxes/get_sandboxes_sandbox_id.py +219 -0
- opensandbox/api/lifecycle/api/sandboxes/get_sandboxes_sandbox_id_endpoints_port.py +221 -0
- opensandbox/api/lifecycle/api/sandboxes/post_sandboxes.py +241 -0
- opensandbox/api/lifecycle/api/sandboxes/post_sandboxes_sandbox_id_pause.py +202 -0
- opensandbox/api/lifecycle/api/sandboxes/post_sandboxes_sandbox_id_renew_expiration.py +231 -0
- opensandbox/api/lifecycle/api/sandboxes/post_sandboxes_sandbox_id_resume.py +202 -0
- opensandbox/api/lifecycle/client.py +284 -0
- opensandbox/api/lifecycle/errors.py +32 -0
- opensandbox/api/lifecycle/models/__init__.py +57 -0
- opensandbox/api/lifecycle/models/create_sandbox_request.py +204 -0
- opensandbox/api/lifecycle/models/create_sandbox_request_env.py +67 -0
- opensandbox/api/lifecycle/models/create_sandbox_request_extensions.py +71 -0
- opensandbox/api/lifecycle/models/create_sandbox_request_metadata.py +68 -0
- opensandbox/api/lifecycle/models/create_sandbox_response.py +138 -0
- opensandbox/api/lifecycle/models/create_sandbox_response_metadata.py +62 -0
- opensandbox/api/lifecycle/models/endpoint.py +62 -0
- opensandbox/api/lifecycle/models/error_response.py +69 -0
- opensandbox/api/lifecycle/models/image_spec.py +91 -0
- opensandbox/api/lifecycle/models/image_spec_auth.py +68 -0
- opensandbox/api/lifecycle/models/list_sandboxes_response.py +101 -0
- opensandbox/api/lifecycle/models/pagination_info.py +110 -0
- opensandbox/api/lifecycle/models/renew_sandbox_expiration_request.py +63 -0
- opensandbox/api/lifecycle/models/renew_sandbox_expiration_response.py +62 -0
- opensandbox/api/lifecycle/models/resource_limits.py +73 -0
- opensandbox/api/lifecycle/models/sandbox.py +151 -0
- opensandbox/api/lifecycle/models/sandbox_metadata.py +62 -0
- opensandbox/api/lifecycle/models/sandbox_status.py +138 -0
- opensandbox/api/lifecycle/py.typed +1 -0
- opensandbox/api/lifecycle/types.py +70 -0
- opensandbox/config/__init__.py +23 -0
- opensandbox/config/connection.py +159 -0
- opensandbox/config/connection_sync.py +131 -0
- opensandbox/constants.py +20 -0
- opensandbox/exceptions/__init__.py +38 -0
- opensandbox/exceptions/sandbox.py +134 -0
- opensandbox/manager.py +240 -0
- opensandbox/models/__init__.py +81 -0
- opensandbox/models/execd.py +233 -0
- opensandbox/models/execd_sync.py +43 -0
- opensandbox/models/filesystem.py +205 -0
- opensandbox/models/sandboxes.py +301 -0
- opensandbox/py.typed +0 -0
- opensandbox/sandbox.py +616 -0
- opensandbox/services/__init__.py +34 -0
- opensandbox/services/command.py +75 -0
- opensandbox/services/filesystem.py +238 -0
- opensandbox/services/health.py +46 -0
- opensandbox/services/metrics.py +48 -0
- opensandbox/services/sandbox.py +175 -0
- opensandbox/sync/__init__.py +23 -0
- opensandbox/sync/adapters/__init__.py +34 -0
- opensandbox/sync/adapters/command_adapter.py +165 -0
- opensandbox/sync/adapters/converter/__init__.py +20 -0
- opensandbox/sync/adapters/converter/execution_event_dispatcher.py +107 -0
- opensandbox/sync/adapters/factory.py +53 -0
- opensandbox/sync/adapters/filesystem_adapter.py +311 -0
- opensandbox/sync/adapters/health_adapter.py +60 -0
- opensandbox/sync/adapters/metrics_adapter.py +71 -0
- opensandbox/sync/adapters/sandboxes_adapter.py +260 -0
- opensandbox/sync/manager.py +206 -0
- opensandbox/sync/sandbox.py +581 -0
- opensandbox/sync/services/__init__.py +32 -0
- opensandbox/sync/services/command.py +80 -0
- opensandbox/sync/services/filesystem.py +245 -0
- opensandbox/sync/services/health.py +47 -0
- opensandbox/sync/services/metrics.py +48 -0
- opensandbox/sync/services/sandbox.py +174 -0
- opensandbox-0.1.2.dev0.dist-info/METADATA +537 -0
- opensandbox-0.1.2.dev0.dist-info/RECORD +151 -0
- opensandbox-0.1.2.dev0.dist-info/WHEEL +4 -0
- opensandbox-0.1.2.dev0.dist-info/licenses/LICENSE +201 -0
opensandbox/__init__.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
OpenSandbox Python SDK
|
|
18
|
+
|
|
19
|
+
Secure, isolated execution environments for code and applications.
|
|
20
|
+
|
|
21
|
+
## Basic Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import asyncio
|
|
25
|
+
from opensandbox import Sandbox
|
|
26
|
+
from opensandbox.models.execd import RunCommandOpts
|
|
27
|
+
from opensandbox.models.sandboxes import SandboxImageSpec
|
|
28
|
+
|
|
29
|
+
async def main():
|
|
30
|
+
# Create a sandbox instance.
|
|
31
|
+
#
|
|
32
|
+
# Note on lifecycle:
|
|
33
|
+
# - Exiting the context manager will call `sandbox.close()` (local HTTP resources only).
|
|
34
|
+
# - You must still call `sandbox.kill()` to terminate the remote sandbox instance.
|
|
35
|
+
async with await Sandbox.create("python:3.11") as sandbox:
|
|
36
|
+
# Write a file
|
|
37
|
+
await sandbox.files.write_file("hello.py", "print('Hello World')")
|
|
38
|
+
|
|
39
|
+
# Execute a command
|
|
40
|
+
result = await sandbox.commands.run("python hello.py")
|
|
41
|
+
print(result.logs.stdout[0].text) # Hello World
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
asyncio.run(main())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Advanced Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from datetime import timedelta
|
|
51
|
+
from opensandbox import Sandbox
|
|
52
|
+
from opensandbox.config import ConnectionConfig
|
|
53
|
+
from opensandbox.models.execd import RunCommandOpts
|
|
54
|
+
from opensandbox.models.sandboxes import SandboxImageSpec, SandboxImageAuth
|
|
55
|
+
|
|
56
|
+
async def main():
|
|
57
|
+
config = ConnectionConfig(
|
|
58
|
+
api_key="your-api-key",
|
|
59
|
+
domain="api.opensandbox.io"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# With private registry auth
|
|
63
|
+
image_spec = SandboxImageSpec(
|
|
64
|
+
"my-registry.com/python:3.11",
|
|
65
|
+
auth=SandboxImageAuth(username="user", password="secret")
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
sandbox = await Sandbox.create(
|
|
69
|
+
image_spec,
|
|
70
|
+
timeout=timedelta(minutes=30),
|
|
71
|
+
env={"PYTHONPATH": "/workspace"},
|
|
72
|
+
connection_config=config,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
# File operations
|
|
77
|
+
await sandbox.files.write_file("script.py", "print('Hello OpenSandbox!')")
|
|
78
|
+
|
|
79
|
+
# Command execution
|
|
80
|
+
result = await sandbox.commands.run("python script.py")
|
|
81
|
+
print(result.logs.stdout[0].text)
|
|
82
|
+
|
|
83
|
+
# Get metrics
|
|
84
|
+
metrics = await sandbox.get_metrics()
|
|
85
|
+
print(f"Memory usage: {metrics.memory_used_in_mib}MB")
|
|
86
|
+
|
|
87
|
+
finally:
|
|
88
|
+
await sandbox.kill()
|
|
89
|
+
await sandbox.close()
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
asyncio.run(main())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For advanced code execution with persistent contexts, see the separate
|
|
96
|
+
`opensandbox-code-interpreter` package.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
from importlib.metadata import PackageNotFoundError
|
|
100
|
+
from importlib.metadata import version as _pkg_version
|
|
101
|
+
|
|
102
|
+
from opensandbox.manager import SandboxManager
|
|
103
|
+
from opensandbox.sandbox import Sandbox
|
|
104
|
+
from opensandbox.sync import SandboxManagerSync, SandboxSync
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
__version__ = _pkg_version("opensandbox")
|
|
108
|
+
except PackageNotFoundError: # pragma: no cover
|
|
109
|
+
# Fallback for editable/uninstalled source checkouts.
|
|
110
|
+
__version__ = "0.0.0"
|
|
111
|
+
|
|
112
|
+
__all__ = [
|
|
113
|
+
"Sandbox",
|
|
114
|
+
"SandboxManager",
|
|
115
|
+
"SandboxSync",
|
|
116
|
+
"SandboxManagerSync",
|
|
117
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
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 layer for OpenSandbox SDK.
|
|
18
|
+
|
|
19
|
+
Implements the service protocols using HTTP API calls.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from opensandbox.adapters.command_adapter import CommandsAdapter
|
|
23
|
+
from opensandbox.adapters.factory import AdapterFactory
|
|
24
|
+
from opensandbox.adapters.filesystem_adapter import FilesystemAdapter
|
|
25
|
+
from opensandbox.adapters.health_adapter import HealthAdapter
|
|
26
|
+
from opensandbox.adapters.metrics_adapter import MetricsAdapter
|
|
27
|
+
from opensandbox.adapters.sandboxes_adapter import SandboxesAdapter
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"AdapterFactory",
|
|
31
|
+
"SandboxesAdapter",
|
|
32
|
+
"FilesystemAdapter",
|
|
33
|
+
"CommandsAdapter",
|
|
34
|
+
"HealthAdapter",
|
|
35
|
+
"MetricsAdapter",
|
|
36
|
+
]
|
|
@@ -0,0 +1,224 @@
|
|
|
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
|
+
Command service adapter implementation.
|
|
18
|
+
|
|
19
|
+
Implementation of Commands that adapts openapi-python-client generated CommandApi.
|
|
20
|
+
This adapter handles command execution within sandboxes, providing both
|
|
21
|
+
synchronous and streaming execution modes with proper session management.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
|
|
27
|
+
import httpx
|
|
28
|
+
|
|
29
|
+
from opensandbox.adapters.converter.event_node import EventNode
|
|
30
|
+
from opensandbox.adapters.converter.exception_converter import (
|
|
31
|
+
ExceptionConverter,
|
|
32
|
+
)
|
|
33
|
+
from opensandbox.adapters.converter.execution_converter import (
|
|
34
|
+
ExecutionConverter,
|
|
35
|
+
)
|
|
36
|
+
from opensandbox.adapters.converter.execution_event_dispatcher import (
|
|
37
|
+
ExecutionEventDispatcher,
|
|
38
|
+
)
|
|
39
|
+
from opensandbox.adapters.converter.response_handler import handle_api_error
|
|
40
|
+
from opensandbox.config import ConnectionConfig
|
|
41
|
+
from opensandbox.exceptions import InvalidArgumentException, SandboxApiException
|
|
42
|
+
from opensandbox.models.execd import Execution, ExecutionHandlers, RunCommandOpts
|
|
43
|
+
from opensandbox.models.sandboxes import SandboxEndpoint
|
|
44
|
+
from opensandbox.services.command import Commands
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CommandsAdapter(Commands):
|
|
50
|
+
"""
|
|
51
|
+
Implementation of Commands that adapts openapi-python-client generated CommandApi.
|
|
52
|
+
|
|
53
|
+
This adapter handles command execution within sandboxes, providing both
|
|
54
|
+
synchronous and streaming execution modes with proper session management.
|
|
55
|
+
|
|
56
|
+
The adapter uses direct httpx streaming for command execution to handle
|
|
57
|
+
Server-Sent Events (SSE) properly, while using the generated API client
|
|
58
|
+
for simpler operations like interrupt.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
RUN_COMMAND_PATH = "/command"
|
|
62
|
+
INTERRUPT_COMMAND_PATH = "/command/{execution_id}/interrupt"
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
connection_config: ConnectionConfig,
|
|
67
|
+
execd_endpoint: SandboxEndpoint,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Initialize the command service adapter.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
connection_config: Connection configuration (shared transport, headers, timeouts)
|
|
74
|
+
execd_endpoint: Endpoint for execd service
|
|
75
|
+
"""
|
|
76
|
+
self.connection_config = connection_config
|
|
77
|
+
self.execd_endpoint = execd_endpoint
|
|
78
|
+
from opensandbox.api.execd import Client
|
|
79
|
+
|
|
80
|
+
protocol = self.connection_config.protocol
|
|
81
|
+
base_url = f"{protocol}://{self.execd_endpoint.endpoint}"
|
|
82
|
+
timeout_seconds = self.connection_config.request_timeout.total_seconds()
|
|
83
|
+
timeout = httpx.Timeout(timeout_seconds)
|
|
84
|
+
|
|
85
|
+
headers = {
|
|
86
|
+
"User-Agent": self.connection_config.user_agent,
|
|
87
|
+
**self.connection_config.headers,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Execd API does not require authentication
|
|
91
|
+
self._client = Client(
|
|
92
|
+
base_url=base_url,
|
|
93
|
+
timeout=timeout,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Inject httpx client (adapter-owned)
|
|
97
|
+
self._httpx_client = httpx.AsyncClient(
|
|
98
|
+
base_url=base_url,
|
|
99
|
+
headers=headers,
|
|
100
|
+
timeout=timeout,
|
|
101
|
+
transport=self.connection_config.transport,
|
|
102
|
+
)
|
|
103
|
+
self._client.set_async_httpx_client(self._httpx_client)
|
|
104
|
+
|
|
105
|
+
# SSE client (read timeout disabled)
|
|
106
|
+
sse_headers = {
|
|
107
|
+
**headers,
|
|
108
|
+
"Accept": "text/event-stream",
|
|
109
|
+
"Cache-Control": "no-cache",
|
|
110
|
+
}
|
|
111
|
+
self._sse_client = httpx.AsyncClient(
|
|
112
|
+
headers=sse_headers,
|
|
113
|
+
timeout=httpx.Timeout(
|
|
114
|
+
connect=timeout_seconds,
|
|
115
|
+
read=None,
|
|
116
|
+
write=timeout_seconds,
|
|
117
|
+
pool=None,
|
|
118
|
+
),
|
|
119
|
+
transport=self.connection_config.transport,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
async def _get_client(self):
|
|
123
|
+
"""Return the client for execd API (no auth required)."""
|
|
124
|
+
return self._client
|
|
125
|
+
|
|
126
|
+
def _get_execd_url(self, path: str) -> str:
|
|
127
|
+
"""Build URL for execd endpoint."""
|
|
128
|
+
protocol = self.connection_config.protocol
|
|
129
|
+
return f"{protocol}://{self.execd_endpoint.endpoint}{path}"
|
|
130
|
+
|
|
131
|
+
async def _get_sse_client(self) -> httpx.AsyncClient:
|
|
132
|
+
"""Return SSE client (read timeout disabled) for execd streaming."""
|
|
133
|
+
return self._sse_client
|
|
134
|
+
|
|
135
|
+
async def run(
|
|
136
|
+
self,
|
|
137
|
+
command: str,
|
|
138
|
+
*,
|
|
139
|
+
opts: RunCommandOpts | None = None,
|
|
140
|
+
handlers: ExecutionHandlers | None = None,
|
|
141
|
+
) -> Execution:
|
|
142
|
+
"""Execute a shell command within the sandbox.
|
|
143
|
+
|
|
144
|
+
This method uses direct httpx streaming to handle SSE responses
|
|
145
|
+
from the execd service.
|
|
146
|
+
"""
|
|
147
|
+
if not command.strip():
|
|
148
|
+
raise InvalidArgumentException("Command cannot be empty")
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
# Convert domain model to API model
|
|
152
|
+
opts = opts or RunCommandOpts()
|
|
153
|
+
json_body = ExecutionConverter.to_api_run_command_json(command, opts)
|
|
154
|
+
|
|
155
|
+
# Prepare URL
|
|
156
|
+
url = self._get_execd_url(self.RUN_COMMAND_PATH)
|
|
157
|
+
|
|
158
|
+
execution = Execution(
|
|
159
|
+
id=None,
|
|
160
|
+
execution_count=None,
|
|
161
|
+
result=[],
|
|
162
|
+
error=None,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Use SSE client for streaming responses (read timeout disabled)
|
|
166
|
+
client = await self._get_sse_client()
|
|
167
|
+
|
|
168
|
+
# Use streaming request for SSE
|
|
169
|
+
async with client.stream("POST", url, json=json_body) as response:
|
|
170
|
+
if response.status_code != 200:
|
|
171
|
+
await response.aread()
|
|
172
|
+
error_body = response.text
|
|
173
|
+
logger.error(
|
|
174
|
+
f"Failed to run command. Status: {response.status_code}, Body: {error_body}"
|
|
175
|
+
)
|
|
176
|
+
raise SandboxApiException(
|
|
177
|
+
message=f"Failed to run command. Status code: {response.status_code}",
|
|
178
|
+
status_code=response.status_code,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
dispatcher = ExecutionEventDispatcher(execution, handlers)
|
|
182
|
+
|
|
183
|
+
async for line in response.aiter_lines():
|
|
184
|
+
if not line.strip():
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
# Handle potential SSE format "data: ..."
|
|
188
|
+
data = line
|
|
189
|
+
if data.startswith("data:"):
|
|
190
|
+
data = data[5:].strip()
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
event_dict = json.loads(data)
|
|
194
|
+
event_node = EventNode(**event_dict)
|
|
195
|
+
await dispatcher.dispatch(event_node)
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.error(f"Failed to parse SSE line: {line}", exc_info=e)
|
|
198
|
+
|
|
199
|
+
return execution
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error(
|
|
203
|
+
"Failed to run command (length: %s)",
|
|
204
|
+
len(command),
|
|
205
|
+
exc_info=e,
|
|
206
|
+
)
|
|
207
|
+
raise ExceptionConverter.to_sandbox_exception(e) from e
|
|
208
|
+
|
|
209
|
+
async def interrupt(self, execution_id: str) -> None:
|
|
210
|
+
"""Interrupt a running command execution."""
|
|
211
|
+
try:
|
|
212
|
+
from opensandbox.api.execd.api.command import interrupt_command
|
|
213
|
+
|
|
214
|
+
client = await self._get_client()
|
|
215
|
+
response_obj = await interrupt_command.asyncio_detailed(
|
|
216
|
+
client=client,
|
|
217
|
+
id=execution_id,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
handle_api_error(response_obj, "Interrupt command")
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
logger.error("Failed to interrupt command", exc_info=e)
|
|
224
|
+
raise ExceptionConverter.to_sandbox_exception(e) from e
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
Model converter utilities for API/domain model mapping.
|
|
18
|
+
|
|
19
|
+
This package provides:
|
|
20
|
+
- ExceptionConverter: Convert various exceptions to SandboxException
|
|
21
|
+
- ResponseHandler: Unified API response handling
|
|
22
|
+
- SandboxModelConverter: Convert between API and domain models
|
|
23
|
+
- FilesystemModelConverter: Convert filesystem-related models
|
|
24
|
+
- MetricsModelConverter: Convert metrics-related models
|
|
25
|
+
- ExecutionConverter: Convert execution-related models
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from opensandbox.adapters.converter.exception_converter import (
|
|
29
|
+
ExceptionConverter,
|
|
30
|
+
parse_sandbox_error,
|
|
31
|
+
)
|
|
32
|
+
from opensandbox.adapters.converter.filesystem_model_converter import (
|
|
33
|
+
FilesystemModelConverter,
|
|
34
|
+
)
|
|
35
|
+
from opensandbox.adapters.converter.metrics_model_converter import (
|
|
36
|
+
MetricsModelConverter,
|
|
37
|
+
)
|
|
38
|
+
from opensandbox.adapters.converter.response_handler import (
|
|
39
|
+
handle_api_error,
|
|
40
|
+
)
|
|
41
|
+
from opensandbox.adapters.converter.sandbox_model_converter import (
|
|
42
|
+
SandboxModelConverter,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"ExceptionConverter",
|
|
47
|
+
"parse_sandbox_error",
|
|
48
|
+
"FilesystemModelConverter",
|
|
49
|
+
"MetricsModelConverter",
|
|
50
|
+
"SandboxModelConverter",
|
|
51
|
+
"handle_api_error",
|
|
52
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
EventNode model for parsing Server-Sent Events from execd.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EventNodeError(BaseModel):
|
|
24
|
+
"""Error details in an event node."""
|
|
25
|
+
|
|
26
|
+
name: str | None = Field(default=None, alias="ename")
|
|
27
|
+
value: str | None = Field(default=None, alias="evalue")
|
|
28
|
+
traceback: list[str] = Field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EventNodeResults(BaseModel):
|
|
32
|
+
"""Results container in an event node."""
|
|
33
|
+
|
|
34
|
+
text: str | None = Field(default=None, alias="text")
|
|
35
|
+
|
|
36
|
+
def get_text(self) -> str:
|
|
37
|
+
"""Get the text representation of the result."""
|
|
38
|
+
return self.text or ""
|
|
39
|
+
|
|
40
|
+
model_config = ConfigDict(extra="allow") # Allow other mime types
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EventNode(BaseModel):
|
|
44
|
+
"""
|
|
45
|
+
Represents a single event from the server stream.
|
|
46
|
+
Corresponds to ServerStreamEvent in OpenAPI spec.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
type: str
|
|
50
|
+
text: str | None = None
|
|
51
|
+
execution_count: int | None = Field(default=None, alias="execution_count")
|
|
52
|
+
execution_time_in_millis: int | None = Field(default=None, alias="execution_time")
|
|
53
|
+
timestamp: int
|
|
54
|
+
results: EventNodeResults | None = None
|
|
55
|
+
error: EventNodeError | None = None
|