Metadata-Version: 2.4
Name: braven
Version: 0.4.1
Summary: Python SDK for the Braven experiment tracker
Author: Braven
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28
Provides-Extra: dataframe
Requires-Dist: pandas>=1.5; extra == "dataframe"
Provides-Extra: plots
Requires-Dist: matplotlib>=3.5; extra == "plots"
Requires-Dist: Pillow>=9.0; extra == "plots"
Provides-Extra: parquet
Requires-Dist: pyarrow>=14; extra == "parquet"
Dynamic: license-file

# braven (Python SDK)

The Braven experiment tracker's Python SDK. **This package
(`packages/sdk-python`, version `0.4.1`) is the single source of truth.**

The former external distribution
([`braven-lab/braven-python`](https://github.com/braven-lab/braven-python)) was
retired and folded in here: this package now carries the newer `0.4.0` object
model (a `Run` from `braven.init()`, the removed-ambient `__getattr__` guard,
image-preview generation) **plus** this repo's Parquet dual-write path, which
the external package never had.

The pipeline worker installs this package from source: `apps/worker/Dockerfile`
builds from the repo root and runs `pip install "/opt/sdk-python[parquet]"`, so
the running pipeline tracks `packages/sdk-python` directly.

## Install

```bash
pip install "braven"                 # requests only
pip install "braven[dataframe]"      # + pandas, for .as_dataframe() and table()
pip install "braven[plots]"          # + matplotlib/Pillow, for uploading a Figure
pip install "braven[parquet]"        # + pyarrow, for the Parquet dual-write
pip install ./packages/sdk-python    # install this repo's 0.4.1 from source
```

Releases are cut from this repo and published to PyPI (see
[Release](#release)). Until the first `sdk-python-v0.4.1` tag is published, the
PyPI `braven` artifact is still the retired external `0.4.0`, so install from
the checkout (`pip install ./packages/sdk-python`, or `uv sync` for
development) to get `0.4.1`.

## Login

```bash
python -m braven login
```

Prompts for your backend URL and an SDK API key (`bvn_api_...`, from your
Braven account's Settings → API keys) and saves them to
`~/.braven/config.json`. The key is sent as `Authorization: Bearer bvn_api_...`
and authenticates as its owning user. A fresh, interactive first run of
`braven.init()` with no saved credentials prompts for the key instead of
failing (wandb-style).

For CI and test scripts, set `BRAVEN_API_KEY` in the environment instead (and
`BRAVEN_API_URL` for a non-default backend): it supplies the credential without
reading or writing `~/.braven/config.json`. With neither an env key nor a saved
config, a non-interactive run gets a clear instruction instead.

## Object model

Every logging call goes through one `Run`, returned by `braven.init()`. The
only thing that differs between a script you run yourself and a pipeline script
is how the Experiment is identified — every method after that is identical.

```python
import braven

run = braven.init(name="My Experiment", company="My Company", project="My Project")

run.config("radius_um", 55.0)
run.summary("q_factor", 18_400)
run.series("spectrum", [0.1, 0.4, 0.2])          # numeric series (JSON)
run.plot_series("snr_vs_temp", y=snr, x=temps, y_label="SNR")  # interactive plot
run.table("cycling_data", dataframe)             # named, columnar (pandas)
run.upload("spectrum.png")
run.finish()
```

`flush=True` (the default) sends each call immediately; pass `flush=False` to
batch into one write at `run.flush()`/`run.finish()`.

Pipeline scripts (run by the Braven worker) call `braven.init()` bare and adopt
the Experiment the platform already created — no credentials or project. The
same script run standalone falls back to **local dry mode**: every call prints
`[braven:local] would …` instead of raising, and an uploaded `Figure` is saved
under `./braven_local_output/` (ADR-0008).

Devices are a coordinate, not a suffix (ADR-0013): `run.set_device(key)`
retargets `run` onto that device's child Experiment, and `run.set_device(None)`
returns to the parent:

```python
for sensor_id, snr in results.items():
    run.set_device(sensor_id)
    run.summary("SNR", snr)
run.set_device(None)
```

## Parquet dual-write

Every data-bearing write — `summary()`, `series()`, `table()`, `plot_series()`
and an uploaded matplotlib `Figure` — also posts its dataset as Parquet to
`/datasets`, so analytics can query it instead of parsing JSON. Requires
`braven[parquet]`; the JSON log is always written first and a dual-write failure
is reported on stderr, never failing the logging call. Datasets are
parent-Experiment scoped — a device-scoped write stays JSON-only, since the
client has no id for the device's child Experiment.

## Optional dependencies

`requests` is the only hard dependency. Everything else is optional and
imported lazily:

| Feature | Install |
|---|---|
| `.as_dataframe()` on a downloaded file | `braven[dataframe]` |
| Uploading a live matplotlib `Figure` (and its list thumbnail) | `braven[plots]` |
| Parquet dual-write | `braven[parquet]` |

## Development

This package is managed by [`uv`](https://docs.astral.sh/uv/) and type-checked
by [`ty`](https://docs.astral.sh/ty/), both from the lock:

```bash
uv sync              # environment from uv.lock (incl. the dev group)
uv run pytest        # test suite
uv run ty check      # type check
```

`ty check` is wired into the repo gate: `packages/sdk-python/package.json`
defines `check-types: uv run ty check`, so `nub run -r check-types` (part of
`nub run check`) checks this package too.

### Release

Publishing is owned by this repo; no external repository is involved.

1. Bump `[project].version` in `packages/sdk-python/pyproject.toml`. It is the
   single version source — the module hard-codes no version.
2. `uv build` (from `packages/sdk-python`) and confirm `dist/` names the
   artifacts `braven-<version>`.
3. Tag the commit `sdk-python-v<version>` and push the tag. A branch push does
   not publish; the tag does.
4. Maintainer setup (once): add a PyPI API token for the `braven` project as the
   repository secret `PYPI_TOKEN` (Settings → Secrets and variables → Actions →
   New repository secret). The workflow passes it to `uv publish` as
   `UV_PUBLISH_TOKEN`.

`.github/workflows/release-sdk-python.yml` fails before building if the tag and
`[project].version` disagree. PyPI refuses to overwrite an existing version, so
bump the version rather than re-tagging.

## License

MIT — see [LICENSE](LICENSE).
