opencomputer-sdk 0.6.1__tar.gz → 0.6.3__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.
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/PKG-INFO +1 -1
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/__init__.py +1 -1
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/image.py +35 -20
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/sandbox.py +45 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/pyproject.toml +1 -1
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/.gitignore +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/README.md +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/run_all_tests.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/stream_demo.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_commands.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_concurrent.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_declarative_images.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_default_template.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_disk_size.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_domain_tls.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_environment.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_exec.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_file_ops.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_large_files.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_multi_template.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_python_sdk.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_reconnect.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_secret_store_fork.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_secretstore.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_shell.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/examples/test_timeout.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/agent.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/commands.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/exec.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/filesystem.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/mounts.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/project.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/pty.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/shell.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/snapshot.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/sse.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/template.py +0 -0
- {opencomputer_sdk-0.6.1 → opencomputer_sdk-0.6.3}/opencomputer/usage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencomputer-sdk
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: Python SDK for OpenComputer - cloud sandbox platform
|
|
5
5
|
Project-URL: Homepage, https://github.com/diggerhq/opensandbox
|
|
6
6
|
Project-URL: Repository, https://github.com/diggerhq/opensandbox
|
|
@@ -6,7 +6,7 @@ import base64
|
|
|
6
6
|
import hashlib
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
-
from dataclasses import dataclass, field
|
|
9
|
+
from dataclasses import dataclass, field, replace
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
|
|
@@ -47,6 +47,10 @@ class Image:
|
|
|
47
47
|
|
|
48
48
|
_base: str = "base"
|
|
49
49
|
_steps: tuple[ImageStep, ...] = field(default_factory=tuple)
|
|
50
|
+
# RAM for the build phase (apt/pip). 0 = server default. Does not pin the
|
|
51
|
+
# output image — the server re-snapshots at the default 1 GB floor, and you
|
|
52
|
+
# size the actual sandbox at create time via memory_mb.
|
|
53
|
+
_builder_memory_mb: int = 0
|
|
50
54
|
|
|
51
55
|
@classmethod
|
|
52
56
|
def base(cls) -> Image:
|
|
@@ -60,36 +64,36 @@ class Image:
|
|
|
60
64
|
|
|
61
65
|
def apt_install(self, packages: list[str]) -> Image:
|
|
62
66
|
"""Install system packages via apt-get."""
|
|
63
|
-
return
|
|
64
|
-
|
|
67
|
+
return replace(
|
|
68
|
+
self,
|
|
65
69
|
_steps=(*self._steps, ImageStep("apt_install", {"packages": packages})),
|
|
66
70
|
)
|
|
67
71
|
|
|
68
72
|
def pip_install(self, packages: list[str]) -> Image:
|
|
69
73
|
"""Install Python packages via pip."""
|
|
70
|
-
return
|
|
71
|
-
|
|
74
|
+
return replace(
|
|
75
|
+
self,
|
|
72
76
|
_steps=(*self._steps, ImageStep("pip_install", {"packages": packages})),
|
|
73
77
|
)
|
|
74
78
|
|
|
75
79
|
def run_commands(self, *commands: str) -> Image:
|
|
76
80
|
"""Run one or more shell commands."""
|
|
77
|
-
return
|
|
78
|
-
|
|
81
|
+
return replace(
|
|
82
|
+
self,
|
|
79
83
|
_steps=(*self._steps, ImageStep("run", {"commands": list(commands)})),
|
|
80
84
|
)
|
|
81
85
|
|
|
82
86
|
def env(self, vars: dict[str, str]) -> Image:
|
|
83
87
|
"""Set environment variables (written to /etc/environment)."""
|
|
84
|
-
return
|
|
85
|
-
|
|
88
|
+
return replace(
|
|
89
|
+
self,
|
|
86
90
|
_steps=(*self._steps, ImageStep("env", {"vars": vars})),
|
|
87
91
|
)
|
|
88
92
|
|
|
89
93
|
def workdir(self, path: str) -> Image:
|
|
90
94
|
"""Set the default working directory."""
|
|
91
|
-
return
|
|
92
|
-
|
|
95
|
+
return replace(
|
|
96
|
+
self,
|
|
93
97
|
_steps=(*self._steps, ImageStep("workdir", {"path": path})),
|
|
94
98
|
)
|
|
95
99
|
|
|
@@ -101,8 +105,8 @@ class Image:
|
|
|
101
105
|
content: String content of the file.
|
|
102
106
|
"""
|
|
103
107
|
encoded = base64.b64encode(content.encode()).decode()
|
|
104
|
-
return
|
|
105
|
-
|
|
108
|
+
return replace(
|
|
109
|
+
self,
|
|
106
110
|
_steps=(*self._steps, ImageStep("add_file", {
|
|
107
111
|
"path": remote_path,
|
|
108
112
|
"content": encoded,
|
|
@@ -121,8 +125,8 @@ class Image:
|
|
|
121
125
|
"""
|
|
122
126
|
with open(local_path, "rb") as f:
|
|
123
127
|
encoded = base64.b64encode(f.read()).decode()
|
|
124
|
-
return
|
|
125
|
-
|
|
128
|
+
return replace(
|
|
129
|
+
self,
|
|
126
130
|
_steps=(*self._steps, ImageStep("add_file", {
|
|
127
131
|
"path": remote_path,
|
|
128
132
|
"content": encoded,
|
|
@@ -147,22 +151,33 @@ class Image:
|
|
|
147
151
|
with open(full, "rb") as f:
|
|
148
152
|
encoded = base64.b64encode(f.read()).decode()
|
|
149
153
|
files.append({"relativePath": rel, "content": encoded})
|
|
150
|
-
return
|
|
151
|
-
|
|
154
|
+
return replace(
|
|
155
|
+
self,
|
|
152
156
|
_steps=(*self._steps, ImageStep("add_dir", {
|
|
153
157
|
"path": remote_path,
|
|
154
158
|
"files": files,
|
|
155
159
|
})),
|
|
156
160
|
)
|
|
157
161
|
|
|
162
|
+
def builder_memory(self, mb: int) -> Image:
|
|
163
|
+
"""Set the RAM (MB) for the build phase. Use this when a build OOMs at the
|
|
164
|
+
default 4 GB (e.g. heavy ``apt``/``pip``/``npm``). Does not affect the
|
|
165
|
+
resulting image's memory — size the sandbox at create time via ``memory_mb``."""
|
|
166
|
+
return replace(self, _builder_memory_mb=mb)
|
|
167
|
+
|
|
158
168
|
def to_dict(self) -> dict[str, Any]:
|
|
159
169
|
"""Returns the manifest as a plain dict (for JSON serialization)."""
|
|
160
|
-
|
|
170
|
+
d: dict[str, Any] = {
|
|
161
171
|
"base": self._base,
|
|
162
172
|
"steps": [s.to_dict() for s in self._steps],
|
|
163
173
|
}
|
|
174
|
+
if self._builder_memory_mb > 0:
|
|
175
|
+
d["builderMemoryMB"] = self._builder_memory_mb
|
|
176
|
+
return d
|
|
164
177
|
|
|
165
178
|
def cache_key(self) -> str:
|
|
166
|
-
"""
|
|
167
|
-
|
|
179
|
+
"""Deterministic content hash for caching. Memory knobs are resource
|
|
180
|
+
params, not image content, so they're excluded (matches the server)."""
|
|
181
|
+
content = {"base": self._base, "steps": [s.to_dict() for s in self._steps]}
|
|
182
|
+
canonical = json.dumps(content, sort_keys=True, separators=(",", ":"))
|
|
168
183
|
return hashlib.sha256(canonical.encode()).hexdigest()
|
|
@@ -70,6 +70,12 @@ class Sandbox:
|
|
|
70
70
|
sandbox_id: str
|
|
71
71
|
status: str = "running"
|
|
72
72
|
template: str = ""
|
|
73
|
+
#: Plaintext preview-URL bearer token, available immediately after a
|
|
74
|
+
#: ``Sandbox.create(preview_auth=...)`` call. Read it once and store
|
|
75
|
+
#: it somewhere durable — the server will not return it again. After
|
|
76
|
+
#: a successful ``rotate_preview_auth_token()`` this value is replaced
|
|
77
|
+
#: with the new token. Empty string when not enabled or when reconnecting.
|
|
78
|
+
preview_auth_token: str = ""
|
|
73
79
|
_api_url: str = ""
|
|
74
80
|
_api_key: str = ""
|
|
75
81
|
_connect_url: str = ""
|
|
@@ -77,6 +83,10 @@ class Sandbox:
|
|
|
77
83
|
_client: httpx.AsyncClient = field(default=None, repr=False)
|
|
78
84
|
_data_client: httpx.AsyncClient = field(default=None, repr=False)
|
|
79
85
|
|
|
86
|
+
@property
|
|
87
|
+
def id(self) -> str:
|
|
88
|
+
return self.sandbox_id
|
|
89
|
+
|
|
80
90
|
@classmethod
|
|
81
91
|
async def create(
|
|
82
92
|
cls,
|
|
@@ -94,6 +104,7 @@ class Sandbox:
|
|
|
94
104
|
image: Image | None = None,
|
|
95
105
|
snapshot: str | None = None,
|
|
96
106
|
on_build_log: Callable[[str], None] | None = None,
|
|
107
|
+
preview_auth: dict[str, str] | None = None,
|
|
97
108
|
) -> Sandbox:
|
|
98
109
|
"""Create a new sandbox instance.
|
|
99
110
|
|
|
@@ -121,6 +132,15 @@ class Sandbox:
|
|
|
121
132
|
image: Declarative Image definition. The server builds and caches it as a checkpoint.
|
|
122
133
|
snapshot: Name of a pre-built snapshot to create the sandbox from.
|
|
123
134
|
on_build_log: Callback for build log streaming when using image/snapshot.
|
|
135
|
+
preview_auth: Require a bearer token on the sandbox's preview URLs.
|
|
136
|
+
When set, every request to ``https://sb-{id}-p{port}.<domain>``
|
|
137
|
+
must include the token in an ``Authorization: Bearer <token>``
|
|
138
|
+
or ``X-OC-Preview-Token`` header. The check happens at the edge
|
|
139
|
+
before traffic reaches the VM. Pass ``{"token": "auto"}`` (or
|
|
140
|
+
omit the key) to have the server generate a 256-bit random
|
|
141
|
+
token; pass an explicit string (>=16 chars) to bring your own.
|
|
142
|
+
The plaintext is returned exactly once and assigned to
|
|
143
|
+
``sandbox.preview_auth_token``.
|
|
124
144
|
"""
|
|
125
145
|
url = api_url or os.environ.get("OPENCOMPUTER_API_URL", "https://app.opencomputer.dev")
|
|
126
146
|
url = url.rstrip("/")
|
|
@@ -165,6 +185,11 @@ class Sandbox:
|
|
|
165
185
|
body["image"] = image.to_dict()
|
|
166
186
|
if snapshot is not None:
|
|
167
187
|
body["snapshot"] = snapshot
|
|
188
|
+
if preview_auth is not None:
|
|
189
|
+
body["previewAuth"] = {
|
|
190
|
+
"scheme": preview_auth.get("scheme", "bearer"),
|
|
191
|
+
"token": preview_auth.get("token", "auto"),
|
|
192
|
+
}
|
|
168
193
|
|
|
169
194
|
if use_sse:
|
|
170
195
|
data = await cls._create_with_sse(client, body, on_build_log)
|
|
@@ -189,6 +214,7 @@ class Sandbox:
|
|
|
189
214
|
sandbox_id=data["sandboxID"],
|
|
190
215
|
status=data.get("status", "running"),
|
|
191
216
|
template=template,
|
|
217
|
+
preview_auth_token=data.get("previewAuthToken", ""),
|
|
192
218
|
_api_url=url,
|
|
193
219
|
_api_key=key,
|
|
194
220
|
_connect_url=connect_url,
|
|
@@ -809,6 +835,25 @@ class Sandbox:
|
|
|
809
835
|
if resp.status_code != 404:
|
|
810
836
|
resp.raise_for_status()
|
|
811
837
|
|
|
838
|
+
async def rotate_preview_auth_token(self) -> str:
|
|
839
|
+
"""Issue a new preview-URL bearer token; invalidate the previous one.
|
|
840
|
+
|
|
841
|
+
The old token stops working immediately — there is no zero-downtime
|
|
842
|
+
dual-token mode in v1, so coordinate the rollover with whoever is
|
|
843
|
+
calling your preview URL. If the sandbox was created without
|
|
844
|
+
``preview_auth``, calling this enables the auth gate from now on.
|
|
845
|
+
|
|
846
|
+
Returns the new plaintext token (also written to
|
|
847
|
+
``self.preview_auth_token``).
|
|
848
|
+
"""
|
|
849
|
+
resp = await self._client.post(
|
|
850
|
+
f"/sandboxes/{self.sandbox_id}/preview/rotate",
|
|
851
|
+
)
|
|
852
|
+
resp.raise_for_status()
|
|
853
|
+
data = resp.json()
|
|
854
|
+
self.preview_auth_token = data["previewAuthToken"]
|
|
855
|
+
return self.preview_auth_token
|
|
856
|
+
|
|
812
857
|
async def create_preview_url(self, port: int, domain: str | None = None, auth_config: dict | None = None) -> dict:
|
|
813
858
|
"""Create a preview URL targeting a specific container port.
|
|
814
859
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|