opencomputer-sdk 0.4.4__tar.gz → 0.4.5__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 (21) hide show
  1. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/.gitignore +7 -0
  2. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/PKG-INFO +1 -1
  3. opencomputer_sdk-0.4.5/examples/test_default_template.py +253 -0
  4. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/opencomputer/sandbox.py +120 -0
  5. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/pyproject.toml +1 -1
  6. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/README.md +0 -0
  7. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/examples/run_all_tests.py +0 -0
  8. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/examples/test_commands.py +0 -0
  9. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/examples/test_concurrent.py +0 -0
  10. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/examples/test_domain_tls.py +0 -0
  11. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/examples/test_environment.py +0 -0
  12. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/examples/test_file_ops.py +0 -0
  13. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/examples/test_multi_template.py +0 -0
  14. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/examples/test_python_sdk.py +0 -0
  15. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/examples/test_reconnect.py +0 -0
  16. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/examples/test_timeout.py +0 -0
  17. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/opencomputer/__init__.py +0 -0
  18. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/opencomputer/commands.py +0 -0
  19. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/opencomputer/filesystem.py +0 -0
  20. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/opencomputer/pty.py +0 -0
  21. {opencomputer_sdk-0.4.4 → opencomputer_sdk-0.4.5}/opencomputer/template.py +0 -0
@@ -43,6 +43,13 @@ sdks/typescript/examples/loom/
43
43
  *.mp4
44
44
  *.gif
45
45
 
46
+ # Terraform
47
+ deploy/terraform/.terraform/
48
+ deploy/terraform/.terraform.lock.hcl
49
+ deploy/terraform/terraform.tfstate*
50
+ deploy/terraform/terraform.tfvars
51
+
46
52
  # Temp files
47
53
  *.tmp
48
54
  *.log
