Metadata-Version: 2.5
Name: gmi-sandbox-sdk
Version: 1.2.0
Summary: GMI Sandbox SDK for Python.
Project-URL: Homepage, https://www.gmicloud.ai/
Author: GMI
License: MIT
License-File: LICENSE
Keywords: gmi,sandbox,sdk
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.27
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Description-Content-Type: text/markdown

---
title: "GMI Sandbox SDK Usage"
description: "Install, configure, and use the GMI Sandbox Python SDK."
---

# GMI Sandbox SDK Usage

## Install

```bash
python -m pip install gmi-sandbox-sdk
```

## Configure

Use environment variables or pass values directly.

To get an API key, log in to `https://console.gmicloud.ai/`, click **API keys**, select **compute**, and click **Create API key**.

```bash
export GMI_SANDBOX_API_KEY="your-api-key"
```

Set `GMI_SANDBOX_IDC_NAME` (or pass `idc_name`) to choose the IDC for creating sandboxes and templates and for listing product specifications. You can leave it unset only if your organization has a default IDC configured; otherwise those requests return 400 `idc_name is required`:

```bash
export GMI_SANDBOX_IDC_NAME="gmi-sandbox-us"
```

## Create a client

```python
from sandbox_sdk import SandboxClient

client = SandboxClient()
```

You can also pass credentials explicitly:

```python
client = SandboxClient(
    api_key="your-api-key",
)
```

### Async client

`AsyncSandboxClient` provides the same resources and operations with native
async HTTP I/O. Use it as an async context manager so pooled connections are
closed automatically:

```python
from sandbox_sdk import AsyncSandboxClient

async with AsyncSandboxClient() as client:
    sandbox = await client.sandboxes.create(template_id="template-id")
    await sandbox.connect()
    result = await sandbox.commands.run("echo hello", wait=True)
    print(result.stdout)
    await sandbox.files.write("/tmp/hello.txt", "hello")
    await sandbox.delete()
```

When not using an async context manager, call `await client.aclose()` after
the client is no longer needed. `aclose()` closes only the HTTP client the SDK
created for you; a `transport` you pass in stays open and remains yours to
close. Requests issued after `aclose()` raise `TransportError`, so create a new
client instead of reusing a closed one.

Both clients only send requests to `http` and `https` URLs, and they validate
every redirect hop against that rule. This is a scheme check: it does not
restrict which hosts a `base_url` you configure can reach.

## Create and use a sandbox

```python
sandbox = client.sandboxes.create(
    template_id="template-id",
)

sandbox.connect()
result = sandbox.commands.run("echo hello", wait=True)
print(result.stdout)

sandbox.files.write("/tmp/hello.txt", "hello")
print(sandbox.files.read("/tmp/hello.txt").decode())

sandbox.delete()
```

If you already have a sandbox ID:

```python
sandbox = client.sandboxes.get("sandbox-id")
sandbox.connect()
```

## Files

```python
sandbox.files.read("/tmp/a.txt")
sandbox.files.write("/tmp/a.txt", "hello")
sandbox.files.write("/tmp/a.bin", b"binary-data")
sandbox.files.upload("./local.txt", "/workspace/local.txt")
sandbox.files.download("/workspace/output.txt", "./output.txt")
```

`read()` and `download()` buffer the complete response in memory. `download()`
returns bytes and can also write to a destination path; use its `byte_range`
argument to retrieve large files in chunks.

## Commands

```python
execution = sandbox.commands.run(
    "python --version",
    cwd="/workspace",
    envs={"PYTHONUNBUFFERED": "1"},
    wait=True,
    wait_timeout_seconds=25,
)

print(execution.status)
print(execution.exit_code)
print(execution.stdout)
print(execution.stderr)
```

`stdout` and `stderr` are each capped at 1 MiB. Check `execution.stdout_truncated`
(and `stderr_truncated`); for large output, write to a file and download it.

You can refresh or cancel a running execution:

```python
execution.refresh()
execution.cancel()
```

## Snapshots

Prepare a sandbox once, save it as a snapshot, and launch new sandboxes from it:

```python
base = client.sandboxes.create(template_id="template-id", timeout=900)
base.wait_until_running()
base.connect()
base.commands.run("npm i -g @anthropic-ai/claude-code", wait=False).wait(timeout=600)

snapshot = base.create_snapshot(name="agent-base")
snapshot.wait_until_ready()

worker = client.sandboxes.create(snapshot_id=snapshot.snapshot_id, timeout=900)
worker.wait_until_running()
worker.connect()
```

Sizing is inherited from the snapshot. Environment variables passed with
`env_vars` are not part of a snapshot; pass them again on each create. Clean up
caches and credentials before taking a snapshot.

```python
client.snapshots.list(source_sandbox_id=base.sandbox_id)
snapshot.update(name="agent-base-v2")
snapshot.delete()   # delete sandboxes launched from it first, or this raises ConflictError
```

## Pause and resume

```python
sandbox.pause()      # state becomes paused
sandbox.resume()     # cold start; pass timeout= to reset the lifetime
sandbox.wait_until_running()
sandbox.connect()    # reconnect before data-plane calls
```

A paused sandbox keeps its disk files except `/tmp`; memory and running
processes are not kept.

## Templates

```python
template = client.templates.get("template-id")
template.update(name="new-name")
template.delete()

builds = template.builds()
build = template.build("build-id")
logs = template.build_logs("build-id", offset=0, limit=100)
```

Creating a template requires an idempotency key:

```python
template = client.templates.create(
    name="demo",
    resources={"type": "preset", "product": "gmi.sandbox.small"},
    build={"source": {"type": "image", "image": "ubuntu:22.04"}},
    idempotency_key="template-1",
)

print(template.template_id)
template.update(name="new-name")
```

Create from a Dockerfile and local build context:

```python
from pathlib import Path

template = client.templates.create(
    name="dockerfile-demo",
    resources={"type": "preset", "product": "gmi.sandbox.small"},
    dockerfile_content=Path("Dockerfile").read_text(),
    build_context_dir=".",
    idempotency_key="template-dockerfile-1",
)
```

The live API accepts one uploaded context archive. The SDK translates
`COPY . <destination>` and `ADD . <destination>` to the API's Template-file
reference syntax. Other local source forms are rejected before a request is
sent. `AsyncSandboxClient.templates.create()` accepts the same arguments.

Rebuild an existing template without replacing it with
`client.templates.builds.create(template.template_id, dockerfile_content=..., build_context_dir=..., start_cmd=..., idempotency_key=...)`; the async client exposes the same call and returns the newly created build, while `template.wait_until_ready()` remains the readiness helper.

## Product specifications

```python
client.product_specifications.list(idc_name="gmi-sandbox-us")
```

## Errors

The SDK raises typed exceptions for HTTP failures:

- `BadRequestError`
- `AuthenticationError`
- `PermissionDeniedError`
- `NotFoundError`
- `ConflictError`
- `RateLimitError`
- `ServerError`

Example:

```python
from sandbox_sdk import NotFoundError

try:
    client.sandboxes.get("missing")
except NotFoundError:
    print("sandbox not found")
```
