# Versions

> Tag, compare and download dataset versions with the Qualia SDK

A version is a tag: a name such as `1.0` on what a dataset yielded at one
moment. A tag never moves. Besides tag names, `stable` (the newest tag), `head`
(what the dataset yields now), a DatasetVersion id and a prefix of a digest or
id resolve too. An upload also has `v0`, as it arrived. Uploads, merged
datasets and mixtures all have versions.

`client.versions` is on the qualia.v1 Versions API, which every key that can
read the dataset reaches. A key restricted to workspaces is refused.

Methods take a version's full name
(`accounts/<account>/datasets/<dataset>/versions/<tag>`) or a tag plus
`dataset=`. `download`, the methods that prepare a version and `curation_record` also
take `<dataset>:<tag>`, with the dataset's slug or id: a version's `reference`,
such as `"my-dataset:1.0"`.

## Tag a version

```python
from qualia import Qualia

client = Qualia()

diff = client.versions.compare("<DATASET_UUID>")  # newest tag → head
tag = client.versions.create(
    "<DATASET_UUID>",
    diff.suggested_version,
    note=diff.draft_note,
    digest=diff.target_digest,  # refused (409) if head moved since
)
print(tag.reference, tag.dataset_version_id)
```

`list(dataset)` pages the tags newest first, `get(tag, dataset=)` reads one,
and `update(tag, dataset=, note=)` edits its note.

## Download a version

```python
result = client.versions.download("my-dataset:1.0", "./my-dataset-1.0")
print(result.destination, result.file_count, result.byte_count)
```

`download` starts the rendering, or picks up the one already there, and waits
until it is done (`timeout=`, default 30 minutes; it keeps rendering after a
timeout). It then fetches every file from its signed link into
`.<name>.qualia-version-partial` beside `dest`, checks each one's size and
sha256 against the listing, and renames the folder to `dest`. `dest` never holds
part of a version. The API key is never sent to a signed link.

| Argument        | Description                                                                                                                                                                                                                     |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version`       | `<dataset>:<tag>`, a full version name, or a tag plus `dataset=`. `head` is refused: tag it first                                                                                                                               |
| `dest`          | The folder to create; its parent is created if missing. Default: `<dataset>-<tag>` here, plus `-v21`, `-mcap`, `-pi` or `-dreamlabs`                                                                                            |
| `layout`        | `lerobot_v3` (default), `lerobot_v2_1`, `mcap_ros2` (ROS 2 MCAP, one `.mcap` per episode), or a partner layout: `pi_mcap_v1` or `dreamlabs_v2_1` (see below)                                                                    |
| `on_progress`   | Called with `{"step": "preparing", "state": ..., "version": "<slug>:<tag>"}` while the server builds the version, then `{"step": "downloading", "files_done", "files_total", "bytes_done", "bytes_total"}` as each file arrives |
| `timeout`       | Seconds to wait for the rendering                                                                                                                                                                                               |
| `poll_interval` | Seconds between reads while it renders (default 2)                                                                                                                                                                              |
| `parallel`      | Files fetched at once (default 4)                                                                                                                                                                                               |

It returns a `VersionDownload`: `destination`, `version`, `materialization`
(its `digest` names the exact files), `layout`, `file_count`, `byte_count`,
`downloaded_file_count` and `reused_file_count`. `to_wire()` gives it as JSON.

A call that stops keeps the files it checked; the same call again checks them
and fetches only the rest. When `dest` already holds exactly this rendering, the
call checks it and fetches nothing. Any other existing `dest` raises
`FileExistsError`. A file whose bytes do not match raises `QualiaError` and
nothing is published.

`pi_mcap_v1` and `dreamlabs_v2_1` are files to upload to Physical Intelligence
or Dream Labs with your own account. They hold the tag's episodes that are not
rejected (`dreamlabs_v2_1`: only the successful ones), with a `README.md` that
says how to upload them (and `upload_to_pi.py` for Pi); an episode that could
not be converted is listed there with its error. See
[Partner layouts](https://junction-docs.qualiastudios.dev/global/guides/download.md#partner-layouts).

Preparing a version in a format the first time needs `data:write`; once it is
prepared, any key that can read the dataset downloads it. To download by dataset
name, with the newest version as the default, use
[`client.data.download`](https://junction-docs.qualiastudios.dev/sdk/resources/data.md#download-a-dataset).

### One step at a time

```python
m = client.versions.start_materialization("my-dataset:1.0", layout="lerobot_v2_1")
m = client.versions.get_materialization(m.name)  # NOT_STARTED, RUNNING, SUCCEEDED, INTERRUPTED
page = client.versions.list_materialization_files(m.name, limit=512)
for f in page.items:
    print(f.path, f.size_bytes, f.sha256, f.uri)  # uri is valid until f.expire_time
```

## Merge history

```python
history = client.versions.merge_history("<MERGED_DATASET_UUID>")
for entry in history.entries:
    print(entry.create_time, entry.kind, len(entry.datasets))
```

## Curation record

What was done to a dataset in the round a version opened: from its creation to
the next tag's (for `head`, since the newest tag). This is the dashboard's
**Curation of 1.0** panel.

```python
record = client.versions.curation_record("my-dataset:1.0")
for event in record.events:  # oldest first
    print(event.create_time, event.actor.display_name, event.summary)
print(record.start_time, record.end_time, record.truncated)
```

A merged dataset's record also reads the uploads it holds, from when its merge
took each in. At most 2000 events are read; a longer round keeps its newest and
sets `truncated`. The events come from the dataset's workspace.
