Metadata-Version: 2.4
Name: o41-metrics-api
Version: 0.2.1
Summary: Python client for the 041 Metrics v2 API
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Dist: niquests>=3.16.0
Requires-Dist: pydantic>=2.13.3
Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
Requires-Dist: ruff>=0.15.12 ; extra == 'dev'
Requires-Dist: ty>=0.0.34 ; extra == 'dev'
Requires-Python: >=3.14, <3.15
Provides-Extra: dev
Description-Content-Type: text/markdown

# 041 Metrics API Client

Credential and host resolution follows the shared
[Metrics client credentials contract](../../docs/metrics/credentials.md).

Python client for the [041 Metrics](https://metrics.041.io/) v2 API. Use it to
find and organize folders and experiments and to read one experiment's record,
catalog, annotations, and series from code.

Pick the right tool:

- **Analysis across experiments:** the `o41-metrics-duckdb` extension attaches
  Metrics as a database with an on-disk cache. Use it for plots, tables, joins,
  and anything spanning a folder.
- **Recording training or evaluation jobs:** `o41-metrics-collector` queues,
  batches, and delivers metrics in the background. This client's write session
  exists for API completeness, tests, and backfills.
- **Programmatic control and one-experiment reads:** this client.

## Install

```bash
uv add o41-metrics-api
```

## Connect

```python
from o41_metrics_api import Metrics

metrics = Metrics()
```

The key comes from `api_key=`, then `O41_METRICS_API_KEY`, then
`SYVAIN_METRICS_API_KEY`, then the login saved by `o41-metrics auth login`, like
the `o41-metrics` CLI. A key goes only to the host it is paired with. An
argument or environment key goes to `host=`, then `O41_METRICS_HOST`, then
`SYVAIN_METRICS_HOST`, then `https://metrics.041.io`, never to the saved login's
host. The saved login's key goes only to the host it was saved for; if `host=`
or `O41_METRICS_HOST` names another one, `Metrics()` raises `MetricsAuthError`.

For example, if a saved login names `https://custom.example.test`, setting
`O41_METRICS_API_KEY` alone sends that key to `https://metrics.041.io`.

`Metrics` is also a context manager that closes its HTTP session.

## Folders and experiments

Address a folder by absolute path or id; `/` is the root. Address an experiment
by id or slug.

```python
mamba = metrics.make_folder("/models/mamba")  # creates missing parents
mamba = metrics.folder("/models/mamba")       # MetricsNotFound when missing
mamba.folders()                               # direct children
mamba.experiments()                           # direct members
mamba.experiments("run-*", recursive=True)    # slug glob, whole subtree
metrics.experiments("syv-2039-*")             # every experiment matching a glob

exp = metrics.experiment("mamba-run-001")
exp.move("/archive")
mamba.rename("mamba-v2")
mamba.move("/")
```

Lookups use a snapshot of the folder tree, read on first use. Changes made
through the client refresh it. A `folder()` or `experiment()` lookup that misses
reads the tree again once before raising. Listings such as `folders()` and
`experiments()` use the snapshot as is, so call `metrics.refresh()` to list what
was created elsewhere. `Folder` and `Experiment` are immutable values. A rename
or move returns a new handle.

## Read one experiment

```python
info = exp.info()          # status, description, meta, error, lifecycle datetimes
catalog = exp.catalog()    # series names with their metadata keys and values
notes = exp.annotations()  # every annotation, newest first

for series in exp.series("loss", where={"split": "valid"}):
    print(series.metadata, series.steps, series.values)
```

`series()` returns one `Series` per metadata partition. Its `steps`,
`timestamps_ms`, and `values` are aligned. Pass one name, several names, or none
to read every series of the experiment. Results follow the order of the names,
or name order when you pass none. One streaming request reads up to 1,000
series. `where` matches exact metadata values, `has=("rank",)` requires a key,
and `x="timestamp"` orders points by time instead of step. The client returns
series only after the stream completes, so partial data is never returned.

## Write session

```python
with metrics.open_experiment(
    "backfill-001", description="Imported run", meta={"seed": 7}, folder="/imports"
) as run:
    run.start()
    run.metric("loss", 0.25, step=1, metadata={"split": "train"})
    run.done()
```

Each call is one acknowledged request. `metrics()` takes many `MetricPoint`s and
sends up to 1000 per request. The session refreshes its experiment token before
it expires and revokes the token when the block exits. Reopening an existing
slug replaces its description and meta. An experiment already in another folder
stays there. A token left idle past its 24 hour expiry cannot be refreshed, so
reopen the experiment instead. For real training jobs use
`o41-metrics-collector`.

## Other events

`metrics.send(event, data)` sends any v2 organization event to `/api/v2/events`
and returns the reply data. The API reference is at
`https://metrics.041.io/api/openapi.json`.

## Errors and retries

Every failure is a `MetricsError` with `code`, `message`, `status`, and
`retryable`. `MetricsNotFound` (also a `LookupError`) reports an unresolved
reference. `MetricsAuthError` reports missing credentials.

Reads and metric writes retry transient failures with backoff for up to five
minutes. Only a rejection the Metrics API wrote is definitive: no reply, a
timeout, a 2xx that is not the API's reply, 408, 425, 429, 5xx, and Cloudflare's
own edge error replies (its `1xxx` errors, whatever status they come with) are
retried. A retried metric keeps its original message id, which the API
deduplicates. Every other write never retries, because repeating it can have
side effects: folder creation, moves, renames, `open_experiment`, lifecycle
events (a replayed `done` would move `done_at`), annotations, and `send()`.

`ExperimentRun` drops non-finite metric values with a warning and sends finite
points in the same batch. Earlier releases raised `ValueError` for the whole
batch. See the shared credentials contract for this compatibility change.
