---
title: "Python SDK"
description: "The Python SDK, one client with a namespace for each product (Cube, Sandbox, Browser, Drive, Database) and the platform's own calls. Standard library only, Python 3.9 and later."
---

> Documentation Index
> Fetch the complete documentation index at: https://glac.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

## Install

No dependencies beyond the standard library; Python 3.9 or later. Calls block, and any number of threads may make them at once. The package is `glac` on PyPI:

```bash
pip install glac
```

## The client

```python
from glac import Glac, Sandbox

glac = Glac()                                       # GLAC_API_KEY and GLAC_API_URL; or Glac(api_key=..., base_url=...)

app = glac.cube.deploy("ghcr.io/acme/api:1.4", name="api", port=8080)   # Cube: waits until it serves
print(app.url)

sbx = glac.sandboxes.create(image="python:3.12")    # Sandbox
print(sbx.run("python3 -c 'print(6*7)'").stdout)

drive = glac.drives.create(name="data", size_bytes=10 << 30)               # Drive: disks that outlive sandboxes

print(Sandbox.run("echo hello").stdout)             # the shorthand: a sandbox for one command, on the default client
```

`glac.cube.deploy` is `glac deploy`: a new version of the app called `api`, which is made first where there is none, waited for until it has the app's traffic. The last line is the one shorthand there is, the sandbox SDKs' one-liner without a client. `with glac.sandboxes.create() as sbx:` deletes the sandbox when the block ends; nothing else deletes anything but the one-off sandbox and your own `delete()`.

`Glac()` reads the key from `GLAC_API_KEY` and the address from `GLAC_API_URL` (default `https://api.glac.ai`).

| Call | What it does |
|---|---|
| `Glac(api_key=, base_url=, timeout_seconds=, connect_timeout_seconds=, retries=)` | The address, the key, the timeouts and the retries. Any number of threads may share a client. |
| `glac.cube` | Cube: apps (below). `glac.workers` is another name for it, which warns that it is deprecated. |
| `glac.sandboxes`, `.snapshots`, `.ssh_keys` | Sandboxes, their snapshots, and the SSH keys that open them (below). |
| `glac.drives`, `glac.volumes`, `glac.backups` | Drives (a control plane's), the volumes underneath them, and the backups of volumes (a control plane's; below). |
| `glac.databases` | Databases: managed Postgres, and a connection string ready for psycopg (below). |
| `glac.domains` | The project's own domains, names for an app or for a port of a sandbox (below). |
| `glac.webhooks`, `glac.keys` | Where the project's events are sent, and its API keys (below). |
| `glac.destinations` | Where the logs, metrics and traces of the project's apps are exported (below). |
| `glac.secrets`, `glac.variables` | The project's secrets and shared variables (below). |
| `glac.deploys` | The project's deploys (`/v1/deploys`, a control plane's): several of its services deployed together (below). |
| `glac.account()`, `update_account`, `usage`, `billing`, `update_billing`, `balance`, `update_balance`, `top_up(usd)`, `charges`, `ledger`, `project`, `events` | The platform's own calls, which belong to no product (below). |
| `glac.request("GET", "/v1/webhooks")`, `glac.pages(path)` | An API the SDK does not wrap: its answer's JSON; every item of a list, page after page. |

The products are namespaces, each holding the client it was reached by. The objects keep the API's names: a Cube is an `App` (the API calls it an app), and `Sandbox`, `Snapshot`, `Drive`, `DriveSnapshot` (a snapshot of a Drive), `Volume`, `Backup` (a backup of a volume), `Domain`, `SshKey`, `Secret`, `Variable`, `Version`, `Instance`, `Deploy`. What is done to one of them is its own method: `sbx.run`, `app.deploy`, `drive.delete`.

**The shorthand.** `Sandbox.run`, `Sandbox.create`, `Sandbox.get`, `Sandbox.get_or_create` and `Sandbox.list` are the calls of `glac.sandboxes` on the client made from the environment the first time one is needed, and shared from then on; `client=` names another. They are the same code. Nothing else works without a client.

`None` means something on some options (`sleep_after_seconds=None` is never sleep, `env={"KEY": None}` removes the key), so an option that is not given is `UNSET` (`from glac import UNSET`), and is left as it was.

## Cube

A Cube, which the API calls an app, is an image that runs as a service at an address of its own, with as many instances as its requests need and none while there are none. Every deploy is a version, which takes the traffic when it is ready. Apps are a control plane's: against a node that stands alone every call is the node's 404 `no_such_route`.

