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.
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/.gitignore +4 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/PKG-INFO +1 -1
- opencomputer_sdk-0.5.4/examples/stream_demo.py +101 -0
- opencomputer_sdk-0.5.4/examples/test_disk_size.py +102 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_exec.py +2 -1
- opencomputer_sdk-0.5.4/examples/test_shell.py +222 -0
- opencomputer_sdk-0.5.4/opencomputer/__init__.py +60 -0
- opencomputer_sdk-0.5.4/opencomputer/exec.py +380 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/sandbox.py +246 -5
- opencomputer_sdk-0.5.4/opencomputer/shell.py +275 -0
- opencomputer_sdk-0.5.4/opencomputer/usage.py +292 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/pyproject.toml +1 -1
- opencomputer_sdk-0.5.2/opencomputer/__init__.py +0 -31
- opencomputer_sdk-0.5.2/opencomputer/exec.py +0 -136
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/README.md +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/run_all_tests.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_commands.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_concurrent.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_declarative_images.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_default_template.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_domain_tls.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_environment.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_file_ops.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_large_files.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_multi_template.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_python_sdk.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_reconnect.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_secret_store_fork.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_secretstore.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/examples/test_timeout.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/agent.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/commands.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/filesystem.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/image.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/project.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/pty.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/snapshot.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/sse.py +0 -0
- {opencomputer_sdk-0.5.2 → opencomputer_sdk-0.5.4}/opencomputer/template.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencomputer-sdk
|
|
3
|
-
Version: 0.5.
|
|
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
|
-
|
|
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"
|