opencomputer-sdk 0.6.2__tar.gz → 0.6.4__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.2 → opencomputer_sdk-0.6.4}/.gitignore +3 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/PKG-INFO +1 -1
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/__init__.py +1 -1
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/image.py +35 -20
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/mounts.py +79 -15
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/sandbox.py +29 -2
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/pyproject.toml +1 -1
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/README.md +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/run_all_tests.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/stream_demo.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_commands.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_concurrent.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_declarative_images.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_default_template.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_disk_size.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_domain_tls.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_environment.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_exec.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_file_ops.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_large_files.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_multi_template.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_python_sdk.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_reconnect.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_secret_store_fork.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_secretstore.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_shell.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_timeout.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/agent.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/commands.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/exec.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/filesystem.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/project.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/pty.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/shell.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/snapshot.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/sse.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/template.py +0 -0
- {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/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.4
|
|
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()
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
"""FUSE-backed
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
"""FUSE-backed filesystem mounts inside a sandbox.
|
|
2
|
+
|
|
3
|
+
Two drivers:
|
|
4
|
+
|
|
5
|
+
- ``rclone`` (default, via :meth:`Mounts.add`): wrap any of rclone's ~40
|
|
6
|
+
backends (S3, GCS, Azure Blob, SFTP, WebDAV, Dropbox, …) behind a simple
|
|
7
|
+
remote+creds shape. Creds are written to a tmpfs file (mode 0600), never
|
|
8
|
+
persisted on the worker.
|
|
9
|
+
- ``command`` (via :meth:`Mounts.add_command`): run your own FUSE daemon /
|
|
10
|
+
mount command. Use this when you already have a FUSE-ready filesystem and
|
|
11
|
+
don't want rclone as a middle layer. Secrets are injected into the daemon's
|
|
12
|
+
process env (never the command line) and never persisted.
|
|
8
13
|
"""
|
|
9
14
|
|
|
10
15
|
from __future__ import annotations
|
|
11
16
|
|
|
12
|
-
from dataclasses import dataclass
|
|
17
|
+
from dataclasses import dataclass, field
|
|
13
18
|
from typing import Literal
|
|
14
19
|
|
|
15
20
|
import httpx
|
|
@@ -21,17 +26,22 @@ MountBackend = Literal["s3", "gcs", "azureblob", "sftp", "webdav", "dropbox"]
|
|
|
21
26
|
class MountInfo:
|
|
22
27
|
"""An active mount as tracked by the worker.
|
|
23
28
|
|
|
24
|
-
``rclone_version`` is the rclone version inside the sandbox
|
|
25
|
-
mount-add time (e.g. ``"v1.65.2"``). rclone is baked into the
|
|
26
|
-
different sandboxes may carry different versions; this lets ops
|
|
27
|
-
backend-specific bug reports quickly.
|
|
29
|
+
``rclone_version`` (rclone driver) is the rclone version inside the sandbox
|
|
30
|
+
captured at mount-add time (e.g. ``"v1.65.2"``). rclone is baked into the
|
|
31
|
+
rootfs, so different sandboxes may carry different versions; this lets ops
|
|
32
|
+
triage backend-specific bug reports quickly.
|
|
28
33
|
"""
|
|
29
34
|
|
|
30
35
|
path: str
|
|
31
|
-
remote: str
|
|
32
36
|
read_only: bool
|
|
37
|
+
driver: str = "rclone"
|
|
38
|
+
# rclone driver
|
|
39
|
+
remote: str = ""
|
|
33
40
|
backend: str = ""
|
|
34
41
|
rclone_version: str = ""
|
|
42
|
+
# command driver
|
|
43
|
+
command: list[str] = field(default_factory=list)
|
|
44
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
35
45
|
|
|
36
46
|
|
|
37
47
|
@dataclass
|
|
@@ -90,6 +100,57 @@ class Mounts:
|
|
|
90
100
|
data = resp.json()
|
|
91
101
|
return _mount_from_dict(data)
|
|
92
102
|
|
|
103
|
+
async def add_command(
|
|
104
|
+
self,
|
|
105
|
+
path: str,
|
|
106
|
+
command: list[str],
|
|
107
|
+
env: dict[str, str] | None = None,
|
|
108
|
+
secrets: dict[str, str] | None = None,
|
|
109
|
+
read_only: bool = True,
|
|
110
|
+
) -> MountInfo:
|
|
111
|
+
"""Mount a filesystem by running your own FUSE daemon / mount command.
|
|
112
|
+
|
|
113
|
+
Use this when you already have a FUSE-ready filesystem (your own VFS,
|
|
114
|
+
gcsfuse, s3fs, …) and don't want rclone as a middle layer. The platform
|
|
115
|
+
manages the mountpoint, env/secret injection, and teardown; ``command``
|
|
116
|
+
establishes the mount.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
path: Absolute mountpoint inside the VM.
|
|
120
|
+
command: argv for the FUSE daemon. Any ``"{mountpoint}"`` token is
|
|
121
|
+
replaced with ``path``.
|
|
122
|
+
env: Env vars for the command (returned by :meth:`list`).
|
|
123
|
+
secrets: Secret env vars — injected into the daemon's process env
|
|
124
|
+
(never the command line, so they don't leak via ``ps``), and
|
|
125
|
+
never recorded or returned by :meth:`list`.
|
|
126
|
+
read_only: Advisory for this driver — your command must honor it.
|
|
127
|
+
Also exported to the daemon as ``OC_MOUNT_READONLY=1``. Default
|
|
128
|
+
``True``.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
>>> await sandbox.mounts.add_command(
|
|
132
|
+
... path="/mnt/data",
|
|
133
|
+
... command=["gcsfuse", "my-bucket", "{mountpoint}"],
|
|
134
|
+
... secrets={"GOOGLE_APPLICATION_CREDENTIALS_JSON": sa_json},
|
|
135
|
+
... )
|
|
136
|
+
"""
|
|
137
|
+
body: dict[str, object] = {
|
|
138
|
+
"path": path,
|
|
139
|
+
"driver": "command",
|
|
140
|
+
"command": command,
|
|
141
|
+
"readOnly": read_only,
|
|
142
|
+
}
|
|
143
|
+
if env is not None:
|
|
144
|
+
body["env"] = env
|
|
145
|
+
if secrets is not None:
|
|
146
|
+
body["secrets"] = secrets
|
|
147
|
+
|
|
148
|
+
resp = await self._client.post(
|
|
149
|
+
f"/sandboxes/{self._sandbox_id}/mounts", json=body
|
|
150
|
+
)
|
|
151
|
+
resp.raise_for_status()
|
|
152
|
+
return _mount_from_dict(resp.json())
|
|
153
|
+
|
|
93
154
|
async def list(self) -> list[MountInfo]:
|
|
94
155
|
"""List the mounts this worker is tracking for the sandbox.
|
|
95
156
|
|
|
@@ -117,8 +178,11 @@ class Mounts:
|
|
|
117
178
|
def _mount_from_dict(data: dict) -> MountInfo:
|
|
118
179
|
return MountInfo(
|
|
119
180
|
path=data["path"],
|
|
120
|
-
|
|
121
|
-
backend=data.get("backend", ""),
|
|
181
|
+
driver=data.get("driver", "rclone"),
|
|
122
182
|
read_only=data.get("readOnly", True),
|
|
183
|
+
remote=data.get("remote", ""),
|
|
184
|
+
backend=data.get("backend", ""),
|
|
123
185
|
rclone_version=data.get("rcloneVersion", ""),
|
|
186
|
+
command=data.get("command", []) or [],
|
|
187
|
+
env=data.get("env", {}) or {},
|
|
124
188
|
)
|
|
@@ -83,6 +83,10 @@ class Sandbox:
|
|
|
83
83
|
_client: httpx.AsyncClient = field(default=None, repr=False)
|
|
84
84
|
_data_client: httpx.AsyncClient = field(default=None, repr=False)
|
|
85
85
|
|
|
86
|
+
@property
|
|
87
|
+
def id(self) -> str:
|
|
88
|
+
return self.sandbox_id
|
|
89
|
+
|
|
86
90
|
@classmethod
|
|
87
91
|
async def create(
|
|
88
92
|
cls,
|
|
@@ -598,18 +602,41 @@ class Sandbox:
|
|
|
598
602
|
pty_key = self._token or self._api_key
|
|
599
603
|
return Pty(self._ops_client, self.sandbox_id, pty_url, pty_key)
|
|
600
604
|
|
|
601
|
-
async def create_checkpoint(
|
|
605
|
+
async def create_checkpoint(
|
|
606
|
+
self,
|
|
607
|
+
name: str,
|
|
608
|
+
kind: str | None = None,
|
|
609
|
+
promote_to_full: bool = False,
|
|
610
|
+
retention_policy: dict | None = None,
|
|
611
|
+
) -> dict:
|
|
602
612
|
"""Create a named checkpoint of the running sandbox.
|
|
603
613
|
|
|
604
614
|
Args:
|
|
605
615
|
name: A unique name for this checkpoint.
|
|
616
|
+
kind: Optional checkpoint kind. Use "full" for disk, memory, and
|
|
617
|
+
device state, or "disk_only" for a disk-only checkpoint that
|
|
618
|
+
restores with a cold boot.
|
|
619
|
+
promote_to_full: For disk-only checkpoints, asynchronously create
|
|
620
|
+
a derived full checkpoint so future forks can use warm restore.
|
|
621
|
+
retention_policy: Optional policy such as
|
|
622
|
+
{"mode": "delete_oldest", "maxCount": 10}. When set, the
|
|
623
|
+
server may delete older eligible checkpoints before creating
|
|
624
|
+
this one.
|
|
606
625
|
|
|
607
626
|
Returns:
|
|
608
627
|
Checkpoint info dict with id, sandboxId, name, status, etc.
|
|
609
628
|
"""
|
|
629
|
+
body = {"name": name}
|
|
630
|
+
if kind is not None:
|
|
631
|
+
body["kind"] = kind
|
|
632
|
+
if promote_to_full:
|
|
633
|
+
body["promoteToFull"] = True
|
|
634
|
+
if retention_policy is not None:
|
|
635
|
+
body["retentionPolicy"] = retention_policy
|
|
636
|
+
|
|
610
637
|
resp = await self._client.post(
|
|
611
638
|
f"/sandboxes/{self.sandbox_id}/checkpoints",
|
|
612
|
-
json=
|
|
639
|
+
json=body,
|
|
613
640
|
)
|
|
614
641
|
resp.raise_for_status()
|
|
615
642
|
return resp.json()
|
|
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
|