| Call | What it does |
|---|---|
| `glac.cube.deploy(image, name=, ...)` | An image as a Cube. To the app of `name`: its own settings that are given (`regions`, `scale`, `metadata`, `keys`, `egress`, `logs`, `traces`, `public`, `depends_on`, `network`) are changed first, then a version is made of the image and the rest. No such app, or no name: the app is made with everything, its first version with it. A name somebody else took in between (409 `name_taken`) is a deploy to their app. Returns the app once the version has its traffic. |
| `glac.cube.create(name=, image=, port=, env=, secrets=, volumes=, drives=, key_drives=, ..., wait=, timeout_seconds=)`, `get(id_or_name)`, `get_or_create(name, ...)`, `list()` | Makes an app (with an `image`, its first version, waited for); finds one; finds or makes one, deploying nothing; lists. `drives` are the Drives its one instance mounts read-write (`ServiceDrive`; an app of many instances or of keys is refused, 409 `drive_single_writer`); `key_drives` give each key's instance of a keyed app Drives of its own (`KeyDrive`), made with its first instance and deleted with the key. |
| `app.deploy(image=, env=, ...)`, `app.version(n)`, `app.versions()`, `app.rollback(version)` | A new version, waited for until it has the traffic, as a `Version`; one version; all of them; traffic to another version. |
| `app.update(...)`, `app.refresh()`, `app.delete()` | A change (of what a version is made of, a new version); read it again; delete. |
| `app.managed_by` | What changes the app alone (a `ManagedBy`). The database it is under, `{"object": "database", "id": "db-…"}`: an update, a deploy, a rollback or a delete of it here is refused, 409 `managed_by_database` (its log is read and cut as any app's). Or the Sandboxes product, `{"object": "sandboxes", "id": None}`: the project's default sandbox app, `sbx-sandbox`, or the app of an image a sandbox was made of, whose instances are the project's sandboxes; every change but naming the default app's image (`app.update(image=)`) is refused, 409 `managed_by_sandboxes`, and it may be deleted. `None` for an app that is none's. |
| `runtime=` of `glac.cube.create`, `deploy`, `get_or_create`, `app.deploy` and `app.update` | The version's [runtime settings](/docs/cube/versions/#runtime-settings), a `ServiceRuntime`: `ulimits` (`{"nofile": 65535}`), `sysctls`, `tmpfs`, `dns`, `shm_size_bytes`; replaced whole. |
| `app.logs(follow=, since=, kinds=, status=, contains=, cursor=, ...)`, `app.log_usage()`, `app.delete_logs(before=)`, `app.logs_state` | The app's log: `OutputLine`, `RequestLine` and `PlatformLine`, from the object store and the machines, picked up again after a lost connection; what it counted; deleting lines; whether each machine that keeps it keeps what is printed there now for the long term ([logs](/docs/platform/logs/#what-is-kept-and-for-how-long)). |
| `app.instance(key)`, `app.get_instance(key)`, `app.instances()`, `app.delete_instance(key)` | Keyed instances, one per key, each an `Instance`, which is a sandbox. |
| `instance.upgrade(version=, when=, keep_days=)`, `app.upgrade_instance(key, version=, when=, keep_days=)`; `upgrade.wait(timeout_seconds=)`; `instance.upgrades()`, `app.instance_upgrades(key)`, `app.instance_upgrade(key, id)` | Moves a key's instance to another version of the app, [in place](/docs/cube/keyed-instances/#a-new-version-for-one-instance): the app's current version unless `version` names one (an id, a number or a `Version`; one that is ready, an older one too). It keeps its id, its node, its key, its address, its tokens and its own Drives with what they hold; its disk, its machine and its processes become the new version's, its open connections end, and its requests are held at the edges while it is switched. Returns an `Upgrade` (`status` `preparing`, `switching`, `done` or `failed`; `from_version`, `to_version`, `timings`, `error`, `put_back`; `kind` (`upgrade`, or `revert`), `when`, `keep_days`, `rollback` (what a revert of it needs, kept for `keep_days`), `reverts`, `batch`; an upgrade asked through the API to wait for its instance to sleep is `waiting` until then, and `cancelled` where it was cancelled (the API's `DELETE`, or its batch stopped): one that was cancelled is over, and its wait raises as one that failed does, with the code `cancelled`), or `None` where the instance is on that version already. `wait()` raises `UpgradeError` (its code `upgrade_failed` or `upgrade_not_here`) where the new version did not come up: the instance is then on the version it was on. `instance.refresh()` reads the instance again, its `version_id` among the rest. `when="asleep"` has it wait (`waiting`) until the instance sleeps and begin then, so that none of its requests is held; `keep_days` (0 to 365, 7 unless given) is how long what a revert needs is kept once it is done, billed as snapshots are. A version whose services mount other Drives or volumes than the instance's is no upgrade in place: it ends `failed` with `upgrade_not_here`. |
| `upgrade.cancel()`, `app.cancel_instance_upgrade(key, upgrade)`; `upgrade.revert(keep_days=)`, `app.revert_instance_upgrade(key, upgrade, keep_days=)` | An upgrade that waits (for its instance to sleep, or for its turn in a batch) cancelled: `cancelled`, the instance on its version (one that has begun is 409 `upgrade_started`). A [revert](/docs/cube/keyed-instances/#a-new-version-for-one-instance): the instance back to what an upgrade kept, its disk, its Drives as they were before it (a snapshot of what they hold now made first) and the version it came from; a new `Upgrade` of `kind` `revert`, `reverts` naming the upgrade, which goes and is waited for as an upgrade is, and keeps what a revert of it needs in its turn. Refused 409 `rollback_gone` where what it goes back to is not kept any more. |
| `app.upgrade_instances(version=, from_versions=, keys=, when=, keep_days=, concurrency=, max_failures=)`; `app.upgrade_batches()`, `app.upgrade_batch(id)`, `app.stop_upgrade_batch(id)`; `batch.wait(timeout_seconds=)`, `batch.refresh()`, `batch.stop()` | A [batch](/docs/cube/keyed-instances/#a-new-version-for-one-instance) of upgrades of the app's keys to one version (the app's current one unless given), of `from_versions` and among `keys` where given: each once its instance sleeps (`when="asleep"`, the default) or as its turn comes (`"now"`), `concurrency` at once (4; two on one node), stopped `failed` after `max_failures` (5). Returns an `UpgradeBatch`: `status` (`running`, `done`, `failed`, `stopped`), `stop_reason`, `counts` (`total`, `waiting`, `preparing`, `switching`, `done`, `failed`, `cancelled`), `upgrades` (each member's key, upgrade, status and error), `skipped` (the keys with an upgrade of their own not over), `origin` (`api`, or `deploy` where the app's `keys` say `upgrade_on_deploy`). `wait()` returns it however it ended, raising nothing; a member whose instance never sleeps keeps it `running`, so `timeout_seconds` raises `GlacTimeoutError`. `stop()` cancels the members that wait. |
| `app.open_instance_port(key, port, from_=[...])`, `app.instance_ports(key)`, `app.close_instance_port(key, port)` | The ports open at the own public IPv6 address of a key's instance, wherever it is: its sandbox's `open_port`, `ports` and `close_port`, by the app and the key (`protocol="udp"` for UDP; [keyed instances](/docs/cube/keyed-instances/#its-own-ipv6-address)). An instance with no address of its own is refused (409 `no_ipv6`). |
| `checkpoint=` of `glac.cube.create`, `deploy`, `get_or_create` and `app.update`; `app.checkpoint`; `sbx.checkpoint` | [Checkpoints](/docs/cube/keyed-instances/#checkpoints) of the app's keyed instances while they are awake (an `AppCheckpoint`, `{"every_seconds": n}`: 1 to 600, 10 when it is left out or `None`; `app.update(checkpoint=None)` takes them away), and how an instance's go (a `CheckpointStatus`: `every_seconds`, `last_at`, `behind_seconds`, `bytes_per_second`, `stored_bytes`, `error`; `None` without them). |
| `app.metrics(start=, end=, step=)` | What the machines that serve the app counted of it, a step a minute (`step="hour"`: an hour): requests by the class of their status, how long they took, its instances and what they used ([metrics](/docs/platform/logs/#metrics)). The API's object. |
| `traces=` of `glac.cube.create`, `deploy`, `get_or_create` and `app.update`; `app.traces` | A span of the platform's for a share of the app's requests, which the instance's own spans are children of (an `AppTraceSettings`: `{"sample_rate": 0.1, "trust_incoming": False}`; none unless given), sent to the project's destinations that take `"traces"` ([traces](/docs/platform/logs/#traces)); what the app has. |
| `app.create_key_secret()`, `app.key_secrets()`, `app.key_secret(id)`, `app.delete_key_secret(id)`, `glac.sign_key(secret, app_id, key, expires_at=, path=)` | The secrets an app's keys are signed with (a `KeySecret`, whose `secret` only the one just made has), and a key signed with one, as a request through the edge carries it ([signed keys](/docs/cube/keyed-instances/#signed-keys)); `path=True` writes it as a path segment. |

```python
from glac import DeployError, Glac

glac = Glac()
app = glac.cube.deploy("public.ecr.aws/docker/library/python:3.12-alpine", name="hello",
                          start_command=["python3", "-m", "http.server", "8000"], port=8000)   # waits for version 1
print(app.url)
app.deploy(env={"STAGE": "prod"})               # a new version; waits until it has the traffic
try:
    app.deploy(start_command="exit 3")
except DeployError as e:                        # the traffic stays where it was
    print(e.code, e.version.number)
app.rollback(1)
for line in app.logs(follow=False):
    print(line)
```

A call that has a version built waits for it unless told not to (`wait=False`), and raises `DeployError` when it fails, with the version's own code and words. `timeout_seconds=` raises `GlacTimeoutError` after that long, and the deploy goes on.

Several services of the project are deployed together with `glac.deploys` ([a project's deploy](/docs/cube/services/#deploying-a-project)): the control plane takes them in the order of what each waits for (stateful services first, then jobs, then the rest, switched together), and a deploy that fails switches nothing that was not switched yet.

```python
d = glac.deploys.create({"api": {"image": "ghcr.io/acme/api:2"}, "web": {}})   # waits until it is over
print(d.status, [(s.service, s.status, s.version) for s in d.services])
```

`create(services, wait=True, timeout_seconds=None)` takes each service's version fields as the API does (`{}`: as it is), and waits (`Deploy.wait`), following a deploy that took its place; one that fails raises `DeployError` with `deploy`, `service` and the deploy's `code` (`job_failed`, `job_timed_out`, `job_lost`, …). `get(id)` and `list()` (newest first) read them; a `Deploy` has `status`, `cause`, `services` (`DeployService`s), `error`, `replaced_by`, `steps` and its times.

## Sandbox

| Call | What it does |
|---|---|
| `glac.sandboxes.create(image=, snapshot=, name=, env=, metadata=, volumes=, drives=, public_ports=, sleep_after_seconds=, sleep_notice_ms=, sleep_hold_max_ms=, expires_at=, egress=, start=, start_command=, ready=, project_network=, hostname=, stop=, checkpoint=, image_pull_secret=)` | Makes a sandbox; every option may be left out. `drives=[DriveMount("/data", drive)]` mounts Drives (below). A service started when it boots: `start=True` (the image's own start command) or `start_command=`, with `ready={"tcp": True, "port": 5432}` (or `{"http": "/health", "port": 8080}`, `{"command": "…"}`); `hostname=` names its machine, and `stop={"signal": "SIGINT", "grace_ms": 30000}` says how its service is stopped; `project_network=True` puts it on the project's private network from the start, at `sbx.project_address`; `VolumeMount("/data", vol, copy=True)` gives a new volume what the image has at that path ([making a sandbox](/docs/sandbox/commands/#making-a-sandbox)). |
| `sbx.app_id`, `sbx.version_id`, `sbx.key`, `sbx.image_digest` | A sandbox made through the control plane is an instance of an app: the project's default sandbox app (`sbx-sandbox`) when it was made with no image, else the app of its image (`glac.apps.get(sbx.app_id).instances()` lists that app's sandboxes, each under its key); the version of that app it is of, the image it was made of (a tag that was moved since is a new version of the app, which the sandboxes made after it are of); its key there, its name or its id; and its image by digest. Said by its create's answer and by every read of it, and kept; `None` for one made from a template or a snapshot, and for a fork. |
| `glac.sandboxes.get(id_or_name)`, `get_or_create(name, ...)`, `list(status=, metadata=)` | Finds one; finds or makes one; lists. |
| `sbx.start_command` | How its start command runs, as its guest said last (a `StartCommandState`): `status` (`running`; `restarting`, started again at `restarting_at`; `exited`), `restarts` (a key's instance and a stateful service's are started again in their machine when their command ends on its own, each start an event `instance.restarted`), `last_exit` (`status`, `signal`, `at`); `None` where its node said nothing of it. An `Instance` has it too. |
| `suspend_after_ms=`, `wake_daily_at=` of `create`, `get_or_create` and `sbx.update`; `sbx.suspend_after_ms`, `sbx.wake_daily_at` | How long it stays paused, once idle has stopped it, before its memory is saved and it sleeps for real (a second to a day; its node holds it to ten minutes at the most), and a time of day in UTC, `"HH:MM"`, at which it is woken every day while it rests, for work of its own that waits for a wake; `None` in an update takes either back. |
| `on_expiry=`, `held_asleep=` of `create`, `get_or_create` and `sbx.update`; `sbx.on_expiry`, `sbx.held_asleep` | What `expires_at` does when it comes: `"delete"` the sandbox (the default), `"sleep"` (put it to sleep and keep it, a request waking it as ever) or `"hold"` (the same, held asleep); and whether a request to one of its ports wakes it while it sleeps: held asleep, its preview address answers 502 `sandbox_paused`, and a call of the API wakes it. `None` in an update takes either back. |
| `init="systemd"` of `create`, `get_or_create` and `run`; `sbx.init` | A whole system: the image's own systemd is the sandbox's PID 1 (without `image`, the platform's Ubuntu with systemd and OpenSSH), so packages, services started with `systemctl`, cron and the journal work as on a machine, and it has a machine ID of its own, kept through sleeps, wakes and moves. Not with `start`, `start_command` or `ready` (400 `invalid_start`), nor with `snapshot`. `sbx.init` is `"systemd"` for one and `None` for the platform's init. |
| `glac.sandboxes.run(command, ...)` | A sandbox for one command: made, used, deleted. |
| `sbx.run(command, cwd=, env=, user=, timeout_seconds=, stdin=, on_stdout=, on_stderr=)` | Runs and waits, however long. Returns a `Result`. |
| `sbx.exec(command, stdin_open=, tty=)` | Starts and returns a `Command` at once: `wait()`, `output()`, `write()`, `kill()`. |
| `sbx.terminal(command=, cols=, rows=, join=)` | A terminal. |
| `sbx.files` | `read`, `read_text`, `write`, `list`, `stat`, `exists`, `mkdir`, `move`, `remove`, and `set_attributes(path, mode=, size_bytes=, modified_at=)`: a file's mode, its length, its time of last modification. |
| `sbx.files.upload(local_path, path)`, `download(path, local_path)`, `read_stream(path, offset=, length=, chunk_size=)`, `write(path, file_or_iterable)` | A file of any length in one call, held whole nowhere, neither here nor by the API: `upload` sends a local file as it reads it, `download` writes the bytes to a local file as they come (put in place when every byte has come), `read_stream` gives them a piece at a time, and `write` also takes a file opened in binary (`open(p, "rb")`) or any iterable of bytes. An answer that ends short of its length raises `GlacConnectionError`: read it again. |
| `sbx.url(port)`, `sbx.rotate_urls()`, `sbx.token(scopes=, expires_in_seconds=)` | A link to a port (signed locally, no request); new link secret; a token for this sandbox only. |
| `sbx.ipv6`, `sbx.open_port(port, from_=[…], protocol="tcp")`, `sbx.ports("tcp")`, `sbx.close_port(port, protocol="tcp")`, `sbx.update(inbound=[…])` | Its own public IPv6 address, where it has one (`None` otherwise); a port opened at it to the sources listed (IPv6 prefixes or addresses, `"::/0"` for everybody), with where it is reached; the ports open; one closed (it returns whether it was open); the whole list, each a dict of `protocol`, `port` and `from`. Inbound is closed by default; what is open stays open while it sleeps and when it moves. See [networking](/docs/platform/networking/#a-sandboxs-own-ipv6-address). |
| `sbx.revoke_token(token)` | That token alone is refused from then on, and every other works as before: a `Token` (whose `id`, `tok_…`, says which), the token itself, or its id. A sandbox keeps 1,000 such revocations that have not run out (409 `too_many_revoked_tokens` past them: `revoke_tokens()` then). Returns a `TokenRevocation` with its `token_id` and `expires_at` (when the token runs out, and its revocation with it). |
| `sbx.revoke_tokens()` | Every token handed out for this sandbox so far stops working (401 `invalid_token`), those in preview links and cookies and those SSH sessions hold too; tokens made after it work. Needs the API key. Returns a `TokenRevocation`: `sandbox_id`, `generation`, `revoked_at`. |
| `sbx.update(...)`, `sbx.refresh()`, `sbx.sleep()`, `sbx.restart()`, `sbx.delete()` | Change it; read it again; sleep now; boot from disk; delete. |
| `checkpoint=` of `glac.sandboxes.create` and `sbx.update`; `sbx.checkpoint` | [Checkpoints](/docs/sandbox/sleep/#checkpoints) while it is awake (`{"every_seconds": 10}`; `sbx.update(checkpoint=None)` takes them away), so that it goes on with its processes when its machine is lost; how they go (a `CheckpointStatus`). 409 `checkpoints_unavailable` where no machine takes them. |
| `sbx.disk_writes` | What its disk has written today (a UTC day), against the day's share at full speed (a `SandboxDiskWrites`): `today_bytes`, of which `checkpoint_copy_bytes` its checkpoints' copies wrote to the host's disk; `full_speed_until_bytes`, the share (100 GiB unless its node is told otherwise; `None`: its node slows nothing, and counts all the same); `limited` and `limited_until`: past the share its disk takes writes at 2 MiB a second until the day ends, and no write fails. `None` on a node that does not copy its disks to an object store, which counts nothing. What it writes to its Drives is not counted here: a Drive's writes are billed by the gigabyte. |
| `sbx.snapshot(name=)`, `sbx.fork(count)`, `sbx.restore(snapshot)` | Snapshot; fork; back to a snapshot in place. |
| `glac.sandboxes.snapshots.get(id_or_name)`, `.list(sandbox_id=)`, `snap.update(name=, expires_at=)` | Snapshots, and whether each is in the object store yet (`replicated`); another name for one (`None`: none), or when it deletes itself (`None`: never). |
| `glac.sandboxes.ssh_keys.create(public_key, sandbox_id=, expires_at=)`, `.get`, `.list` | Public keys that open sandboxes through the SSH gateway; a private key is refused before anything is sent. |
| `sbx.run_code(code)`, `sbx.agent("claude")`, `Desktop(sbx)` | Stateful code execution, coding agents, and the screen of a desktop, in sandboxes made from the code interpreter, agents and desktop images. |

`glac.sandboxes.run` (and `Sandbox.run`, on the class) is a sandbox for one command; `sbx.run` runs a command in the sandbox `sbx`. Output as it comes:

```python
res = sbx.run("make", on_stdout=lambda text: print(text, end=""))   # watch and wait

cmd = sbx.exec("node server.js")                                    # do not wait
for piece in cmd.output():                                          # piece.stream, .text, .data, .offset
    print(piece.text, end="")
```

Output arrives as the exact bytes the command wrote. `res.stdout` is a `str` (UTF-8, with undecodable bytes replaced), `res.stdout_raw` the `bytes`.

## Drive

A Drive is a disk of the project's that outlives its sandboxes: mounted read-write by one sandbox at a time, when the sandbox is made or while it runs, and kept, with what it holds, when the sandbox goes, for the next sandbox to mount: of its region, or of another whose machines read its home's object store (below). It is copied into its region's object store as it is written, so a sandbox on another node mounts it too: it is brought there first. Only what is written is billed (`used_bytes`). Its snapshots are versions of it in the object store, which any number of sandboxes mount read-only at once, which new Drives are forked from, and which it is restored to (below). Drives are a control plane's (`/v1/drives`): against a node that stands alone every call is the node's 404 `no_such_route`, and its disks are volumes (below).

| Call | What it does |
|---|---|
| `glac.drives.create(name=, size_bytes=, region=, durability=)`, `get(id_or_name)`, `list()` | Makes a Drive, empty but for its file system; every option may be left out (`size_bytes`: a whole number of 4 KiB blocks, at least 16 MiB; 64 GiB unless given, or 5 GiB on Free; `region`: `na`, `eu` or `ap`; `durability`: `standard`, or `strict`, whose guest's fsync returns once the object store has every write before it). Finds one; lists them, newest first. |
| `glac.drives.update(drive, name=)`, `glac.drives.delete(drive)` | Renames it (`None`: no name); deletes it, with what it holds. |
| `glac.drives.attach(sandbox, drive, path, mode=)`, `glac.drives.detach(sandbox, drive, force=)` | Mounts it in a sandbox that exists, read-write, at once (a sandbox that sleeps is woken); unmounts it (`force=True` while a process uses it). Each returns the Drive. |
| `drive.attach(sandbox, path)`, `drive.detach(sandbox)`, `drive.update(name=)`, `drive.refresh()`, `drive.delete()`; `drive.snapshot(name=, fs_consistent=)`, `drive.snapshots()`, `drive.fork(name=, from_snapshot=)`, `drive.restore(snapshot)` | The same, on a `Drive` (`fork` returns the new Drive, `restore` the Drive itself): `id` (a `vol-…`: a Drive is a volume), `name`, `size_bytes`, `used_bytes`, `attached_to` (`DriveAttachment`: the sandbox, the path, the mode; read-only mounts of its snapshots are not among them), `region`, `home`, `status`, `lease` (`DriveLease`: the node of its one writer, the epoch, until when), `app_id` and `key` (a keyed instance's), `parent_drive_id` and `root_drive_id` (the Drive it was forked from and the first of its line, which may be deleted since; `None` for a Drive that is no fork), `managed_by` (a `ManagedBy`, of a database's Drive; `None` for one that is none's), `created_at`. |
| `glac.drives.snapshot(drive, name=, fs_consistent=)`, `snapshots(drive)`, `all_snapshots(drive_deleted=)`, `get_snapshot(drive, snapshot)`, `delete_snapshot(drive, snapshot)` | Takes a snapshot (a `DriveSnapshot`); lists a Drive's snapshots, newest first, or the project's of every Drive (`drive_deleted=True`: those of Drives deleted since alone); finds one; deletes one. `drive` is a `Drive`, an id or a name (a deleted Drive's id); `snapshot` a `DriveSnapshot` or its id (`dsnap-…`). |
| `glac.drives.fork(drive, name=, from_snapshot=)`, `glac.drives.restore(drive, snapshot)` | A new Drive whose first version is the snapshot's, or the Drive's as it is now: returns the new Drive. The Drive back to one of its snapshots: returns the Drive. |
| `glac.drives.create(..., history_seconds=)`, `update(drive, history_seconds=)`; `glac.drives.versions(drive, at=)`, `fork(drive, at=)`, `restore(drive, at=)`; `drive.versions(at=)`, `drive.history_seconds`, `drive.history` | A Drive's window of versions (slice DB8): how far back it can be restored, or forked, by a time, 3600 to 86400 seconds (none unless given; turned off, what it kept goes). Every 30 seconds in which it was written the window gets a version, the Drive at one moment; what they keep alone is billed as a snapshot's is. A restore or a fork to a time takes the newest version at or before it, which the versions call with that time names. `versions` gives `DriveVersion`s (`at`, `version`, `consistency`, `exclusive_bytes`), the newest first; `restore` takes a snapshot or `at`, one of the two (a `ValueError` here for both or neither); `update(history_seconds=None)` turns the window off. |
| `glac.drives.attach_snapshot(sandbox, snapshot, path, drive=)`, `glac.drives.detach_snapshot(sandbox, snapshot, force=)` | Mounts a snapshot read-only in a sandbox that exists, at once; takes the mount away. Each returns the snapshot. A snapshot given by its id needs `drive=` (a `DriveSnapshot` carries its Drive); `detach_snapshot` refuses anything that is no snapshot's id before sending (`ValueError`), since the same path takes a Drive's read-write mount away. |
| `snap.refresh()`, `snap.fork(name=)`, `snap.restore()`, `snap.attach(sandbox, path)`, `snap.detach(sandbox, force=)`, `snap.delete()` | The same, on a `DriveSnapshot`: `id`, `drive_id` (its Drive, which may be deleted since), `name`, `consistency` (`frozen`, `paused`, `at_rest`), `used_bytes` (what the Drive held when it was taken), `exclusive_bytes` (what it keeps alone, which it is billed for), `exclusive_at`, `version` (`DriveSnapshotVersion`: `number`, `epoch`), `created_at`. |
| `DriveMount(path, drive, mode=None, snapshot=None)` | A Drive for `glac.sandboxes.create(drives=[...])` and `get_or_create`: 8 with the volumes at the most. With `snapshot` (a `DriveSnapshot` or its id), that snapshot read-only: it sends `mode: "snapshot"` and the snapshot's id. |

```python
from glac import DriveMount

drive = glac.drives.create(name="data", size_bytes=10 << 30)
sbx = glac.sandboxes.create(drives=[DriveMount("/data", drive)])
sbx.run("echo kept > /data/note")
glac.drives.detach(sbx, drive)
other = glac.sandboxes.create()
glac.drives.attach(other, drive, "/data")
print(other.run("cat /data/note").stdout)      # kept
```

- A Drive is one sandbox's at a time: one that another sandbox has is refused (409 `drive_attached`), and so is deleting one that is attached. A keyed instance's Drive goes with its key (409 `key_drive`).
- A database's Drive (`drive.managed_by`, `{"object": "database", "id": "db-…"}`) is mounted read-write by the database's instance alone, and renamed, restored and deleted through the database: those calls here, and a read-write mount of it, are refused (409 `managed_by_database`). Its snapshots and forks are the project's to take, and a snapshot of it anybody's to mount read-only.
- One that another node holds is brought to the sandbox's node first; while the object store does not hold it as it is yet (503 `copy_behind`), or it could not be brought (503 `drive_not_brought`), the call is `retryable`: the client tries again as its `retries` say, then raises, for you to ask again a moment later.
- **Across regions** (slice D9). A Drive's home (`home`) is the region whose object store holds its copy: the region it was made in. A sandbox of another region mounts it too, where that region's machines read its home's store (409 `not_on_one_machine` where they do not): the Drive is let go as a version and taken over there, read from its home's store as its guest reads it, and what its guest writes goes to the store of the region it is in now (`region`). Its home follows it once it has been there for an hour, or at once with `glac.drives.move_home(drive)` (`drive.move_home()`): its machine copies what is left in its home's store into its own, and `home` is the new region once nothing is left there; `drive.home_move` says how far the move is (`asked_at`, `asked_by`, `copied_objects`, `copied_bytes`, `objects_left`, `error`). Its old home's copy goes once no version kept needs it (a day after). A Drive's home moves once in six hours at the most (409 `home_moved_recently`); one whose home is the region it is in is 409 `home_here`.
- The plan's limits: 3 Drives of at most 5 GiB, 15 GiB together, on Free; 500 of at most 1 TiB on a paid plan (403 `drive_limit_reached`).
- `glac.usage(drive=...)`, `kind="drive"` and `group_by="drive"` report what Drives kept, were written and read cold. The events `drive.created`, `drive.deleted`, `drive.attached` and `drive.detached` carry an `EventDrive` or an `EventDriveAttached`, and `drive.snapshot.created` and `drive.snapshot.deleted` an `EventDriveSnapshot`.
- `mode` of `glac.drives.attach` is `read_write`: a read-only mount of a snapshot is `attach_snapshot`'s, which returns the snapshot (`mode="snapshot"` without one is 400 `missing_field`).

**Snapshots of a Drive.** A snapshot is a version of the Drive in the object store: nothing is copied for it, nor for a fork, a restore or a read-only mount of it.

```python
snap = drive.snapshot(name="before-upgrade")             # a running sandbox has the Drive: its writes held a moment, "paused"
reader = glac.sandboxes.create(drives=[DriveMount("/before", drive, snapshot=snap)])   # read-only, any number of sandboxes
copy = glac.drives.fork(drive, name="data-copy", from_snapshot=snap)
glac.drives.detach(sbx, drive)
glac.drives.restore(drive, snap)                         # once no sandbox has it read-write
```

- A Drive a running sandbox has is snapshotted with its writes held for a moment, ten seconds at the most (then 503 `unavailable`, retryable): `fs_consistent=True` freezes its file system first (`consistency` `frozen`; 409 `not_frozen` where the sandbox's agent cannot), otherwise its machine is paused, as at a power cut (`paused`). A Drive no running sandbox has is taken as it is (`at_rest`).
- A snapshot outlives its Drive, which is deleted without it: it is listed, read, forked and mounted by the Drive's id after the Drive is deleted, until it is deleted itself (`snap.delete()`). It is billed for what it keeps alone (`exclusive_bytes`), by the hour, at the Drives' data price ($0.02 a GB-month); `glac.usage(snapshot=snap.id)` reports it.
- A fork is a new Drive, mounted at once, the Drive's size, which says what it was made of (`parent_drive_id`, `root_drive_id`); it counts to the plan's Drives (403 `drive_limit_reached`), and a name the project has is 409 `name_taken`. **It is billed by its `used_bytes`, its full contents, like any Drive: a fork of a 10 GB snapshot is billed as a 10 GB Drive.**
- A restore writes a new version after the Drive's newest whose content is the snapshot's; the versions in between stay where a snapshot or a fork keeps them, so restoring to an older snapshot and then to a newer one works. It is refused while a sandbox has the Drive read-write (409 `drive_attached`: detach it first).
- A read-only mount: any number of sandboxes on any node of the Drive's region, at once, whatever has the Drive and after it is deleted; the guest cannot write it. Its blocks come from the object store as the guest reads them: **those cold reads cost nothing, and show under that sandbox in usage** (`glac.usage(sandbox=...)`, `drive_cold_read_bytes`); what that sandbox's machine holds already is read for nothing. A sandbox with a read-only mount does not move to another machine.
- Where the Drive's node copies nothing to an object store there are no snapshots, forks or restores (409 `not_copied`). An app's version mounts its Drives read-write only (`mode` `snapshot` there is 400 `invalid_field`): read-only snapshot mounts are a sandbox's.

`glac.volumes` is the API's `/v1/volumes`, the disks underneath Drives, kept for the transition: what only it has is a node that stands alone, whose disks are volumes, and shared volumes, file systems several sandboxes mount at once. A control plane makes no plain volume for a project's key.

```python
from glac import VolumeMount

vol = glac.volumes.create(name="team", shared=True)     # no size: a large limit, and the space of what is written
sbx = glac.sandboxes.create(volumes=[VolumeMount("/team", vol)])
print([v.name for v in glac.volumes.list()], glac.volumes.get("team").id)
```

A volume that is mounted is not deleted (409 `conflict`): `sbx.update(volumes=[])` takes it off.

**Backups of volumes.** A backup is a copy of a volume (a Drive is one) at one instant, in the object store, kept until it expires: taken by a call, every day by the volume's rule, or before a service's new version replaces its instance. Unlike a snapshot of a Drive it is a copy of its own, a volume is made from it alone, and it outlives its volume; a volume's backups keep only what changed since the one before, and a block several of them need is billed once, on the earliest. It is `making` until the store has all of it, then `ready`, and billed from then by what the store holds of it (`stored_bytes`) at the price of what sleeping sandboxes keep ([backups](/docs/drive/volumes/#backups)). Backups are a control plane's.

```python
import time

b = glac.volumes.backup("data", expires_in_days=7)      # "making", then "ready"
while b.refresh().status == "making":
    time.sleep(1)
copy = glac.backups.restore(b, name="data-back")        # a new volume: "restoring", then "ready"
```

| Call | What it does |
|---|---|
| `glac.volumes.backup(volume, expires_in_days=)`, `vol.backup(expires_in_days=)`, `glac.volumes.backups(volume)` | Takes one now, a `Backup`, kept `expires_in_days` (1 to 365) or as the volume's rule says (30 days unless it was set); a volume's backups, newest first (a deleted volume's by its id). A node that copies nothing to an object store has none (409 `not_copied`). |
| `glac.backups.list(volume=)`, `get(backup)`, `delete(backup)`, `restore(backup, name=)` | The project's backups, newest first, those of deleted volumes among them, or one volume's; one; deletes one (nothing more is billed for it; a volume made from it stays); a new volume made from a ready one, the same as `glac.volumes.create(from_backup=, name=)` (409 `backup_not_ready` while it is being made, 409 `backup_unusable` once it failed). |
| `b.refresh()`, `b.restore(name=)`, `b.delete()` | The same, on a `Backup`: `id` (`bak-…`), `volume_id`, `volume_name` (the volume's name then), `status` (`making`, `ready`, `failed`), `reason` (`manual`, `daily`, `before_version`), `consistency` (`frozen`, `crash`), `frozen_ms` (how long the volume's writers waited for the copy; `None` where it was not frozen), `size_bytes` (what the volume held), `volume_size_bytes` (the most it held: what a volume made from it holds), `stored_bytes` (what it is billed for), `own_bytes` (what deleting it would save), `shared_bytes`, `shared`, `region`, `failure`, `created_at`, `ready_at` (from when it is billed), `expires_at`. |

- A volume made from a backup has the size and kind of the volume it was taken of (`size_bytes` and `shared` are not given with `from_backup`: `ValueError`), in the backup's region. Its `status` is `restoring` until it holds the backup whole, then `ready`, and nothing attaches it before; `from_backup` names the backup. Where the machine it was placed on cannot bring it back, the control plane makes it once more on another machine of the region: `vol.info["restore_attempt"]` is 2 then, and `vol.info["first_failure"]` says why the first try failed. If that fails too, it is `failed` (`vol.info["failure"]`), and can only be deleted.
- `glac.usage(backup=...)`, `kind="backup"` and `group_by="backup"` report what backups kept, on the meter of state (`state_gb_hours`); `glac.billing()["meters"]["state"]["backups_gb_hours"]` says how much of that meter was backups.

## Domains

A domain of the project's is a name of your own that leads to an app, as its default address does, or to a port of a sandbox ([your own domain](/docs/cube/addresses/#your-own-domain)). Domains are a control plane's.

| Call | What it does |
|---|---|
| `glac.domains.create(domain, app=)`, or `(domain, sandbox=, port=)`, with `proxied=` | Registers the name, `pending_verification` until DNS shows a record of `domain.verification["records"]` (a CNAME, which also leads the name here, or a TXT record); 409 `domain_exists` when the project has it. |
| `glac.domains.get(id_or_name)`, `list(app=, sandbox=)`, `delete(domain)` | Finds one; lists them, or those that lead to an app or a sandbox; deletes one. |
| `domain.update(app=, sandbox=, port=, proxied=)`, `domain.refresh()`, `domain.delete()` | Points it elsewhere; reads it again (`status`, `certificate`); deletes it. |

```python
domain = glac.domains.create("api.example.com", app="api")
print(domain.verification["records"])
```

## Browser

**Not open yet.** Browser is not open to accounts yet: what follows is the SDK as it stands, ahead of its opening.

A session is a hosted Chromium that Playwright, Puppeteer or any other client of the Chrome DevTools Protocol (CDP) drives from outside. It is awake while a client is connected and falls asleep, its pages kept, `sleep_after_seconds` after the last one leaves. Its `cdp_url` is all a CDP client needs: the credential is in the address's host name, so it stays out of `repr`.

| Call | What it does |
|---|---|
| `glac.browsers.create(kind=, name=, region=, sleep_after_seconds=, connection_idle_seconds=, delete_after_idle_seconds=, metadata=, profile=, proxy=)` | A new session, awake (`kind`: `chromium-headless`, the default, `chromium`, on a screen, or [`lightpanda`](/docs/browser/#lightpanda), a browser without a layout engine). The name of one there is answers that one, woken, with its pages. `profile`: a profile's name, or `{"name", "save", "version"}`, loaded before the session is answered (below). `proxy`: your own proxy's URL (below). |
| `glac.browsers.get(id_or_name)`, `list(status=, kind=, name=)` | Finds one; lists them, the newest first (`status`: `awake`, `asleep`, `unavailable` or `failed`). One whose sandbox fails before it runs is no session: its create is refused, 503 `session_failed`. |
| `session.update(...)`, `session.sleep()`, `session.rotate_url()`, `session.delete(save_profile=None)` | Changes its times, metadata or proxy; puts it to sleep now (an open connection stays, and its next message wakes it); new addresses (the old ones are refused, and the connections they let in end); ends it (one made with a profile to save saves it first, unless `save_profile=False`). `glac.browsers` takes a session, an id or a name for each. |
| `session.cdp_url` | Where a CDP client connects: Playwright's `connect_over_cdp`, Puppeteer's `browserURL`. |
| `session.status`, `session.error` | `awake`, `asleep`, `unavailable` (its node cannot be reached) or `failed` (its sandbox failed after its create was answered: its browser stopped and did not come back, say); `error` says why, `{"code", "message"}` as its node says it, and is `None` unless it failed. The event `browser.failed` says so too (its data typed `EventBrowserFailed`). |
| `session.connect(playwright=None)`, `session.close()` | A Playwright browser connected over CDP, where Playwright is installed (`pip install playwright`, no browser of its own needed); `close` stops the Playwright it started. |
| `session.live_url`, `session.live(interactive=True)` | Where its page is watched live in a browser: a headless session's tab drawn as it changes, a headful one's screen; the viewer's mouse and keyboard reach the page. `live(interactive=False)` gives an address of its own where nothing reaches the page, to show people or to put in a page of your own. A viewer keeps the session awake, as a CDP client does. Both are rotated by `rotate_url` and need the key's `url` scope. |
| `session.save_profile(name=None)`, `session.profile` | Saves its profile now, as the newest version of `name` (the profile it was made with when not given); the profile it was made with or saved last (`{"name", "save", "version"}`). |
| `glac.browsers.profiles.list()`, `get(name)`, `delete(name)` | The project's profiles, each a `BrowserProfile` with its newest `version`, `bytes`, `saved_at` and every one of its `versions`. |
| `session.proxy`, `session.proxy_bytes` | Its own proxy, `{"url", "username"}` (never the password), or `None`; what went through it, both ways, in bytes (`session.proxy_sent_bytes` and `proxy_received_bytes`, each way). |

```python
with glac.browsers.create(name="agent-42") as session:   # deleted when the block ends
    browser = session.connect()
    page = browser.contexts[0].pages[0]
    page.goto("https://example.com")
    page.screenshot(path="shot.png")
```

A session is billed by the Cube resources it uses, as any instance is: its CPU while it computes, its memory while it is awake and its traffic out ([pricing and plans](/docs/platform/pricing/)). `session.awake_seconds` is how long it has been awake.

A profile is what says who a browser is to the sites it visited: its cookies, Local Storage, IndexedDB, Session Storage and preferences, without the cache. The platform keeps it for the project, not billed on its own, and a session never holds a key to it. Log in once, save, and every session made with the profile is logged in:

```python
session = glac.browsers.create(profile={"name": "acme-login", "save": True})   # saved again when it is deleted
browser = session.connect()
# ... log in ...
session.save_profile()                       # the browser restarts, and a connected client reconnects
later = glac.browsers.create(profile="acme-login")   # logged in
```

A save stops the browser as it is closed, so that it writes what it holds, and starts it again; loading starts the session's browser on the profile before the session is answered (a few hundred milliseconds for a profile of a megabyte). One save of a profile at a time: another while one is under way is refused 409 `profile_busy`. A profile deleted is gone at once; its archives are deleted 30 days later.

On a paid plan a session's web traffic can go out through an HTTP proxy of your own, which takes `CONNECT` (403 `proxy_not_on_plan` on the free plan). Its connections to ports 80 and 443 go through the proxy by a relay beside the browser, which is told nothing of it; QUIC is refused there, so the browser speaks HTTP over TCP. The credentials are kept by the platform and are in no answer (`session.proxy` shows the address and the user name):

```python
session = glac.browsers.create(name="agent-42", proxy="http://user:password@proxy.example.com:3128")
session.update(proxy="http://other:password@proxy.example.com:3128")   # the browser goes on; its next connections go through this one
session.update(proxy=None)                                             # none
print(session.refresh().proxy_bytes)                                   # what went through it, both ways
```

`proxy` also takes `{"url": ...}`. A page whose proxy takes no tunnel is answered 502 `proxy_failed` over HTTP (an HTTPS one fails its handshake). What went through the proxy is the session's traffic as any. See [your own proxy](/docs/browser/sessions/#your-own-proxy).

A session's id is a sandbox's: `glac.sandboxes.get(session.id)` runs commands in it. With Playwright's async API, `await p.chromium.connect_over_cdp(session.cdp_url)` does what `connect` does. Sessions are a control plane's.

## Databases

A database is a managed Postgres: a PostgreSQL of its own on a Drive, asleep when nothing asks anything of it and woken by a connection, its sessions kept. It is made in about two seconds, with a root branch, `main`, and an owner, `glacdb_owner` (a member of `glac_superuser`, no superuser). The owner's password is in the answer of `create` and of `reset_password` alone, and kept nowhere: `connection_string()` of the database one of those returned has it.

```python
import psycopg

db = glac.databases.create("shop")                     # ready, with the owner's password
with psycopg.connect(db.connection_string()) as conn:  # from a sandbox or a service of the project
    conn.execute("create table orders (id bigserial primary key, total numeric)")
```

| Call | What it does |
|---|---|
| `glac.databases.create(name, version=, region=, database_name=, size=, storage=, sleep_after_seconds=, suspend_after_seconds=, wake_daily_at=, metadata=, history_seconds=, tiering=)` | Makes a database and waits for its first start: `ready`, or `creating` (`db.refresh()` until it is ready). `version`: 16, 17 or 18 (the default); `size`: `{"vcpus", "memory_bytes"}`, upper bounds (1 vCPU and 1 GiB); `storage`: `{"size_bytes", "durability"}`, its Drive, `strict` (the default: a commit returns once the object store has it) or `standard`; `sleep_after_seconds`: 1 to 3600 (1); `suspend_after_seconds`: how long it stays paused, a connection answered in a millisecond or two, before its memory is saved and it sleeps for real, 1 to 86400 (300); `wake_daily_at`: a time of day in UTC, `"HH:MM"`, at which it is woken every day while it rests (its `pg_cron` jobs wake it at their own times anyway). The name of one there is answers that one, without its password. `glac.databases.update(db, suspend_after_seconds=, wake_daily_at=)` (and `db.update(...)`) changes these two, every branch's at once; `None` takes either back (to 300, and to none). On a Free plan a database's rest is fixed, as Neon's Free scales to zero: paused a second after its last query (`sleep_after_seconds` 1) and asleep five minutes later (`suspend_after_seconds` 300); another value is refused 400 `invalid_field`, on a create and on a change. |
| `glac.databases.get(id_or_name)`, `list(name=)`, `delete(db, final_snapshot=True)` | Finds one; lists them, the newest first; deletes one with its branches, keeping a snapshot of its Drive seven days (`db.final_snapshot`) unless `final_snapshot=False`. No answer but a create's and a reset's has the password. |
| `glac.databases.branches(db)`, `branch(db, "main")`, `sleep(db, "main")`, `wake(db, "main")` | Its branches (`main` first, then its own, the oldest first), one of them, and a branch put to sleep or woken now (its memory saved, its sessions there when it wakes; a connection wakes it too). |
| `glac.databases.create_branch(db, name, parent=, expires_at=)`, `db.create_branch(name, ...)`; `glac.databases.delete_branch(db, branch)`, `db.delete_branch(branch)`, `branch.delete()` | A branch of its own: its parent (`main` unless `parent` names another) as it is now, nothing copied, with a PostgreSQL, an endpoint and a Drive of its own, at `<database>-<name>` on the project's network; deleted by itself at `expires_at` where one is given. Its roles and passwords are its parent's: the branch's `connection_string()` has the owner's password where the `Database` it was made from has it. A delete stops its PostgreSQL and deletes its Drive; not `main` (409 `root_branch`), nor a branch others were made of (409 `has_branches`). |
| `glac.databases.create(..., history_seconds=)`, `update(db, history_seconds=)`, `restore(db, at=, exact=)`, `create_branch(db, name, at=, exact=)`; `db.update(history_seconds=)`, `db.restore(at=, exact=)`, `db.history_seconds`, `db.history`; `create(..., tiering=)`, `update(db, tiering=)`, `db.info["tiering"]` | Its history (slices DB8 and DB5): how far back its root branch can be restored, or branched from, by a time: a day unless given; 0 (none) to 30 days on a paid plan, a day on Free. A restore takes the root branch back in place: to a whole minute or a version's own moment, the newest version of its Drive's window at or before it (one every 30 seconds in which it was written, a day of it at the most); further back, to the second, or with `exact=True`, the moment itself, to the millisecond, by its WAL archive (its WAL replayed to it: seconds to minutes, the call waits). Its endpoint, its connection strings and its owner's password stay what they were, its connections drop, and its state before is kept as a branch of its own, `main_old_<the restore's moment>`; a restore whose new version does not serve within five minutes is undone (503 `database_restore_failed`). A branch of a time is made of that moment instead (sent as `from: {at, exact}`). A time its history does not have is 400 `invalid_history`, its message saying from when the window and the archive reach. A time to the second (or `exact`) is the archive's or refused, never the window's version before it: 400 `invalid_history` where the archive does not reach it (a database that keeps none, or a time before its oldest base), 503 `database_archive_unavailable` (retryable) while the archive's store refuses its segments; each refusal names the window's version at or before the time, whose own moment can be asked for instead. One whose blocks the store does not give is refused 503 `drive_unreadable` (retryable), nothing made. An outage of the object store long enough that more of its WAL waited than its Drive keeps for the log gives that WAL up, so that the database stays up: a gap of its archive, in `db.history["gaps"]` (`DatabaseHistoryGap`: `from`, `to`, `from_lsn`, `to_lsn`): when the WAL given up began, until when the archive restores no time (the end of the first base taken after it; null until there is one), and where in the log it begins and ends. A time in a gap is restored only from its Drive's window (a whole minute, or a version's own moment); to the second, or exact, it is 400 `invalid_history`. The gap is said as events too: `database.history_gap` when it is first found (once a gap, while the database is awake), and `database.history_resumed` when the archive restores again after it; their `data` is an `EventDatabaseGap`: the database, the gap's `from`, `to` (None of the first), `from_lsn` and `to_lsn`, `segments` (how many segments of 16 MB were given up) and, of the first, `error` (what the object store's last refusal said). `db.history` says it as it stands: `from`, the earliest time it can be restored to now (None until the first version, about three minutes after it is ready), and `stored_bytes`. `tiering=True` or `False` turns its tiered storage on or off (slice DB18: a paid plan; each branch's PostgreSQL restarts once for it), and `tiering` in what it says is whether it is on and what it bills by now (`tiered_bytes`). |
| `glac.databases.reset_password(db, role=)` | A new password for its owner, in this answer alone: the old one is refused from the next login on, and a connection that is open stays. |
| `glac.databases.roles(db)`, `reset_role_password(db, role)`; `db.roles()`, `db.reset_role_password(role)` | The roles of its root branch that log in, read from PostgreSQL itself (`{"name", "owner"}`): its owner and the roles you made (`CREATE ROLE … LOGIN`), the platform's not among them; and a new password for any of them, in that answer alone: a `DatabaseRoleSecret` with the role's `password` and the root branch's connection strings of that role (`connection_string()`); the owner's is made of the database the API answers. A branch of its own keeps the password it was made with. |
| `db.connection_string(password=, pooled=, private=)` | A connection string psycopg, psql and SQLAlchemy's `postgresql+psycopg://` take as it is: the public one where the API gives one, else the one on the project's network, with the owner's password of the answer it came with, or `password` (percent-encoded). |
| `db.default_branch`, `branch.connection_uri`, `pooled_uri`, `private_uri`, `endpoint`, `state` | Its root branch and the connection strings as the API gives them; its endpoint (`ep-<adjective>-<noun>-<8 hex>`); `awake`, `paused` or `asleep`. `db.refresh()`, `db.sleep()`, `db.wake()`, `db.branches()`, `db.reset_password()`, `db.delete()` are the database's own. |

A database is reached on the project's network, from its sandboxes (made with `project_network`) and its services, at its name: that is its `private_uri`. Where an edge serves PostgreSQL it is reached from anywhere at its public address too (`connection_uri`, port 5432, TLS alone; the pooler's: `pooled_uri`); they are None where none does, or where the database has no region to be named under, and the connection string takes them where they are given. Neither `repr` nor anything else here shows the password. A database's app and Drive are listed with `managed_by` and changed through `glac.databases` alone. Databases are a control plane's.

## Managed Applications

An application of the catalogue ([Rivet](/docs/managed-applications/rivet/), say) is installed in the project as its own Cube resources: its apps, Volumes, secrets and buckets, labelled with the installation, changed through it alone, and billed as they are ([Managed Applications](/docs/managed-applications/)). An install's answer has every output, the secret ones too (an admin token, an endpoint with it in), and a read shows those to a key with the `admin` scope alone: an installation object keeps each output an earlier answer gave that a later one hides.

```python
rivet = glac.catalogue.get("rivet")                  # its parameters, and a month of it in Cube's meters
print(rivet.calculator["month"])
engine = glac.installations.create("rivet", parameters={"disk_gib": 20})   # waits until it is ready
print(engine.outputs["endpoint"])                    # RIVET_ENDPOINT, its token in it
```

| Call | What it does |
|---|---|
| `glac.catalogue.list()`, `glac.catalogue.get(app, version=)` | The catalogue's applications, each a `CatalogueApp` (`app`, `version`, `title`, `summary`, `publisher`, `verified`, `licence`, `versions`, `parameters`: a JSON Schema, `outputs`, `calculator` with its `month`); one, at its newest version or the one named, with its `definition`. |
| `glac.installations.create(app, name=, version=, region=, parameters=, requires=, wait=True, timeout_seconds=)` | Installs it, and waits until it is ready (`wait=False`: at once). One that failed raises `InstallationError`, with its words and the `installation`; one still being made after `timeout_seconds`, `GlacTimeoutError`. |
| `glac.installations.list(app=)`, `glac.installations.get(id_or_name)` | The project's installations, the newest first; one. |
| `installation.update(version=, parameters=, wait=True)`, `installation.delete()`, `refresh()`, `wait(timeout_seconds=)` | Another version, or its parameters as a merge patch (`None` takes one back to its default); deleted with everything it is made of (`status`: `deleted`, or `deleting`). |
| `installation.status`, `failure`, `outputs`, `secret_outputs`, `parameters`, `apps`, `volumes`, `buckets`, `requires`, `required_by` | As the API says them. |

Managed Applications are a control plane's.

## Secrets and variables

```python
glac.secrets.put("GHCR", "me:ghp_…", allowed_hosts=["ghcr.io"])     # never given back; goes to these hosts only
glac.secrets.update("GHCR", allowed_hosts=["ghcr.io", "*.example.com"])
glac.variables.put("REGION", "eu")                                 # an app's env refers to it: "${{ shared.REGION }}"
print([s.name for s in glac.secrets.list()], {v.name: v.value for v in glac.variables.list()})
glac.secrets.delete("GHCR")
```

A secret is known by its name alone, so each call takes a name or a `Secret` that was listed, and no answer has its value. Shared variables are a control plane's.

## The project, the account and the bill

| Call | What it does |
|---|---|
| `glac.project()`, `glac.events(type=, sandbox=, limit=, cursor=)` | The project of the key, and a page of its events. |
| `glac.usage(start=, end=, group_by=, sandbox=, metadata=, kind=, volume=, drive=, snapshot=, backup=, exact=)` | What was used: by sandbox, or a report from the hourly books. |
| `glac.account()`, `glac.update_account(...)` | The account, its month of traffic, where its notices go (`email=`; `email_verified`, and `email_undeliverable`, an `EmailUndeliverable`, while mail to it bounces: see [where notices go](/docs/platform/projects/#where-notices-go)), the language of its notices (`language=`: "en", "de" or "fr", or `None` to follow the billing address again), its billing address and tax id; and behind a control plane `keeps` (an `AccountKeeps`): what the key's project keeps of Drives and volumes, by the numbers a create is held to, each with what it has (`volumes`, `volume_bytes`, `volumes_bytes`, whose `limit` is `None` for no limit, and `unsized_bytes`: see [raising limits](/docs/platform/raising-limits/)). |
| `glac.billing()`, `glac.update_billing(plan=, spend_limit_usd=, start_now=)` | The plan and the bill; a plan, a spend limit. |
| `glac.balance()`, `glac.update_balance(auto_reload=, ..., start_now=)`, `glac.top_up(usd, start_now=)`, `glac.charges()`, `glac.ledger()` | The Credits held, auto reload, a top-up, the card's charges and the ledger. |

**Webhooks, API keys, spend limits, the project.** `glac.webhooks` sends the project's events to an address of yours: `create(url, events=, enabled=)` (the `secret` that signs each delivery is in that answer alone), `get`, `list`, `update`, `delete`. `glac.keys` are the project's API keys: `create(name=, scopes=, expires_at=)` (the key itself, `key`, is in that answer alone), `get`, `list`, `delete`. `glac.limits()` and `glac.update_limits(usd)` are the project's spend limit and what was spent this month (`None` takes the limit away), and `sbx.limits()` and `sbx.update_limits(usd)` a sandbox's own. `glac.request_limit(limit, wanted, why)` asks for one of the account's [limits raised on request](/docs/platform/raising-limits/) (a cap by its name, `"volumes"` or `"volume_bytes"`; bytes of the two in bytes; 409 `limit_request_pending` while one of the same limit waits), and `glac.limit_requests(limit=, cursor=)` and `glac.limit_request(id)` read them with the operator's answers (`LimitRequest`: `status` `pending`, `granted` or `refused`, `granted`, `answer`). `glac.update_project(delete_after_idle_days=30)` sets after how many days asleep its sandboxes are deleted (`None`: never). A control plane's. Webhooks and API keys need a key with the `admin` scope, and so do setting the project's limit and `update_project`; setting a sandbox's limit needs `write`.

**Log destinations, the audit log, the billing documents.** `glac.destinations` is where the logs (and the metrics and the requests' traces: `signals=["logs", "traces"]`) of the project's apps are exported (OpenTelemetry endpoints): `create(name, endpoint=, headers=, ...)`, which tries it at once with one record (`status["connected"]`), `get`, `list`, `update`, `delete`, `test` (one record now); a header's value is a credential, sent when a destination is made or changed and in no answer (what is returned has the headers' names alone). `glac.audit(limit=, cursor=)` is a page of the project's audit log, the newest first (who, what method, what path, what status): every call that was no reading, a refused one too, and every reading of an app's log by an operator. `glac.statement("2026-09")` is a month's statement (this month's unless given; a `date` in the month too) with each database's own line in it (`databases`: what it used in the month, its history and its tiered data apart, and what that comes to at the list prices), `glac.invoices()` the invoices at Stripe (with the PDF's link), `glac.notices()` the mail the account was sent, `glac.withdrawals()` what it may withdraw from now and the withdrawals it made (a consumer in the EEA or the UK may withdraw from a purchase within 14 days), and `glac.withdraw(charge_id, name=, email=)` the buyer's own statement of withdrawal, received at once. A control plane's. Making, changing, testing and deleting a destination, reading the audit log and withdrawing need a key with the `admin` scope; the rest, one that may read.

Each returns the API's answer as a dict; `TypedDict`s (`Account`, `Billing`, `Balance`, `Charge`, `Project`, `Event`, `EventAutoReloadNoRequest`, `EventInstanceRestarted`, `EventInstanceUpgraded`, `EventInstanceReverted`, `EventInstanceUpgradeFailed`, `EventLimitRequest`, …) say what is in it. The bill, the balance and the project are a control plane's: a node that stands alone answers 404 `no_such_route`.

**The data of an event.** Every type of event the control plane has has a `TypedDict` of its `data` (`event["data"]`), named after the description's schema of it. A database's: `EventDatabase` (`database.created`, with its `region` and PostgreSQL `version`; `database.deleted`, with `final_snapshot_id`, the snapshot of its Drive kept seven days), `EventDatabaseBranch` (`database.branch_created`, with its `parent`; `database.branch_deleted`, with its `reason`), `EventDatabaseRestored` (`database.restored`: the moment `at`, and `old_branch_id`, the branch the state before is kept as), `EventDatabaseArchive` (`database.archive_failing`: `since`, how many segments in a row `failures`, `archived_until`, how far a restore reaches meanwhile, and the object store's `error`; `database.archive_recovered`: `recovered_at`) and `EventDatabaseGap`. The others likewise: a sandbox's (`EventSandboxSlept`, `EventSandboxWoke`, `EventSandboxMoved`, `EventSandboxDeleted`, …), an app's and its keys' (`EventApp`, `EventAppFailed`, `EventKeyCreated`, …), the project's deploys and key, backups, domains, destinations and Drives' snapshots (`EventProjectDeployed`, `EventBackup`, `EventDomain`, `EventDestination`, `EventDriveSnapshot`, …), and the account's plan, top-ups, refunds, withdrawals, balance, Credits, storage, egress cap, spend limits, hold and debts (`EventSubscriptionPeriod`, `EventTopupCollected`, `EventWithdrawalReceived`, `EventBalance`, `EventInDebt`, …). A key that not every event of a type has is one its type leaves out.

**Buying at once (`start_now`).** A consumer in the European Economic Area or the United Kingdom (an account with no tax id that is verified or being verified) may withdraw from a purchase within 14 days, and what it buys is provided at once only at its own express request. `top_up(20, start_now=True)`, `update_billing(plan="p20", start_now=True)` (a plan that is bought: from Free, or a move up) and `update_balance(auto_reload=True, start_now=True)` (for every auto reload while auto reload stays on) carry that request, in the words the console shows beside its box (and `GET /v1/billing/withdrawals` has, `right.start_now`):

> I ask for this purchase to be provided at once, within the 14 days in which I may withdraw from it. If I withdraw, I pay for what was provided until then. I understand that once all the Credits of a top-up are used, it has been provided in full and I can no longer withdraw from it.

The service starts now; within the 14 days the buyer may still withdraw, and pays for what was provided until then; once it is fully provided (a top-up whose Credits are all used), the right to withdraw is lost. Give it only when the person buying asks for that: the SDK never sends it unless it is given. Without it, such an account is refused: 400 `start_now_required`, `param` `start_now`, in words that name its country and quote the request, and nothing is bought. An account whose auto reload is on without the request reloads nothing, and is told once a month (the event `auto_reload.no_request`).

## Errors

A non-zero exit code is a result: read `res.exit_code`. An error of the API raises `GlacError`, whatever product the call is of, with `status`, `type`, `code`, `param`, `retryable`, `request_id` and `message`, and `docs_url`, which leads to the code on the [errors](/docs/errors/) page. No connection is `GlacConnectionError`; a timeout is `GlacTimeoutError` (also a `TimeoutError`). A version that did not come up, or a project's deploy that failed, is `DeployError`, and a log reading that was given up `LogsError`; both are `GlacError`s. Errors about money (402 `account_in_debt`, 403 `trust_limit_exceeded`, 409 `no_card`, 400 `start_now_required`, …) carry the control plane's own message, which says what lifts them.

## Behaviour worth knowing

- **Sleeping sandboxes.** No call fails because a sandbox sleeps or is starting: it waits and goes on. There is no wake call. `run()` and `wait()` count as waiting, so the sandbox stays awake while they wait; reading `cmd.output()` does not.
- **Retries.** A call with no answer, or a `retryable` error, is retried twice (`retries=`), from 0.2 s doubling. Every `POST` carries an `Idempotency-Key`, so a retry never makes two sandboxes or runs a command twice.
- **Timeouts.** `Glac(timeout_seconds=)` (900 unless you say) is how long to wait for the API, on top of the command's own time. `run(timeout_seconds=)` is the command's limit: it is killed, and the result says `timed_out`.
- **Threads.** A `Glac`, a `Sandbox` and a `Command` may be used from several threads. There is no asyncio API yet: use `await asyncio.to_thread(sbx.run, "make")`.

Source: https://glac.ai/docs/sdks/python/index.mdx
