opencomputer-sdk 0.4.5__tar.gz → 0.4.7__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 (29) hide show
  1. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/.gitignore +4 -0
  2. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/PKG-INFO +1 -1
  3. opencomputer_sdk-0.4.7/examples/test_declarative_images.py +337 -0
  4. opencomputer_sdk-0.4.7/examples/test_exec.py +138 -0
  5. opencomputer_sdk-0.4.7/opencomputer/__init__.py +27 -0
  6. opencomputer_sdk-0.4.7/opencomputer/agent.py +339 -0
  7. opencomputer_sdk-0.4.7/opencomputer/exec.py +136 -0
  8. opencomputer_sdk-0.4.7/opencomputer/image.py +168 -0
  9. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/opencomputer/pty.py +4 -0
  10. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/opencomputer/sandbox.py +169 -13
  11. opencomputer_sdk-0.4.7/opencomputer/snapshot.py +105 -0
  12. opencomputer_sdk-0.4.7/opencomputer/sse.py +73 -0
  13. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/pyproject.toml +1 -1
  14. opencomputer_sdk-0.4.5/opencomputer/__init__.py +0 -19
  15. opencomputer_sdk-0.4.5/opencomputer/template.py +0 -75
  16. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/README.md +0 -0
  17. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/examples/run_all_tests.py +0 -0
  18. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/examples/test_commands.py +0 -0
  19. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/examples/test_concurrent.py +0 -0
  20. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/examples/test_default_template.py +0 -0
  21. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/examples/test_domain_tls.py +0 -0
  22. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/examples/test_environment.py +0 -0
  23. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/examples/test_file_ops.py +0 -0
  24. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/examples/test_multi_template.py +0 -0
  25. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/examples/test_python_sdk.py +0 -0
  26. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/examples/test_reconnect.py +0 -0
  27. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/examples/test_timeout.py +0 -0
  28. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/opencomputer/commands.py +0 -0
  29. {opencomputer_sdk-0.4.5 → opencomputer_sdk-0.4.7}/opencomputer/filesystem.py +0 -0
@@ -49,6 +49,10 @@ deploy/terraform/.terraform.lock.hcl
49
49
  deploy/terraform/terraform.tfstate*
50
50
  deploy/terraform/terraform.tfvars
51
51
 
