solari-desktop 0.1.1__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 (37) hide show
  1. solari_desktop-0.1.1/.gitignore +71 -0
  2. solari_desktop-0.1.1/PKG-INFO +162 -0
  3. solari_desktop-0.1.1/README.md +149 -0
  4. solari_desktop-0.1.1/examples/quickstart.py +65 -0
  5. solari_desktop-0.1.1/examples/sandbox_quickstart.py +63 -0
  6. solari_desktop-0.1.1/pyproject.toml +26 -0
  7. solari_desktop-0.1.1/solari_desktop/__init__.py +145 -0
  8. solari_desktop-0.1.1/solari_desktop/_http.py +142 -0
  9. solari_desktop-0.1.1/solari_desktop/client.py +439 -0
  10. solari_desktop-0.1.1/solari_desktop/desktop.py +437 -0
  11. solari_desktop-0.1.1/solari_desktop/errors.py +148 -0
  12. solari_desktop-0.1.1/solari_desktop/handle.py +758 -0
  13. solari_desktop-0.1.1/solari_desktop/image.py +140 -0
  14. solari_desktop-0.1.1/solari_desktop/sandbox.py +34 -0
  15. solari_desktop-0.1.1/solari_desktop/sandbox_client.py +455 -0
  16. solari_desktop-0.1.1/solari_desktop/solari.py +99 -0
  17. solari_desktop-0.1.1/solari_desktop/template_client.py +203 -0
  18. solari_desktop-0.1.1/solari_desktop/transport.py +349 -0
  19. solari_desktop-0.1.1/solari_desktop/types.py +431 -0
  20. solari_desktop-0.1.1/solari_desktop/volume_client.py +115 -0
  21. solari_desktop-0.1.1/solari_desktop/ws.py +127 -0
  22. solari_desktop-0.1.1/tests/test_autopause.py +47 -0
  23. solari_desktop-0.1.1/tests/test_charts.py +50 -0
  24. solari_desktop-0.1.1/tests/test_commands.py +41 -0
  25. solari_desktop-0.1.1/tests/test_git.py +245 -0
  26. solari_desktop-0.1.1/tests/test_http.py +80 -0
  27. solari_desktop-0.1.1/tests/test_image.py +49 -0
  28. solari_desktop-0.1.1/tests/test_lifecycle.py +79 -0
  29. solari_desktop-0.1.1/tests/test_parity.py +53 -0
  30. solari_desktop-0.1.1/tests/test_preview.py +41 -0
  31. solari_desktop-0.1.1/tests/test_recording.py +170 -0
  32. solari_desktop-0.1.1/tests/test_smoke.py +60 -0
  33. solari_desktop-0.1.1/tests/test_templates.py +136 -0
  34. solari_desktop-0.1.1/tests/test_transport.py +195 -0
  35. solari_desktop-0.1.1/tests/test_volumes.py +102 -0
  36. solari_desktop-0.1.1/tests_live/test_desktop_live.py +300 -0
  37. solari_desktop-0.1.1/tests_live/test_sandbox_live.py +86 -0
