opencomputer-sdk 0.6.0__tar.gz → 0.6.1__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.0 → opencomputer_sdk-0.6.1}/PKG-INFO +1 -1
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/__init__.py +5 -1
- opencomputer_sdk-0.6.1/opencomputer/mounts.py +124 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/sandbox.py +58 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/pyproject.toml +1 -1
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/.gitignore +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/README.md +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/run_all_tests.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/stream_demo.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_commands.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_concurrent.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_declarative_images.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_default_template.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_disk_size.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_domain_tls.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_environment.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_exec.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_file_ops.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_large_files.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_multi_template.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_python_sdk.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_reconnect.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_secret_store_fork.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_secretstore.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_shell.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/examples/test_timeout.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/agent.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/commands.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/exec.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/filesystem.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/image.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/project.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/pty.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/shell.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/snapshot.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/sse.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/opencomputer/template.py +0 -0
- {opencomputer_sdk-0.6.0 → opencomputer_sdk-0.6.1}/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.1
|
|
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
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""OpenComputer Python SDK - cloud sandbox platform."""
|
|
2
2
|
|
|
3
|
-
from opencomputer.sandbox import Sandbox, ScalingLockedError, PlanLimitError
|
|
3
|
+
from opencomputer.sandbox import Sandbox, ScalingLockedError, PlanLimitError, SandboxFamilyLimitError
|
|
4
4
|
from opencomputer.agent import Agent, AgentEvent, AgentSession, AgentSessionInfo
|
|
5
5
|
from opencomputer.filesystem import Filesystem
|
|
6
6
|
from opencomputer.exec import Exec, ProcessResult, ExecSession, ExecSessionInfo
|
|
7
7
|
from opencomputer.image import Image
|
|
8
|
+
from opencomputer.mounts import Mounts, MountInfo
|
|
8
9
|
from opencomputer.pty import Pty, PtySession
|
|
9
10
|
from opencomputer.shell import Shell, ShellBusyError, ShellClosedError
|
|
10
11
|
from opencomputer.template import Template
|
|
@@ -29,6 +30,7 @@ __all__ = [
|
|
|
29
30
|
"Sandbox",
|
|
30
31
|
"ScalingLockedError",
|
|
31
32
|
"PlanLimitError",
|
|
33
|
+
"SandboxFamilyLimitError",
|
|
32
34
|
"Agent",
|
|
33
35
|
"AgentEvent",
|
|
34
36
|
"AgentSession",
|
|
@@ -39,6 +41,8 @@ __all__ = [
|
|
|
39
41
|
"ExecSession",
|
|
40
42
|
"ExecSessionInfo",
|
|
41
43
|
"Image",
|
|
44
|
+
"Mounts",
|
|
45
|
+
"MountInfo",
|
|
42
46
|
"Pty",
|
|
43
47
|
"PtySession",
|
|
44
48
|
"Shell",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""FUSE-backed remote filesystem mounts inside a sandbox.
|
|
2
|
+
|
|
3
|
+
Mounts use ``rclone mount`` under the hood — one driver covering ~40 backends
|
|
4
|
+
(S3, GCS, Azure Blob, SFTP, WebDAV, Dropbox, etc.). Credentials are passed
|
|
5
|
+
inline, written to a tmpfs file inside the VM (mode 0600), and never persisted
|
|
6
|
+
on the worker. v1 does NOT auto-restore mounts on hibernate/wake — callers
|
|
7
|
+
re-issue ``add(...)`` after a wake if they need the mount back.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
MountBackend = Literal["s3", "gcs", "azureblob", "sftp", "webdav", "dropbox"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class MountInfo:
|
|
22
|
+
"""An active mount as tracked by the worker.
|
|
23
|
+
|
|
24
|
+
``rclone_version`` is the rclone version inside the sandbox captured at
|
|
25
|
+
mount-add time (e.g. ``"v1.65.2"``). rclone is baked into the rootfs, so
|
|
26
|
+
different sandboxes may carry different versions; this lets ops triage
|
|
27
|
+
backend-specific bug reports quickly.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
path: str
|
|
31
|
+
remote: str
|
|
32
|
+
read_only: bool
|
|
33
|
+
backend: str = ""
|
|
34
|
+
rclone_version: str = ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class Mounts:
|
|
39
|
+
"""Mount remote filesystems via rclone+FUSE inside the sandbox."""
|
|
40
|
+
|
|
41
|
+
_client: httpx.AsyncClient
|
|
42
|
+
_sandbox_id: str
|
|
43
|
+
|
|
44
|
+
async def add(
|
|
45
|
+
self,
|
|
46
|
+
path: str,
|
|
47
|
+
remote: str,
|
|
48
|
+
backend: MountBackend | None = None,
|
|
49
|
+
creds: dict[str, str] | None = None,
|
|
50
|
+
rclone_config: str | None = None,
|
|
51
|
+
read_only: bool = True,
|
|
52
|
+
mount_options: list[str] | None = None,
|
|
53
|
+
) -> MountInfo:
|
|
54
|
+
"""Mount a remote filesystem at ``path`` inside the sandbox.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
path: Absolute path inside the VM where the remote will be mounted.
|
|
58
|
+
remote: rclone remote spec — ``"<name>:<path>"`` (e.g. ``"s3:my-bucket/prefix"``).
|
|
59
|
+
backend: One of ``s3``, ``gcs``, ``azureblob``, ``sftp``, ``webdav``,
|
|
60
|
+
``dropbox``. Determines how ``creds`` are templated into the
|
|
61
|
+
rclone config. Omit when passing ``rclone_config`` directly.
|
|
62
|
+
creds: Backend-specific config keys (rclone field names — e.g. for
|
|
63
|
+
S3: ``access_key_id``, ``secret_access_key``, ``region``).
|
|
64
|
+
rclone_config: Raw rclone config string. Overrides ``backend`` and
|
|
65
|
+
``creds`` — useful for backends not in the typed list or for
|
|
66
|
+
advanced tuning.
|
|
67
|
+
read_only: Default ``True``. Object-store FUSE mounts have
|
|
68
|
+
well-known write footguns; opt in to RW explicitly.
|
|
69
|
+
mount_options: Extra args appended to ``rclone mount`` (e.g.
|
|
70
|
+
``["--dir-cache-time", "1m"]``).
|
|
71
|
+
"""
|
|
72
|
+
body: dict[str, object] = {
|
|
73
|
+
"path": path,
|
|
74
|
+
"remote": remote,
|
|
75
|
+
"readOnly": read_only,
|
|
76
|
+
}
|
|
77
|
+
if backend is not None:
|
|
78
|
+
body["backend"] = backend
|
|
79
|
+
if creds is not None:
|
|
80
|
+
body["creds"] = creds
|
|
81
|
+
if rclone_config is not None:
|
|
82
|
+
body["rcloneConfig"] = rclone_config
|
|
83
|
+
if mount_options is not None:
|
|
84
|
+
body["mountOptions"] = mount_options
|
|
85
|
+
|
|
86
|
+
resp = await self._client.post(
|
|
87
|
+
f"/sandboxes/{self._sandbox_id}/mounts", json=body
|
|
88
|
+
)
|
|
89
|
+
resp.raise_for_status()
|
|
90
|
+
data = resp.json()
|
|
91
|
+
return _mount_from_dict(data)
|
|
92
|
+
|
|
93
|
+
async def list(self) -> list[MountInfo]:
|
|
94
|
+
"""List the mounts this worker is tracking for the sandbox.
|
|
95
|
+
|
|
96
|
+
Returns empty after hibernate/wake — re-issue ``add()`` for any mounts
|
|
97
|
+
you need back.
|
|
98
|
+
"""
|
|
99
|
+
resp = await self._client.get(f"/sandboxes/{self._sandbox_id}/mounts")
|
|
100
|
+
resp.raise_for_status()
|
|
101
|
+
data = resp.json() or []
|
|
102
|
+
return [_mount_from_dict(entry) for entry in data]
|
|
103
|
+
|
|
104
|
+
async def remove(self, path: str) -> None:
|
|
105
|
+
"""Unmount a path previously passed to ``add()``. No-op if not mounted."""
|
|
106
|
+
try:
|
|
107
|
+
resp = await self._client.delete(
|
|
108
|
+
f"/sandboxes/{self._sandbox_id}/mounts", params={"path": path}
|
|
109
|
+
)
|
|
110
|
+
if resp.status_code == 404:
|
|
111
|
+
return
|
|
112
|
+
resp.raise_for_status()
|
|
113
|
+
except httpx.HTTPError:
|
|
114
|
+
raise
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _mount_from_dict(data: dict) -> MountInfo:
|
|
118
|
+
return MountInfo(
|
|
119
|
+
path=data["path"],
|
|
120
|
+
remote=data["remote"],
|
|
121
|
+
backend=data.get("backend", ""),
|
|
122
|
+
read_only=data.get("readOnly", True),
|
|
123
|
+
rclone_version=data.get("rcloneVersion", ""),
|
|
124
|
+
)
|
|
@@ -12,6 +12,7 @@ from opencomputer.agent import Agent
|
|
|
12
12
|
from opencomputer.exec import Exec
|
|
13
13
|
from opencomputer.filesystem import Filesystem
|
|
14
14
|
from opencomputer.image import Image
|
|
15
|
+
from opencomputer.mounts import Mounts
|
|
15
16
|
from opencomputer.pty import Pty
|
|
16
17
|
from opencomputer.sse import parse_sse_stream
|
|
17
18
|
|
|
@@ -33,6 +34,14 @@ class PlanLimitError(Exception):
|
|
|
33
34
|
"""
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
class SandboxFamilyLimitError(Exception):
|
|
38
|
+
"""Raised when a resource-changing call is blocked by a sandbox family.
|
|
39
|
+
Kept for compatibility with older API responses.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
code = "sandbox_family_scale_disabled"
|
|
43
|
+
|
|
44
|
+
|
|
36
45
|
def _raise_scaling_error(resp: httpx.Response, action: str) -> None:
|
|
37
46
|
"""Inspect a non-OK scaling response and raise the most specific error.
|
|
38
47
|
Falls back to ``raise_for_status`` so callers still see HTTP details for
|
|
@@ -43,6 +52,8 @@ def _raise_scaling_error(resp: httpx.Response, action: str) -> None:
|
|
|
43
52
|
body = {}
|
|
44
53
|
if resp.status_code == 403 and isinstance(body, dict) and body.get("code") == "scaling_locked":
|
|
45
54
|
raise ScalingLockedError(body.get("error", "scaling is locked on this sandbox"))
|
|
55
|
+
if resp.status_code == 403 and isinstance(body, dict) and body.get("code") == "sandbox_family_scale_disabled":
|
|
56
|
+
raise SandboxFamilyLimitError(body.get("error", "sandbox family does not allow scaling"))
|
|
46
57
|
if resp.status_code == 402:
|
|
47
58
|
msg = body.get("error", "plan limit exceeded") if isinstance(body, dict) else "plan limit exceeded"
|
|
48
59
|
raise PlanLimitError(msg)
|
|
@@ -75,7 +86,10 @@ class Sandbox:
|
|
|
75
86
|
api_url: str | None = None,
|
|
76
87
|
envs: dict[str, str] | None = None,
|
|
77
88
|
metadata: dict[str, str] | None = None,
|
|
89
|
+
burst: bool | None = None,
|
|
90
|
+
sandbox_family: str | None = None,
|
|
78
91
|
disk_mb: int | None = None,
|
|
92
|
+
memory_mb: int | None = None,
|
|
79
93
|
secret_store: str | None = None,
|
|
80
94
|
image: Image | None = None,
|
|
81
95
|
snapshot: str | None = None,
|
|
@@ -90,11 +104,18 @@ class Sandbox:
|
|
|
90
104
|
api_url: API URL (or OPENCOMPUTER_API_URL env var).
|
|
91
105
|
envs: Environment variables to inject. Overrides store secrets.
|
|
92
106
|
metadata: Custom metadata key-value pairs.
|
|
107
|
+
burst: Create a Burst Sandbox. Disk is preserved across
|
|
108
|
+
infrastructure restarts; processes may restart.
|
|
109
|
+
sandbox_family: Internal/legacy placement family. Prefer
|
|
110
|
+
``burst=True`` for public API usage.
|
|
93
111
|
disk_mb: Workspace disk size in MB (default 20480 = 20GB). Any
|
|
94
112
|
additional GB above 20GB is metered at a per-second rate
|
|
95
113
|
comparable to EBS gp3. Closed beta: requests above 20GB
|
|
96
114
|
require the org's ``max_disk_mb`` to be raised. Contact us:
|
|
97
115
|
https://cal.com/team/digger/opencomputer-founder-chat
|
|
116
|
+
memory_mb: Memory in MB. On a snapshot/checkpoint fork the server
|
|
117
|
+
clamps this to [snapshot memory, 16 GB]; the new sandbox's
|
|
118
|
+
``memory_mb`` reflects the effective value.
|
|
98
119
|
secret_store: Secret store name — resolves encrypted secrets
|
|
99
120
|
and egress allowlist.
|
|
100
121
|
image: Declarative Image definition. The server builds and caches it as a checkpoint.
|
|
@@ -130,8 +151,14 @@ class Sandbox:
|
|
|
130
151
|
body["envs"] = envs
|
|
131
152
|
if metadata:
|
|
132
153
|
body["metadata"] = metadata
|
|
154
|
+
if burst is not None:
|
|
155
|
+
body["burst"] = burst
|
|
156
|
+
if sandbox_family:
|
|
157
|
+
body["sandboxFamily"] = sandbox_family
|
|
133
158
|
if disk_mb is not None:
|
|
134
159
|
body["diskMB"] = disk_mb
|
|
160
|
+
if memory_mb is not None:
|
|
161
|
+
body["memoryMB"] = memory_mb
|
|
135
162
|
if secret_store:
|
|
136
163
|
body["secretStore"] = secret_store
|
|
137
164
|
if image is not None:
|
|
@@ -270,6 +297,32 @@ class Sandbox:
|
|
|
270
297
|
resp = await self._client.post(f"/sandboxes/{self.sandbox_id}/power-cycle")
|
|
271
298
|
resp.raise_for_status()
|
|
272
299
|
|
|
300
|
+
async def hibernate(self) -> None:
|
|
301
|
+
"""Hibernate the sandbox.
|
|
302
|
+
|
|
303
|
+
Snapshots the running VM (RAM + disk) to storage and frees its worker
|
|
304
|
+
slot. The sandbox keeps its ID, disks, env, and secrets; in-memory
|
|
305
|
+
process state is preserved and restored on :meth:`wake`. Compute
|
|
306
|
+
billing stops while hibernated (only storage is metered).
|
|
307
|
+
"""
|
|
308
|
+
resp = await self._client.post(f"/sandboxes/{self.sandbox_id}/hibernate")
|
|
309
|
+
resp.raise_for_status()
|
|
310
|
+
self.status = "hibernated"
|
|
311
|
+
|
|
312
|
+
async def wake(self, timeout: int | None = None) -> None:
|
|
313
|
+
"""Wake a hibernated sandbox.
|
|
314
|
+
|
|
315
|
+
Restores the VM from its hibernation snapshot on a worker — running
|
|
316
|
+
processes resume where they left off. Optionally set a new idle
|
|
317
|
+
``timeout`` (seconds; ``0`` = persistent, never auto-hibernate).
|
|
318
|
+
"""
|
|
319
|
+
body: dict[str, Any] = {}
|
|
320
|
+
if timeout is not None:
|
|
321
|
+
body["timeout"] = timeout
|
|
322
|
+
resp = await self._client.post(f"/sandboxes/{self.sandbox_id}/wake", json=body)
|
|
323
|
+
resp.raise_for_status()
|
|
324
|
+
self.status = "running"
|
|
325
|
+
|
|
273
326
|
async def is_running(self) -> bool:
|
|
274
327
|
"""Check if the sandbox is still running."""
|
|
275
328
|
try:
|
|
@@ -482,6 +535,11 @@ class Sandbox:
|
|
|
482
535
|
"""Access filesystem operations."""
|
|
483
536
|
return Filesystem(self._ops_client, self.sandbox_id)
|
|
484
537
|
|
|
538
|
+
@property
|
|
539
|
+
def mounts(self) -> Mounts:
|
|
540
|
+
"""Mount remote filesystems (S3, GCS, SFTP, …) via rclone+FUSE."""
|
|
541
|
+
return Mounts(self._ops_client, self.sandbox_id)
|
|
542
|
+
|
|
485
543
|
@property
|
|
486
544
|
def exec(self) -> Exec:
|
|
487
545
|
"""Access session-based command execution."""
|
|
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
|