Metadata-Version: 2.4
Name: sail-sdk
Version: 0.1.2
Summary: Python SDK for the Sail sandbox platform
Project-URL: Homepage, https://app.sailresearch.com
Project-URL: Repository, https://github.com/sailresearch/sail
Project-URL: Issues, https://github.com/sailresearch/sail/issues
Author: Sail
License-Expression: Apache-2.0
Keywords: grpc,sail,sailbox,sandbox,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: grpcio>=1.80.0
Requires-Dist: protobuf>=6.31.1
Provides-Extra: dev
Requires-Dist: grpcio-tools>=1.80.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# sail-sdk

Python SDK for Sail sailboxes.

## Install

```bash
pip install sail-sdk
```

```bash
uv add sail-sdk
```

## Configure

The SDK reads configuration from environment variables:

```bash
export SAIL_API_KEY=sk_...
```

By default the SDK talks to production:

- API: `https://api.sailresearch.com`
- Sailbox scheduler: `sailbox-scheduler.sailresearch.com:443`

For dev, set:

```bash
export SAIL_MODE=dev
```

You can also override endpoints directly:

```bash
export SAIL_API_URL=https://dev.sailresearch.com
export SAIL_SCHEDULER_URL=sailbox-scheduler.dev.sailresearch.com:443
```

## Create A Sailbox

```python
import sail

app = sail.App.find(name="example-app", mint_if_missing=True)

sb = sail.Sailbox.create(
    app=app,
    image=sail.Image.debian_arm64,
    name="sandbox-1",
    cpu=1,
    memory=512,
)

print(sb.sailbox_id)
print(sb.status)
print(sb.worker_address)
```

`Sailbox.create` returns after the VM is running. Supported create arguments are:

- `app`: a `sail.App` from `App.find`
- `image`: a `sail.Image` value or a built custom image
- `name`: sailbox name
- `cpu`: vCPU count, default `1`
- `memory`: memory in MiB, default `512`
- `ingress_ports`: optional list of guest ports to expose

AMD64 image values still exist in the SDK for compatibility, but new sailboxes currently must use arm64 images. The scheduler rejects AMD64 sailbox create and image build requests.

## Exec

```python
result = sb.exec("echo hi", timeout=5).wait()

print(result.stdout)
print(result.stderr)
print(result.returncode)
```

`timeout` is the command runtime budget in seconds. Omit it to let the command run without an SDK-provided runtime limit.

Background exec starts a detached process and waits only for the launcher shell:

```python
sb.exec("python3 -m http.server 3000", background=True).wait()
```

Only one exec request may run at a time for a sailbox. If another exec is already active, the SDK raises `sail.SailboxExecAlreadyRunningError`.

## Networking

Expose guest ports when creating the sailbox:

```python
sb = sail.Sailbox.create(
    app=app,
    image=sail.Image.debian_arm64,
    name="sandbox-net",
    ingress_ports=[3000],
)

sb.exec("python3 -m http.server 3000", background=True).wait()

listener = sb.listener(3000)
print(listener.url)
print(listener.route_status)
```

Use `listeners()` to list every exposed port:

```python
for listener in sb.listeners():
    print(listener.port, listener.url, listener.route_status)
```

Use `request()` to ask the worker proxy to make an outbound HTTP request on behalf of the sailbox:

```python
req = sb.request(
    "POST",
    "https://example.com/api",
    json={"hello": "world"},
    idempotency_key="example-1",
)

completed = req.wait()
print(completed.status)
if completed.response:
    print(completed.response.status_code)
    print(completed.response.text)
```

`idempotency_key` is required for `request()`. `data` and `json` are mutually exclusive.

## Lifecycle

```python
sb.checkpoint()  # durably checkpoint while keeping the sailbox running
sb.stop()        # checkpoint and stop
sb.start()       # resume a stopped sailbox
sb.terminate()   # permanently terminate
```

After `stop()`, `exec()` raises `SailboxExecutionError` until `start()` succeeds.

## Custom Images

Start from the arm64 Debian base image, add build steps, then call `build()`:

```python
image = (
    sail.Image.debian_arm64.apt_install("git", "curl")
    .pip_install("requests")
    .run_commands("python3 -m pip show requests >/tmp/requests.txt")
    .env({"APP_ENV": "demo"})
)

built_image = image.build(timeout=1800)

sb = sail.Sailbox.create(
    app=app,
    image=built_image,
    name="custom-image-demo",
)
```

Supported image build helpers:

- `apt_install(*packages)`
- `pip_install(*packages)`
- `run_commands(*commands)`
- `env(dict[str, str])`
- `build(timeout=1800)`

## Images

Current image properties:

```python
arm64_image = sail.Image.debian_arm64
arm_image = sail.Image.debian_arm
```

Compatibility aliases still present but currently rejected by the scheduler for sailbox create and image build:

```python
amd64_image = sail.Image.debian_amd64
amd_image = sail.Image.debian_amd
```

## Errors

Common SDK exceptions:

- `sail.SailboxCreationError`
- `sail.SailboxExecutionError`
- `sail.SailboxExecAlreadyRunningError`
- `sail.SailboxExecRequestNotFoundError`
- `sail.SailboxTerminatedError`
- `sail.ImageBuildError`

## Examples

- `examples/sailbox_smoke.py`: start an arm64 Debian sailbox and run exec commands.
- `examples/sailbox_custom_image.py`: build an arm64 custom image with `apt_install`, `pip_install`, `run_commands`, and `env`, then launch a sailbox from it.

## Publishing

Build a distributable package locally from the repo root:

```bash
just python-sdk-build
```

Publish from a developer machine with a PyPI token:

```bash
export UV_PUBLISH_TOKEN=pypi-...
just python-sdk-publish
```

The repository also includes a GitHub Actions release workflow at `.github/workflows/python-sdk-publish.yml`.
It publishes when you push a tag like `python-sdk-v0.1.0`, after verifying that the tag version matches `sail.__version__`.

Recommended setup:

1. Create the `sail-sdk` project on PyPI.
2. Configure PyPI Trusted Publishing for this GitHub repository and the `python-sdk-publish.yml` workflow.
3. Bump `sdk/python/src/sail/__about__.py`.
4. Push a matching tag: `git tag python-sdk-v0.1.0 && git push origin python-sdk-v0.1.0`
