Metadata-Version: 2.4
Name: bonya
Version: 1.0.0
Summary: Synchronous Python SDK for Bonya sandboxes
License-Expression: MIT
Project-URL: Repository, https://github.com/bonyai/bonya-python
Keywords: bonya,sandbox,sdk
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<1,>=0.27
Requires-Dist: websockets>=13
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: twine>=6.2; extra == "dev"
Dynamic: license-file

# Bonya Python SDK

Run code in a fast, isolated sandbox — from Python.

```bash
python -m pip install bonya
```

```python
from bonya import Bonya

with Bonya() as client:                       # reads BONYA_API_KEY
    with client.create_sandbox() as sandbox:  # deleted on the way out
        result = sandbox.exec(["echo", "hello"], check=True)
        print(result.stdout)                  # hello
```

That is a real VM: it boots in about a second, runs anything Linux runs, and is
gone when the `with` block ends.

The SDK is synchronous. It talks to the Bonya REST API over HTTPS
(`httpx`), and to the edge over a WebSocket (`websockets`) for streaming exec
and terminal sessions. Python 3.10 or newer; fully typed (`py.typed`).

## Configuration

Every setting has an environment-variable fallback:

| Argument | Environment variable | Default |
|---|---|---|
| `api_key` | `BONYA_API_KEY` | *required* |
| `endpoint` | `BONYA_ENDPOINT` | `https://api.bonya.ai` |
| `edge_endpoint` | `BONYA_EDGE_ENDPOINT` | the API endpoint |
| `organization_id` | `BONYA_ORGANIZATION_ID` | your personal organization |
| `ca_bundle` | `BONYA_CA_BUNDLE` | system trust store |
| `timeout` | — | `30` (seconds) |
| `max_retries` | — | `2` |
| `filesystem_read_limit` | — | 64 MiB |

An empty `organization_id` is an error rather than a silent fallback to your
personal organization.

## What you can do

| I want to… | Call |
|---|---|
| Start a sandbox | `client.create_sandbox(template=, image=, snapshot=, gpu=, network=, env=, secret_sets=)` |
| Reconnect to one | `client.get_sandbox(id)` / `.get_sandbox_by_name(name)` |
| Find my sandboxes | `client.list_sandboxes(states=, name=)` |
| Run a command | `sandbox.exec(cmd)` |
| Stream output live | `sandbox.exec_stream(cmd)` |
| Keep a process running after I disconnect | `sandbox.create_session(name, cmd)` / `.attach_session(name)` |
| Move files in and out | `sandbox.read_file` / `.write_file` / `.upload_file` / `.download_file` / `.list_files` / `.stat_file` / `.mkdir_file` / `.remove_file` / `.move_file` |
| Expose a port to the web | `sandbox.create_preview(port)` |
| Capture disk state | `sandbox.snapshot()` / `.list_snapshots()` |
| Clone a running sandbox | `sandbox.fork(count)` |
| Pause or keep awake | `sandbox.pause("warm" \| "cold")` / `.keepalive(seconds)` / `.resume()` |
| Restrict egress | `sandbox.update_network(NetworkPolicy(mode="allowlist", allow=[NetworkRule(domain="pypi.org")]))` |
| Run a managed or scheduled job | `client.run_job(spec)` / `client.create_job_schedule(schedule, job)` |
| See GPU capacity | `client.list_gpu_options()` |
| Save a launch configuration | `client.create_template(name, config=TemplateConfig(...))` / `.list_templates()` / `.update_template()` / `.delete_template()` |
| Pull private images | `client.create_registry_connection(...)` / `.validate_registry_connection(id, image)` |
| Keep credentials out of code | `client.create_secret(name, value)` / `.create_secret_set(name, values)` (values are write-only) |
| Switch organization | `client.list_organizations()` / `client.organization_id = id` |

Everything is a **flat method** on `Bonya` or `Sandbox`.

### Streaming exec

```python
from bonya import Exit, Stderr, Stdout

with sandbox.exec_stream(["bash", "-lc", "echo out; echo err >&2"]) as session:
    for event in session:
        if isinstance(event, Stdout):
            print("out:", event.data.decode(), end="")
        elif isinstance(event, Stderr):
            print("err:", event.data.decode(), end="")
        elif isinstance(event, Exit):
            print("exit:", event.exit_code)
```

### Persistent sessions

```python
sandbox.create_session("server", ["bash"])
with sandbox.attach_session("server") as stream:
    stream.write("npm run dev\n")
    for event in stream:
        ...  # Stdout, Exit, SessionEnded, SessionOutputDropped
# leaving the block detaches; the process keeps running
```

### Jobs

```python
from bonya import JobSandboxSpec, JobSpec

run = client.run_job(JobSpec(new_sandbox=JobSandboxSpec(), cmd=["./run-tests.sh"]))
print(run.status, run.result.exit_code)
```

### Snapshots and forks

```python
snapshot = sandbox.snapshot(name="baseline")
restored = client.create_sandbox(snapshot=snapshot.id)
forks = sandbox.fork(2).sandboxes
```

## Errors

Every error extends `BonyaError` (with `sandbox_id`, `idempotency_key`, `code`).
Catch the class you care about: `AuthenticationError`, `SandboxNotFoundError`,
`SandboxCreationTimeoutError` (carries the `idempotency_key` so create can be
retried), `ExecFailedError`, `TimeoutError`, `NoCapacityError`,
`RateLimitedError`, `OutcomeUnknownError`, `RemoteFileNotFoundError`, and so on.
API keys and capability tokens are redacted from error messages.

Create, exec, snapshot and other mutations accept an `idempotency_key`; the SDK
generates one and reuses it across its own retries, so a retried call joins the
original attempt instead of repeating it.

## Examples

Runnable programs live in [`examples/`](examples): `quickstart.py`,
`exec_streaming.py`, `files.py`, `sessions.py`, `previews.py`, `snapshots.py`.

## Development

```bash
python -m pip install -e '.[dev]'
make check      # mypy + pytest
```

MIT licensed.