55
+ delme
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencomputer-sdk
3
- Version: 0.4.4
3
+ Version: 0.4.5
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
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Default Template Verification Test
4
+
5
+ Verifies that the "default" template image contains all expected packages:
6
+ 1. Python 3 + pip + venv
7
+ 2. Node.js 20 + npm
8
+ 3. Build tools (gcc, g++, make, cmake)
9
+ 4. Git + git-lfs
10
+ 5. Common utilities (curl, wget, jq, tar, zip, unzip, etc.)
11
+ 6. System libraries (libssl, libffi, zlib, sqlite3)
12
+ 7. Locale (en_US.UTF-8 available)
13
+ 8. Workspace directory
14
+
15
+ Usage:
16
+ python examples/test_default_template.py
17
+ """
18
+
19
+ import asyncio
20
+ import os
21
+ import sys
22
+
23
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
24
+ from opencomputer import Sandbox
25
+
26
+ GREEN = "\033[32m"
27
+ RED = "\033[31m"
28
+ BOLD = "\033[1m"
29
+ DIM = "\033[2m"
30
+ RESET = "\033[0m"
31
+
32
+ passed = 0
33
+ failed = 0
34
+
35
+
36
+ def green(msg: str) -> None:
37
+ print(f"{GREEN}✓ {msg}{RESET}")
38
+
39
+
40
+ def red(msg: str) -> None:
41
+ print(f"{RED}✗ {msg}{RESET}")
42
+
43
+
44
+ def bold(msg: str) -> None:
45
+ print(f"{BOLD}{msg}{RESET}")
46
+
47
+
48
+ def dim(msg: str) -> None:
49
+ print(f"{DIM} {msg}{RESET}")
50
+
51
+
52
+ def check(desc: str, condition: bool, detail: str = "") -> None:
53
+ global passed, failed
54
+ if condition:
55
+ green(desc)
56
+ passed += 1
57
+ else:
58
+ red(f"{desc} ({detail})" if detail else desc)
59
+ failed += 1
60
+
61
+
62
+ async def expect_command(
63
+ sandbox: Sandbox,
64
+ desc: str,
65
+ cmd: str,
66
+ contains: str | None = None,
67
+ ) -> str:
68
+ """Run a command and check it exits 0 and stdout contains expected substring."""
69
+ result = await sandbox.commands.run(cmd)
70
+ out = (result.stdout.strip() or result.stderr.strip())
71
+ dim(f"$ {cmd}")
72
+ dim(f" → {out}")
73
+
74
+ if result.exit_code != 0:
75
+ check(desc, False, f"exit code {result.exit_code}")
76
+ return out
77
+
78
+ if contains is not None:
79
+ check(desc, contains.lower() in out.lower(),
80
+ f'expected "{contains}" in output')
81
+ else:
82
+ check(desc, True)
83
+ return out
84
+
85
+
86
+ async def main() -> None:
87
+ global passed, failed
88
+
89
+ bold("\n╔══════════════════════════════════════════════════╗")
90
+ bold("║ Default Template Verification Test ║")
91
+ bold("╚══════════════════════════════════════════════════╝\n")
92
+
93
+ sandbox = None
94
+
95
+ try:
96
+ sandbox = await Sandbox.create(template="default", timeout=120)
97
+ green(f"Created sandbox: {sandbox.sandbox_id}")
98
+ print()
99
+
100
+ # ── 1. Python ──
101
+ bold("━━━ 1. Python 3 ━━━\n")
102
+
103
+ await expect_command(sandbox, "python3 is installed",
104
+ "python3 --version", contains="Python 3")
105
+ await expect_command(sandbox, "python symlink works",
106
+ "python --version", contains="Python 3")
107
+ await expect_command(sandbox, "pip3 is installed",
108
+ "pip3 --version", contains="pip")
109
+ await expect_command(sandbox, "python3-venv is available",
110
+ "python3 -m venv --help 2>&1 | head -1", contains="usage")
111
+
112
+ pip_install = await sandbox.commands.run(
113
+ "pip3 install --no-cache-dir cowsay 2>&1", timeout=30)
114
+ check("pip install works", pip_install.exit_code == 0,
115
+ f"exit {pip_install.exit_code}")
116
+ cowsay = await sandbox.commands.run(
117
+ "python3 -c 'import cowsay; print(\"pip-ok\")'")
118
+ check("installed Python package importable",
119
+ "pip-ok" in cowsay.stdout.strip())
120
+ print()
121
+
122
+ # ── 2. Node.js ──
123
+ bold("━━━ 2. Node.js 20 + npm ━━━\n")
124
+
125
+ node_version = await expect_command(sandbox, "node is installed",
126
+ "node --version")
127
+ check("Node.js is v20.x", node_version.startswith("v20"),
128
+ f"got {node_version}")
129
+ await expect_command(sandbox, "npm is installed", "npm --version")
130
+
131
+ await sandbox.commands.run(
132
+ "mkdir -p /tmp/npm-test && cd /tmp/npm-test && npm init -y 2>&1",
133
+ timeout=10)
134
+ npm_install = await sandbox.commands.run(
135
+ "cd /tmp/npm-test && npm install is-odd 2>&1", timeout=30)
136
+ check("npm install works", npm_install.exit_code == 0,
137
+ f"exit {npm_install.exit_code}")
138
+ node_run = await sandbox.commands.run(
139
+ """node -e "console.log(require('is-odd')(3))" """,
140
+ cwd="/tmp/npm-test")
141
+ check("installed npm package works",
142
+ node_run.stdout.strip() == "true",
143
+ f'got "{node_run.stdout.strip()}"')
144
+ print()
145
+
146
+ # ── 3. Build tools ──
147
+ bold("━━━ 3. Build tools ━━━\n")
148
+
149
+ await expect_command(sandbox, "gcc is installed",
150
+ "gcc --version 2>&1 | head -1", contains="gcc")
151
+ await expect_command(sandbox, "g++ is installed",
152
+ "g++ --version 2>&1 | head -1", contains="g++")
153
+ await expect_command(sandbox, "make is installed",
154
+ "make --version 2>&1 | head -1", contains="make")
155
+ await expect_command(sandbox, "cmake is installed",
156
+ "cmake --version 2>&1 | head -1", contains="cmake")
157
+ await expect_command(sandbox, "pkg-config is installed",
158
+ "pkg-config --version")
159
+
160
+ await sandbox.files.write("/tmp/hello.c",
161
+ '#include <stdio.h>\nint main() { printf("compiled-ok\\n"); return 0; }')
162
+ compile_result = await sandbox.commands.run(
163
+ "gcc -o /tmp/hello /tmp/hello.c && /tmp/hello")
164
+ check("C compilation + execution works",
165
+ compile_result.stdout.strip() == "compiled-ok",
166
+ f'got "{compile_result.stdout.strip()}"')
167
+ print()
168
+
169
+ # ── 4. Git ──
170
+ bold("━━━ 4. Git ━━━\n")
171
+
172
+ await expect_command(sandbox, "git is installed",
173
+ "git --version", contains="git version")
174
+ await expect_command(sandbox, "git-lfs is installed",
175
+ "git lfs version 2>&1 | head -1", contains="git-lfs")
176
+ print()
177
+
178
+ # ── 5. Networking & utilities ──
179
+ bold("━━━ 5. Networking & common utilities ━━━\n")
180
+
181
+ await expect_command(sandbox, "curl is installed",
182
+ "curl --version 2>&1 | head -1", contains="curl")
183
+ await expect_command(sandbox, "wget is installed",
184
+ "wget --version 2>&1 | head -1", contains="wget")
185
+ await expect_command(sandbox, "ssh client is installed",
186
+ "ssh -V 2>&1", contains="OpenSSH")
187
+ await expect_command(sandbox, "jq is installed",
188
+ "jq --version", contains="jq")
189
+ await expect_command(sandbox, "tar is installed",
190
+ "tar --version 2>&1 | head -1", contains="tar")
191
+ await expect_command(sandbox, "zip is installed",
192
+ "zip --version 2>&1 | head -2 | tail -1", contains="zip")
193
+ await expect_command(sandbox, "unzip is installed",
194
+ "unzip -v 2>&1 | head -1", contains="unzip")
195
+ await expect_command(sandbox, "rsync is installed",
196
+ "rsync --version 2>&1 | head -1", contains="rsync")
197
+ await expect_command(sandbox, "htop is installed",
198
+ "htop --version 2>&1 | head -1", contains="htop")
199
+ await expect_command(sandbox, "tree is installed",
200
+ "tree --version 2>&1", contains="tree")
201
+ print()
202
+
203
+ # ── 6. System libraries ──
204
+ bold("━━━ 6. System libraries ━━━\n")
205
+
206
+ await expect_command(sandbox, "sqlite3 is installed",
207
+ "sqlite3 --version 2>&1 | head -1")
208
+ await expect_command(sandbox, "libssl headers present",
209
+ "test -f /usr/include/openssl/ssl.h && echo ok",
210
+ contains="ok")
211
+ await expect_command(
212
+ sandbox, "libffi headers present",
213
+ "test -f /usr/include/ffi.h && echo ok "
214
+ "|| (dpkg -L libffi-dev 2>/dev/null | grep ffi.h | head -1)",
215
+ contains="ffi")
216
+ await expect_command(sandbox, "zlib headers present",
217
+ "test -f /usr/include/zlib.h && echo ok",
218
+ contains="ok")
219
+ print()
220
+
221
+ # ── 7. Locale ──
222
+ bold("━━━ 7. Locale ━━━\n")
223
+
224
+ await expect_command(sandbox, "en_US.UTF-8 locale is available",
225
+ "locale -a 2>&1 | grep -i en_US.utf8",
226
+ contains="en_US")
227
+ print()
228
+
229
+ # ── 8. Workspace ──
230
+ bold("━━━ 8. Workspace directory ━━━\n")
231
+
232
+ await expect_command(sandbox, "/workspace exists",
233
+ "test -d /workspace && echo ok", contains="ok")
234
+ print()
235
+
236
+ except Exception as e:
237
+ red(f"Fatal error: {e}")
238
+ failed += 1
239
+ finally:
240
+ if sandbox:
241
+ await sandbox.kill()
242
+ green("Sandbox killed")
243
+
244
+ # --- Summary ---
245
+ bold("========================================")
246
+ bold(f" Results: {passed} passed, {failed} failed")
247
+ bold("========================================\n")
248
+ if failed > 0:
249
+ sys.exit(1)
250
+
251
+
252
+ if __name__ == "__main__":
253
+ asyncio.run(main())
@@ -184,6 +184,126 @@ class Sandbox:
184
184
  pty_key = self._token or self._api_key
185
185
  return Pty(self._ops_client, self.sandbox_id, pty_url, pty_key)
186
186
 
187
+ async def create_checkpoint(self, name: str) -> dict:
188
+ """Create a named checkpoint of the running sandbox.
189
+
190
+ Args:
191
+ name: A unique name for this checkpoint.
192
+
193
+ Returns:
194
+ Checkpoint info dict with id, sandboxId, name, status, etc.
195
+ """
196
+ resp = await self._client.post(
197
+ f"/sandboxes/{self.sandbox_id}/checkpoints",
198
+ json={"name": name},
199
+ )
200
+ resp.raise_for_status()
201
+ return resp.json()
202
+
203
+ async def list_checkpoints(self) -> list[dict]:
204
+ """List all checkpoints for this sandbox."""
205
+ resp = await self._client.get(f"/sandboxes/{self.sandbox_id}/checkpoints")
206
+ resp.raise_for_status()
207
+ return resp.json()
208
+
209
+ async def restore_checkpoint(self, checkpoint_id: str) -> None:
210
+ """Restore the sandbox to a previous checkpoint (in-place revert).
211
+
212
+ The VM is rebooted from the checkpoint's drives. After restore,
213
+ internal clients are refreshed automatically.
214
+
215
+ Args:
216
+ checkpoint_id: UUID of the checkpoint to restore.
217
+ """
218
+ resp = await self._client.post(
219
+ f"/sandboxes/{self.sandbox_id}/checkpoints/{checkpoint_id}/restore",
220
+ )
221
+ resp.raise_for_status()
222
+
223
+ # Refresh connection info since the VM was rebooted
224
+ info = await self._client.get(f"/sandboxes/{self.sandbox_id}")
225
+ info.raise_for_status()
226
+ data = info.json()
227
+ self._connect_url = data.get("connectURL", "")
228
+ self._token = data.get("token", "")
229
+ if self._connect_url and self._token:
230
+ if self._data_client is not None:
231
+ await self._data_client.aclose()
232
+ self._data_client = httpx.AsyncClient(
233
+ base_url=self._connect_url,
234
+ headers={"Authorization": f"Bearer {self._token}"},
235
+ timeout=30.0,
236
+ )
237
+
238
+ @classmethod
239
+ async def create_from_checkpoint(
240
+ cls,
241
+ checkpoint_id: str,
242
+ timeout: int = 300,
243
+ api_key: str | None = None,
244
+ api_url: str | None = None,
245
+ ) -> Sandbox:
246
+ """Create a new sandbox from an existing checkpoint (fork).
247
+
248
+ Args:
249
+ checkpoint_id: UUID of the checkpoint to fork from.
250
+ timeout: Sandbox timeout in seconds (default 300).
251
+ api_key: API key (or OPENCOMPUTER_API_KEY env var).
252
+ api_url: API URL (or OPENCOMPUTER_API_URL env var).
253
+ """
254
+ url = api_url or os.environ.get("OPENCOMPUTER_API_URL", "https://app.opencomputer.dev")
255
+ url = url.rstrip("/")
256
+ key = api_key or os.environ.get("OPENCOMPUTER_API_KEY", "")
257
+
258
+ api_base = url if url.endswith("/api") else f"{url}/api"
259
+
260
+ headers = {}
261
+ if key:
262
+ headers["X-API-Key"] = key
263
+
264
+ client = httpx.AsyncClient(base_url=api_base, headers=headers, timeout=120.0)
265
+
266
+ resp = await client.post(
267
+ f"/sandboxes/from-checkpoint/{checkpoint_id}",
268
+ json={"timeout": timeout},
269
+ )
270
+ resp.raise_for_status()
271
+ data = resp.json()
272
+
273
+ connect_url = data.get("connectURL", "")
274
+ token = data.get("token", "")
275
+
276
+ data_client = None
277
+ if connect_url and token:
278
+ data_client = httpx.AsyncClient(
279
+ base_url=connect_url,
280
+ headers={"Authorization": f"Bearer {token}"},
281
+ timeout=30.0,
282
+ )
283
+
284
+ return cls(
285
+ sandbox_id=data["sandboxID"],
286
+ status=data.get("status", "running"),
287
+ _api_url=url,
288
+ _api_key=key,
289
+ _connect_url=connect_url,
290
+ _token=token,
291
+ _client=client,
292
+ _data_client=data_client,
293
+ )
294
+
295
+ async def delete_checkpoint(self, checkpoint_id: str) -> None:
296
+ """Delete a checkpoint.
297
+
298
+ Args:
299
+ checkpoint_id: UUID of the checkpoint to delete.
300
+ """
301
+ resp = await self._client.delete(
302
+ f"/sandboxes/{self.sandbox_id}/checkpoints/{checkpoint_id}",
303
+ )
304
+ if resp.status_code != 404:
305
+ resp.raise_for_status()
306
+
187
307
  async def create_preview_url(self, port: int, domain: str | None = None, auth_config: dict | None = None) -> dict:
188
308
  """Create a preview URL targeting a specific container port.
189
309
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "opencomputer-sdk"
7
- version = "0.4.4"
7
+ version = "0.4.5"
8
8
  description = "Python SDK for OpenComputer - cloud sandbox platform"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"