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.
- solari_desktop-0.1.1/.gitignore +71 -0
- solari_desktop-0.1.1/PKG-INFO +162 -0
- solari_desktop-0.1.1/README.md +149 -0
- solari_desktop-0.1.1/examples/quickstart.py +65 -0
- solari_desktop-0.1.1/examples/sandbox_quickstart.py +63 -0
- solari_desktop-0.1.1/pyproject.toml +26 -0
- solari_desktop-0.1.1/solari_desktop/__init__.py +145 -0
- solari_desktop-0.1.1/solari_desktop/_http.py +142 -0
- solari_desktop-0.1.1/solari_desktop/client.py +439 -0
- solari_desktop-0.1.1/solari_desktop/desktop.py +437 -0
- solari_desktop-0.1.1/solari_desktop/errors.py +148 -0
- solari_desktop-0.1.1/solari_desktop/handle.py +758 -0
- solari_desktop-0.1.1/solari_desktop/image.py +140 -0
- solari_desktop-0.1.1/solari_desktop/sandbox.py +34 -0
- solari_desktop-0.1.1/solari_desktop/sandbox_client.py +455 -0
- solari_desktop-0.1.1/solari_desktop/solari.py +99 -0
- solari_desktop-0.1.1/solari_desktop/template_client.py +203 -0
- solari_desktop-0.1.1/solari_desktop/transport.py +349 -0
- solari_desktop-0.1.1/solari_desktop/types.py +431 -0
- solari_desktop-0.1.1/solari_desktop/volume_client.py +115 -0
- solari_desktop-0.1.1/solari_desktop/ws.py +127 -0
- solari_desktop-0.1.1/tests/test_autopause.py +47 -0
- solari_desktop-0.1.1/tests/test_charts.py +50 -0
- solari_desktop-0.1.1/tests/test_commands.py +41 -0
- solari_desktop-0.1.1/tests/test_git.py +245 -0
- solari_desktop-0.1.1/tests/test_http.py +80 -0
- solari_desktop-0.1.1/tests/test_image.py +49 -0
- solari_desktop-0.1.1/tests/test_lifecycle.py +79 -0
- solari_desktop-0.1.1/tests/test_parity.py +53 -0
- solari_desktop-0.1.1/tests/test_preview.py +41 -0
- solari_desktop-0.1.1/tests/test_recording.py +170 -0
- solari_desktop-0.1.1/tests/test_smoke.py +60 -0
- solari_desktop-0.1.1/tests/test_templates.py +136 -0
- solari_desktop-0.1.1/tests/test_transport.py +195 -0
- solari_desktop-0.1.1/tests/test_volumes.py +102 -0
- solari_desktop-0.1.1/tests_live/test_desktop_live.py +300 -0
- 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
|
+
]
|