cubesandbox 0.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.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: cubesandbox
3
+ Version: 0.1.0
4
+ Summary: Python SDK for CubeSandbox
5
+ Author: TencentCloud CubeSandbox Team
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/TencentCloud/CubeSandbox
8
+ Requires-Python: >=3.9
9
+ Requires-Dist: httpx>=0.27
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=7.0; extra == "dev"
12
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
13
+ Requires-Dist: mypy>=1.0; extra == "dev"
14
+ Requires-Dist: ruff>=0.4; extra == "dev"
@@ -0,0 +1,233 @@
1
+ <p align="center">
2
+ <strong>cubesandbox</strong> — Python SDK for CubeSandbox
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://github.com/TencentCloud/CubeSandbox"><img src="https://img.shields.io/badge/CubeSandbox-GitHub-blue" alt="CubeSandbox" /></a>
7
+ <a href="../../LICENSE"><img src="https://img.shields.io/badge/License-Apache_2.0-green" alt="Apache 2.0" /></a>
8
+ <img src="https://img.shields.io/badge/Python-3.9%2B-blue" alt="Python 3.9+" />
9
+ <img src="https://img.shields.io/badge/version-0.1.0-orange" alt="v0.1.0" />
10
+ </p>
11
+
12
+ ---
13
+
14
+ `cubesandbox` is the official Python SDK for [CubeSandbox](https://github.com/TencentCloud/CubeSandbox).
15
+ It provides a simple, Pythonic interface to create sandboxes, execute code,
16
+ and control the full sandbox lifecycle — including pause/resume with memory snapshot.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install cubesandbox
22
+ ```
23
+
24
+ Or install from source:
25
+
26
+ ```bash
27
+ cd sdk/python
28
+ pip install -e .
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ Set the required environment variables:
34
+
35
+ ```bash
36
+ export CUBE_API_URL=http://<your-cubeapi-host>:3000
37
+ export CUBE_TEMPLATE_ID=<your-template-id>
38
+
39
+ # Required for remote access (bypasses DNS for *.cube.app)
40
+ export CUBE_PROXY_NODE_IP=<your-cubeproxy-node-ip>
41
+ ```
42
+
43
+ Run your first sandbox:
44
+
45
+ ```python
46
+ from cubesandbox import Sandbox
47
+
48
+ with Sandbox.create() as sb:
49
+ result = sb.run_code("1 + 1")
50
+ print(result.text) # "2"
51
+ ```
52
+
53
+ ## Features
54
+
55
+ ### Execute code
56
+
57
+ ```python
58
+ from cubesandbox import Sandbox
59
+
60
+ with Sandbox.create() as sb:
61
+ # Simple expression
62
+ result = sb.run_code("x = 42\nx * 2")
63
+ print(result.text) # "84"
64
+
65
+ # Capture stdout
66
+ result = sb.run_code('print("hello")')
67
+ print(result.logs.stdout) # ["hello\n"]
68
+
69
+ # Stream output in real time
70
+ sb.run_code(
71
+ 'for i in range(3): print(i)',
72
+ on_stdout=lambda msg: print("out:", msg.text),
73
+ )
74
+ ```
75
+
76
+ ### Persistent variables within a sandbox
77
+
78
+ Variables assigned in one `run_code` call persist for the lifetime of the sandbox —
79
+ no separate context object needed:
80
+
81
+ ```python
82
+ with Sandbox.create() as sb:
83
+ sb.run_code("x = 100")
84
+ result = sb.run_code("x + 1")
85
+ print(result.text) # "101"
86
+ ```
87
+
88
+
89
+ ### Pause & resume
90
+
91
+ ```python
92
+ sb = Sandbox.create()
93
+
94
+ # Pause — preserves memory snapshot, polls until state=paused
95
+ sb.pause() # wait=True, timeout=30s by default
96
+ sb.pause(wait=False) # fire-and-forget
97
+ sb.pause(timeout=60, interval=0.5) # custom poll params
98
+
99
+ # Resume by connecting — auto-resumes paused sandbox
100
+ sb2 = Sandbox.connect(sb.sandbox_id)
101
+ ```
102
+
103
+ ### Network policy
104
+
105
+ ```python
106
+ import json
107
+ from cubesandbox import Sandbox
108
+
109
+ # Deny all outbound traffic
110
+ with Sandbox.create(metadata={"network-policy": "deny-all"}) as sb:
111
+ result = sb.run_code(
112
+ "import urllib.request; urllib.request.urlopen('http://example.com')"
113
+ )
114
+ print(result.error.name) # "URLError"
115
+
116
+ # Custom allow-list — NOTE: only IP addresses are supported, not domain names
117
+ rules = json.dumps({"allow": ["151.101.0.0/16"]})
118
+ with Sandbox.create(
119
+ metadata={"network-policy": "custom", "network-rules": rules}
120
+ ) as sb:
121
+ sb.run_code("import subprocess; subprocess.run(['pip', 'install', 'requests'])")
122
+ ```
123
+
124
+ ### Host-directory mount
125
+
126
+ ```python
127
+ import json
128
+ from cubesandbox import Sandbox
129
+
130
+ mounts = json.dumps([{"hostPath": "/data/shared", "mountPath": "/mnt/data"}])
131
+ with Sandbox.create(metadata={"hostdir-mount": mounts}) as sb:
132
+ result = sb.run_code("open('/mnt/data/hello.txt').read()")
133
+ print(result.text)
134
+ ```
135
+
136
+ ### List & health check
137
+
138
+ ```python
139
+ from cubesandbox import Sandbox
140
+
141
+ print(Sandbox.health()) # {"status": "ok", "sandboxes": 4}
142
+ print(Sandbox.list()) # list of running sandbox dicts
143
+ print(Sandbox.list_v2()) # v2 API (supports filtering)
144
+ ```
145
+
146
+ ## Configuration
147
+
148
+ | Environment Variable | Required | Default | Description |
149
+ |---|:---:|---|---|
150
+ | `CUBE_API_URL` | ✅ | `http://127.0.0.1:3000` | CubeAPI management plane address |
151
+ | `CUBE_TEMPLATE_ID` | ✅ | — | Template ID for sandbox creation |
152
+ | `CUBE_PROXY_NODE_IP` | remote | — | CubeProxy node IP, bypasses DNS for `*.cube.app` |
153
+ | `CUBE_PROXY_PORT_HTTP` | | `80` | CubeProxy HTTP port |
154
+ | `CUBE_SANDBOX_DOMAIN` | | `cube.app` | Sandbox domain suffix |
155
+
156
+ You can also pass a `Config` object directly:
157
+
158
+ ```python
159
+ from cubesandbox import Config, Sandbox
160
+
161
+ cfg = Config(
162
+ api_url="http://10.0.0.1:3000",
163
+ template_id="tpl-xxxxxxxxxxxxxxxxxxxxxxxx",
164
+ proxy_node_ip="10.0.0.1",
165
+ )
166
+ with Sandbox.create(config=cfg) as sb:
167
+ print(sb.run_code("2 ** 10").text) # "1024"
168
+ ```
169
+
170
+ ## API Reference
171
+
172
+ ### `Sandbox` — class methods
173
+
174
+ | Method | Description |
175
+ |---|---|
176
+ | `Sandbox.create(template, *, timeout, env_vars, metadata, config)` | `POST /sandboxes` — create a new sandbox |
177
+ | `Sandbox.connect(sandbox_id, *, config)` | `POST /sandboxes/:id/connect` — connect (auto-resumes if paused) |
178
+ | `Sandbox.list(config)` | `GET /sandboxes` — list running sandboxes (v1) |
179
+ | `Sandbox.list_v2(config)` | `GET /v2/sandboxes` — list sandboxes (v2) |
180
+ | `Sandbox.health(config)` | `GET /health` — service health check |
181
+
182
+ ### `Sandbox` — instance methods
183
+
184
+ | Method | Description |
185
+ |---|---|
186
+ | `sb.run_code(code, *, on_stdout, on_stderr, on_result, on_error, envs, timeout)` | `POST /execute` — execute code, returns `Execution` |
187
+ | `sb.get_info()` | `GET /sandboxes/:id` — get sandbox state and metadata |
188
+ | `sb.get_info()` | `GET /sandboxes/:id` — get sandbox detail |
189
+ | `sb.pause(*, wait, timeout, interval)` | `POST /sandboxes/:id/pause` — pause sandbox |
190
+ | `sb.resume(timeout)` | `POST /sandboxes/:id/resume` — resume (deprecated, use `connect`) |
191
+ | `sb.kill()` | `DELETE /sandboxes/:id` — destroy sandbox |
192
+ | `sb.get_host(port)` | Return virtual hostname `{port}-{id}.{domain}` |
193
+
194
+ ### `Execution` object
195
+
196
+ | Attribute | Type | Description |
197
+ |---|---|---|
198
+ | `.text` | `str \| None` | Final expression value (main result) |
199
+ | `.logs.stdout` | `list[str]` | All stdout lines |
200
+ | `.logs.stderr` | `list[str]` | All stderr lines |
201
+ | `.error` | `ExecutionError \| None` | Exception info if execution failed |
202
+ | `.results` | `list[Result]` | All result events |
203
+
204
+ ## Examples
205
+
206
+ | Script | Description |
207
+ |---|---|
208
+ | `examples/create_and_run.py` | Create sandbox and run code |
209
+ | `examples/context.py` | Kernel context (server-side not yet implemented) |
210
+ | `examples/lifecycle.py` | Pause / connect / kill |
211
+ | `examples/list_and_health.py` | List sandboxes and health check |
212
+ | `examples/network_policy.py` | Network policy (deny-all / custom) |
213
+ | `examples/volume.py` | Host-directory mount |
214
+ | `examples/run_all.py` | Run all examples |
215
+
216
+ ## DNS Bypass (Remote Access)
217
+
218
+ When running outside the CubeSandbox node, `*.cube.app` cannot be resolved by the OS DNS.
219
+ Set `CUBE_PROXY_NODE_IP` to enable `IPOverrideTransport`: all data-plane connections are
220
+ routed directly to that IP with the virtual `Host` header preserved for CubeProxy routing.
221
+
222
+ ```
223
+ Without CUBE_PROXY_NODE_IP:
224
+ SDK → OS DNS (*.cube.app) → CubeProxy
225
+
226
+ With CUBE_PROXY_NODE_IP:
227
+ SDK → TCP direct to CUBE_PROXY_NODE_IP:80
228
+ Host: 49999-{sandboxID}.cube.app (preserved for routing)
229
+ ```
230
+
231
+ ## License
232
+
233
+ Apache-2.0 © 2026 Tencent Inc.
@@ -0,0 +1,24 @@
1
+ # Copyright (c) 2026 Tencent Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from .sandbox import Sandbox
5
+ from ._config import Config
6
+ from ._models import Execution, Result, Logs, ExecutionError, OutputMessage
7
+ from ._exceptions import CubeSandboxError, SandboxNotFoundError, ApiError
8
+ from ._commands import CommandResult
9
+
10
+ __all__ = [
11
+ "Sandbox",
12
+ "Config",
13
+ "Execution",
14
+ "Result",
15
+ "Logs",
16
+ "ExecutionError",
17
+ "OutputMessage",
18
+ "CubeSandboxError",
19
+ "SandboxNotFoundError",
20
+ "ApiError",
21
+ "CommandResult",
22
+ ]
23
+
24
+ __version__ = "0.1.0"
@@ -0,0 +1,51 @@
1
+ # Copyright (c) 2026 Tencent Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from .sandbox import Sandbox
10
+
11
+
12
+ @dataclass
13
+ class CommandResult:
14
+ stdout: str
15
+ stderr: str
16
+ exit_code: int
17
+
18
+
19
+ class Commands:
20
+ def __init__(self, sandbox: "Sandbox") -> None:
21
+ self._sandbox = sandbox
22
+
23
+ def run(self, cmd: str, *, timeout: float | None = None, **kwargs) -> CommandResult:
24
+ """Run a shell command inside the sandbox via Python subprocess."""
25
+ code = (
26
+ "import subprocess as _sp\n"
27
+ f"_r = _sp.run({cmd!r}, shell=True, capture_output=True, text=True)\n"
28
+ "import sys as _sys\n"
29
+ "_sys.stdout.write(_r.stdout)\n"
30
+ "_sys.stderr.write(_r.stderr)\n"
31
+ "print(_r.returncode)\n"
32
+ )
33
+ stdout_lines: list[str] = []
34
+ execution = self._sandbox.run_code(
35
+ code,
36
+ timeout=timeout,
37
+ on_stdout=lambda m: stdout_lines.append(m.text),
38
+ )
39
+ # last stdout line is the exit code printed by the wrapper
40
+ all_stdout = "".join(stdout_lines)
41
+ lines = all_stdout.splitlines()
42
+ if lines and lines[-1].strip().lstrip("-").isdigit():
43
+ exit_code = int(lines[-1].strip())
44
+ stdout = "\n".join(lines[:-1])
45
+ if lines[:-1]: # restore trailing newline if there was content
46
+ stdout += "\n"
47
+ else:
48
+ exit_code = 1 if execution.error else 0
49
+ stdout = all_stdout
50
+ stderr = "".join(execution.logs.stderr)
51
+ return CommandResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
@@ -0,0 +1,33 @@
1
+ # Copyright (c) 2026 Tencent Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ from dataclasses import dataclass, field
8
+
9
+
10
+ @dataclass
11
+ class Config:
12
+ """SDK configuration. All fields can be set via environment variables."""
13
+
14
+ api_url: str = field(
15
+ default_factory=lambda: os.environ.get("CUBE_API_URL", "http://127.0.0.1:3000")
16
+ )
17
+ template_id: str | None = field(
18
+ default_factory=lambda: os.environ.get("CUBE_TEMPLATE_ID")
19
+ )
20
+ proxy_node_ip: str | None = field(
21
+ default_factory=lambda: os.environ.get("CUBE_PROXY_NODE_IP")
22
+ )
23
+ proxy_port: int = field(
24
+ default_factory=lambda: int(os.environ.get("CUBE_PROXY_PORT_HTTP", "80"))
25
+ )
26
+ sandbox_domain: str = field(
27
+ default_factory=lambda: os.environ.get("CUBE_SANDBOX_DOMAIN", "cube.app")
28
+ )
29
+ timeout: int = 300
30
+ request_timeout: float = 30.0
31
+
32
+ def __post_init__(self) -> None:
33
+ self.api_url = self.api_url.rstrip("/")
@@ -0,0 +1,16 @@
1
+ # Copyright (c) 2026 Tencent Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+
7
+ class CubeSandboxError(Exception):
8
+ def __init__(self, message: str, status_code: int | None = None):
9
+ super().__init__(message)
10
+ self.status_code = status_code
11
+
12
+
13
+ class SandboxNotFoundError(CubeSandboxError): ...
14
+ class TemplateNotFoundError(CubeSandboxError): ...
15
+ class AuthenticationError(CubeSandboxError): ...
16
+ class ApiError(CubeSandboxError): ...
@@ -0,0 +1,19 @@
1
+ # Copyright (c) 2026 Tencent Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from .sandbox import Sandbox
9
+
10
+
11
+ class Filesystem:
12
+ def __init__(self, sandbox: "Sandbox") -> None:
13
+ self._sandbox = sandbox
14
+
15
+ def read(self, path: str) -> str:
16
+ result = self._sandbox.run_code(f"open({path!r}).read()")
17
+ if result.error:
18
+ raise IOError(f"Failed to read {path}: {result.error.value}")
19
+ return result.text or ""
@@ -0,0 +1,62 @@
1
+ # Copyright (c) 2026 Tencent Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+ from typing import List, Optional
8
+
9
+
10
+ @dataclass
11
+ class Logs:
12
+ stdout: List[str] = field(default_factory=list)
13
+ stderr: List[str] = field(default_factory=list)
14
+
15
+
16
+ @dataclass
17
+ class ExecutionError:
18
+ name: str
19
+ value: str
20
+ traceback: List[str] = field(default_factory=list)
21
+
22
+
23
+ @dataclass
24
+ class Result:
25
+ text: Optional[str] = None
26
+ html: Optional[str] = None
27
+ markdown: Optional[str] = None
28
+ svg: Optional[str] = None
29
+ png: Optional[str] = None
30
+ jpeg: Optional[str] = None
31
+ pdf: Optional[str] = None
32
+ latex: Optional[str] = None
33
+ json_data: Optional[dict] = None
34
+ javascript: Optional[str] = None
35
+ is_main_result: bool = False
36
+ extra: Optional[dict] = None
37
+
38
+
39
+ @dataclass
40
+ class Execution:
41
+ results: List[Result] = field(default_factory=list)
42
+ logs: Logs = field(default_factory=Logs)
43
+ error: Optional[ExecutionError] = None
44
+ execution_count: Optional[int] = None
45
+
46
+ @property
47
+ def text(self) -> Optional[str]:
48
+ """Text of the main result (last expression value)."""
49
+ for r in self.results:
50
+ if r.is_main_result:
51
+ return r.text
52
+ return None
53
+
54
+ def __repr__(self) -> str:
55
+ return f"Execution(text={self.text!r}, error={self.error})"
56
+
57
+
58
+ @dataclass
59
+ class OutputMessage:
60
+ text: str
61
+ timestamp: str = ""
62
+ is_stderr: bool = False
@@ -0,0 +1,64 @@
1
+ # Copyright (c) 2026 Tencent Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import logging
8
+ from typing import Callable, Optional
9
+
10
+ from ._models import Execution, ExecutionError, OutputMessage, Result
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def _parse_line(
16
+ execution: Execution,
17
+ line: str,
18
+ on_stdout: Optional[Callable[[OutputMessage], None]] = None,
19
+ on_stderr: Optional[Callable[[OutputMessage], None]] = None,
20
+ on_result: Optional[Callable[[Result], None]] = None,
21
+ on_error: Optional[Callable[[ExecutionError], None]] = None,
22
+ ) -> None:
23
+ if not line:
24
+ return
25
+ try:
26
+ data = json.loads(line)
27
+ except json.JSONDecodeError:
28
+ logger.debug("_parse_line: malformed JSON, skipping: %r", line)
29
+ return
30
+
31
+ event_type = data.pop("type", None)
32
+
33
+ if event_type == "result":
34
+ result = Result(**{k: v for k, v in data.items() if k in Result.__dataclass_fields__})
35
+ execution.results.append(result)
36
+ if on_result:
37
+ on_result(result)
38
+
39
+ elif event_type == "stdout":
40
+ text = data.get("text", "")
41
+ execution.logs.stdout.append(text)
42
+ if on_stdout:
43
+ on_stdout(OutputMessage(text, data.get("timestamp", "")))
44
+
45
+ elif event_type == "stderr":
46
+ text = data.get("text", "")
47
+ execution.logs.stderr.append(text)
48
+ if on_stderr:
49
+ on_stderr(OutputMessage(text, data.get("timestamp", ""), is_stderr=True))
50
+
51
+ elif event_type == "error":
52
+ execution.error = ExecutionError(
53
+ name=data.get("name", ""),
54
+ value=data.get("value", ""),
55
+ traceback=data.get("traceback", []),
56
+ )
57
+ if on_error:
58
+ on_error(execution.error)
59
+
60
+ elif event_type == "number_of_executions":
61
+ execution.execution_count = data.get("execution_count")
62
+
63
+ else:
64
+ logger.debug("_parse_line: unknown event type %r, skipping", event_type)
@@ -0,0 +1,56 @@
1
+ # Copyright (c) 2026 Tencent Inc.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import ssl
7
+
8
+ import httpx
9
+
10
+ from ._config import Config
11
+
12
+
13
+ class IPOverrideTransport(httpx.HTTPTransport):
14
+ """Routes all TCP connections to a fixed IP:port while preserving the Host header.
15
+
16
+ Equivalent to ``curl --resolve host:port:ip``.
17
+ Used when ``CUBE_PROXY_NODE_IP`` is set to bypass DNS resolution of ``*.cube.app``.
18
+ """
19
+
20
+ def __init__(self, ip: str, port: int, ssl_context: ssl.SSLContext | None = None, **kw):
21
+ super().__init__(verify=ssl_context or True, **kw)
22
+ self._ip = ip
23
+ self._port = port
24
+
25
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
26
+ original_host = request.url.host
27
+ url = request.url.copy_with(host=self._ip, port=self._port)
28
+ proxied = httpx.Request(
29
+ method=request.method,
30
+ url=url,
31
+ headers=[
32
+ (k, original_host if k.lower() == "host" else v)
33
+ for k, v in request.headers.raw
34
+ ],
35
+ content=request.content,
36
+ )
37
+ return super().handle_request(proxied)
38
+
39
+
40
+ def build_client(config: Config) -> httpx.Client:
41
+ """Build an httpx client for sandbox stream requests.
42
+
43
+ When ``config.proxy_node_ip`` is set, all connections are routed directly
44
+ to that IP, bypassing DNS. The ``Host`` header retains the virtual hostname
45
+ so CubeProxy can route to the correct sandbox.
46
+ """
47
+ if config.proxy_node_ip:
48
+ transport = IPOverrideTransport(config.proxy_node_ip, config.proxy_port)
49
+ else:
50
+ transport = httpx.HTTPTransport()
51
+
52
+ return httpx.Client(
53
+ transport=transport,
54
+ timeout=httpx.Timeout(connect=config.request_timeout, read=None, write=30, pool=30),
55
+ follow_redirects=True,
56
+ )