# Configuration

> Configure the Qualia SDK client

## Environment Variables

| Variable                              | Description                                                                                            |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `QUALIA_API_KEY`                      | Your API key (used if not passed to constructor)                                                       |
| `QUALIA_BASE_URL`                     | Override the API base URL (default: `https://api.qualiastudios.dev`)                                   |
| `QUALIA_ACCOUNT`                      | Optional account UUID; default the API key's own (see [Account](#account))                             |
| `QUALIA_WORKSPACE`                    | Workspace UUID to act in instead of the API key's own workspace; rarely needed                         |
| `QUALIA_QUIET`                        | Set to `1` to silence the live per-phase upload progress written to stderr                             |
| `QUALIA_UPLOAD_SERVER_BUDGET_SECONDS` | How long the server may go without reporting an upload step before the client gives up (default 3600)  |
| `QUALIA_SERVER_STALE_SECONDS`         | How long the server's live preparation heartbeat may go stale before the client gives up (default 240) |
| `QUALIA_UPLOAD_BOUNDARY_CACHE_DIR`    | Directory for the upload boundary cache; set to an empty string to disable it (see below)              |

:::caution[Point the client at this environment]
The SDK and CLI talk to `https://api.qualiastudios.dev` unless told otherwise. Your account lives on `https://junction-api.qualiastudios.dev`, so set:

```bash
export QUALIA_BASE_URL=https://junction-api.qualiastudios.dev
```
:::

```python
from qualia import Qualia

# Uses QUALIA_API_KEY environment variable
client = Qualia()

# Or pass explicitly
client = Qualia(api_key="your-api-key")
```

Each setting comes from the constructor argument, then the environment
variable, then the CLI's config file (written by `qualia auth login`). The
file's base URL and account are used only together with its key: once
`QUALIA_API_KEY` is set or `api_key=` is passed, nothing is read from the file.

## Account

Tasks, datasets, episodes, versions, mixtures and checks live under an account,
at `accounts/{account}/…`. A new user has one personal account: an organization
of one, with one workspace.

An API key belongs to exactly one account, so the client needs no account
setting. It names the account `accounts/-` in every call, and the API reads `-`
as the key's own account. No call is made first.

```python
from qualia import Qualia

client = Qualia(api_key="your-api-key")
client.tasks.list()  # GET /v1/accounts/-/tasks: the key's own account
```

`account=`, `QUALIA_ACCOUNT` and `qualia auth login --account` remain as an
explicit override; with a key they can only name the key's own account.

To learn the account's UUID, ask `GET /v1/whoami` (any valid key may).
`client.whoami()` returns that answer: the account and workspace ids and
names, and the key's name, prefix, scope (`organization`, `workspace` or
`restricted`), scopes and expiry.

