VMs
solari-desktop: classes DesktopClient and Desktop. A VM is a GUI desktop you drive with mouse, keyboard, and screenshots, and stream live. See the Python SDK hub for install and configuration.
pip install solari-desktopContents
DesktopClient: DesktopClient(), create(), connect(), get(), pause(), resume(), attach(), destroy(), aclose(), volumesDesktop: health(), screenshot(), mouse.*, keyboard.*, display.*, clipboard.*, open(), stream.*, record.*, process.*, ports.*, pkg.install(), exec(), exec_stream(), fs.*, inherited members- Types:
CreateDesktopResponse,GetDesktopResponse,DesktopLifecycleResponse,DeleteDesktopResponse,DesktopStatus,HealthResult,ExecResult,ExecStreamChunk,ProcessInfo,PortInfo,PkgInstallResult
mem_mb, timeout_ms), but the dataclasses they return mirror the wire and stay camelCase: result.exitCode, view.memMb, stat.modTimeMs. This applies to all of solari-desktop.DesktopClient
The VM entry point. Talks the gateway HTTP API.
Properties
volumesVolumeClient: persistent volume CRUD. Same surface asSandboxClient.volumes.
Constructors
DesktopClient()
DesktopClient(
*,
api_key: str,
base_url: str,
http: httpx.AsyncClient | None = None,
call_timeout_ms: int | None = None,
) -> DesktopClientCreates a client. Does not open any connection. All arguments are keyword-only.
Parameters:
api_keystr: required.base_urlstr: required, e.g.https://api.getsolari.com.httphttpx.AsyncClient | None: reuse an existing client; otherwise one is created and owned here.call_timeout_msint | None: per-RPC timeout for handles this client creates. Default300_000.
Returns: DesktopClient
Raises: SolariError if api_key or base_url is missing.
Example:
from solari_desktop import DesktopClient
client = DesktopClient(
api_key="slr_live_...",
base_url="https://api.getsolari.com",
)
# also an async context manager, which calls aclose() on exit
async with DesktopClient(api_key="slr_live_...", base_url="...") as client:
...SyncDesktopClient takes the same arguments (minus http) and drives a private event loop, so it works outside asyncio. The Desktop handle it returns is still async.Methods
create()
async create(
*,
template: str = "default",
ttl_seconds: int | None = None,
resolution: str | None = None,
cpu: int | None = None,
mem_mb: int | None = None,
metadata: dict[str, str] | None = None,
record: bool | None = None,
timeout_ms: int | None = None,
lifecycle: dict | None = None,
volumes: list | None = None,
) -> DesktopCreates a VM and returns a live handle. The call sends an idempotency key, so retrying is safe.
Parameters:
templatestr: e.g."office". Default"default".resolutionstr | None: initial display size, e.g."1280x720".cpuint | None: vCPUs, 1 to 16. Grown on assign via vCPU hot-add.mem_mbint | None: RAM in MiB, 2048 to 65536. Grown via virtio-mem hotplug.recordbool | None: record server-side. See the caveat below, because the Python SDK does not hand you the presigned playback URL.timeout_msint | None: rolling idle window; resets on every use. Overridesttl_secondsand the 30-minute default.lifecycledict | None: idle policy with camelCase wire keys, e.g.{"onTimeout": "pause", "autoResume": True}.metadatadict[str, str] | None: opaque labels.volumeslist | None: attachments, e.g.[{"volumeId": "vol_x", "path": "/data"}].ttl_secondsint | None: legacy TTL; prefertimeout_ms.
Returns: Desktop
Raises: AuthError, PlanError, ConcurrencyLimitError, NoCapacityError, or GatewayError.
Example:
vm = await client.create(
template="office",
resolution="1280x720",
cpu=2,
mem_mb=4096,
)recordingUrl on the 201, but create() returns a Desktop handle, which drops it. get() never populates GetDesktopResponse.recordingUrl either, so it is always None. For a retrievable capture use record.start() / record.stop() plus download_url(), which write an mp4 inside the guest.connect()
async connect(session_id: str) -> DesktopRe-attaches by id, resuming the session first if it is paused. The canonical way back to a VM when all you have is its id.
Parameters:
session_idstr: the session id.
Returns: Desktop
Raises: GatewayError if the id is unknown.
Example:
vm = await client.connect(session_id) # resumes if paused
await vm.keyboard.type("back again")get()
async get(session_id: str) -> GetDesktopResponseFetches a session’s current status. Returns the raw record, not a handle. The gateway does not re-issue control URLs here.
Parameters:
session_idstr: the session id.
Returns: GetDesktopResponse: sessionId, status, expiresAt.
Example:
st = await client.get(session_id)
print(st.status) # "ready" | "paused" | ...pause()
async pause(session_id: str) -> DesktopLifecycleResponseSaves full RAM + disk state so it resumes exactly where it left off. Billing for compute stops and it stops counting against your concurrency limit. Prefer Desktop.pause() when you hold a live handle.
Parameters:
session_idstr: the session id.
Returns: DesktopLifecycleResponse: sessionId, status.
Example:
await client.pause(session_id)resume()
async resume(session_id: str) -> DesktopRestores full RAM + disk state and returns a new handle. Always resumes. Use connect() if the session may already be running.
Parameters:
session_idstr: the session id.
Returns: Desktop. Its expiresAt is ""; call get() for the new deadline.
Example:
vm = await client.resume(session_id)attach()
attach(session: CreateDesktopResponse) -> DesktopRebuilds a handle from a saved create response. Synchronous, with no network call. Use it to carry a session across processes.
Parameters:
sessionCreateDesktopResponse:sessionId,streamUrl,controlUrl,expiresAt.
Returns: Desktop
Example:
import json
from solari_desktop import CreateDesktopResponse
with open("session.json") as f:
vm = client.attach(CreateDesktopResponse(**json.load(f)))destroy()
async destroy(session_id: str) -> DeleteDesktopResponseDestroys a session. Idempotent.
Parameters:
session_idstr: the session id.
Returns: DeleteDesktopResponse: ok.
Example:
await client.destroy(session_id)aclose()
async aclose() -> NoneCloses the underlying HTTP clients, if this client owns them. Does not touch live sessions.
Returns: None
Example:
await client.aclose()volumes
Identical to SandboxClient.volumes: create(), list(), get(), delete().
Example:
vol = await client.volumes.create(name="assets")
vm = await client.create(
template="office",
volumes=[{"volumeId": vol["volumeId"], "path": "/data"}],
)Desktop
A live GUI session. Construct via DesktopClient.create().
Properties
idstr: the session id.sessionIdstr: alias ofid.streamUrlstr:wss://URL serving RFB (VNC) bytes for the live view.controlUrlstr:wss://JSON-RPC control channel.expiresAtstr: ISO 8601 expiry.connectedbool: whether the control channel is open.
Methods
health()
async health() -> HealthResultReadiness probe for the display, VNC server, and guest agent.
Returns: HealthResult: ready, display, vnc.
Example:
h = await vm.health()
if not h.ready:
raise RuntimeError("desktop not ready")screenshot()
async screenshot(*, format: "png" | "jpeg" = "png", quality: int | None = None) -> bytesCaptures the current screen as decoded image bytes.
Parameters:
format"png" | "jpeg": default"png".qualityint | None: JPEG quality 1 to 100; ignored for PNG.
Returns: bytes
Example:
with open("shot.png", "wb") as f:
f.write(await vm.screenshot())
jpeg = await vm.screenshot(format="jpeg", quality=80)mouse.*
async mouse.move(x: int, y: int, *, humanize: bool | None = None) -> None
async mouse.click(x: int, y: int, *, button: MouseButton | None = None, humanize: bool | None = None) -> None
async mouse.double_click(x: int, y: int, *, button: MouseButton | None = None) -> None
async mouse.down(x: int, y: int, button: MouseButton = "left") -> None
async mouse.up(x: int, y: int, button: MouseButton = "left") -> None
async mouse.scroll(x: int, y: int, *, button: MouseButton | None = None, humanize: bool | None = None) -> None
async mouse.drag(frm: dict, to: dict, button: MouseButton = "left") -> NoneDrive the pointer at absolute screen coordinates. The origin is the top-left of the display.
Parameters:
x,yint: absolute coordinates.buttonMouseButton:"left","middle", or"right". The SDK maps the name to the X11 code the guest expects.humanizebool | None: use a humanized trajectory instead of teleporting.frm,todict: drag endpoints, each{"x": int, "y": int}.
Returns: None
Example:
await vm.mouse.move(400, 300, humanize=True)
await vm.mouse.click(400, 300)
await vm.mouse.double_click(120, 80)
await vm.mouse.scroll(400, 300)
await vm.mouse.drag({"x": 100, "y": 100}, {"x": 500, "y": 400})keyboard.*
async keyboard.type(text: str) -> None
async keyboard.press(keys: str | list[str]) -> None
async keyboard.hotkey(*keys: str) -> None
async keyboard.down(keys: str | list[str]) -> None
async keyboard.up(keys: str | list[str]) -> NoneType literal text, or press key chords. hotkey() is press() with variadic arguments.
Parameters:
textstr: literal text to type.keysstr | list[str]: key names, e.g."Return"or["ctrl", "c"].
Returns: None
Example:
await vm.keyboard.type("hello world")
await vm.keyboard.press("Return")
await vm.keyboard.hotkey("ctrl", "c")
await vm.keyboard.down("shift") # hold
await vm.keyboard.press("Left")
await vm.keyboard.up("shift")display.*
async display.set(w: int, h: int) -> None
async display.size() -> dict # {"w": int, "h": int}
async display.cursor() -> dict # {"x": int, "y": int}Read or change the display resolution and read the cursor position.
Parameters:
w,hint: new resolution in pixels.
Returns: None / dict / dict, the two getters return raw dicts, not dataclasses.
Example:
await vm.display.set(1920, 1080)
size = await vm.display.size() # {"w": 1920, "h": 1080}
cursor = await vm.display.cursor() # {"x": 400, "y": 300}clipboard.*
async clipboard.get() -> str
async clipboard.set(text: str) -> NoneRead or write the guest clipboard. get() returns "" when the clipboard is empty.
Parameters:
textstr: content to place on the clipboard.
Returns: str / None
Example:
await vm.clipboard.set("pasted from the host")
await vm.keyboard.hotkey("ctrl", "v")
print(await vm.clipboard.get())open()
async open(name: str, args: list[str] | None = None) -> intLaunches a GUI application by name and returns its pid.
Parameters:
namestr: executable name, e.g."firefox".argslist[str] | None: arguments.
Returns: int, the pid.
Example:
pid = await vm.open("firefox", ["https://example.com"])stream.*
async stream.start() -> dict # {"streamUrl": str}
async stream.stop() -> NoneReturns the embeddable live-view URL. The stream URL is minted at create time, so start() is a getter and stop() is a no-op. The RFB socket belongs to the caller.
Returns: dict, only streamUrl, the same value as vm.streamUrl / None.
Example:
s = await vm.stream.start()
print(s["streamUrl"]) # hand it to a noVNC clientrecord.*
async record.start(fps: int | None = None, format: str | None = None, path: str | None = None) -> dict
async record.stop() -> dictRecords the session to an mp4 inside the guest. Retrieve it afterwards with download_url().
Parameters:
fpsint | None: capture frame rate.format,pathstr | None: output format and in-guest path.
Returns: dict, raw RPC results, not dataclasses. start() carries path and fps; stop() carries path and sizeBytes.
Example:
await vm.record.start(fps=15)
await vm.keyboard.type("recorded work")
result = await vm.record.stop()
dl = await vm.download_url(result["path"])process.*
async process.list() -> list[ProcessInfo]
async process.start(cmd: str, *, args: list[str] | None = None, cwd: str | None = None) -> int
async process.kill(pid: int) -> None
async process.signal(pid: int, signal: int | None = None) -> NoneInspect and control guest processes by pid. For output capture use commands.run() instead.
Parameters:
cmdstr: program to start.argslist[str] | None,cwdstr | None.pidint: target process.signalint | None: signal number.
Returns: list[ProcessInfo] / int (the pid) / None
Example:
pid = await vm.process.start("xterm")
for p in await vm.process.list():
print(p.pid, p.name)
await vm.process.kill(pid)ports.*
async ports.list() -> list[PortInfo]Lists TCP sockets listening inside the guest.
Returns: list[PortInfo], each with port, addr, optional pid.
Example:
for p in await vm.ports.list():
print(p.port, p.addr, p.pid)pkg.install()
async pkg.install(manager: "apt" | "pip" | "npm", packages: list[str]) -> PkgInstallResultInstalls packages in the guest.
Parameters:
managerPackageManager:"apt","pip", or"npm".packageslist[str]: package names.
Returns: PkgInstallResult: exitCode, stdout, stderr. A failed install returns a non-zero exitCode; it does not raise.
Example:
r = await vm.pkg.install("apt", ["imagemagick"])
if r.exitCode != 0:
print(r.stderr)exec()
async exec(
cmd: str,
*,
args: list[str] | None = None,
cwd: str | None = None,
timeout_ms: int | None = None,
stream: bool | None = None,
) -> ExecResultRuns a command to completion. A v1 convenience. Prefer commands.run(), which adds streaming callbacks plus per-command env and user.
Parameters:
cmdstr: program to run.argslist[str] | None,cwdstr | None,timeout_msint | None: passed to the guest.
Returns: ExecResult: exitCode, stdout, stderr.
Example:
r = await vm.exec("xdotool", args=["getactivewindow"])exec_stream()
async exec_stream(
cmd: str,
on_chunk: Callable[[ExecStreamChunk], None],
*,
args: list[str] | None = None,
cwd: str | None = None,
timeout_ms: int | None = None,
) -> ExecResultRuns a command, delivering output chunks as they arrive. A v1 convenience. Prefer commands.run() with on_stdout.
Parameters:
cmdstr: program to run.on_chunkCallable[[ExecStreamChunk], None]: called withstream,text,bytes. Positional, not keyword.args,cwd,timeout_ms: as exec().
Returns: ExecResult
Example:
import sys
await vm.exec_stream(
"apt-get",
lambda c: sys.stdout.write(c.text),
args=["update"],
)fs.*
async fs.read(path) / fs.read_text(path) / fs.write(path, data, mode=None)
async fs.list(path) / fs.stat(path) / fs.remove(path, recursive=False) / fs.mkdir(path)A v1 alias for a subset of files.*, which is the canonical surface and has more methods.
Example:
await vm.fs.write("/tmp/note.txt", "hi") # same as vm.files.write(...)Inherited members
Desktop extends the same SessionHandle base as Sandbox. These all work on a VM:
| Member | Purpose |
|---|---|
connect(), reconnect(), close() | Control-channel lifecycle. |
commands.run(), commands.start(), pty.create() | Run commands and open PTYs. |
run_code(), create_code_context() | Stateful kernel execution with rich outputs. |
files.*, download_url(), upload_url() | Filesystem access and signed transfer URLs. |
git.* | Clone, status, commit, push, pull, checkout, log. |
env(), volumes.mount() | Session env vars and volume mounts. |
metrics(), snapshot(), revert() | Usage, checkpoints, in-place restore. |
pause(), resume(), set_timeout(), kill() | Session lifecycle. |
preview_url() | Public URL for an in-guest port. |
Types
CreateDesktopResponse
sessionIdstr: signed, opaque session capability.streamUrlstr:wss://RFB stream.controlUrlstr:wss://JSON-RPC control channel.expiresAtstr: ISO 8601 expiry.recordingUrlstr | None: parsed from the201whenrecord=True. Only reachable if you build the dataclass yourself;create()consumes the response and returns a Desktop.
GetDesktopResponse
sessionIdstr.statusDesktopStatus:starting,ready,paused,releasing,gone.expiresAtstr.recordingUrlstr | None: declared on the dataclass but never filled in by get(); alwaysNonein practice.
DesktopLifecycleResponse / DeleteDesktopResponse
DesktopLifecycleResponse:sessionIdstr,statusDesktopStatus.DeleteDesktopResponse:okbool.
HealthResult
readybool,displaybool,vncbool.
ExecResult / ExecStreamChunk
ExecResult:exitCodeint,stdoutstr,stderrstr.ExecStreamChunk:stream"stdout" | "stderr",textstr (decoded UTF-8),bytesbytes (raw).
ProcessInfo / PortInfo / PkgInstallResult
ProcessInfo:pidint,namestr,cmdstr | None.PortInfo:portint,addrstr,pidint | None.PkgInstallResult:exitCodeint,stdoutstr,stderrstr.
Errors are shared across products. See Errors.
