Metadata-Version: 2.4
Name: buro
Version: 0.0.14
Summary: Experiment tracker and lab journal made for humans — and sexy human-agent interaction for AI research
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: lz4>=4.3
Requires-Dist: nvidia-ml-py>=12.0
Requires-Dist: psutil>=6.0
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: mutmut<3,>=2.5; extra == 'dev'
Requires-Dist: numpy>=1.26; extra == 'dev'
Requires-Dist: pandas>=2.0; extra == 'dev'
Requires-Dist: pillow>=10.0; extra == 'dev'
Requires-Dist: polars>=0.20; extra == 'dev'
Requires-Dist: psycopg2-binary>=2.9; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: respx>=0.22; extra == 'dev'
Provides-Extra: pandas
Requires-Dist: pandas>=2.0; extra == 'pandas'
Provides-Extra: polars
Requires-Dist: polars>=0.20; extra == 'polars'
Description-Content-Type: text/markdown

# buro

An experiment tracker and lab journal made for humans — and for sexy human ⇄ agent
interaction in AI research. Log your runs, metrics, and media to a
[Buro](https://github.com/dunnolab/buro) server.

## Install

```bash
pip install buro
```

## Quickstart

```python
import buro

run = buro.init(project="my-project")        # or "team-slug/my-project"
for step in range(100):
    buro.log({"loss": 1.0 / (step + 1), "acc": step / 100}, step=step)
buro.finish()
```

`init(project=...)` resolves the project against the server and auto-creates it
if it doesn't exist. `project` is a slug ref: `"slug"` (personal) or
`"team-slug/slug"` (team).

## Authenticate

Log in once on your machine:

```bash
buro login --api-url https://<your-buro-server>
buro whoami
```

The SDK resolves credentials in this order:

1. `buro.setup(api_key=..., api_url=...)` in code
2. `BURO_API_KEY` / `BURO_API_URL` environment variables
3. `~/.buro/credentials` (written by `buro login`)

On a cluster or in CI, the env-var path is usually easiest:

```bash
export BURO_API_KEY=buro_key_...
export BURO_API_URL=https://<your-buro-server>
```

## Log media

```python
buro.log({"sample": buro.Image("path/to/image.png")})   # numpy array or PIL image also work
# also available: buro.Audio, buro.Video
```

## Code tracking

Every run automatically snapshots the **source code that actually ran**, so the
compare view can show exactly what changed between two runs — not just which
hyperparameters differed.

It works by *tracing*, not scanning: buro watches the Python modules your run
imports and keeps the ones that are *your* code — everything outside the
standard library, your installed packages, and buro itself. Each file is
recorded under its **import path** (`models/encoder.py`, not an absolute path on
your machine), hashed, and uploaded once — identical files are shared across
runs, so a hyperparameter sweep that doesn't touch the code uploads nothing new.

Because it follows the imports rather than walking a directory, the snapshot is:

- **exactly your run's code** — the entry script plus the modules it imported,
  across packages; never a stray sweep of your whole repo, a sibling project, or
  config/secret files that happen to sit nearby;
- **the same on every machine** — a file is identified by how it's imported, so
  the same code on your laptop and on a cluster diffs as *unchanged*;
- **best-effort** — snapshotting never slows down or crashes your run.

Config files (`config.yaml`, lockfiles, …) are **not** captured — their values
already live in your run config (`buro.init(config=...)`). For an unusual layout,
or to pin exactly what's captured, set `BURO_CODE_ROOT=/path/to/project`.

### What gets captured

The snapshot is **Python source only**: your entry script plus the imported user
modules that resolve to a `.py`/`.pyi` file — recorded by import path, hashed, and
deduplicated.

These are **not** captured:

- **Compiled extension modules** — `.so` / `.pyd` / `.dylib` (Cython, pybind11, and
  CUDA extensions, plus JIT-compiled artifacts such as Triton's `cuda_utils`). Build
  output, not source.
- **Python bytecode** — `.pyc` without source.
- **Any file detected as binary** — a NUL byte in its first bytes.
- **Third-party / standard-library packages** — already excluded by the user-code
  filter.
- **Non-imported files** — configs, lockfiles, data, and native source
  (`.cu` / `.cpp`) that isn't itself an imported module; the snapshot follows
  `sys.modules`, not a directory walk.

The identity of compiled and third-party code lives in the **environment capture**
instead (`pip freeze`, CUDA / driver, GPU, host metadata). When modules are skipped,
buro logs one line naming them — visible in your console and the run's Logs.

## System metrics

Every run also logs host **system metrics** in the background — no setup needed:
CPU utilization (system-wide, plus `system/cpu.busiest` for the single hottest
core), RAM, disk and network throughput in MB/s, and — on NVIDIA hosts — per-GPU
utilization, memory, temperature, and power.

To stay light on storage, buro **samples often but logs rarely**: it reads the
counters every ~2s and writes one windowed summary every ~30s (mean for gauges,
peak for the busiest core, rate for I/O). Tune or turn it off:

```python
buro.setup(
    system_emit_sec=30,           # how often a summary point is written (default 30s)
    system_sample_sec=2,          # how often counters are sampled underneath (default 2s)
    system_metrics_enabled=True,  # set False to disable system metrics entirely
)
```

## Reading your data (agents + analysis)

Everything you log is queryable back — from Python and the shell — built for
agent-driven research and quick analysis.

```python
from buro import query

qc = query.connect(api_key="buro_key_...", base_url="https://<your-buro-server>")

run = qc.run("a1b2c3d4")               # 8-hex short id (from the UI or `buro runs`)
run.scalars()                          # -> the run's metric names

s = run.scalar("val/loss").fetch()     # a faithful series + authoritative stats
s.stats.min                            # exact whole-series min: {step, value}
s.points[-1]                           # the real logged points, with timestamps

run.scalar("val/loss").range(9000, 10000).downsample(200).fetch()   # zoom a window
qc.summaries(["a1b2c3d4", "e5f6a7b8"], names=["val/loss"]).rows()    # compare runs
```

`fetch()` returns both the curve (`.points` — real logged points, real
timestamps) and **authoritative `.stats`** (`min`/`max`/`first`/`last` as
`{step, value}`, plus `count`/`mean`) computed over the whole requested range.
Read exact extrema from `.stats` — never by reducing the (possibly downsampled)
points. `.range(a, b)` and `.downsample(n)` keep `.stats` scoped to the window.

The same surface is a CLI, JSON-friendly for scripts and agents:

```bash
buro runs team-slug/my-project                            # list runs + short ids
buro scalars series a1b2c3d4 --name val/loss --json
buro scalars summary --runs a1b2c3d4,e5f6a7b8 --names val/loss --json
```

## Docs

- `wandb` API compatibility: [`docs/wandb-compatibility.md`](docs/wandb-compatibility.md)
- Release/publishing process: [`docs/publishing.md`](docs/publishing.md)
