opencomputer-sdk 0.5.2__tar.gz → 0.5.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 (39) hide show
  1. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/.gitignore +4 -0
  2. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/PKG-INFO +1 -1
  3. opencomputer_sdk-0.5.4/examples/stream_demo.py +101 -0
  4. opencomputer_sdk-0.5.4/examples/test_disk_size.py +102 -0
  5. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_exec.py +2 -1
  6. opencomputer_sdk-0.5.4/examples/test_shell.py +222 -0
  7. opencomputer_sdk-0.5.4/opencomputer/__init__.py +60 -0
  8. opencomputer_sdk-0.5.4/opencomputer/exec.py +380 -0
  9. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/sandbox.py +246 -5
  10. opencomputer_sdk-0.5.4/opencomputer/shell.py +275 -0
  11. opencomputer_sdk-0.5.4/opencomputer/usage.py +292 -0
  12. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/pyproject.toml +1 -1
  13. opencomputer_sdk-0.5.2/opencomputer/__init__.py +0 -31
  14. opencomputer_sdk-0.5.2/opencomputer/exec.py +0 -136
  15. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/README.md +0 -0
  16. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/run_all_tests.py +0 -0
  17. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_commands.py +0 -0
  18. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_concurrent.py +0 -0
  19. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_declarative_images.py +0 -0
  20. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_default_template.py +0 -0
  21. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_domain_tls.py +0 -0
  22. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_environment.py +0 -0
  23. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_file_ops.py +0 -0
  24. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_large_files.py +0 -0
  25. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_multi_template.py +0 -0
  26. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_python_sdk.py +0 -0
  27. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_reconnect.py +0 -0
  28. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_secret_store_fork.py +0 -0
  29. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_secretstore.py +0 -0
  30. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_timeout.py +0 -0
  31. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/agent.py +0 -0
  32. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/commands.py +0 -0
  33. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/filesystem.py +0 -0
  34. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/image.py +0 -0
  35. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/project.py +0 -0
  36. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/pty.py +0 -0
  37. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/snapshot.py +0 -0
  38. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/sse.py +0 -0
  39. {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/template.py +0 -0
@@ -10,6 +10,10 @@ Thumbs.db
10
10
  .idea/
11
11
  .vscode/
12
12
 
13
+ # Claude Code session state — never check in
14
+ .claude/*.lock
15
+ .claude/settings.local.json
16
+
13
17
  # Environment files
14
18
  .env
15
19
  .env.*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencomputer-sdk
3
- Version: 0.5.2
3
+ Version: 0.5.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
@@ -0,0 +1,101 @@
1
+ """stream_demo.py — simulates apt-install-style output over ~6 seconds
2
+ via shell.run(), printing each chunk with its arrival timestamp so you
3
+ can see exactly how streaming feels in practice.
4
+
5
+ Usage:
6
+ python sdks/python/examples/stream_demo.py
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import os
13
+ import time
14
+
15
+ from opencomputer import Sandbox
16
+
17
+ API_URL = os.environ.get("OPENCOMPUTER_API_URL", "https://app.opencomputer.dev")
18
+ API_KEY = os.environ.get("OPENCOMPUTER_API_KEY", "opensandbox-dev")
19
+
20
+ # Pseudo-apt output. Uses /bin/echo (external, flushes on exit) rather
21
+ # than bash builtins so glibc block-buffering doesn't hide the streaming.
22
+ APT_SIM = r"""
23
+ /bin/echo "Reading package lists..."
24
+ sleep 0.3
25
+ /bin/echo "Building dependency tree..."
26
+ sleep 0.2
27
+ /bin/echo "Reading state information..."
28
+ sleep 0.4
29
+ /bin/echo "The following NEW packages will be installed:"
30
+ /bin/echo " libfoo1 libfoo-dev pkg-a pkg-b pkg-c"
31
+ sleep 0.3
32
+ /bin/echo "0 upgraded, 5 newly installed, 0 to remove and 0 not upgraded."
33
+ /bin/echo "Need to get 4,321 kB of archives."
34
+ /bin/echo "After this operation, 12.3 MB of additional disk space will be used."
35
+ sleep 0.4
36
+ for i in 1 2 3 4 5; do
37
+ /bin/echo "Get:$i http://deb.example.com/debian bookworm/main amd64 pkg-$i"
38
+ sleep 0.2
39
+ done
40
+ /bin/echo "Fetched 4,321 kB in 1s (4,321 kB/s)"
41
+ sleep 0.3
42
+ for i in 1 2 3 4 5; do
43
+ /bin/echo "Selecting previously unselected package pkg-$i."
44
+ /bin/echo "Unpacking pkg-$i (1.0-$i) ..."
45
+ sleep 0.15
46
+ /bin/echo "Setting up pkg-$i (1.0-$i) ..."
47
+ sleep 0.15
48
+ done
49
+ /bin/echo "W: Could not fetch http://example.com/deprecated — ignoring" >&2
50
+ /bin/echo "Processing triggers for libc-bin (2.36-9+deb12u3) ..."
51
+ sleep 0.4
52
+ /bin/echo "Processing triggers for man-db (2.11.2-2) ..."
53
+ sleep 0.5
54
+ /bin/echo "done."
55
+ """
56
+
57
+
58
+ async def main() -> None:
59
+ print(f"API: {API_URL}")
60
+ sb = await Sandbox.create(api_url=API_URL, api_key=API_KEY, template="base")
61
+ print(f"sandbox: {sb.sandbox_id}")
62
+
63
+ t0 = time.monotonic()
64
+ out_count = 0
65
+ err_count = 0
66
+ last_t = t0
67
+
68
+ try:
69
+ sh = await sb.exec.shell()
70
+
71
+ def on_out(b: bytes) -> None:
72
+ nonlocal out_count, last_t
73
+ out_count += 1
74
+ now = time.monotonic()
75
+ gap = now - last_t
76
+ last_t = now
77
+ # Show dt-since-start and gap-since-last-chunk.
78
+ print(f"[+{now - t0:5.2f}s Δ{gap:4.2f}s OUT] {b.decode().rstrip()}")
79
+
80
+ def on_err(b: bytes) -> None:
81
+ nonlocal err_count, last_t
82
+ err_count += 1
83
+ now = time.monotonic()
84
+ gap = now - last_t
85
+ last_t = now
86
+ print(f"[+{now - t0:5.2f}s Δ{gap:4.2f}s ERR] {b.decode().rstrip()}")
87
+
88
+ print("--- starting simulated apt-install ---")
89
+ r = await sh.run(APT_SIM, on_stdout=on_out, on_stderr=on_err)
90
+ total = time.monotonic() - t0
91
+ print("--- done ---")
92
+ print(f"exit={r.exit_code} total_wall={total:.2f}s "
93
+ f"stdout_chunks={out_count} stderr_chunks={err_count}")
94
+ print(f"stdout_bytes={len(r.stdout)} stderr_bytes={len(r.stderr)}")
95
+ await sh.close()
96
+ finally:
97
+ await sb.kill()
98
+
99
+
100
+ if __name__ == "__main__":
101
+ asyncio.run(main())
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Disk Size Test
4
+
5
+ Verifies the `disk_mb` sandbox creation parameter:
6
+ 1. Default (no arg) → 20GB workspace
7
+ 2. Explicit 20GB → 20GB workspace
8
+ 3. Explicit 30GB → 30GB workspace (requires org `max_disk_mb` >= 30720)
9
+ 4. Below minimum → rejected with 400
10
+
11
+ Larger disk sizes are in closed beta — contact us to raise your org's
12
+ max_disk_mb ceiling: https://cal.com/team/digger/opencomputer-founder-chat
13
+
14
+ Usage:
15
+ python examples/test_disk_size.py
16
+
17
+ Environment:
18
+ OPENCOMPUTER_API_URL (default: http://localhost:8080)
19
+ OPENCOMPUTER_API_KEY (default: test-key)
20
+ """
21
+
22
+ import asyncio
23
+ import os
24
+ import sys
25
+
26
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
27
+ from opencomputer import Sandbox
28
+
29
+ API_URL = os.environ.get("OPENCOMPUTER_API_URL", "http://localhost:8080")
30
+ API_KEY = os.environ.get("OPENCOMPUTER_API_KEY", "test-key")
31
+
32
+ GREEN = "\033[32m"
33
+ RED = "\033[31m"
34
+ BOLD = "\033[1m"
35
+ DIM = "\033[2m"
36
+ RESET = "\033[0m"
37
+
38
+ passed = 0
39
+ failed = 0
40
+
41
+
42
+ def green(msg: str) -> None:
43
+ print(f"{GREEN}✓ {msg}{RESET}")
44
+
45
+
46
+ def red(msg: str) -> None:
47
+ print(f"{RED}✗ {msg}{RESET}")
48
+
49
+
50
+ def bold(msg: str) -> None:
51
+ print(f"{BOLD}{msg}{RESET}")
52
+
53
+
54
+ def dim(msg: str) -> None:
55
+ print(f"{DIM} {msg}{RESET}")
56
+
57
+
58
+ async def run_case(label: str, disk_mb: int | None, expect_size: str | None, expect_reject: bool = False) -> None:
59
+ global passed, failed
60
+ bold(f"\n{label}")
61
+ kwargs = dict(api_url=API_URL, api_key=API_KEY, timeout=120)
62
+ if disk_mb is not None:
63
+ kwargs["disk_mb"] = disk_mb
64
+ try:
65
+ async with await Sandbox.create(**kwargs) as sb:
66
+ dim(f"created {sb.sandbox_id}")
67
+ r = await sb.exec.run("df -h /home/sandbox | tail -1")
68
+ dim(f"df: {r.stdout.strip()}")
69
+ if expect_reject:
70
+ red("expected rejection but sandbox was created")
71
+ failed += 1
72
+ elif expect_size and expect_size in r.stdout:
73
+ green(f"workspace reports {expect_size}")
74
+ passed += 1
75
+ else:
76
+ red(f"expected {expect_size} in df output")
77
+ failed += 1
78
+ except Exception as e:
79
+ if expect_reject:
80
+ green(f"rejected as expected ({str(e).splitlines()[0]})")
81
+ passed += 1
82
+ else:
83
+ red(f"unexpected error: {e}")
84
+ failed += 1
85
+
86
+
87
+ async def main() -> None:
88
+ bold("OpenComputer SDK — disk size test")
89
+ dim(f"API: {API_URL}")
90
+
91
+ await run_case("default (no disk_mb)", None, "20G")
92
+ await run_case("explicit 20GB", 20480, "20G")
93
+ await run_case("30GB", 30720, "30G")
94
+ await run_case("below minimum (8GB)", 8192, None, expect_reject=True)
95
+
96
+ print()
97
+ bold(f"Results: {passed} passed, {failed} failed")
98
+ sys.exit(1 if failed > 0 else 0)
99
+
100
+
101
+ if __name__ == "__main__":
102
+ asyncio.run(main())
@@ -86,7 +86,8 @@ async def main():
86
86
 
87
87
  # 8. exec.start() + list + kill
88
88
  print("\n--- 8. exec.start('sleep 60') + list + kill ---")
89
- session_id = await sandbox.exec.start("sleep", args=["60"])
89
+ session = await sandbox.exec.start("sleep", args=["60"])
90
+ session_id = session.session_id
90
91
  print(f" session: {session_id}")
91
92
 
92
93
  sessions = await sandbox.exec.list()
@@ -0,0 +1,222 @@
1
+ """test_shell.py — End-to-end test for exec.shell() (stateful shell sessions).
2
+
3
+ Usage:
4
+ cd sdks/python
5
+ python examples/test_shell.py
6
+
7
+ Environment:
8
+ OPENCOMPUTER_API_URL (default: http://localhost:8080)
9
+ OPENCOMPUTER_API_KEY (default: opensandbox-dev)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import os
16
+
17
+ from opencomputer import Sandbox, ShellBusyError, ShellClosedError
18
+
19
+
20
+ API_URL = os.environ.get("OPENCOMPUTER_API_URL", "http://localhost:8080")
21
+ API_KEY = os.environ.get("OPENCOMPUTER_API_KEY", "opensandbox-dev")
22
+
23
+ passed = 0
24
+ failed = 0
25
+
26
+
27
+ def check(cond: bool, msg: str) -> None:
28
+ global passed, failed
29
+ if cond:
30
+ passed += 1
31
+ print(f" ✓ {msg}")
32
+ else:
33
+ failed += 1
34
+ print(f" ✗ {msg}")
35
+
36
+
37
+ async def main() -> None:
38
+ print("=== OpenSandbox Python Shell API Test ===\n")
39
+ print(f"API: {API_URL}")
40
+
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. basic run + exit code
48
+ print("\n--- 2. shell.run('echo hello') ---")
49
+ sh = await sandbox.exec.shell()
50
+ r1 = await sh.run("echo hello")
51
+ print(f' stdout: "{r1.stdout.strip()}" exit={r1.exit_code}')
52
+ check(r1.exit_code == 0, "exit 0")
53
+ check(r1.stdout.strip() == "hello", "stdout matches")
54
+ check(r1.stderr == "", "stderr empty")
55
+
56
+ # 3. cwd persists
57
+ print("\n--- 3. cwd persists across run() calls ---")
58
+ await sh.run("cd /tmp")
59
+ pwd = await sh.run("pwd")
60
+ print(f' pwd: "{pwd.stdout.strip()}"')
61
+ check(pwd.stdout.strip() == "/tmp", "cwd persisted")
62
+
63
+ # 4. exported env persists
64
+ print("\n--- 4. exported env persists ---")
65
+ await sh.run("export MY_SHELL_VAR=persistence-works")
66
+ env_r = await sh.run("echo $MY_SHELL_VAR")
67
+ print(f' echo: "{env_r.stdout.strip()}"')
68
+ check(env_r.stdout.strip() == "persistence-works", "env persisted")
69
+
70
+ # 5. non-zero exit — subshell so it doesn't kill the outer shell
71
+ print("\n--- 5. non-zero exit code ---")
72
+ r_fail = await sh.run("( exit 7 )")
73
+ print(f" exit: {r_fail.exit_code}")
74
+ check(r_fail.exit_code == 7, f"exit 7 (got {r_fail.exit_code})")
75
+ r_fail2 = await sh.run("false && echo nope")
76
+ check(r_fail2.exit_code == 1, f"exit 1 from false (got {r_fail2.exit_code})")
77
+
78
+ # 6. stderr vs stdout separation
79
+ print("\n--- 6. stderr separated from stdout ---")
80
+ r_err = await sh.run("echo to-out; echo to-err >&2")
81
+ print(f' stdout="{r_err.stdout.strip()}" stderr="{r_err.stderr.strip()}"')
82
+ check(r_err.stdout.strip() == "to-out", "stdout is to-out")
83
+ check(r_err.stderr.strip() == "to-err", "stderr is to-err")
84
+ check("__OC_" not in r_err.stderr, "sentinel token hidden from returned stderr")
85
+
86
+ # 7. streaming callbacks — print live so interleaving is visible,
87
+ # then assert chunks were spaced out in time (not buffered to the end).
88
+ print("\n--- 7. streaming callbacks ---")
89
+ import time
90
+ out_chunks: list[bytes] = []
91
+ err_chunks: list[bytes] = []
92
+ out_times: list[float] = []
93
+ err_times: list[float] = []
94
+ t0 = time.monotonic()
95
+
96
+ def on_out(b: bytes) -> None:
97
+ out_chunks.append(b)
98
+ out_times.append(time.monotonic())
99
+ print(f" [+{time.monotonic() - t0:5.2f}s OUT] {b!r}")
100
+
101
+ def on_err(b: bytes) -> None:
102
+ err_chunks.append(b)
103
+ err_times.append(time.monotonic())
104
+ print(f" [+{time.monotonic() - t0:5.2f}s ERR] {b!r}")
105
+
106
+ # Use /bin/echo (external) rather than bash's builtin — the builtin
107
+ # goes through glibc stdio which block-buffers when stdout is a pipe,
108
+ # so output lands in one chunk at the end. Each /bin/echo is a fresh
109
+ # process that flushes on exit, making the streaming observable.
110
+ r_stream = await sh.run(
111
+ "for i in 1 2 3; do /bin/echo stdout-$i; /bin/echo stderr-$i >&2; sleep 0.2; done",
112
+ on_stdout=on_out,
113
+ on_stderr=on_err,
114
+ )
115
+ joined_out = b"".join(out_chunks).decode()
116
+ joined_err = b"".join(err_chunks).decode()
117
+ check("stdout-1" in joined_out and "stdout-3" in joined_out, "stdout stream callbacks fired")
118
+ check("stderr-1" in joined_err and "stderr-3" in joined_err, "stderr stream callbacks fired")
119
+ check("__OC_" not in joined_err, "sentinel token hidden from on_stderr")
120
+ check(r_stream.exit_code == 0, "streaming run exits 0")
121
+ # Command sleeps 0.2s × 3 = 0.6s. Ideally chunks stream as they are
122
+ # produced. In practice, frames may be coalesced by upstream proxies
123
+ # (CF/ALB) or gRPC flow control, so a single-chunk delivery is legal
124
+ # — we log the span for visibility but don't fail on it.
125
+ stdout_span = (out_times[-1] - out_times[0]) if len(out_times) >= 2 else 0.0
126
+ print(f" (stdout span across {len(out_times)} chunks: {stdout_span:.2f}s)")
127
+
128
+ # 8. concurrent run rejects
129
+ print("\n--- 8. concurrent run rejects ---")
130
+ slow_task = asyncio.create_task(sh.run("sleep 0.3; echo done"))
131
+ await asyncio.sleep(0.05)
132
+ busy_err: Exception | None = None
133
+ try:
134
+ await sh.run("echo should-not-run")
135
+ except Exception as e:
136
+ busy_err = e
137
+ check(isinstance(busy_err, ShellBusyError), "got ShellBusyError on concurrent run")
138
+ slow_r = await slow_task
139
+ check(slow_r.stdout.strip() == "done", "original run still completes")
140
+
141
+ # 9. shell functions persist
142
+ print("\n--- 9. shell functions persist ---")
143
+ await sh.run("greet() { echo hi-$1; }")
144
+ fn_r = await sh.run("greet world")
145
+ check(fn_r.stdout.strip() == "hi-world", "defined function callable in later run")
146
+
147
+ # 10. close
148
+ print("\n--- 10. close ---")
149
+ await sh.close()
150
+ closed_err: Exception | None = None
151
+ try:
152
+ await sh.run("echo after-close")
153
+ except Exception as e:
154
+ closed_err = e
155
+ check(isinstance(closed_err, ShellClosedError), "run after close rejects with ShellClosedError")
156
+
157
+ # 11. shell with cwd/env at construction
158
+ print("\n--- 11. shell(cwd=..., env=...) initial state ---")
159
+ sh2 = await sandbox.exec.shell(cwd="/etc", env={"SHELL_INIT_VAR": "from-init"})
160
+ r11a = await sh2.run("pwd")
161
+ check(r11a.stdout.strip() == "/etc", "initial cwd honored")
162
+ r11b = await sh2.run("echo $SHELL_INIT_VAR")
163
+ check(r11b.stdout.strip() == "from-init", "initial env honored")
164
+ await sh2.close()
165
+
166
+ # 12. terminal-tab semantic: `exit` in user command closes the shell
167
+ print("\n--- 12. exit N closes the shell (terminal-tab semantic) ---")
168
+ sh_exit = await sandbox.exec.shell()
169
+ exit_err: Exception | None = None
170
+ try:
171
+ await sh_exit.run("exit 42")
172
+ except Exception as e:
173
+ exit_err = e
174
+ check(isinstance(exit_err, ShellClosedError), "exit 42 rejects the pending run with ShellClosedError")
175
+ after_exit_err: Exception | None = None
176
+ try:
177
+ await sh_exit.run("echo after")
178
+ except Exception as e:
179
+ after_exit_err = e
180
+ check(isinstance(after_exit_err, ShellClosedError), "subsequent run rejects once shell is closed")
181
+
182
+ # 13. reattach to an open shell by sessionId
183
+ print("\n--- 13. reattach_shell revisits an open shell ---")
184
+ sh_a = await sandbox.exec.shell()
185
+ await sh_a.run("cd /tmp")
186
+ await sh_a.run("export REATTACH_VAR=round-trip")
187
+ reattach_id = sh_a.session_id
188
+ # Drop the reference without closing — server-side bash keeps running.
189
+ sh_b = await sandbox.exec.reattach_shell(reattach_id)
190
+ check(sh_b.session_id == reattach_id, "reattached shell has the same sessionId")
191
+ r_pwd = await sh_b.run("pwd")
192
+ check(r_pwd.stdout.strip() == "/tmp", f'reattach preserves cwd (got "{r_pwd.stdout.strip()}")')
193
+ r_env2 = await sh_b.run("echo $REATTACH_VAR")
194
+ check(r_env2.stdout.strip() == "round-trip", f'reattach preserves env (got "{r_env2.stdout.strip()}")')
195
+ await sh_b.close()
196
+
197
+ # 14. exec.background alias
198
+ print("\n--- 14. exec.background alias ---")
199
+ bg_exit = {"code": -999}
200
+
201
+ def on_bg_exit(code: int) -> None:
202
+ bg_exit["code"] = code
203
+
204
+ bg_sess = await sandbox.exec.background(
205
+ "sh", args=["-c", "echo bg-ok; sleep 0.2"], on_exit=on_bg_exit
206
+ )
207
+ await bg_sess.done
208
+ await bg_sess.close()
209
+ check(bg_exit["code"] == 0, f"exec.background returns exit code (got {bg_exit['code']})")
210
+
211
+ finally:
212
+ print("\n--- Killing sandbox ---")
213
+ await sandbox.kill()
214
+ check(sandbox.status == "stopped", "sandbox stopped")
215
+
216
+ print(f"\n=== Results: {passed} passed, {failed} failed ===")
217
+ if failed > 0:
218
+ raise SystemExit(1)
219
+
220
+
221
+ if __name__ == "__main__":
222
+ asyncio.run(main())
@@ -0,0 +1,60 @@
1
+ """OpenComputer Python SDK - cloud sandbox platform."""
2
+
3
+ from opencomputer.sandbox import Sandbox, ScalingLockedError, PlanLimitError
4
+ from opencomputer.agent import Agent, AgentEvent, AgentSession, AgentSessionInfo
5
+ from opencomputer.filesystem import Filesystem
6
+ from opencomputer.exec import Exec, ProcessResult, ExecSession, ExecSessionInfo
7
+ from opencomputer.image import Image
8
+ from opencomputer.pty import Pty, PtySession
9
+ from opencomputer.shell import Shell, ShellBusyError, ShellClosedError
10
+ from opencomputer.template import Template
11
+ from opencomputer.project import SecretStore
12
+ from opencomputer.snapshot import Snapshots
13
+ from opencomputer.usage import (
14
+ Usage,
15
+ Tags,
16
+ UsageSandboxItem,
17
+ UsageTagItem,
18
+ UsageTotals,
19
+ UsageUntaggedBucket,
20
+ UsageBySandboxResponse,
21
+ UsageByTagResponse,
22
+ SandboxUsageResponse,
23
+ TagKeyInfo,
24
+ )
25
+
26
+ __all__ = [
27
+ "Sandbox",
28
+ "ScalingLockedError",
29
+ "PlanLimitError",
30
+ "Agent",
31
+ "AgentEvent",
32
+ "AgentSession",
33
+ "AgentSessionInfo",
34
+ "Filesystem",
35
+ "Exec",
36
+ "ProcessResult",
37
+ "ExecSession",
38
+ "ExecSessionInfo",
39
+ "Image",
40
+ "Pty",
41
+ "PtySession",
42
+ "Shell",
43
+ "ShellBusyError",
44
+ "ShellClosedError",
45
+ "Template",
46
+ "SecretStore",
47
+ "Snapshots",
48
+ "Usage",
49
+ "Tags",
50
+ "UsageSandboxItem",
51
+ "UsageTagItem",
52
+ "UsageTotals",
53
+ "UsageUntaggedBucket",
54
+ "UsageBySandboxResponse",
55
+ "UsageByTagResponse",
56
+ "SandboxUsageResponse",
57
+ "TagKeyInfo",
58
+ ]
59
+
60
+ __version__ = "0.5.4"