52
+ # Dev environment state
53
+ deploy/ec2/.dev-env-state*
54
+ deploy/ec2/*.pem
55
+
52
56
  # Temp files
53
57
  *.tmp
54
58
  *.log
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencomputer-sdk
3
- Version: 0.4.5
3
+ Version: 0.4.7
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,337 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Declarative Image Builder & Snapshots Test (Python SDK)
4
+
5
+ Tests both patterns for defining sandbox environments:
6
+
7
+ Pattern 1 — On-demand images (cached by content hash):
8
+ image = Image.base().apt_install(["curl"])
9
+ sandbox = await Sandbox.create(image=image)
10
+
11
+ Pattern 2 — Pre-built named snapshots:
12
+ await snapshots.create(name="my-env", image=image)
13
+ sandbox = await Sandbox.create(snapshot="my-env")
14
+
15
+ Usage:
16
+ python examples/test_declarative_images.py
17
+ """
18
+
19
+ import asyncio
20
+ import sys
21
+ import time
22
+
23
+ from opencomputer import Sandbox, Image, Snapshots
24
+
25
+ passed = 0
26
+ failed = 0
27
+
28
+
29
+ def green(msg: str) -> None:
30
+ print(f"\033[32m\u2713 {msg}\033[0m")
31
+
32
+
33
+ def red(msg: str) -> None:
34
+ print(f"\033[31m\u2717 {msg}\033[0m")
35
+
36
+
37
+ def bold(msg: str) -> None:
38
+ print(f"\033[1m{msg}\033[0m")
39
+
40
+
41
+ def dim(msg: str) -> None:
42
+ print(f"\033[2m {msg}\033[0m")
43
+
44
+
45
+ def check(desc: str, condition: bool, detail: str = "") -> None:
46
+ global passed, failed
47
+ if condition:
48
+ green(desc)
49
+ passed += 1
50
+ else:
51
+ red(f"{desc} ({detail})" if detail else desc)
52
+ failed += 1
53
+
54
+
55
+ async def main() -> None:
56
+ global passed, failed
57
+
58
+ bold("\n╔══════════════════════════════════════════════════════╗")
59
+ bold("║ Declarative Image Builder & Snapshots Test (Python) ║")
60
+ bold("╚══════════════════════════════════════════════════════════╝\n")
61
+
62
+ sandboxes: list[Sandbox] = []
63
+ snapshots = Snapshots()
64
+
65
+ try:
66
+ # ── Test: Image builder basics ──────────────────────────────
67
+ bold("━━━ Test: Image builder basics ━━━\n")
68
+
69
+ base = Image.base()
70
+ with_curl = base.apt_install(["curl"])
71
+ with_python = with_curl.pip_install(["requests"])
72
+
73
+ check("Image.base() uses default base", base.to_dict()["base"] == "base")
74
+ check("Image is immutable — apt_install returns new instance", len(base.to_dict()["steps"]) == 0)
75
+ check("Chained image has 1 step", len(with_curl.to_dict()["steps"]) == 1)
76
+ check("Further chained image has 2 steps", len(with_python.to_dict()["steps"]) == 2)
77
+
78
+ check("Different images have different cache keys", base.cache_key() != with_curl.cache_key())
79
+ check("Same image produces same cache key", base.cache_key() == Image.base().cache_key())
80
+
81
+ # File operations
82
+ with_file = base.add_file("/workspace/config.json", '{"key": "value"}')
83
+ check("add_file creates a step", len(with_file.to_dict()["steps"]) == 1)
84
+ check("add_file step type is correct", with_file.to_dict()["steps"][0]["type"] == "add_file")
85
+ print()
86
+
87
+ # ── Pattern 1: On-demand image creation ────────────────────
88
+ bold("━━━ Pattern 1: On-demand image (first build — cold) ━━━\n")
89
+
90
+ image = (
91
+ Image.base()
92
+ .apt_install(["curl", "jq"])
93
+ .run_commands("mkdir -p /workspace/project")
94
+ .env({"MY_VAR": "hello-from-image", "PROJECT_ROOT": "/workspace/project"})
95
+ .workdir("/workspace/project")
96
+ )
97
+
98
+ dim(f"Cache key: {image.cache_key()}")
99
+ dim("Creating sandbox from image (this will build on first run)...")
100
+
101
+ t1_start = time.time()
102
+ sandbox1 = await Sandbox.create(image=image, timeout=300)
103
+ t1_elapsed = int((time.time() - t1_start) * 1000)
104
+ sandboxes.append(sandbox1)
105
+
106
+ green(f"Sandbox created: {sandbox1.sandbox_id} ({t1_elapsed}ms)")
107
+ check("Sandbox created successfully", sandbox1.status in ("running", "creating"))
108
+ print()
109
+
110
+ # ── Verify installed packages ──────────────────────────────
111
+ bold("━━━ Verify: Packages installed ━━━\n")
112
+
113
+ curl_check = await sandbox1.commands.run("which curl")
114
+ check("curl is installed", curl_check.exit_code == 0, f"exit={curl_check.exit_code}")
115
+
116
+ jq_check = await sandbox1.commands.run("which jq")
117
+ check("jq is installed", jq_check.exit_code == 0, f"exit={jq_check.exit_code}")
118
+ print()
119
+
120
+ # ── Verify env vars ────────────────────────────────────────
121
+ bold("━━━ Verify: Environment variables ━━━\n")
122
+
123
+ env_check = await sandbox1.commands.run("bash -lc 'echo $MY_VAR'")
124
+ check("MY_VAR is set", env_check.stdout.strip() == "hello-from-image", f'got: "{env_check.stdout.strip()}"')
125
+
126
+ root_check = await sandbox1.commands.run("bash -lc 'echo $PROJECT_ROOT'")
127
+ check("PROJECT_ROOT is set", root_check.stdout.strip() == "/workspace/project", f'got: "{root_check.stdout.strip()}"')
128
+ print()
129
+
130
+ # ── Cache hit: second sandbox from same image ──────────────
131
+ bold("━━━ Pattern 1: Cache hit (second sandbox, same image) ━━━\n")
132
+
133
+ dim("Creating second sandbox from same image (should be cached)...")
134
+ t2_start = time.time()
135
+ sandbox2 = await Sandbox.create(image=image, timeout=300)
136
+ t2_elapsed = int((time.time() - t2_start) * 1000)
137
+ sandboxes.append(sandbox2)
138
+
139
+ green(f"Second sandbox created: {sandbox2.sandbox_id} ({t2_elapsed}ms)")
140
+
141
+ if t1_elapsed > 5000:
142
+ check(
143
+ f"Cache hit is faster ({t2_elapsed}ms vs {t1_elapsed}ms cold build)",
144
+ t2_elapsed < t1_elapsed,
145
+ f"cold={t1_elapsed}ms, cached={t2_elapsed}ms",
146
+ )
147
+ else:
148
+ dim(f"Cold build was fast ({t1_elapsed}ms) — skipping speedup check")
149
+
150
+ curl_check2 = await sandbox2.commands.run("which curl")
151
+ check("Cached sandbox also has curl", curl_check2.exit_code == 0)
152
+ print()
153
+
154
+ # Kill sandboxes before creating more
155
+ for sb in sandboxes:
156
+ try:
157
+ await sb.kill()
158
+ except Exception:
159
+ pass
160
+ sandboxes.clear()
161
+
162
+ # ── Pattern 2: Pre-built named snapshot ────────────────────
163
+ bold("━━━ Pattern 2: Create named snapshot ━━━\n")
164
+
165
+ snapshot_image = (
166
+ Image.base()
167
+ .run_commands(
168
+ "echo 'snapshot-marker' > /workspace/snapshot-test.txt",
169
+ "mkdir -p /workspace/data",
170
+ )
171
+ .env({"SNAPSHOT_ENV": "from-snapshot"})
172
+ )
173
+
174
+ dim("Creating snapshot 'test-env-py'...")
175
+ snapshot_info = await snapshots.create(name="test-env-py", image=snapshot_image)
176
+ green(f"Snapshot created: {snapshot_info['name']} (status={snapshot_info['status']})")
177
+ check("Snapshot status is ready", snapshot_info["status"] == "ready")
178
+ print()
179
+
180
+ # ── List snapshots ─────────────────────────────────────────
181
+ bold("━━━ Verify: List snapshots ━━━\n")
182
+
183
+ snapshot_list = await snapshots.list()
184
+ found = next((s for s in snapshot_list if s["name"] == "test-env-py"), None)
185
+ check("Snapshot appears in list", found is not None)
186
+ print()
187
+
188
+ # ── Get snapshot by name ───────────────────────────────────
189
+ bold("━━━ Verify: Get snapshot by name ━━━\n")
190
+
191
+ fetched = await snapshots.get("test-env-py")
192
+ check("Fetched snapshot matches", fetched["name"] == "test-env-py")
193
+ check("Fetched snapshot is ready", fetched["status"] == "ready")
194
+ print()
195
+
196
+ # ── Create sandbox from named snapshot ─────────────────────
197
+ bold("━━━ Pattern 2: Create sandbox from named snapshot ━━━\n")
198
+
199
+ dim("Creating sandbox from snapshot 'test-env-py'...")
200
+ sandbox3 = await Sandbox.create(snapshot="test-env-py", timeout=300)
201
+ sandboxes.append(sandbox3)
202
+
203
+ check("Sandbox created successfully", sandbox3.status in ("running", "creating"))
204
+
205
+ marker_check = await sandbox3.commands.run("cat /workspace/snapshot-test.txt")
206
+ check("Snapshot marker file exists", marker_check.stdout.strip() == "snapshot-marker", f'got: "{marker_check.stdout.strip()}"')
207
+
208
+ snapshot_env_check = await sandbox3.commands.run("bash -lc 'echo $SNAPSHOT_ENV'")
209
+ check("Snapshot env var is set", snapshot_env_check.stdout.strip() == "from-snapshot", f'got: "{snapshot_env_check.stdout.strip()}"')
210
+ print()
211
+
212
+ # ── Delete snapshot ────────────────────────────────────────
213
+ bold("━━━ Cleanup: Delete snapshot ━━━\n")
214
+
215
+ await snapshots.delete("test-env-py")
216
+ green("Snapshot 'test-env-py' deleted")
217
+
218
+ list_after = await snapshots.list()
219
+ deleted_found = next((s for s in list_after if s["name"] == "test-env-py"), None)
220
+ check("Snapshot no longer in list", deleted_found is None)
221
+ print()
222
+
223
+ # Kill sandbox before next test
224
+ for sb in sandboxes:
225
+ try:
226
+ await sb.kill()
227
+ except Exception:
228
+ pass
229
+ sandboxes.clear()
230
+
231
+ # ── Test: addFile step ─────────────────────────────────────
232
+ bold("━━━ Test: add_file — bake files into the image ━━━\n")
233
+
234
+ file_image = (
235
+ Image.base()
236
+ .add_file("/workspace/config.json", '{"env": "production", "debug": false}')
237
+ .add_file("/workspace/setup.sh", "#!/bin/bash\necho 'Hello from setup!'")
238
+ .run_commands("chmod +x /workspace/setup.sh")
239
+ )
240
+
241
+ dim("Creating sandbox with embedded files...")
242
+ sandbox4 = await Sandbox.create(image=file_image, timeout=300)
243
+ sandboxes.append(sandbox4)
244
+
245
+ config_check = await sandbox4.commands.run("cat /workspace/config.json")
246
+ check("add_file wrote config.json", '"env": "production"' in config_check.stdout, f'got: "{config_check.stdout.strip()}"')
247
+
248
+ setup_check = await sandbox4.commands.run("/workspace/setup.sh")
249
+ check("add_file wrote executable script", setup_check.stdout.strip() == "Hello from setup!", f'got: "{setup_check.stdout.strip()}"')
250
+ print()
251
+
252
+ # Kill sandbox before next test
253
+ for sb in sandboxes:
254
+ try:
255
+ await sb.kill()
256
+ except Exception:
257
+ pass
258
+ sandboxes.clear()
259
+
260
+ # ── Test: Build log streaming ──────────────────────────────
261
+ bold("━━━ Test: Build log streaming (on_build_log callback) ━━━\n")
262
+
263
+ log_image = Image.base().run_commands("echo 'build step 1'", "echo 'build step 2'")
264
+
265
+ build_logs: list[str] = []
266
+ dim("Creating sandbox with build log streaming...")
267
+ sandbox5 = await Sandbox.create(
268
+ image=log_image,
269
+ timeout=300,
270
+ on_build_log=lambda log: (build_logs.append(log), dim(f" build: {log}")),
271
+ )
272
+ sandboxes.append(sandbox5)
273
+
274
+ check("Build log callback was called", len(build_logs) > 0, f"got {len(build_logs)} logs")
275
+ check("Sandbox created via SSE stream", sandbox5.status in ("running", "creating"))
276
+ print()
277
+
278
+ # Kill sandbox before next test
279
+ for sb in sandboxes:
280
+ try:
281
+ await sb.kill()
282
+ except Exception:
283
+ pass
284
+ sandboxes.clear()
285
+
286
+ # ── Test: Snapshot build log streaming ──────────────────────
287
+ bold("━━━ Test: Snapshot build log streaming (on_build_logs) ━━━\n")
288
+
289
+ snapshot_log_image = Image.base().run_commands("echo 'snapshot build'")
290
+
291
+ snapshot_logs: list[str] = []
292
+ dim("Creating snapshot with build log streaming...")
293
+ streamed_snapshot = await snapshots.create(
294
+ name="test-streamed-py",
295
+ image=snapshot_log_image,
296
+ on_build_logs=lambda log: (snapshot_logs.append(log), dim(f" snapshot build: {log}")),
297
+ )
298
+
299
+ check("Snapshot build log callback was called", len(snapshot_logs) > 0, f"got {len(snapshot_logs)} logs")
300
+ check("Streamed snapshot has name", streamed_snapshot["name"] == "test-streamed-py")
301
+ check("Streamed snapshot is ready", streamed_snapshot["status"] == "ready")
302
+
303
+ await snapshots.delete("test-streamed-py")
304
+ green("Cleaned up test-streamed-py snapshot")
305
+ print()
306
+
307
+ except Exception as err:
308
+ red(f"Fatal error: {err}")
309
+ import traceback
310
+ traceback.print_exc()
311
+ failed += 1
312
+ finally:
313
+ bold("━━━ Cleanup ━━━\n")
314
+ for sb in sandboxes:
315
+ try:
316
+ await sb.kill()
317
+ dim(f"Killed {sb.sandbox_id}")
318
+ except Exception:
319
+ pass
320
+ try:
321
+ await snapshots.delete("test-env-py")
322
+ except Exception:
323
+ pass
324
+ try:
325
+ await snapshots.delete("test-streamed-py")
326
+ except Exception:
327
+ pass
328
+
329
+ bold("========================================")
330
+ bold(f" Results: {passed} passed, {failed} failed")
331
+ bold("========================================\n")
332
+ if failed > 0:
333
+ sys.exit(1)
334
+
335
+
336
+ if __name__ == "__main__":
337
+ asyncio.run(main())
@@ -0,0 +1,138 @@
1
+ """
2
+ test_exec.py — End-to-end test for the session-based exec API (Python SDK)
3
+
4
+ Usage:
5
+ cd sdks/python
6
+ pip install -e .
7
+ python examples/test_exec.py
8
+
9
+ Environment:
10
+ OPENCOMPUTER_API_URL (default: http://localhost:8080)
11
+ OPENCOMPUTER_API_KEY (default: opensandbox-dev)
12
+ """
13
+
14
+ import asyncio
15
+ import os
16
+
17
+ from opencomputer import Sandbox
18
+
19
+ API_URL = os.environ.get("OPENCOMPUTER_API_URL", "http://localhost:8080")
20
+ API_KEY = os.environ.get("OPENCOMPUTER_API_KEY", "opensandbox-dev")
21
+
22
+ passed = 0
23
+ failed = 0
24
+
25
+
26
+ def check(condition: bool, msg: str):
27
+ global passed, failed
28
+ if condition:
29
+ passed += 1
30
+ print(f" ✓ {msg}")
31
+ else:
32
+ failed += 1
33
+ print(f" ✗ {msg}")
34
+
35
+
36
+ async def main():
37
+ print("=== OpenSandbox Exec API Test (Python) ===\n")
38
+ print(f"API: {API_URL}")
39
+
40
+ # 1. Create sandbox
41
+ print("\n--- 1. Creating sandbox ---")
42
+ sandbox = await Sandbox.create(api_url=API_URL, api_key=API_KEY, template="base")
43
+ print(f" Sandbox: {sandbox.sandbox_id} ({sandbox.status})")
44
+ check(sandbox.status == "running", "sandbox is running")
45
+
46
+ try:
47
+ # 2. exec.run() — quick command
48
+ print("\n--- 2. exec.run('echo hello world') ---")
49
+ result = await sandbox.exec.run("echo hello world")
50
+ print(f' stdout: "{result.stdout.strip()}"')
51
+ check(result.exit_code == 0, f"exit code is 0 (got {result.exit_code})")
52
+ check(result.stdout.strip() == "hello world", "stdout matches")
53
+
54
+ # 3. exec.run() — ls
55
+ print("\n--- 3. exec.run('ls /') ---")
56
+ ls_result = await sandbox.exec.run("ls /")
57
+ check(ls_result.exit_code == 0, "exit code is 0")
58
+ check("usr" in ls_result.stdout, "stdout contains 'usr'")
59
+ check("bin" in ls_result.stdout, "stdout contains 'bin'")
60
+
61
+ # 4. exec.run() with env vars
62
+ print("\n--- 4. exec.run with env vars ---")
63
+ env_result = await sandbox.exec.run(
64
+ "echo $MY_VAR-$FOO", env={"MY_VAR": "hello", "FOO": "bar"}
65
+ )
66
+ print(f' output: "{env_result.stdout.strip()}"')
67
+ check(env_result.stdout.strip() == "hello-bar", "env vars passed correctly")
68
+
69
+ # 5. exec.run() with cwd
70
+ print("\n--- 5. exec.run('pwd') with cwd=/tmp ---")
71
+ cwd_result = await sandbox.exec.run("pwd", cwd="/tmp")
72
+ print(f' pwd: "{cwd_result.stdout.strip()}"')
73
+ check(cwd_result.stdout.strip() == "/tmp", "cwd is /tmp")
74
+
75
+ # 6. exec.run() — non-zero exit code
76
+ print("\n--- 6. exec.run('exit 42') ---")
77
+ fail_result = await sandbox.exec.run("exit 42")
78
+ print(f" exit: {fail_result.exit_code}")
79
+ check(fail_result.exit_code == 42, f"exit code is 42 (got {fail_result.exit_code})")
80
+
81
+ # 7. exec.run() — stderr
82
+ print("\n--- 7. exec.run stderr ---")
83
+ stderr_result = await sandbox.exec.run("echo error-msg >&2")
84
+ print(f' stderr: "{stderr_result.stderr.strip()}"')
85
+ check(stderr_result.stderr.strip() == "error-msg", "stderr captured")
86
+
87
+ # 8. exec.start() + list + kill
88
+ print("\n--- 8. exec.start('sleep 60') + list + kill ---")
89
+ session_id = await sandbox.exec.start("sleep", args=["60"])
90
+ print(f" session: {session_id}")
91
+
92
+ sessions = await sandbox.exec.list()
93
+ sleep_sessions = [s for s in sessions if s.session_id == session_id]
94
+ check(len(sleep_sessions) == 1, "session appears in list")
95
+ check(sleep_sessions[0].running, "session is running")
96
+
97
+ await sandbox.exec.kill(session_id)
98
+ print(" killed")
99
+ await asyncio.sleep(0.5)
100
+
101
+ sessions_after = await sandbox.exec.list()
102
+ killed = [s for s in sessions_after if s.session_id == session_id]
103
+ if killed:
104
+ check(not killed[0].running, "session is no longer running after kill")
105
+ else:
106
+ check(True, "session cleaned up after kill")
107
+
108
+ # 9. File write + read via exec
109
+ print("\n--- 9. Write file + cat ---")
110
+ await sandbox.files.write("/tmp/test.txt", "Hello from Python SDK!\n")
111
+ cat_result = await sandbox.exec.run("cat /tmp/test.txt")
112
+ print(f' cat: "{cat_result.stdout.strip()}"')
113
+ check(cat_result.stdout.strip() == "Hello from Python SDK!", "file content matches")
114
+
115
+ # 10. Multi-command script
116
+ print("\n--- 10. Multi-command script ---")
117
+ script_result = await sandbox.exec.run(
118
+ "echo hostname=$(hostname); echo user=$(whoami); echo arch=$(uname -m)"
119
+ )
120
+ print(f" {script_result.stdout.strip()}")
121
+ check("hostname=" in script_result.stdout, "has hostname")
122
+ check("user=" in script_result.stdout, "has user")
123
+
124
+ finally:
125
+ # 11. Cleanup
126
+ print("\n--- 11. Killing sandbox ---")
127
+ await sandbox.kill()
128
+ check(sandbox.status == "stopped", "sandbox stopped")
129
+ await sandbox.close()
130
+
131
+ # Summary
132
+ print(f"\n=== Results: {passed} passed, {failed} failed ===")
133
+ if failed > 0:
134
+ raise SystemExit(1)
135
+
136
+
137
+ if __name__ == "__main__":
138
+ asyncio.run(main())
@@ -0,0 +1,27 @@
1
+ """OpenComputer Python SDK - cloud sandbox platform."""
2
+
3
+ from opencomputer.sandbox import Sandbox
4
+ from opencomputer.agent import Agent, AgentEvent, AgentSession, AgentSessionInfo
5
+ from opencomputer.filesystem import Filesystem
6
+ from opencomputer.exec import Exec, ProcessResult, ExecSessionInfo
7
+ from opencomputer.image import Image
8
+ from opencomputer.pty import Pty, PtySession
9
+ from opencomputer.snapshot import Snapshots
10
+
11
+ __all__ = [
12
+ "Sandbox",
13
+ "Agent",
14
+ "AgentEvent",
15
+ "AgentSession",
16
+ "AgentSessionInfo",
17
+ "Filesystem",
18
+ "Exec",
19
+ "ProcessResult",
20
+ "ExecSessionInfo",
21
+ "Image",
22
+ "Pty",
23
+ "PtySession",
24
+ "Snapshots",
25
+ ]
26
+
27
+ __version__ = "0.5.0"