Solari

Python SDK

The official Python clients for the three Solari products: browsers (a connected Playwright Browser in the cloud), VMs (GUI desktops you drive with mouse, keyboard, and screenshots), and sandboxes (headless microVMs for commands, code, and files). Browsers ship as solari-browser, VMs as solari-desktop, and sandboxes as solari-sandbox (both depending on solari-core).

Installation

PackageProductClasses
solari-browserCloud browsersSolari, BrowserSession
solari-desktopVMs (GUI desktops)DesktopClient, Desktop
solari-sandboxSandboxes (headless)SandboxClient, Sandbox
pip install solari-desktop     # desktops (computer-use)
pip install solari-sandbox     # code sandboxes
pip install solari-browser     # browsers

All need Python >=3.9. solari-browser depends on httpx and patchright. solari-desktop and solari-sandbox each depend on solari-core (the shared transport, session handles, and types), which pip installs automatically — mirroring npm’s @solarisdk/core + @solarisdk/desktop + @solarisdk/sandbox split.

One key across every package
The same slr_live_… key authenticates solari-desktop, solari-sandbox, and solari-browser, all against https://api.getsolari.com.
Why the patchright pin looks wrong
solari-browser pins patchright>=1.59,<1.60, a range rather than ==1.59.3. That is deliberate: the pool’s wire-protocol gate compares major.minor only, so any 1.59.x client talks to its 1.59.3 server, and PyPI has no 1.59.3 at all (the Node and Python patchright release trains differ). A mismatched major.minor makes every launch() fail with HTTP 428. You do not need patchright install chromium, because the browser runs remotely.

Getting Started

Launch a browser

import asyncio
from solari_browser import Solari

async def main():
    async with Solari(api_key="slr_live_...") as solari:
        async with await solari.launch(stealth=True, proxy="us") as browser:
            page = await browser.new_page()
            await page.goto("https://example.com")
            print(await page.title())
        # browser exit releases the session; solari exit closes the client

asyncio.run(main())

Create a VM

import asyncio
from solari_desktop import DesktopClient

async def main():
    async with DesktopClient(
        api_key="slr_live_...",
        base_url="https://api.getsolari.com",
    ) as client:
        vm = await client.create(template="office", resolution="1280x720")
        await vm.connect()  # open the control channel first
        await vm.keyboard.type("hello")
        png = await vm.screenshot()
        await vm.kill()

asyncio.run(main())

Create a sandbox

import asyncio
from solari_sandbox import SandboxClient

async def main():
    async with SandboxClient(
        api_key="slr_live_...",
        base_url="https://api.getsolari.com",
    ) as client:
        sbx = await client.create(template="base")
        await sbx.connect()  # open the control channel first
        r = await sbx.commands.run("echo", args=["hi"])
        print(r.stdout)  # "hi"
        await sbx.kill()

asyncio.run(main())

VMs and sandboxes at once

There is no unified client — mirroring @solarisdk/desktop and @solarisdk/sandbox on npm, each package exposes its own client. Import DesktopClient from solari-desktop and SandboxClient from solari-sandbox; both accept the same slr_live_… key and re-export the shared handles and types from solari-core.

from solari_desktop import DesktopClient
from solari_sandbox import SandboxClient

desktops  = DesktopClient(api_key="slr_live_...")
sandboxes = SandboxClient(api_key="slr_live_...")

vm  = await desktops.create(template="office")
sbx = await sandboxes.create(template="base")

Configuration

Every client takes keyword-only constructor arguments. api_key is always required.

OptionTypeDefaultClientNotes
api_keystrrequiredallSent as Authorization: Bearer. Raises if empty.
base_urlstrsee notesallSolari: resolved from region; setting it ignores region. DesktopClient / SandboxClient: required, e.g. https://api.getsolari.com.
regionstr"us-west"SolariOnly "us-west" today. Unknown region raises.
timeout_msint90_000SolariPer attempt, not per call.
max_attemptsint2SolariTotal attempts. 2 means one retry.
backoff_msint500SolariFixed sleep between attempts, not exponential.
call_timeout_msint300_000DesktopClient, SandboxClientPer-RPC timeout given to handles this client creates.
httphttpx.AsyncClientown clientDesktopClient, SandboxClientReuse an existing client. When omitted, one is created and owned by the SDK.
kind"sandbox" | "desktop""sandbox"SandboxClientThe flavour create() makes.
No environment variables are read
No constructor reads os.environ. Pass api_key (and base_url where required) explicitly.
Async first, with sync wrappers
Every client is async, built on httpx. solari-sandbox ships SyncSandboxClient and solari-desktop ships SyncDesktopClient; both also re-export SyncVolumeClient and SyncTemplateClient from solari-core. These drive a private event loop. The Desktop / Sandbox handles they return are still async, so prefer the async clients when driving a session.
The two packages retry differently
solari-browser retries 502, 503, 504 only, with a fixed backoff_ms pause and max_attempts total tries. solari-desktop retries only idempotent requests (GET, DELETE, or anything carrying an idempotency key) on network errors, any 5xx, or a retryable body hint. It tries up to 5 times with exponential backoff plus jitter. Neither retries 429.

Reference

ClassPackagePurpose
Solarisolari-browserBrowser entry point: sessions, profiles, replays.
BrowserSessionsolari-browserA live browser + its session. Returned by launch().
DesktopClientsolari-desktopVM entry point: create, connect, pause, resume, destroy.
Desktopsolari-desktopA live GUI session: mouse, keyboard, screenshots, apps.
SandboxClientsolari-sandboxSandbox entry point: create, list, snapshots, volumes.
Sandboxsolari-sandboxA live headless session: commands, code, files, git. Shared base of Desktop.