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.
Files changed (38) hide show
  1. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/.gitignore +3 -0
  2. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/PKG-INFO +1 -1
  3. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/__init__.py +1 -1
  4. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/image.py +35 -20
  5. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/mounts.py +79 -15
  6. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/sandbox.py +29 -2
  7. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/pyproject.toml +1 -1
  8. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/README.md +0 -0
  9. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/run_all_tests.py +0 -0
  10. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/stream_demo.py +0 -0
  11. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_commands.py +0 -0
  12. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_concurrent.py +0 -0
  13. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_declarative_images.py +0 -0
  14. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_default_template.py +0 -0
  15. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_disk_size.py +0 -0
  16. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_domain_tls.py +0 -0
  17. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_environment.py +0 -0
  18. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_exec.py +0 -0
  19. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_file_ops.py +0 -0
  20. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_large_files.py +0 -0
  21. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_multi_template.py +0 -0
  22. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_python_sdk.py +0 -0
  23. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_reconnect.py +0 -0
  24. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_secret_store_fork.py +0 -0
  25. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_secretstore.py +0 -0
  26. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_shell.py +0 -0
  27. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/examples/test_timeout.py +0 -0
  28. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/agent.py +0 -0
  29. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/commands.py +0 -0
  30. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/exec.py +0 -0
  31. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/filesystem.py +0 -0
  32. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/project.py +0 -0
  33. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/pty.py +0 -0
  34. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/shell.py +0 -0
  35. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/snapshot.py +0 -0
  36. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/sse.py +0 -0
  37. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/template.py +0 -0
  38. {opencomputer_sdk-0.6.2 → opencomputer_sdk-0.6.4}/opencomputer/usage.py +0 -0
@@ -18,6 +18,9 @@ Thumbs.db
18
18
  .env
19
19
  .env.*
20
20
  !.env.example
21
+ # Cloudflare Worker local secrets (wrangler dev)
22
+ .dev.vars
23
+ .dev.vars.*
21
24
 
22
25
  # Per-developer dev-box lifecycle state (written by deploy/*/deploy-qemu-dev.sh)
23
26
  deploy/**/.qemu-dev-state-*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencomputer-sdk
3
- Version: 0.6.2
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
@@ -65,4 +65,4 @@ __all__ = [
65
65
  "TagKeyInfo",
66
66
  ]
67
67
 
68
- __version__ = "0.6.0"
68
+ __version__ = "0.6.3"
@@ -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 Image(
64
- _base=self._base,
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 Image(
71
- _base=self._base,
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 Image(
78
- _base=self._base,
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 Image(
85
- _base=self._base,
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 Image(
92
- _base=self._base,
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 Image(
105
- _base=self._base,
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 Image(
125
- _base=self._base,
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 Image(
151
- _base=self._base,
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
- return {
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
- """Compute a deterministic content hash for caching."""
167
- canonical = json.dumps(self.to_dict(), sort_keys=True, separators=(",", ":"))
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 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.
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 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.
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
- remote=data["remote"],
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(self, name: str) -> dict:
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={"name": name},
639
+ json=body,
613
640
  )
614
641
  resp.raise_for_status()
615
642
  return resp.json()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "opencomputer-sdk"
7
- version = "0.6.2"
7
+ version = "0.6.4"
8
8
  description = "Python SDK for OpenComputer - cloud sandbox platform"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"