A full resource name, such as `accounts/<ACCOUNT_UUID>/datasets/<DATASET_UUID>`,
carries its own account. An explicit account must be a lowercase UUID;
`accounts/<uuid>` is accepted too. To find yours, see
[Finding your account id](https://junction-docs.qualiastudios.dev/cli/overview.md#3-your-account).

## Waiting on a long upload

An upload's server-side phases — staged-data verification and ingest — are
bounded by **silence, not elapsed time**. Ingest has no upper bound: it parses
every bag and transcodes every camera, so a large upload is free to take as long
as it takes.

`QUALIA_UPLOAD_SERVER_BUDGET_SECONDS` is how long the server may go without
reporting a step on your upload; each step it does report restarts that clock.
Two signals end the wait earlier: the server publishing a failure for the
upload, and its live preparation heartbeat going stale for
`QUALIA_SERVER_STALE_SECONDS` (which must stay under the 300 s lease TTL).

Raise them for a slow link or a very large session; lower them to fail faster in
CI. To watch progress instead of waiting blind, pass `on_progress` to
`client.data.upload()`.

## Upload speed

`client.data.upload()` sizes its send from the machine it runs on, so most
uploads need no setting. Every value below is bounded; a value out of range
fails at import and names the variable.

| Variable                            | Default                                        | What it bounds                                                                                      |
| ----------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `QUALIA_UPLOAD_BUFFER_BUDGET_BYTES` | 1/64 of memory, 256 MiB–1 GiB                  | Packed bytes held in memory at once, across every file. Peak memory is about 3–4× this              |
| `QUALIA_UPLOAD_R2_CONCURRENCY`      | `16`                                           | Storage connections, so PUTs in flight (1–64)                                                       |
| `QUALIA_UPLOAD_WINDOWS_IN_FLIGHT`   | starts at `3`, adapts between a floor and `8`  | 64 MiB windows in flight. The floor keeps every storage connection busy; setting this caps it (1–8) |
| `QUALIA_UPLOAD_TREE_WINDOWS`        | `2` with a budget of 512 MiB or more, else `1` | 256 MiB groups of small files packed and sent at once, at most one per 192 MiB of budget (1–4)      |
| `QUALIA_UPLOAD_FILE_CONCURRENCY`    | half the cores, 4–8                            | Large (over 64 MiB) or edited files packed at once; the CLI's `--upload-workers` (1–32)             |

"Memory" is the lower of the machine's RAM and the smallest memory limit on
the process's cgroup or any parent of it, such as a container's limit or a
systemd service's `MemoryMax`/`MemoryHigh`. "Cores" are the ones the process
may run on.

The live progress reports the window depth it settled at (`upload window depth
…`), and the receipt's `transfer_metrics` has each phase's time and request
count. On a thin uplink, lower `QUALIA_UPLOAD_R2_CONCURRENCY` before anything
else; on a small machine, lower `QUALIA_UPLOAD_BUFFER_BUDGET_BYTES`.

## Upload boundary cache

`client.data.upload()` keeps a small on-disk cache of chunk boundaries so that
an edited file can reuse its unchanged chunks even after the client process
restarts. It holds chunk IDs, offsets and sizes only — no file bytes, API keys,
presigned URLs or manifest IDs — and every reuse is re-authorized and
re-checked for presence on the server at upload time. A file's cached
boundaries are only ever compared against a fresh chunking of the current
file; the cache cannot change how a file is cut.

It saves stored bytes on changed-file re-uploads after a restart. It does not
make uploads faster, and no speedup is claimed.

**Location.** `$XDG_CACHE_HOME/qualia/upload-boundaries-v1`, or
`~/.cache/qualia/upload-boundaries-v1` when `XDG_CACHE_HOME` is unset. The
cache is one SQLite file, `boundaries.sqlite3`, in that directory. The
directory is created with mode `0700` and must stay private and owned by you,
otherwise the cache is treated as unavailable.

**Overriding or disabling.** Set `QUALIA_UPLOAD_BOUNDARY_CACHE_DIR` to a
private directory to move it, or to an empty string to disable it. The cache
is also disabled automatically when its directory is inside the directory being
uploaded (for example, uploading your home directory with the default
location), so cache files never become part of a dataset.

**What is cached.** Changed files of at most 64 MiB, cut into between 2 and
8,192 chunks, in ordinary directory uploads. A file small enough to be a single
chunk is skipped: there is no interior boundary for a later edit to reuse.
Entries are keyed by the API base URL, the repository authorized for the
upload, the file's format label, the native chunker profile, and the file's
content hash, so a different account, server, repository or SDK build never
matches another's entry.

**Bounds and eviction.** The cache holds at most 128 entries and 32 MiB of
encoded hints, and the database file is capped at 64 MiB. When a new entry
would exceed the entry or payload limit, the least recently used entries (by
last store or load time) are deleted first, before the new one is written.

**When it is written.** An upload collects its hints in memory, bounded by the
same 128-entry and 32 MiB limits, and writes them once after the transfer
finishes and before the dataset is published. An upload that fails during
transfer writes nothing. One that fails after that, such as a source file
changing before publication, may leave its hints behind; they are harmless,
because every hint is checked against the server before it is used. When a
directory offers more hints than the cache can hold, the largest files win,
since they are the ones a later edit can save the most bytes on.
Nothing is vacuumed, repaired or migrated automatically.

**Failure behaviour.** Any filesystem, SQLite or data error is a cache miss,
and a failed write is skipped; the upload continues on the ordinary path. A
cache damaged by a crash or power loss stays a miss until you delete the
directory. Deleting it at any time is safe.

## Custom HTTP Client

Use a custom httpx client for advanced configuration like timeouts and connection limits.

```python
import httpx
from qualia import Qualia

custom_client = httpx.Client(
    timeout=60.0,
    limits=httpx.Limits(max_connections=10),
)

client = Qualia(api_key="...", httpx_client=custom_client)
```

## Context Manager

Automatically close the client when done using a context manager.

```python
from qualia import Qualia

with Qualia(api_key="...") as client:
    models = client.models.types()
# Client is automatically closed
```
