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
| Package | Product | Classes |
|---|---|---|
solari-browser | Cloud browsers | Solari, BrowserSession |
solari-desktop | VMs (GUI desktops) | DesktopClient, Desktop |
solari-sandbox | Sandboxes (headless) | SandboxClient, Sandbox |
pip install solari-desktop # desktops (computer-use)
pip install solari-sandbox # code sandboxes
pip install solari-browser # browsersAll 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.
slr_live_… key authenticates solari-desktop, solari-sandbox, and solari-browser, all against https://api.getsolari.com.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.
| Option | Type | Default | Client | Notes |
|---|---|---|---|---|
api_key | str | required | all | Sent as Authorization: Bearer. Raises if empty. |
base_url | str | see notes | all | Solari: resolved from region; setting it ignores region. DesktopClient / SandboxClient: required, e.g. https://api.getsolari.com. |
region | str | "us-west" | Solari | Only "us-west" today. Unknown region raises. |
timeout_ms | int | 90_000 | Solari | Per attempt, not per call. |
max_attempts | int | 2 | Solari | Total attempts. 2 means one retry. |
backoff_ms | int | 500 | Solari | Fixed sleep between attempts, not exponential. |
call_timeout_ms | int | 300_000 | DesktopClient, SandboxClient | Per-RPC timeout given to handles this client creates. |
http | httpx.AsyncClient | own client | DesktopClient, SandboxClient | Reuse an existing client. When omitted, one is created and owned by the SDK. |
kind | "sandbox" | "desktop" | "sandbox" | SandboxClient | The flavour create() makes. |
os.environ. Pass api_key (and base_url where required) explicitly.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.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
| Class | Package | Purpose |
|---|---|---|
Solari | solari-browser | Browser entry point: sessions, profiles, replays. |
BrowserSession | solari-browser | A live browser + its session. Returned by launch(). |
DesktopClient | solari-desktop | VM entry point: create, connect, pause, resume, destroy. |
Desktop | solari-desktop | A live GUI session: mouse, keyboard, screenshots, apps. |
SandboxClient | solari-sandbox | Sandbox entry point: create, list, snapshots, volumes. |
Sandbox | solari-sandbox | A live headless session: commands, code, files, git. Shared base of Desktop. |
