# Python SDK

The whole API from Python, for plain code and for asyncio. It waits for a machine to be running, sends a request again where that is safe, and raises every refusal as an error with the API's code.

```shell
pip install agental-sdk
```

- Installed as `agental-sdk`, imported as `agental`: `from agental import Agental`.
- Python 3.10 or later, with `httpx` and nothing else.
- Typed: it carries `py.typed`.
- Names are the API's in snake case, such as `exit_code` and `idempotency_key`. One that is a Python keyword ends in `_`: a message's sender is `from_`.

## Make a client

Make an API key in the console, under API keys, and put it in `AGENTAL_API_KEY`. A client made with nothing reads it from there.

```python
from agental import Agental

agental = Agental()  # reads AGENTAL_API_KEY

sandbox = agental.sandboxes.create(size="small")
result = sandbox.exec("uname -a")
print(result.exit_code, result.stdout)

sandbox.files.write("app/main.py", "print('hello')\n")
print(sandbox.exec(["python3", "/opt/agental/app/main.py"]).stdout)

sandbox.stop()  # the disk is kept
```

- `create` answers when the machine is running. A sandbox you have already is `agental.sandboxes.get(id)`, and `agental.sandboxes.list()` gives all of them.
- A command given as a string is a line of shell, run by `sh -c`. Given as a list, its arguments are passed exactly, with no shell.
- A command gets 60 seconds unless you pass `timeout`, in seconds, up to 90, since a proxy in front of the API stops waiting after 100 seconds. One that runs out is killed and exits `124`. Longer work goes to `sandbox.commands.start`, which takes up to 600 and answers at once.
- File paths are relative to the work folder, `/opt/agental`. `files.read` gives text, and `files.read_bytes` bytes.
- A time can be a `datetime`, which must say its time zone, or a string in ISO 8601.

## Output as it arrives

```python
for event in sandbox.exec_stream("npm test"):
    if event.type == "exit":
        print("exited", event.exit_code)
    else:
        print(event.data, end="")
```

For a command that takes more than a minute, `sandbox.commands.run` runs it without holding a request open, and `commands.start` starts it to be looked at later. See [Running commands](https://agental.io/docs/commands).

## With asyncio

`AsyncAgental` is the same client, awaited.

```python
import asyncio

from agental import AsyncAgental


async def main() -> None:
    async with AsyncAgental() as agental:
        sandbox = await agental.sandboxes.create()
        print((await sandbox.exec("uname -a")).stdout)
        await sandbox.stop()


asyncio.run(main())
```

`async with` closes the client's connections at the end; without it, call `await agental.aclose()`. `Agental` does the same with `with`, or `agental.close()`. `exec_stream` is iterated with `async for`.

## Where the key comes from

- `api_key`, when you pass one.
- Otherwise `AGENTAL_API_KEY`.
- Otherwise the key the [agental command](https://agental.io/docs/cli) keeps on this computer, in `~/.config/agental/credentials.json`, for the API it signed in to and no other.

The API is `base_url`, otherwise `AGENTAL_API_URL`, otherwise the API the command signed in to, otherwise `https://agental.io`.

## Options

| Option | What it does |
| --- | --- |
| `api_key` | The API key. Left out, as above. |
| `base_url` | Where the API answers. Left out, as above. |
| `max_retries` | How many times a request that is safe to send again is sent again. 3 unless you say. |
| `retry_delay` | The shortest wait before it is sent again, in seconds. 1.0 unless you say. |
| `poll_interval` | How often something waited for, such as a machine starting, is looked at, in seconds. 2.0 unless you say. |
| `http_client` | An `httpx.Client` to send requests with, or an `httpx.AsyncClient` for `AsyncAgental`. Left out, one of its own. |

## What is in it

Each guide shows its calls in Python. Where to find them:

| Calls | Guide |
| --- | --- |
| `agental.sandboxes.create`, `get`, `list`; `sandbox.stop`, `start`, `update`, `delete` | [Sandbox lifecycle](https://agental.io/docs/lifecycle) |
| `sandbox.exec`, `exec_stream`, `commands.run`, `commands.start` | [Running commands](https://agental.io/docs/commands) |
| `sandbox.files.read`, `read_bytes`, `write` | [Files](https://agental.io/docs/files) |
| `sandbox.browser` | [Browser](https://agental.io/docs/browser) |
| `sandbox.desktop` | [Desktop](https://agental.io/docs/desktop) |
| `sandbox.snapshot`, `restore`; `agental.snapshots` | [Snapshots](https://agental.io/docs/snapshots) |
| `sandbox.fork` | [Forking](https://agental.io/docs/fork) |
| `agental.templates`, `sandbox.secrets` | [Templates and secrets](https://agental.io/docs/templates) |
| `template.set_network`, `sandbox.set_network` | [Outbound rules](https://agental.io/docs/outbound-rules) |
| `sandbox.expose`, `routes` | [Public URLs](https://agental.io/docs/ports) |
| `sandbox.email`, `agental.email` | [Email](https://agental.io/docs/email) |
| `agental.create_webhook`, `list_webhooks`; `verify_webhook`, imported on its own | [Webhooks](https://agental.io/docs/webhooks) |
| `sandbox.metrics`, `agental.metrics`, `running_over_time` | [Metrics](https://agental.io/docs/metrics) |
| `agental.usage`, `sandbox.usage` | [Usage](https://agental.io/docs/usage) |
| `agental.set_telemetry`, `test_telemetry` | [OpenTelemetry](https://agental.io/docs/opentelemetry) |
| `agental.billing`, `prices`, `checkout`, `burst`, `set_burst_budget` | [Billing](https://agental.io/docs/billing) |
| `agental.api_keys` | [API keys](https://agental.io/docs/api-keys) |
| `agental.export_account` | [Your account and data](https://agental.io/docs/account) |

## What it does for you

- A create or a fork is sent with an `Idempotency-Key`. If its answer is lost, or the machine is not confirmed yet, the same request is sent again with the same key, which gives the same sandbox back and never a second one. Pass `idempotency_key` yourself to be able to ask again after your process has died.
- Reads, file writes, starts and stops are sent again when a proxy answers in the API's place, when the connection drops, and when the sandbox was moved to another server during the request.
- A command is never sent twice. One that was cut off may have done part of its work, so whether to run it again is yours to decide.
- A delete carries the confirm header the API asks for.

## Errors

```python
from agental import ApiError

try:
    sandbox.exec("make")
except ApiError as error:
    if error.code == "sandbox_not_running":
        sandbox.start()
    else:
        raise
```

| Error | What it means |
| --- | --- |
| `ApiError` | The API's own refusal, with `status`, `code` and `message`. Decide from `code`: [Errors](https://agental.io/docs/errors) lists them. |
| `ProxyError` | A 502, a 504 or any answer with no code in it: something in front of the API answered, and the request may still be running. |
| `AgentalConnectionError` | The API was not reached. |
| `AgentalTimeoutError` | A wait ran out, such as for a machine to start. |

All of them are `AgentalError`. A command that exits non-zero is not an error: read `exit_code`.

> **Next:** Building with the OpenAI Agents SDK? [agental-openai-agents](https://agental.io/docs/openai-agents) runs its agents in an agental sandbox. The [TypeScript SDK](https://agental.io/docs/typescript) is the same API in TypeScript.

---

Every page of these docs as Markdown: [llms.txt](https://agental.io/llms.txt)