@@ -0,0 +1,71 @@
1
+ # build artifacts
2
+ dist/
3
+ **/dist/
4
+ **/node_modules/
5
+ **/target/
6
+ *.log
7
+
8
+ # VM images / snapshots / kernels — large binaries, never committed
9
+ *.img
10
+ *.ext4
11
+ *.qcow2
12
+ *.raw
13
+ *.snapshot
14
+ /snapshots/build/
15
+ /guest/rootfs/build/
16
+ /guest/kernel/build/
17
+ vmlinux
18
+ bzImage
19
+
20
+ # terraform state
21
+ *.tfstate
22
+ *.tfstate.*
23
+ .terraform/
24
+ *.tfvars.local
25
+
26
+ # secrets / env
27
+ .env
28
+ .env.*
29
+ *.pem
30
+
31
+ # Python
32
+ __pycache__/
33
+ *.pyc
34
+ .venv/
35
+
36
+ # NOTE: tests/sdk/ts-request-fixtures.json is deliberately NOT ignored.
37
+ #
38
+ # It was, and that quietly disabled two gates at once:
39
+ #
40
+ # 1. ci.yml's "Committed fixture must match what the SDK emits today" step runs
41
+ # `git diff --exit-code tests/sdk/ts-request-fixtures.json`. On an ignored,
42
+ # never-tracked file that command can only ever succeed — it had never once
43
+ # been able to fail.
44
+ # 2. The Go/Rust/C++/Python contract suites now assert against that fixture from
45
+ # inside their OWN CI jobs, none of which run the TypeScript half. If the file
46
+ # is not committed, it is simply absent there.
47
+ #
48
+ # It is generated, but it is generated from the REFERENCE SDK and checked in as
49
+ # the baseline the other four bindings are held to — regenerating it and diffing
50
+ # is exactly how a wire change is made to announce itself.
51
+
52
+ # Built guest-agent binaries (produced by go build / build-rootfs staging)
53
+ guest/agent/agent
54
+ guest/rootfs/guest-agent
55
+
56
+ # build-rootfs.sh template staging + generated Dockerfile (regenerated each run)
57
+ guest/rootfs/guest-agent.service
58
+ guest/rootfs/template.packages
59
+ guest/rootfs/template-files/
60
+ guest/rootfs/.Dockerfile.gen
61
+ guest/rootfs/out/
62
+
63
+ # local test artifacts
64
+ staging-desktop.png
65
+ desktop.png
66
+ infra/terraform/snapshots-volumes/.build/
67
+
68
+ # deploy-e2e scratch state: holds a presigned REPO_URL with live STS creds
69
+ # (ASIA... + X-Amz-Security-Token). Never commit. Also stale PHASE_* here makes
70
+ # a bare re-run skip bake+verify and exit 0 with zero assertions.
71
+ infra/.deploy-e2e/
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: solari-desktop
3
+ Version: 0.1.1
4
+ Summary: Python SDK for Solari Desktop — managed, hardware-isolated Linux desktops with a computer-use action API.
5
+ Project-URL: Homepage, https://getsolari.com
6
+ Author: Solari
7
+ License: MIT
8
+ Keywords: automation,computer-use,desktop,solari,vnc
9
+ Requires-Python: >=3.9
10
+ Requires-Dist: httpx>=0.24
11
+ Requires-Dist: websockets>=11.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # solari-desktop (Python)
15
+
16
+ Python SDK for **Solari Desktop** — create managed, hardware-isolated Linux
17
+ desktops that boot in well under a second, embed the live VNC stream, and drive
18
+ them with a **computer-use** action API (exec, fs, mouse/keyboard, screenshot,
19
+ clipboard, processes).
20
+
21
+ This is a faithful port of the TypeScript `@solarisdk/sdk` package; the
22
+ class, method, and field names match it one-for-one (snake_case where TS uses
23
+ camelCase for method/parameter names; wire/response field names are kept as-is).
24
+
25
+ ```
26
+ SDK ──HTTPS──▶ Gateway (create / get / destroy sessions)
27
+ SDK ──WSS────▶ Gateway (control channel: computer-use JSON-RPC)
28
+ ──WSS────▶ (stream channel: RFB/VNC bytes, embed in a viewer)
29
+ ```
30
+
31
+ ## Install
32
+
33
+ ```sh
34
+ pip install solari-desktop
35
+ ```
36
+
37
+ Depends only on [`httpx`](https://www.python-httpx.org/) (HTTP) and
38
+ [`websockets`](https://websockets.readthedocs.io/) (control channel). Requires
39
+ Python 3.9+.
40
+
41
+ ## Usage
42
+
43
+ The primary API is **async** (matching the asyncio WebSocket control channel):
44
+
45
+ ```python
46
+ import asyncio
47
+ from solari_desktop import DesktopClient
48
+
49
+
50
+ async def main() -> None:
51
+ async with DesktopClient(
52
+ api_key="...",
53
+ base_url="https://api.getsolari.com",
54
+ ) as client:
55
+ # Create a session (assigned from the warm pool — typically sub-second).
56
+ desktop = await client.create(
57
+ template="default",
58
+ resolution="1280x720",
59
+ timeout_ms=1_800_000, # rolling idle window: auto-pause after 30 min idle
60
+ )
61
+
62
+ # Embed this URL in a VNC viewer to watch the live desktop.
63
+ print(desktop.streamUrl)
64
+
65
+ # Open the control channel, then drive the desktop.
66
+ await desktop.connect()
67
+
68
+ shot = await desktop.screenshot(format="png") # bytes
69
+ out = await desktop.exec("ls", args=["-la", "/home"])
70
+ print(out.exitCode, out.stdout)
71
+
72
+ await desktop.mouse.click(640, 360, humanize=True)
73
+ await desktop.keyboard.press(["ctrl", "c"])
74
+ await desktop.fs.write("/tmp/note.txt", "hello")
75
+ text = await desktop.fs.read_text("/tmp/note.txt")
76
+
77
+ # Public preview URL for an in-guest port (requires PREVIEW_DOMAIN).
78
+ preview = await desktop.preview_url(3000) # {"url": ..., "token"?: ...}
79
+ print(preview["url"])
80
+
81
+ # Pause (snapshot RAM+disk, free the slot) and resume later.
82
+ await desktop.pause()
83
+ await desktop.resume()
84
+
85
+ await desktop.close()
86
+ await client.destroy(desktop.sessionId)
87
+
88
+
89
+ asyncio.run(main())
90
+ ```
91
+
92
+ A thin synchronous wrapper, `SyncDesktopClient`, is available for the
93
+ session-lifecycle calls (`create`, `get`, `destroy`, `attach`) when you are not
94
+ running an event loop. The returned `Desktop` handle remains async.
95
+
96
+ ## Client API (`DesktopClient`)
97
+
98
+ | Method | Description |
99
+ |---|---|
100
+ | `DesktopClient(api_key=, base_url=, http=None, call_timeout_ms=None)` | Construct a client. |
101
+ | `await create(template="default", ttl_seconds=None, timeout_ms=None, resolution=None, metadata=None, record=None, lifecycle=None)` → `Desktop` | `POST /desktops`. `timeout_ms` sets a rolling idle window; `lifecycle` controls idle behaviour (default auto-pause). |
102
+ | `await get(session_id)` | `GET /desktops/:id` → `GetDesktopResponse(sessionId, status, expiresAt)`. |
103
+ | `await destroy(session_id)` | `DELETE /desktops/:id` → `DeleteDesktopResponse(ok=True)` (idempotent). |
104
+ | `attach(session)` → `Desktop` | Re-create a handle from a saved `CreateDesktopResponse`. |
105
+ | `await aclose()` | Close the owned HTTP client. Also usable as an async context manager. |
106
+
107
+ Gateway errors are mapped to typed errors: `AuthError` (401), `PlanError`
108
+ (402), `ConcurrencyLimitError` (429), `NoCapacityError` (503), and a generic
109
+ `GatewayError` otherwise — all subclasses of `SolariError`.
110
+
111
+ ## Desktop handle (`Desktop`)
112
+
113
+ Properties: `sessionId`, `streamUrl`, `controlUrl`, `expiresAt`, `connected`.
114
+
115
+ `await connect()` opens the control WebSocket; `await close()` tears it down.
116
+ Each action sends a `{ id, method, params }` JSON-RPC frame and awaits the
117
+ matching `{ id, ok, result }` reply, correlated by `id`, with a per-call
118
+ timeout (`TimeoutError`). A failed RPC raises `ActionError`.
119
+
120
+ ### Computer-use actions
121
+
122
+ | Surface | Methods |
123
+ |---|---|
124
+ | `await desktop.exec(cmd, args=None, cwd=None, timeout_ms=None)` | → `ExecResult(exitCode, stdout, stderr)` |
125
+ | `desktop.fs` | `read(path)→bytes`, `read_text(path)→str`, `write(path, data, mode=None)`, `list(path)→list[FsEntry]` |
126
+ | `desktop.mouse` | `move(x,y,humanize=)`, `click(x,y,button=,humanize=)`, `down/up(x,y,button=)`, `scroll(x,y,...)` |
127
+ | `desktop.keyboard` | `type(text)`, `press(keys)`, `down(keys)`, `up(keys)` |
128
+ | `await desktop.screenshot(format="png", quality=None)` | → `bytes` |
129
+ | `desktop.display` | `set(w, h)` |
130
+ | `desktop.clipboard` | `get()→str`, `set(text)` |
131
+ | `desktop.process` | `list()→list[ProcessInfo]`, `kill(pid)` |
132
+ | `await desktop.health()` | → `HealthResult(ready, display, vnc)` |
133
+
134
+ ### Lifecycle & preview
135
+
136
+ Since the 2026-07 desktop/sandbox VM consolidation, a desktop is backed by the
137
+ same unified session record as a sandbox, so the handle carries the full
138
+ lifecycle surface (routed to `/sandboxes/:id/*`):
139
+
140
+ | Method | Description |
141
+ |---|---|
142
+ | `await desktop.pause()` / `await desktop.resume()` | Pause (snapshot RAM+disk, free the slot) / resume (re-acquires a slot; may raise `ConcurrencyLimitError` if the org is at cap). |
143
+ | `await desktop.set_timeout(timeout_ms)` | Extend the rolling idle keep-alive → `{ expiresAt }`. |
144
+ | `await desktop.metrics()` / `await desktop.snapshot(name=None)` / `await desktop.revert(id)` | Metrics + in-place snapshot/restore. |
145
+ | `await desktop.preview_url(port)` | Public preview URL `{ url, token? }` for an in-guest port. |
146
+
147
+ See `examples/quickstart.py` for an end-to-end script.
148
+
149
+ ## Deviations from the TypeScript SDK
150
+
151
+ - **Async-first.** The TS SDK is promise-based; this SDK exposes the same
152
+ surface as `async def` coroutines, plus `SyncDesktopClient` for the
153
+ lifecycle calls. The TS SDK does not split sync/async, so no async/sync split
154
+ is exposed beyond that convenience wrapper.
155
+ - **Naming.** Method and parameter names are snake_case (`read_text`,
156
+ `ttl_seconds`, `call_timeout_ms`); constructor args are keyword-only. Wire
157
+ and response field names (`sessionId`, `exitCode`, `streamUrl`, …) are kept
158
+ verbatim so dataclass attributes match the JSON.
159
+ - **Bytes vs `Uint8Array`.** `fs.read`/`screenshot` return `bytes`;
160
+ `fs.write` accepts `bytes | str`.
161
+ - **HTTP/WS deps.** Uses `httpx` and `websockets` instead of `fetch`/`ws`.
162
+ ```
@@ -0,0 +1,149 @@
1
+ # solari-desktop (Python)
2
+
3
+ Python SDK for **Solari Desktop** — create managed, hardware-isolated Linux
4
+ desktops that boot in well under a second, embed the live VNC stream, and drive
5
+ them with a **computer-use** action API (exec, fs, mouse/keyboard, screenshot,
6
+ clipboard, processes).
7
+
8
+ This is a faithful port of the TypeScript `@solarisdk/sdk` package; the
9
+ class, method, and field names match it one-for-one (snake_case where TS uses
10
+ camelCase for method/parameter names; wire/response field names are kept as-is).
11
+
12
+ ```
13
+ SDK ──HTTPS──▶ Gateway (create / get / destroy sessions)
14
+ SDK ──WSS────▶ Gateway (control channel: computer-use JSON-RPC)
15
+ ──WSS────▶ (stream channel: RFB/VNC bytes, embed in a viewer)
16
+ ```
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ pip install solari-desktop
22
+ ```
23
+
24
+ Depends only on [`httpx`](https://www.python-httpx.org/) (HTTP) and
25
+ [`websockets`](https://websockets.readthedocs.io/) (control channel). Requires
26
+ Python 3.9+.
27
+
28
+ ## Usage
29
+
30
+ The primary API is **async** (matching the asyncio WebSocket control channel):
31
+
32
+ ```python
33
+ import asyncio
34
+ from solari_desktop import DesktopClient
35
+
36
+
37
+ async def main() -> None:
38
+ async with DesktopClient(
39
+ api_key="...",
40
+ base_url="https://api.getsolari.com",
41
+ ) as client:
42
+ # Create a session (assigned from the warm pool — typically sub-second).
43
+ desktop = await client.create(
44
+ template="default",
45
+ resolution="1280x720",
46
+ timeout_ms=1_800_000, # rolling idle window: auto-pause after 30 min idle
47
+ )
48
+
49
+ # Embed this URL in a VNC viewer to watch the live desktop.
50
+ print(desktop.streamUrl)
51
+
52
+ # Open the control channel, then drive the desktop.
53
+ await desktop.connect()
54
+
55
+ shot = await desktop.screenshot(format="png") # bytes
56
+ out = await desktop.exec("ls", args=["-la", "/home"])
57
+ print(out.exitCode, out.stdout)
58
+
59
+ await desktop.mouse.click(640, 360, humanize=True)
60
+ await desktop.keyboard.press(["ctrl", "c"])
61
+ await desktop.fs.write("/tmp/note.txt", "hello")
62
+ text = await desktop.fs.read_text("/tmp/note.txt")
63
+
64
+ # Public preview URL for an in-guest port (requires PREVIEW_DOMAIN).
65
+ preview = await desktop.preview_url(3000) # {"url": ..., "token"?: ...}
66
+ print(preview["url"])
67
+
68
+ # Pause (snapshot RAM+disk, free the slot) and resume later.
69
+ await desktop.pause()
70
+ await desktop.resume()
71
+
72
+ await desktop.close()
73
+ await client.destroy(desktop.sessionId)
74
+
75
+
76
+ asyncio.run(main())
77
+ ```
78
+
79
+ A thin synchronous wrapper, `SyncDesktopClient`, is available for the
80
+ session-lifecycle calls (`create`, `get`, `destroy`, `attach`) when you are not
81
+ running an event loop. The returned `Desktop` handle remains async.
82
+
83
+ ## Client API (`DesktopClient`)
84
+
85
+ | Method | Description |
86
+ |---|---|
87
+ | `DesktopClient(api_key=, base_url=, http=None, call_timeout_ms=None)` | Construct a client. |
88
+ | `await create(template="default", ttl_seconds=None, timeout_ms=None, resolution=None, metadata=None, record=None, lifecycle=None)` → `Desktop` | `POST /desktops`. `timeout_ms` sets a rolling idle window; `lifecycle` controls idle behaviour (default auto-pause). |
89
+ | `await get(session_id)` | `GET /desktops/:id` → `GetDesktopResponse(sessionId, status, expiresAt)`. |
90
+ | `await destroy(session_id)` | `DELETE /desktops/:id` → `DeleteDesktopResponse(ok=True)` (idempotent). |
91
+ | `attach(session)` → `Desktop` | Re-create a handle from a saved `CreateDesktopResponse`. |
92
+ | `await aclose()` | Close the owned HTTP client. Also usable as an async context manager. |
93
+
94
+ Gateway errors are mapped to typed errors: `AuthError` (401), `PlanError`
95
+ (402), `ConcurrencyLimitError` (429), `NoCapacityError` (503), and a generic
96
+ `GatewayError` otherwise — all subclasses of `SolariError`.
97
+
98
+ ## Desktop handle (`Desktop`)
99
+
100
+ Properties: `sessionId`, `streamUrl`, `controlUrl`, `expiresAt`, `connected`.
101
+
102
+ `await connect()` opens the control WebSocket; `await close()` tears it down.
103
+ Each action sends a `{ id, method, params }` JSON-RPC frame and awaits the
104
+ matching `{ id, ok, result }` reply, correlated by `id`, with a per-call
105
+ timeout (`TimeoutError`). A failed RPC raises `ActionError`.
106
+
107
+ ### Computer-use actions
108
+
109
+ | Surface | Methods |
110
+ |---|---|
111
+ | `await desktop.exec(cmd, args=None, cwd=None, timeout_ms=None)` | → `ExecResult(exitCode, stdout, stderr)` |
112
+ | `desktop.fs` | `read(path)→bytes`, `read_text(path)→str`, `write(path, data, mode=None)`, `list(path)→list[FsEntry]` |
113
+ | `desktop.mouse` | `move(x,y,humanize=)`, `click(x,y,button=,humanize=)`, `down/up(x,y,button=)`, `scroll(x,y,...)` |
114
+ | `desktop.keyboard` | `type(text)`, `press(keys)`, `down(keys)`, `up(keys)` |
115
+ | `await desktop.screenshot(format="png", quality=None)` | → `bytes` |
116
+ | `desktop.display` | `set(w, h)` |
117
+ | `desktop.clipboard` | `get()→str`, `set(text)` |
118
+ | `desktop.process` | `list()→list[ProcessInfo]`, `kill(pid)` |
119
+ | `await desktop.health()` | → `HealthResult(ready, display, vnc)` |
120
+
121
+ ### Lifecycle & preview
122
+
123
+ Since the 2026-07 desktop/sandbox VM consolidation, a desktop is backed by the
124
+ same unified session record as a sandbox, so the handle carries the full
125
+ lifecycle surface (routed to `/sandboxes/:id/*`):
126
+
127
+ | Method | Description |
128
+ |---|---|
129
+ | `await desktop.pause()` / `await desktop.resume()` | Pause (snapshot RAM+disk, free the slot) / resume (re-acquires a slot; may raise `ConcurrencyLimitError` if the org is at cap). |
130
+ | `await desktop.set_timeout(timeout_ms)` | Extend the rolling idle keep-alive → `{ expiresAt }`. |
131
+ | `await desktop.metrics()` / `await desktop.snapshot(name=None)` / `await desktop.revert(id)` | Metrics + in-place snapshot/restore. |
132
+ | `await desktop.preview_url(port)` | Public preview URL `{ url, token? }` for an in-guest port. |
133
+
134
+ See `examples/quickstart.py` for an end-to-end script.
135
+
136
+ ## Deviations from the TypeScript SDK
137
+
138
+ - **Async-first.** The TS SDK is promise-based; this SDK exposes the same
139
+ surface as `async def` coroutines, plus `SyncDesktopClient` for the
140
+ lifecycle calls. The TS SDK does not split sync/async, so no async/sync split
141
+ is exposed beyond that convenience wrapper.
142
+ - **Naming.** Method and parameter names are snake_case (`read_text`,
143
+ `ttl_seconds`, `call_timeout_ms`); constructor args are keyword-only. Wire
144
+ and response field names (`sessionId`, `exitCode`, `streamUrl`, …) are kept
145
+ verbatim so dataclass attributes match the JSON.
146
+ - **Bytes vs `Uint8Array`.** `fs.read`/`screenshot` return `bytes`;
147
+ `fs.write` accepts `bytes | str`.
148
+ - **HTTP/WS deps.** Uses `httpx` and `websockets` instead of `fetch`/`ws`.
149
+ ```
@@ -0,0 +1,65 @@
1
+ """Quickstart: create a desktop, screenshot it, run a command, print the
2
+ stream URL you can embed to watch the live session.
3
+
4
+ python examples/quickstart.py
5
+
6
+ Requires SOLARI_API_KEY (and optionally SOLARI_BASE_URL) in the env.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import os
13
+
14
+ from solari_desktop import DesktopClient
15
+
16
+
17
+ async def main() -> None:
18
+ api_key = os.environ.get("SOLARI_API_KEY")
19
+ if not api_key:
20
+ raise SystemExit("set SOLARI_API_KEY")
21
+
22
+ async with DesktopClient(
23
+ api_key=api_key,
24
+ base_url=os.environ.get("SOLARI_BASE_URL", "https://api.getsolari.com"),
25
+ ) as client:
26
+ # 1. Create a desktop (warm-pool assign -> ~sub-second).
27
+ desktop = await client.create(
28
+ template="default",
29
+ resolution="1280x720",
30
+ ttl_seconds=3600,
31
+ )
32
+
33
+ print("session:", desktop.sessionId)
34
+ print("stream this URL to watch the desktop:", desktop.streamUrl)
35
+
36
+ # 2. Open the control channel for computer-use actions.
37
+ await desktop.connect()
38
+
39
+ # 3. Wait until the desktop is fully ready, then act on it.
40
+ health = await desktop.health()
41
+ print("ready:", health.ready, "vnc:", health.vnc)
42
+
43
+ # 4. Take a screenshot and save it locally.
44
+ png = await desktop.screenshot(format="png")
45
+ with open("desktop.png", "wb") as fh:
46
+ fh.write(png)
47
+ print(f"saved screenshot ({len(png)} bytes) to desktop.png")
48
+
49
+ # 5. Run a command in the guest.
50
+ result = await desktop.exec("echo", args=["hello from the guest"])
51
+ print("exec exitCode:", result.exitCode)
52
+ print("exec stdout:", result.stdout.strip())
53
+
54
+ # 6. Drive input.
55
+ await desktop.mouse.move(640, 360, humanize=True)
56
+ await desktop.keyboard.type("hello world")
57
+
58
+ # 7. Tear down.
59
+ await desktop.close()
60
+ await client.destroy(desktop.sessionId)
61
+ print("destroyed session")
62
+
63
+
64
+ if __name__ == "__main__":
65
+ asyncio.run(main())
@@ -0,0 +1,63 @@
1
+ """Sandbox quickstart (Python): the unified ``SolariClient`` drives a headless
2
+ sandbox — run commands, run rich code, watch files, snapshot/fork, get a public
3
+ preview URL — then boots a recorded GUI desktop.
4
+
5
+ SOLARI_API_KEY=... python3 examples/sandbox_quickstart.py
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import os
12
+
13
+ from solari_desktop import SolariClient
14
+
15
+
16
+ async def main() -> None:
17
+ api_key = os.environ.get("SOLARI_API_KEY")
18
+ if not api_key:
19
+ raise SystemExit("set SOLARI_API_KEY")
20
+
21
+ pt = SolariClient(
22
+ api_key=api_key,
23
+ base_url=os.environ.get("SOLARI_BASE_URL", "https://api.getsolari.com"),
24
+ )
25
+
26
+ # --- headless sandbox ---------------------------------------------------
27
+ sbx = await pt.sandboxes.create(template="base", metadata={"demo": "1"})
28
+ await sbx.connect()
29
+
30
+ res = await sbx.commands.run("bash", env={"GREETING": "hi"}, on_stdout=lambda d: print(d, end=""))
31
+ print("exit:", res.exitCode)
32
+
33
+ proc = await sbx.commands.start("cat")
34
+ proc.on_data(lambda stream, data: print(f"[{stream}] {data}", end=""))
35
+ await proc.stdin("echo me\n")
36
+ await proc.kill()
37
+
38
+ code = await sbx.run_code("import sys; print(sys.version)", language="python")
39
+ print("code results:", len(code.results), "error:", code.error)
40
+
41
+ await sbx.files.write("/tmp/hello.txt", "world")
42
+ stop = await sbx.files.watch("/tmp", lambda ev: print("fs event:", ev.type, ev.path))
43
+ print("search hits:", len(await sbx.files.search("/tmp", "world")))
44
+ await stop()
45
+
46
+ snap_id = await sbx.snapshot("after-setup")
47
+ fork = await pt.sandboxes.create(template="base", from_snapshot=snap_id)
48
+ await fork.kill()
49
+ await sbx.kill()
50
+
51
+ # --- GUI desktop (recorded) --------------------------------------------
52
+ d = await pt.desktops.create(template="office", record=True)
53
+ await d.connect()
54
+ await d.mouse.double_click(100, 100)
55
+ await d.keyboard.hotkey("ctrl", "s")
56
+ print("desktop stream:", (await d.stream.start())["streamUrl"])
57
+ await d.close()
58
+ await pt.desktops.destroy(d.sessionId)
59
+ await pt.aclose()
60
+
61
+
62
+ if __name__ == "__main__":
63
+ asyncio.run(main())
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "solari-desktop"
7
+ version = "0.1.1"
8
+ description = "Python SDK for Solari Desktop — managed, hardware-isolated Linux desktops with a computer-use action API."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Solari" }]
13
+ keywords = ["solari", "desktop", "computer-use", "vnc", "automation"]
14
+ dependencies = [
15
+ "httpx>=0.24",
16
+ "websockets>=11.0",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://getsolari.com"
21
+
22
+ [tool.hatch.build.targets.wheel]
23
+ packages = ["solari_desktop"]
24
+
25
+ [tool.ruff]
26
+ line-length = 100
@@ -0,0 +1,145 @@
1
+ """solari_desktop — Python SDK for Solari Desktop.
2
+
3
+ Mirrors the public surface of the TypeScript ``@solarisdk/sdk`` package
4
+ (``sdk/src/index.ts``): the :class:`DesktopClient` entry point, the
5
+ :class:`Desktop` session handle and its computer-use action API, and the typed
6
+ error hierarchy.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .client import DesktopClient, SyncDesktopClient
12
+ from .desktop import Desktop, DesktopConfig, ExecStreamHandler
13
+ from .handle import SessionConfig, SessionHandle, SessionHooks
14
+ from .image import CompiledImage, Image, LocalCopy
15
+ from .solari import SolariClient, SyncSolariClient
16
+ from .sandbox import Sandbox
17
+ from .sandbox_client import SandboxClient, SyncSandboxClient
18
+ from .template_client import SyncTemplateClient, TemplateClient
19
+ from .volume_client import SyncVolumeClient, VolumeClient
20
+ from .errors import (
21
+ ActionError,
22
+ AuthError,
23
+ ConcurrencyLimitError,
24
+ ConnectionError,
25
+ GatewayError,
26
+ NoCapacityError,
27
+ SolariError,
28
+ PlanError,
29
+ TimeoutError,
30
+ )
31
+ from .types import (
32
+ CodeLanguage,
33
+ CodeResultItem,
34
+ CommandResult,
35
+ CreateDesktopResponse,
36
+ CreateSandboxResponse,
37
+ DeleteDesktopResponse,
38
+ DesktopLifecycleResponse,
39
+ DesktopStatus,
40
+ ExecResult,
41
+ ExecStreamChunk,
42
+ FsEntry,
43
+ FsSearchMatch,
44
+ FsStat,
45
+ FsWatchEvent,
46
+ GatewayErrorBody,
47
+ GitBranch,
48
+ GitCommit,
49
+ GitStatus,
50
+ GetDesktopResponse,
51
+ HealthResult,
52
+ KeyAction,
53
+ MetricsResult,
54
+ MouseAction,
55
+ MouseButton,
56
+ PackageManager,
57
+ PkgInstallResult,
58
+ PortInfo,
59
+ ProcessInfo,
60
+ RpcErrorBody,
61
+ RpcResponse,
62
+ RunCodeResult,
63
+ SandboxKind,
64
+ SandboxState,
65
+ SandboxView,
66
+ ScreenshotFormat,
67
+ SnapshotView,
68
+ )
69
+
70
+ __version__ = "0.1.0"
71
+
72
+ __all__ = [
73
+ # unified
74
+ "SolariClient",
75
+ "SyncSolariClient",
76
+ # image builder
77
+ "Image",
78
+ "CompiledImage",
79
+ "LocalCopy",
80
+ # client
81
+ "DesktopClient",
82
+ "SyncDesktopClient",
83
+ "SandboxClient",
84
+ "SyncSandboxClient",
85
+ "TemplateClient",
86
+ "SyncTemplateClient",
87
+ "VolumeClient",
88
+ "SyncVolumeClient",
89
+ # handles
90
+ "Desktop",
91
+ "DesktopConfig",
92
+ "ExecStreamHandler",
93
+ "Sandbox",
94
+ "SessionHandle",
95
+ "SessionConfig",
96
+ "SessionHooks",
97
+ # errors
98
+ "ActionError",
99
+ "AuthError",
100
+ "ConcurrencyLimitError",
101
+ "ConnectionError",
102
+ "GatewayError",
103
+ "NoCapacityError",
104
+ "SolariError",
105
+ "PlanError",
106
+ "TimeoutError",
107
+ # types
108
+ "CreateDesktopResponse",
109
+ "CreateSandboxResponse",
110
+ "DeleteDesktopResponse",
111
+ "DesktopLifecycleResponse",
112
+ "DesktopStatus",
113
+ "ExecResult",
114
+ "ExecStreamChunk",
115
+ "FsEntry",
116
+ "FsStat",
117
+ "GatewayErrorBody",
118
+ "GetDesktopResponse",
119
+ "HealthResult",
120
+ "KeyAction",
121
+ "MouseAction",
122
+ "MouseButton",
123
+ "PackageManager",
124
+ "PkgInstallResult",
125
+ "PortInfo",
126
+ "ProcessInfo",
127
+ "RpcErrorBody",
128
+ "RpcResponse",
129
+ "ScreenshotFormat",
130
+ # v2 types
131
+ "CodeLanguage",
132
+ "CodeResultItem",
133
+ "CommandResult",
134
+ "FsSearchMatch",
135
+ "FsWatchEvent",
136
+ "GitBranch",
137
+ "GitCommit",
138
+ "GitStatus",
139
+ "MetricsResult",
140
+ "RunCodeResult",
141
+ "SandboxKind",
142
+ "SandboxState",
143
+ "SandboxView",
144
+ "SnapshotView",
145
+ ]