# Preparation

> Wait for an upload's preparation, read what it found and confirm its task names

Every upload is prepared after it lands. The prepare run sorts the takes into
tasks (task discovery) and gives each task a Task: a published one it matched,
or a draft. It also splits takes into pieces and reads the task names the
dataset's own metadata gives. Everything it does is a proposal or a flag:
nothing is removed and no Task is published. `client.preparation` reads and
changes what the dataset page's **Tasks** tab shows.

Every method takes a dataset id, its full name
(`accounts/{account}/datasets/{dataset}`) or the preparation's name
(`.../preparation`). A merged dataset or a mixture has no run of its own: its
uploads are prepared one by one, so `get` answers state `NONE`. Nothing here
starts a run. `client.preparation` is in qualia-sdk 0.8.0rc12 and later; on
0.8.0rc11 it raises `AttributeError`.

## Read what preparation found

```python
from qualia import Qualia

client = Qualia()

prep = client.preparation.get(dataset_id)
print(prep.state, prep.run_id, prep.error)
for warning in prep.warnings:
    print("warning:", warning)

found = prep.discovery  # None on runs from before discovery
for task in found.tasks if found else []:
    # task_id: the matched Task or the draft ("" when no draft was made);
    # linked_tasks, below, says which
    print(task.name, task.episode_count, task.health_flag, task.task_id)
    print("  references:", task.reference_episode_ids)

names = prep.task_strings  # the dataset's own task names
for c in names.candidates if names else []:
    print(c.key, c.text, c.episode_count, c.blocked_reason, c.confirmation)
```

`state` is `NONE`, `QUEUED`, `RUNNING`, `SUCCEEDED`, `FAILED` or `SKIPPED`.
`discovery.status` is `RUNNING`, `DONE`, `FAILED` or `FALLBACK` (one task for
the whole dataset). A run discovery did not split has its one Task in
`task_match` instead.

A found task with a `task_id` is a Task you can read, edit and publish with
`client.tasks`. One without a Task can become one:

```python
task = client.tasks.create(
    found.tasks[0].name[:32],
    references=[
        {"role": "SUCCESS", "episode": f"{dataset_name}/episodes/{episode}"}
        for episode in found.tasks[0].reference_episode_ids[:3]
    ],
)
client.datasets.update(dataset_name, linked_task=task.name)
```

## Tell an existing Task from a new draft

Preparation links each found task's episodes to a Task: one that already
existed in the account, when it matches, else a draft it writes. The
preparation names the Task (`task_id`) without saying which it is. Only the one
Task of a run that discovery did not split carries `task_match.matched`.
`linked_tasks` reads each named Task's published revisions once and tells them
apart where a publication predates queueing or the server explicitly matched
a published revision. Otherwise a published Task's origin is `UNKNOWN`:

```python
prep = client.preparation.get(dataset_id)
for linked in client.preparation.linked_tasks(dataset_id, preparation=prep):
    print(linked.display_name, linked.origin, linked.revision, linked.publish_time)
    if linked.origin == "EXISTING":
        print("  episodes linked to it now:", linked.linked_episode_count)
        print("  revisions checks use:", linked.linked_revisions)
        print("  their task labels:", linked.labels)
```

| `origin`    | The Task                                                                                                                                                       |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `EXISTING`  | Had a published revision before the prepare run was queued, or the server explicitly matched a published revision. Checks use the stored link revisions        |
| `NEW_DRAFT` | Has no published revision: a draft a prepare run wrote for this upload (this run, or an earlier run of the upload whose draft was kept; the two read the same) |
| `MISSING`   | Cannot be read any more: deleted, or out of the key's reach                                                                                                    |
| `UNKNOWN`   | The available data cannot establish the origin: no known publication predates queueing, or timestamps are missing                                              |

The API has no stable completion timestamp. `update_time` is the last change
and can advance after success when calibration finishes. It cannot distinguish
a draft published after preparation from a Task published during preparation.
`DRAFT_PUBLISHED` and `PUBLISHED_DURING_PREPARATION` remain accepted origin values
for compatibility, but are no longer inferred from that mutable timestamp.

`revision` is the Task's newest published revision (0 for none) and
`publish_time` when that was published; `episode_count` is how many takes the
run sorted into the task. For an `EXISTING` Task, `linked_tasks` reads every
page of its current episode links: `linked_episode_count` is how many are linked to
it now (they may have been linked elsewhere since the run), and `labels` the
task labels they carry with their counts, including the empty label.
`linked_revisions` gives `(revision, episode_count)` pairs from the stored
links, including inherited links; 0 means a draft link. Checks use those
revisions even when `revision` names a newer publication. To judge those
episodes against a Task of your own while keeping every label, including an
empty one, use `link_task`:

