Metadata-Version: 2.5
Name: hectiqlab
Version: 0.2.0
Summary: Record runs, stages, metrics, datasets and models to Hectiq Lab, from a script or the command line.
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic-settings>=2.3
Requires-Dist: pydantic>=2.7
Requires-Dist: rich>=13.7
Requires-Dist: tomli>=2.0; python_version < '3.11'
Provides-Extra: cli
Requires-Dist: typer>=0.12; extra == 'cli'
Provides-Extra: parquet
Requires-Dist: pyarrow>=15; extra == 'parquet'
Provides-Extra: tui
Requires-Dist: textual-plotext>=1.0; extra == 'tui'
Requires-Dist: textual>=8.2; extra == 'tui'
Requires-Dist: typer>=0.12; extra == 'tui'
Description-Content-Type: text/markdown

# hectiqlab

Record runs, stages, metrics, datasets and models to [Hectiq Lab](https://lab.hectiq.ai), from a training script or the command line. Everything goes to the API the web app reads, over HTTP, from one background thread, so a training loop never waits on the network.

This package lives beside `pyhectiqlab` (in `sdk/`) and does not replace it: both can be installed together, and they share `~/.hectiq-lab/credentials.toml`.

```bash
pip install hectiqlab                 # the SDK
pip install "hectiqlab[cli]"          # and the `hlab` command line
pip install "hectiqlab[tui]"          # and the dashboard `hlab ui` opens
pip install "hectiqlab[tui,parquet]"  # and parquet previews in it, which take pyarrow
```

## A script

```python
import hectiqlab as hl

with hl.run(
    "two-tower, 3 epochs",
    category="training",
    tags=["ENG-42"],
    config={"lr": 1e-3, "epochs": 3},
):
    data = hl.datasets.get("interactions").unwrap()
    hl.datasets.attach(data.id)
    with hl.stage("train"):
        for epoch in hl.each(range(3), "epoch"):
            loss = train_one_epoch()
            hl.add_metric("loss", step=epoch, value=loss)
    hl.models.create("checkpoints/", name="two-tower").result()
```

A run captures its commit, branch, diff, installed packages, machine and command line as it opens, and ends `completed`, `failed` (with its traceback) or `stopped`, whether it was opened with `with` or left open until the script crashed or exited. One exit it cannot see: a run opened without `with` in a script that calls `sys.exit(1)` ends `completed`, because Python never hands a `SystemExit` to the exception hook; open the run with `with` to record the exit status. Stages nest: a `with`, a decorated function or a named `each` opens a stage inside whatever is open. Reads and changes return a `Result`; uploads and downloads return a `Future`, and an upload made while a run is open holds the run open until it lands, however long that takes (the bar shows it moving; Ctrl-C ends the run `stopped` without it).

An API that is down, slow or refusing never raises in the middle of a training loop: a failed read or change is an `Err`, and what a run recorded that did not land is said once, as a warning, when the run closes (`run.result()` raises it instead). A mistake in the call itself does raise, at once: a metric or a stage with no run open, a category the web app does not know.

Uploads and downloads draw a progress bar on stderr when it is a terminal. `progress=False` on the call, or `HECTIQLAB_PROGRESS=false`, draws none; `progress=True` draws one anyway.

The project comes from `project=`, `hl.set_project(...)`, `HECTIQLAB_PROJECT`, or the `.hectiqlab/config.toml` that `hlab init` writes. `HECTIQLAB_DISABLE=1`, or a `RANK`/`LOCAL_RANK` other than 0, records nothing.

A run names the ticket it serves with a tag, `ENG-42`; nothing here speaks to Linear. The ticket names its runs by citing them: every run read, in Python or with `--json`, carries `url`, its page in the web app. `hlab run list --tag ENG-42 --json` hands an agent, or a person, every run a ticket's work made, each with the address to cite.

## The command line

```bash
hlab login                      # make a key with your email and password
hlab init hectiq-ai/recsys      # this checkout records to that project
hlab run list --tag ENG-42 --json   # each run with its url, to cite on the ticket
hlab run get 42 --json          # runs are named by their rank, or their id
hlab run stages 42
hlab run tag 42 best            # dataset and model tag the same way
hlab metric get 42 loss
hlab dataset create data/ --name interactions --no-upload
hlab model download two-tower --version 1.3
hlab artifact get art_4f2c
```

Every command that reads or changes something takes `--json`, and prints one JSON document on stdout; prompts and progress bars go to stderr.

## The dashboard

`hlab ui` opens a terminal dashboard onto a project, `hectiqlab.tui`; `?` lists the keys. It opens on the project the SDK would record to (`-p`, `HECTIQLAB_PROJECT`, or `.hectiqlab/config.toml`), and on the switcher when nothing names one; `P` switches from anywhere. `1` to `5` are the dashboard, runs, datasets, models and tags.

Everything it shows comes from the API the web app reads, through the SDK's own read functions: it refreshes every second, and reads the series of the open runs and of those that ended in the last fourteen days, a hundred runs to a request. It reads only the series on screen: an older run's come when it is opened. An open run that has given no sign of life for an hour, no change to its row, no sample, no stage, is shown as silent and read once a minute. A file previews over its signed address a range at a time, so a 20 GB parquet shows its schema, its row count and its first rows without being downloaded; without the `parquet` extra, a parquet says what to install instead.

A screen (`tui/screens/`, built from `tui/widgets/`) never calls the SDK. It asks `tui/reads/`, and draws the SDK's own records. Importing `hectiqlab` never imports the dashboard.

## Development

```bash
cd sdk_v2
uv sync
uv run ruff check . && uv run ruff format --check . && uv run ty check && uv run lint-imports
uv run pytest -m "not integration"
```

The integration tests talk to the API running in `api/compose.yaml`, logged in as the user they seed it with, whose email and password come from your environment. They are skipped when the stack is not up or the login is not set, and fail instead with `HECTIQLAB_INTEGRATION=required`, as in CI:

```bash
docker compose -f ../api/compose.yaml up -d --build
export HECTIQLAB_TEST_EMAIL=you@example.com HECTIQLAB_TEST_PASSWORD=...  # 8+ chars, upper, lower, digit
uv run pytest -m integration
```

The stack, its login, the fresh key and project each test gets, and the clean process every test starts from are in `testing/hectiqlab_stack.py`, a pytest plugin every test loads. It is not part of the wheel. The dashboard's tests, in `tests/tui/`, read a seeded project instead of the API, except those in `tests/tui/integration/`.

On a pull request that touches `api/` or `sdk_v2/`, `.github/workflows/hectiqlab_checks.yml` runs these checks and the unit tests on Python 3.10 and 3.13, and the integration tests against one stack.

## Releasing

`hectiqlab` is one package, the SDK, the command line and the dashboard at one version. A release is one button: **Actions → Release hectiqlab → Run workflow** on `main`, picking `patch`, `minor` or `major` (or `gh workflow run release_hectiqlab.yml -f bump=minor`).

The workflow (`.github/workflows/release_hectiqlab.yml`) runs the checks, including the integration tests; moves the version and re-locks; builds the package and checks its metadata; commits `Release hectiqlab X.Y.Z` to `main` and tags it `hectiqlab-vX.Y.Z`; creates the GitHub release with the subjects of the commits under `sdk_v2/` since the last release; then publishes it to PyPI. Nobody edits a version by hand. If only the publishing fails, re-running that job publishes the distributions already built.

The version steps live in `.github/scripts/release_hectiqlab.py`, which runs the same on a clone (`bump`, `notes`, `check`).

Once, before the first release, and close together, because a pending publisher does not reserve the name ([PyPI docs](https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/)):

1. On pypi.org, under your account's **Publishing** page, add a GitHub Actions pending publisher for the project `hectiqlab`: repository `HectiqAI/hectiq-lab-revision`, workflow `release_hectiqlab.yml`, environment `pypi`.
2. In the repository's settings, create the `pypi` environment. Required reviewers on it make every release wait for an approval before it publishes.
3. Run the first release. It ships the version the pyproject already declares, `0.1.0`, whichever bump is picked; every release after it bumps.