```python
cursor = None
while True:
    page = client.episodes.list(
        dataset_id, filter=f'linked_task = "{linked.task}"', cursor=cursor
    )
    for episode in page.items:
        client.episodes.link_task(episode.name, my_task.name)
    if not page.page_info.has_next:
        break
    cursor = page.page_info.end_cursor
```

`linked_tasks` is in qualia-sdk 0.8.0rc14 and later; on 0.8.0rc13 it raises
`AttributeError`.

## Wait for preparation to end

Preparation starts by itself after an upload and takes minutes. Automatic
checks depend on policy and upload eligibility. Free quality checks default
on; paid video (VLM) checks default off and require opt-in. An eligible paid
check may run after preparation if the current policy still enables it and
admission succeeds.

`wait` covers preparation only. It reads the preparation every `interval`
seconds until it has ended, and returns that reading:

```python
dataset_id = client.data.upload("./takes", "box-pick")["dataset_id"]

prep = client.preparation.wait(
    dataset_id,
    timeout=1800,  # seconds; the default is 3600
    on_progress=lambda p: print(p.state, p.discovery.status if p.discovery else ""),
)
if prep.state == "FAILED":
    raise SystemExit(prep.error)
for warning in prep.warnings:  # for example, task discovery fell back
    print("warning:", warning)
```

| What `get` reads                                                         | What `wait` does                                                                                                                                                                             |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NONE`: no prepare run                                                   | Reads again for up to 30 seconds, then returns it. An upload's run is queued as the upload settles; a merged dataset, a mixture and a deployment that does not prepare uploads never get one |
| `QUEUED` or `RUNNING`                                                    | Reads again                                                                                                                                                                                  |
| `QUEUED` or `RUNNING`, `discovery.status` already `FAILED` or `FALLBACK` | Reads again: the run goes on without task discovery. `on_progress` gets this reading at once, and a timeout's message carries `discovery.reason`                                             |
| `SUCCEEDED`                                                              | Returns it. With `discovery.status` `FAILED` or `FALLBACK` it ended with a fallback: `warnings` and `discovery.reason` say why, so check the tasks before publishing one                     |
| `FAILED`                                                                 | Returns it, never raises: `error` says why                                                                                                                                                   |
| `SKIPPED`                                                                | Returns it: there was nothing to prepare                                                                                                                                                     |
| An ended state, `discovery.status` `RUNNING`                             | Reads again: task discovery is running again, after `confirm_task_strings`                                                                                                                   |

`on_progress` is called with each reading whose `state`, `discovery.status` or
`discovery.reason` differs from the one before, the first reading included.
After `timeout` seconds `wait` raises `TimeoutError`, whose message says what
the last reading showed: the state, task discovery's status and reason, the
run and when it last changed. Timing out stops nothing: the run goes on, and
`get` or another `wait` reads it later. A failed read that is transient (a
5xx, a timeout) is retried, as in [`checks.wait`](https://junction-docs.qualiastudios.dev/sdk/resources/checks.md).

`wait` is in qualia-sdk 0.8.0rc14 and later; on 0.8.0rc13 it raises `AttributeError`.

## Confirm the dataset's task names

When `task_strings.confirmable` is true, a person decides which of the
dataset's own task names to use as tasks. The set you send replaces the
decisions on the run's names: a name not listed is cleared, and `[]` clears
them all. Send one choice per usable name, as the Tasks tab does: the ones to
use `confirmed=True` with a Task name of at most 32 characters, the rest
`confirmed=False`.

```python
prep = client.preparation.get(dataset_id)
usable = [
    c for c in prep.task_strings.candidates
    if not c.blocked and not c.looks_like_code
]
client.preparation.confirm_task_strings(
    dataset_id,
    prep.run_id,
    [{"key": c.key, "display_name": "Fold the towel", "confirmed": True}
     for c in usable],
)
```

Confirmed names start task discovery again, which drafts a Task for each.
Read the preparation again to see them, or `wait` for the re-run to end.

| Refusal                   | Why                                                                       |
| ------------------------- | ------------------------------------------------------------------------- |
| 409 `ABORTED`             | A newer run replaced the names you read: read again and decide again      |
| 400 `FAILED_PRECONDITION` | A blocked name (outcome words, a code, already a task label), or no names |
| 400 `INVALID_ARGUMENT`    | A key the run does not have, a key twice, or a name over 32 characters    |
| 403 `PERMISSION_DENIED`   | A workspace's service key: a person decides on task names                 |

A personal API key decides as the person who made it, and the decision records
the key.

The CLI has the same: `qualia dataset preparation` (`--wait` waits; its table
tells each Task apart) and `qualia dataset confirm-task-names`.
