buro 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- buro-0.0.1/.gitignore +9 -0
- buro-0.0.1/.mutmut-cache +0 -0
- buro-0.0.1/Makefile +7 -0
- buro-0.0.1/PKG-INFO +81 -0
- buro-0.0.1/README.md +60 -0
- buro-0.0.1/docs/publishing.md +37 -0
- buro-0.0.1/docs/wandb-compatibility.md +87 -0
- buro-0.0.1/examples/MANUAL_TESTING.md +172 -0
- buro-0.0.1/examples/log_demo.py +110 -0
- buro-0.0.1/pyproject.toml +44 -0
- buro-0.0.1/scripts/check_release_version.py +73 -0
- buro-0.0.1/scripts/conftest.py +10 -0
- buro-0.0.1/scripts/mutation_check.py +179 -0
- buro-0.0.1/src/buro/__init__.py +201 -0
- buro-0.0.1/src/buro/_compat.py +37 -0
- buro-0.0.1/src/buro/buffer.py +44 -0
- buro-0.0.1/src/buro/cli.py +130 -0
- buro-0.0.1/src/buro/client.py +189 -0
- buro-0.0.1/src/buro/code_capture.py +201 -0
- buro-0.0.1/src/buro/code_snapshot.py +335 -0
- buro-0.0.1/src/buro/code_snapshot_uploader.py +159 -0
- buro-0.0.1/src/buro/config.py +41 -0
- buro-0.0.1/src/buro/credentials.py +42 -0
- buro-0.0.1/src/buro/errors.py +42 -0
- buro-0.0.1/src/buro/log_capture.py +168 -0
- buro-0.0.1/src/buro/media.py +145 -0
- buro-0.0.1/src/buro/run.py +414 -0
- buro-0.0.1/src/buro/settings.py +69 -0
- buro-0.0.1/src/buro/slug_ref.py +33 -0
- buro-0.0.1/src/buro/system_metrics.py +96 -0
- buro-0.0.1/src/buro/wal.py +35 -0
- buro-0.0.1/tests/conftest.py +0 -0
- buro-0.0.1/tests/e2e/__init__.py +0 -0
- buro-0.0.1/tests/e2e/conftest.py +244 -0
- buro-0.0.1/tests/e2e/fake_server.py +39 -0
- buro-0.0.1/tests/e2e/project_tree.py +66 -0
- buro-0.0.1/tests/e2e/test_capture_scenarios.py +217 -0
- buro-0.0.1/tests/e2e/test_lifecycle_scenarios.py +237 -0
- buro-0.0.1/tests/test___init__.py +61 -0
- buro-0.0.1/tests/test__compat.py +51 -0
- buro-0.0.1/tests/test_buffer.py +58 -0
- buro-0.0.1/tests/test_check_release_version.py +71 -0
- buro-0.0.1/tests/test_cli.py +270 -0
- buro-0.0.1/tests/test_client.py +335 -0
- buro-0.0.1/tests/test_code_capture.py +260 -0
- buro-0.0.1/tests/test_code_snapshot.py +1102 -0
- buro-0.0.1/tests/test_code_snapshot_uploader.py +408 -0
- buro-0.0.1/tests/test_config.py +33 -0
- buro-0.0.1/tests/test_credentials.py +57 -0
- buro-0.0.1/tests/test_integration.py +487 -0
- buro-0.0.1/tests/test_log_capture.py +385 -0
- buro-0.0.1/tests/test_media.py +154 -0
- buro-0.0.1/tests/test_run.py +1102 -0
- buro-0.0.1/tests/test_settings.py +91 -0
- buro-0.0.1/tests/test_slug_init.py +221 -0
- buro-0.0.1/tests/test_system_metrics.py +31 -0
- buro-0.0.1/tests/test_wal.py +39 -0
- buro-0.0.1/tests/test_wandb_compat.py +68 -0
- buro-0.0.1/uv.lock +841 -0
buro-0.0.1/.gitignore
ADDED
buro-0.0.1/.mutmut-cache
ADDED
|
Binary file
|
buro-0.0.1/Makefile
ADDED
buro-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: buro
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Experiment tracker and lab journal made for humans — and sexy human-agent interaction for AI research
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: httpx>=0.27
|
|
7
|
+
Requires-Dist: lz4>=4.3
|
|
8
|
+
Requires-Dist: pathspec>=0.12
|
|
9
|
+
Requires-Dist: psutil>=6.0
|
|
10
|
+
Requires-Dist: typer>=0.12
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: mutmut<3,>=2.5; extra == 'dev'
|
|
13
|
+
Requires-Dist: numpy>=1.26; extra == 'dev'
|
|
14
|
+
Requires-Dist: pillow>=10.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: psycopg2-binary>=2.9; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
17
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
18
|
+
Provides-Extra: gpu
|
|
19
|
+
Requires-Dist: pynvml>=12.0; extra == 'gpu'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# buro
|
|
23
|
+
|
|
24
|
+
An experiment tracker and lab journal made for humans — and for sexy human ⇄ agent
|
|
25
|
+
interaction in AI research. Log your runs, metrics, and media to a
|
|
26
|
+
[Buro](https://github.com/dunnolab/buro) server.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install buro
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import buro
|
|
38
|
+
|
|
39
|
+
run = buro.init(project="my-project") # or "team-slug/my-project"
|
|
40
|
+
for step in range(100):
|
|
41
|
+
buro.log({"loss": 1.0 / (step + 1), "acc": step / 100}, step=step)
|
|
42
|
+
buro.finish()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`init(project=...)` resolves the project against the server and auto-creates it
|
|
46
|
+
if it doesn't exist. `project` is a slug ref: `"slug"` (personal) or
|
|
47
|
+
`"team-slug/slug"` (team).
|
|
48
|
+
|
|
49
|
+
## Authenticate
|
|
50
|
+
|
|
51
|
+
Log in once on your machine:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
buro login --api-url https://<your-buro-server>
|
|
55
|
+
buro whoami
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The SDK resolves credentials in this order:
|
|
59
|
+
|
|
60
|
+
1. `buro.setup(api_key=..., api_url=...)` in code
|
|
61
|
+
2. `BURO_API_KEY` / `BURO_API_URL` environment variables
|
|
62
|
+
3. `~/.buro/credentials` (written by `buro login`)
|
|
63
|
+
|
|
64
|
+
On a cluster or in CI, the env-var path is usually easiest:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export BURO_API_KEY=buro_key_...
|
|
68
|
+
export BURO_API_URL=https://<your-buro-server>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Log media
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
buro.log({"sample": buro.Image("path/to/image.png")}) # numpy array or PIL image also work
|
|
75
|
+
# also available: buro.Audio, buro.Video
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Docs
|
|
79
|
+
|
|
80
|
+
- `wandb` API compatibility: [`docs/wandb-compatibility.md`](docs/wandb-compatibility.md)
|
|
81
|
+
- Release/publishing process: [`docs/publishing.md`](docs/publishing.md)
|
buro-0.0.1/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# buro
|
|
2
|
+
|
|
3
|
+
An experiment tracker and lab journal made for humans — and for sexy human ⇄ agent
|
|
4
|
+
interaction in AI research. Log your runs, metrics, and media to a
|
|
5
|
+
[Buro](https://github.com/dunnolab/buro) server.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install buro
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import buro
|
|
17
|
+
|
|
18
|
+
run = buro.init(project="my-project") # or "team-slug/my-project"
|
|
19
|
+
for step in range(100):
|
|
20
|
+
buro.log({"loss": 1.0 / (step + 1), "acc": step / 100}, step=step)
|
|
21
|
+
buro.finish()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`init(project=...)` resolves the project against the server and auto-creates it
|
|
25
|
+
if it doesn't exist. `project` is a slug ref: `"slug"` (personal) or
|
|
26
|
+
`"team-slug/slug"` (team).
|
|
27
|
+
|
|
28
|
+
## Authenticate
|
|
29
|
+
|
|
30
|
+
Log in once on your machine:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
buro login --api-url https://<your-buro-server>
|
|
34
|
+
buro whoami
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The SDK resolves credentials in this order:
|
|
38
|
+
|
|
39
|
+
1. `buro.setup(api_key=..., api_url=...)` in code
|
|
40
|
+
2. `BURO_API_KEY` / `BURO_API_URL` environment variables
|
|
41
|
+
3. `~/.buro/credentials` (written by `buro login`)
|
|
42
|
+
|
|
43
|
+
On a cluster or in CI, the env-var path is usually easiest:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export BURO_API_KEY=buro_key_...
|
|
47
|
+
export BURO_API_URL=https://<your-buro-server>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Log media
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
buro.log({"sample": buro.Image("path/to/image.png")}) # numpy array or PIL image also work
|
|
54
|
+
# also available: buro.Audio, buro.Video
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Docs
|
|
58
|
+
|
|
59
|
+
- `wandb` API compatibility: [`docs/wandb-compatibility.md`](docs/wandb-compatibility.md)
|
|
60
|
+
- Release/publishing process: [`docs/publishing.md`](docs/publishing.md)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Publishing the buro SDK to PyPI
|
|
2
|
+
|
|
3
|
+
Releases are published by `.github/workflows/sdk-publish.yml` when a GitHub
|
|
4
|
+
Release with a `sdk-v<version>` tag is published. Auth is PyPI Trusted
|
|
5
|
+
Publishing (OIDC) — there are no secrets to manage.
|
|
6
|
+
|
|
7
|
+
## Cut a release
|
|
8
|
+
|
|
9
|
+
1. Bump the single version source on a branch and merge to `main`:
|
|
10
|
+
```python
|
|
11
|
+
# sdk/src/buro/__init__.py
|
|
12
|
+
__version__ = "0.0.2"
|
|
13
|
+
```
|
|
14
|
+
(`pyproject.toml` reads this automatically via hatchling — do not edit a
|
|
15
|
+
version there.)
|
|
16
|
+
|
|
17
|
+
2. Create the GitHub Release from the merged commit:
|
|
18
|
+
```bash
|
|
19
|
+
gh release create sdk-v0.0.2 --title "SDK 0.0.2" --notes "..."
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The workflow then runs the unit tests, builds the sdist + wheel, asserts the
|
|
23
|
+
tag matches the built version, runs `twine check`, and uploads to PyPI.
|
|
24
|
+
|
|
25
|
+
## Guards (why a bad release won't reach PyPI)
|
|
26
|
+
|
|
27
|
+
- The tag must be `sdk-v<version>` or the job is skipped (monorepo guard).
|
|
28
|
+
- The tag's version must equal the built artifact version
|
|
29
|
+
(`scripts/check_release_version.py`) or the run fails before upload.
|
|
30
|
+
- A version already on PyPI is rejected by PyPI — bump and re-tag.
|
|
31
|
+
|
|
32
|
+
## One-time setup (already done)
|
|
33
|
+
|
|
34
|
+
- **pypi.org pending publisher**: project `buro`, owner `dunnolab`, repo
|
|
35
|
+
`buro`, workflow `sdk-publish.yml`, environment `pypi`.
|
|
36
|
+
- **GitHub repo Environment `pypi`** (optionally with a required reviewer to
|
|
37
|
+
gate each publish behind one approval click).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# wandb Compatibility Matrix
|
|
2
|
+
|
|
3
|
+
`buro` accepts a subset of wandb's API surface so existing training scripts can switch with minimal edits. This document is the **source of truth** for how each surface behaves.
|
|
4
|
+
|
|
5
|
+
## Level legend
|
|
6
|
+
|
|
7
|
+
| Level | Meaning | User experience |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **L0** | Identical — same signature, same effect | Silent |
|
|
10
|
+
| **L1** | Equivalent semantics, impl details may differ | Silent |
|
|
11
|
+
| **L2** | Param accepted; visual/extra effect dropped. No data loss | INFO once per (process, api, param) |
|
|
12
|
+
| **L3** | Call works but some data dropped/truncated | WARNING every call |
|
|
13
|
+
| **L4** | Unsupported; silent no-op would be dangerous | `NotImplementedError` |
|
|
14
|
+
|
|
15
|
+
**Rule:** every wandb-shaped surface in `buro` MUST declare a level here. Undeclared = L4.
|
|
16
|
+
|
|
17
|
+
## Matrix
|
|
18
|
+
|
|
19
|
+
### `buro.Image` (vs `wandb.Image`)
|
|
20
|
+
|
|
21
|
+
| Surface | Level | V1 behavior | Since |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| `Image(data)` — array / PIL / path | L0 | Identical | 0.1 |
|
|
24
|
+
| `Image(caption=...)` | L0 | Sent to server and rendered in panel + modal | 0.2 |
|
|
25
|
+
| `Image(file_type=...)` | L0 | Identical | 0.2 |
|
|
26
|
+
| `Image(mode=...)` | L2 | Accepted, ignored. INFO once | 0.2 |
|
|
27
|
+
| `Image(classes=...)` | L2 | Stored as sidecar JSONB on server, not rendered | 0.2 |
|
|
28
|
+
| `Image(boxes=...)` | L2 | Stored as sidecar JSONB, not rendered | 0.2 |
|
|
29
|
+
| `Image(masks=...)` | L2 | Stored as sidecar JSONB, not rendered | 0.2 |
|
|
30
|
+
| `Image(grouping=...)` | L2 | Ignored | 0.2 |
|
|
31
|
+
|
|
32
|
+
### `buro.log`
|
|
33
|
+
|
|
34
|
+
| Surface | Level | V1 behavior | Since |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| `log({"k": Image(...)})` | L0 | Identical | 0.2 |
|
|
37
|
+
| `log({"k": [Image, Image, ...]})` | L3 | Keeps first, WARN every call | 0.2 |
|
|
38
|
+
|
|
39
|
+
### `buro.Run.fail()`
|
|
40
|
+
|
|
41
|
+
`wandb` doesn't have a direct equivalent — failure is signaled via `wandb.finish(exit_code=N)` (non-zero exit_code → "failed" state).
|
|
42
|
+
|
|
43
|
+
| Surface | Level | V1 behavior | Since |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `run.fail()` | L1 | Attests `terminal_reason=user_failed` on the server. No wandb equivalent — use instead of `wandb.finish(exit_code=1)` for explicit failure | 0.2 |
|
|
46
|
+
| `run.fail(reason)` | L1 | Reason string carried in `terminal_reason_detail.message` | 0.2 |
|
|
47
|
+
| `wandb.finish(exit_code=N)` | L2 | Accepted, `exit_code` arg ignored. Users should migrate to `run.finish()` for success / `run.fail(reason)` for failure | 0.2 |
|
|
48
|
+
|
|
49
|
+
### Run state / `terminal_reason`
|
|
50
|
+
|
|
51
|
+
`wandb` exposes `wandb.Run.state` as a string property (`running`, `finished`, `failed`, `crashed`). `buro` does **not** currently expose `run.state` (undeclared = L4 = `NotImplementedError` on access).
|
|
52
|
+
|
|
53
|
+
When/if we add `run.state` in the future, it will map our `terminal_reason` to wandb's shape:
|
|
54
|
+
|
|
55
|
+
| buro state | wandb `run.state` |
|
|
56
|
+
|---|---|
|
|
57
|
+
| (live, no terminal_reason) | `"running"` |
|
|
58
|
+
| `terminal_reason=user_finished` | `"finished"` |
|
|
59
|
+
| `terminal_reason=user_failed` | `"failed"` |
|
|
60
|
+
| `terminal_reason=exception_hooked` | `"failed"` (closest wandb equivalent for an SDK-caught exception) |
|
|
61
|
+
| `terminal_reason=interrupted` | `"failed"` (no wandb equivalent for SIGINT/SIGTERM; closest signal is failure) |
|
|
62
|
+
| `terminal_reason=unknown` | `"crashed"` (closest match; our model considers this the honest "we don't know" bucket) |
|
|
63
|
+
|
|
64
|
+
In V1 the SDK installs `sys.excepthook`, `signal.SIGINT`/`SIGTERM` handlers, and an `atexit` log-drain hook. These do not shadow any wandb name — they're SDK-internal infrastructure that produces `terminal_reason` writes on the server. `wandb` has similar (and similar-named) hooks; the difference users will observe is that buro never invents a `crashed` label for runs that exit silently — those stay `terminal_reason=unknown` until the server's heartbeat timeout fires.
|
|
65
|
+
|
|
66
|
+
### Unsupported (L4)
|
|
67
|
+
|
|
68
|
+
| Surface | V1 behavior |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `buro.Artifact` | `NotImplementedError` |
|
|
71
|
+
| `buro.Table` | `NotImplementedError` |
|
|
72
|
+
| `buro.Plotly` | `NotImplementedError` |
|
|
73
|
+
| `buro.Html` | `NotImplementedError` |
|
|
74
|
+
| `buro.Object3D` | `NotImplementedError` |
|
|
75
|
+
| `buro.Molecule` | `NotImplementedError` |
|
|
76
|
+
|
|
77
|
+
## Known V1 limits
|
|
78
|
+
|
|
79
|
+
- **Quota over-count on failed media uploads.** If `POST /runs/{id}/media` succeeds but the subsequent S3 PUT fails, the quota counter stays incremented. No reconcile job. Failure rate expected low; revisit if it matters.
|
|
80
|
+
- **No retry inside `_upload_media`.** A single transient S3 error logs WARNING and continues; the metric log call is not retried.
|
|
81
|
+
- **No frontend renderer for `Audio` / `Video`.** SDK classes upload but no viewer V1.
|
|
82
|
+
|
|
83
|
+
## Adding a new surface
|
|
84
|
+
|
|
85
|
+
1. Add a row to the matrix above with declared level and behavior.
|
|
86
|
+
2. If the level is L2 / L3 / L4: route through `buro._compat.emit_once` / `warn` / `unsupported`.
|
|
87
|
+
3. Add tests to `sdk/tests/test_wandb_compat.py` matching the level's contract.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Manual testing — Buro SDK credential profiles
|
|
2
|
+
|
|
3
|
+
A runbook for exercising the SDK the way real users run it. The journey is
|
|
4
|
+
always the same (create a project, log metrics, finish); what changes is **how
|
|
5
|
+
credentials reach the SDK**. Resolution precedence is:
|
|
6
|
+
|
|
7
|
+
> `buro.setup()` in code > `BURO_API_KEY` env var > `~/.buro/credentials` file
|
|
8
|
+
|
|
9
|
+
Driver script: [`log_demo.py`](./log_demo.py). It hardcodes no key — it relies
|
|
10
|
+
on whatever the environment provides, and prints which source it resolved so
|
|
11
|
+
each profile is observable.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## One-time setup
|
|
16
|
+
|
|
17
|
+
Each step says which directory to run it from — `npm run dev` blocks its
|
|
18
|
+
terminal, so use a separate terminal per long-running process.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. Backend stack — run from the REPO ROOT
|
|
22
|
+
# (--build picks up the device-auth migration + endpoints)
|
|
23
|
+
cd /path/to/buro # the repo root
|
|
24
|
+
docker compose up -d --build
|
|
25
|
+
|
|
26
|
+
# 2. Web frontend — run from the REPO ROOT, in its own terminal.
|
|
27
|
+
# Hosts /cli-auth and proxies /api -> :8000 on port 5173.
|
|
28
|
+
# Keep 5173 free; the device flow opens that exact URL.
|
|
29
|
+
cd /path/to/buro/web
|
|
30
|
+
npm install && npm run dev # leave running
|
|
31
|
+
|
|
32
|
+
# 3. Install the SDK so `buro` (CLI) and `import buro` (library) work.
|
|
33
|
+
# Run from the SDK directory (use a fresh venv if you prefer):
|
|
34
|
+
cd /path/to/buro/sdk
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> The example commands below assume you run them from the **repo root**
|
|
39
|
+
> (so `python sdk/examples/log_demo.py ...` resolves). Adjust the path if
|
|
40
|
+
> you `cd` elsewhere.
|
|
41
|
+
|
|
42
|
+
Then create an account: open <http://localhost:5173> and sign up (registration
|
|
43
|
+
is open in the compose stack).
|
|
44
|
+
|
|
45
|
+
All profiles below assume the server is at `http://localhost:8000`.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Profile A — Laptop, logged in (credentials file)
|
|
50
|
+
|
|
51
|
+
The everyday dev flow: authorize once via the browser, then just run scripts.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
buro login --api-url http://localhost:8000
|
|
55
|
+
# -> prints a code, opens http://localhost:5173/cli-auth?user_code=...
|
|
56
|
+
# -> approve in the browser
|
|
57
|
+
# -> terminal prints "Logged in as <you>"
|
|
58
|
+
|
|
59
|
+
buro whoami # prints your email (reads ~/.buro/credentials)
|
|
60
|
+
cat ~/.buro/credentials # {"api_url": "...", "api_key": "buro_key_..."} (mode 0600)
|
|
61
|
+
|
|
62
|
+
python sdk/examples/log_demo.py --project cli-login-test
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Expect:** `Credential source : ~/.buro/credentials file`, then 40 logged
|
|
66
|
+
steps. **Verify:** open <http://localhost:5173>, project `cli-login-test`, the
|
|
67
|
+
run shows `loss`/`accuracy` charts.
|
|
68
|
+
|
|
69
|
+
> Tip: grab the raw key for the next profiles — `cat ~/.buro/credentials`. You
|
|
70
|
+
> can also mint a dedicated key in the web UI under account settings.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Profile B — Cluster / CI job, env var only (no file)
|
|
75
|
+
|
|
76
|
+
The unattended path: a remote box with no `buro login`, only an exported key.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Simulate "no credentials file on this machine"
|
|
80
|
+
mv ~/.buro/credentials ~/.buro/credentials.bak # (restore later)
|
|
81
|
+
|
|
82
|
+
export BURO_API_KEY=buro_key_... # the raw key from Profile A
|
|
83
|
+
export BURO_API_URL=http://localhost:8000
|
|
84
|
+
|
|
85
|
+
python sdk/examples/log_demo.py --project cli-login-test --name cluster-run
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Expect:** `Credential source : BURO_API_KEY env var`; the run logs without any
|
|
89
|
+
login. **Verify:** the `cluster-run` run appears in the UI.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# cleanup
|
|
93
|
+
unset BURO_API_KEY BURO_API_URL
|
|
94
|
+
mv ~/.buro/credentials.bak ~/.buro/credentials
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Profile C — Explicit configuration in code (`buro.setup`)
|
|
100
|
+
|
|
101
|
+
A user who configures the SDK in code (e.g. keys pulled from their job's own
|
|
102
|
+
secret store). `setup()` wins over env and file.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Pass the key explicitly; the script calls buro.setup(api_key=..., api_url=...)
|
|
106
|
+
python sdk/examples/log_demo.py --project cli-login-test --name in-code \
|
|
107
|
+
--api-key buro_key_... --api-url http://localhost:8000
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Expect:** `Credential source : setup() in code (--api-key)`. **Verify:** the
|
|
111
|
+
`in-code` run appears in the UI.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Profile D — Precedence & negative paths
|
|
116
|
+
|
|
117
|
+
**D1 — env overrides a stale file.** With BOTH a file and an env var present,
|
|
118
|
+
the env var must win:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# (ensure ~/.buro/credentials exists from Profile A)
|
|
122
|
+
export BURO_API_KEY=buro_key_... # any valid key
|
|
123
|
+
export BURO_API_URL=http://localhost:8000
|
|
124
|
+
python sdk/examples/log_demo.py --project cli-login-test --name precedence
|
|
125
|
+
# Expect: "Credential source : BURO_API_KEY env var" and api_url = the env one
|
|
126
|
+
unset BURO_API_KEY BURO_API_URL
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**D2 — setup() overrides env + file.** With a file present and an env var
|
|
130
|
+
exported, ALSO pass `--api-key`: the printed source must be `setup() in code`.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
export BURO_API_KEY=buro_key_SHOULD_BE_IGNORED
|
|
134
|
+
python sdk/examples/log_demo.py --project cli-login-test \
|
|
135
|
+
--api-key buro_key_... --api-url http://localhost:8000
|
|
136
|
+
# Expect: "Credential source : setup() in code (--api-key)"
|
|
137
|
+
unset BURO_API_KEY
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**D3 — no credentials anywhere → clean error, not a traceback.**
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
mv ~/.buro/credentials ~/.buro/credentials.bak 2>/dev/null
|
|
144
|
+
env -u BURO_API_KEY -u BURO_API_URL python sdk/examples/log_demo.py --project x
|
|
145
|
+
# Expect: "No API key found in any source." + the three options, exit code 1
|
|
146
|
+
mv ~/.buro/credentials.bak ~/.buro/credentials 2>/dev/null
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**D4 — logout clears the file.**
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
buro logout # -> "Logged out." (removes ~/.buro/credentials)
|
|
153
|
+
buro whoami # -> "Not logged in." exit 1
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## What "pass" looks like
|
|
159
|
+
|
|
160
|
+
| Profile | Source line printed | Outcome |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| A | `~/.buro/credentials file` | run logged, visible in UI |
|
|
163
|
+
| B | `BURO_API_KEY env var` | run logged, no login needed |
|
|
164
|
+
| C | `setup() in code (--api-key)` | run logged |
|
|
165
|
+
| D1 | `BURO_API_KEY env var` (file ignored) | env wins |
|
|
166
|
+
| D2 | `setup() in code (--api-key)` | setup wins over env+file |
|
|
167
|
+
| D3 | `none` | clean message, exit 1, no traceback |
|
|
168
|
+
| D4 | — | logout removes file; whoami fails cleanly |
|
|
169
|
+
|
|
170
|
+
> Note: `buro logout` clears the **local** file only; it does not revoke the key
|
|
171
|
+
> server-side (out of scope by design). Keys do not expire; only the 10-minute
|
|
172
|
+
> login *handshake* does.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""End-user smoke for the Buro SDK: create a project and log to it.
|
|
3
|
+
|
|
4
|
+
This is the script the manual-testing runbook (MANUAL_TESTING.md) drives
|
|
5
|
+
across the credential environments a real user hits:
|
|
6
|
+
|
|
7
|
+
A. Laptop, logged in -> creds resolved from ~/.buro/credentials
|
|
8
|
+
B. Cluster/CI, env var only -> creds from BURO_API_KEY / BURO_API_URL
|
|
9
|
+
C. Explicit in code -> pass --api-key/--api-url (calls buro.setup)
|
|
10
|
+
D. Precedence / negative -> observe which source the resolver picks
|
|
11
|
+
|
|
12
|
+
It intentionally hardcodes NO key. With --api-key it mimics a user who
|
|
13
|
+
configures the SDK in code; without it, the SDK resolves credentials the
|
|
14
|
+
same way it would in any real run: setup() > BURO_API_KEY env > ~/.buro file.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
buro login --api-url http://localhost:8000 # profile A, once
|
|
18
|
+
python sdk/examples/log_demo.py --project cli-login-test
|
|
19
|
+
|
|
20
|
+
BURO_API_KEY=buro_key_... BURO_API_URL=http://localhost:8000 \\
|
|
21
|
+
python sdk/examples/log_demo.py --project cli-login-test # profile B
|
|
22
|
+
|
|
23
|
+
python sdk/examples/log_demo.py --project p \\
|
|
24
|
+
--api-key buro_key_... --api-url http://localhost:8000 # profile C
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import math
|
|
29
|
+
import os
|
|
30
|
+
import random
|
|
31
|
+
import time
|
|
32
|
+
|
|
33
|
+
import buro
|
|
34
|
+
from buro.credentials import load as load_credentials
|
|
35
|
+
from buro.settings import resolve_api_key, resolve_api_url
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _mask(key: str) -> str:
|
|
39
|
+
if not key:
|
|
40
|
+
return "<none>"
|
|
41
|
+
return f"{key[:12]}…{key[-4:]}" if len(key) > 20 else "<set>"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _credential_source(explicit_key: str | None) -> str:
|
|
45
|
+
"""Mirror the resolver's precedence — for display only — so you can SEE
|
|
46
|
+
which credential environment is actually being exercised."""
|
|
47
|
+
if explicit_key:
|
|
48
|
+
return "setup() in code (--api-key)"
|
|
49
|
+
if os.environ.get("BURO_API_KEY"):
|
|
50
|
+
return "BURO_API_KEY env var"
|
|
51
|
+
if load_credentials().get("api_key"):
|
|
52
|
+
return "~/.buro/credentials file"
|
|
53
|
+
return "none"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> int:
|
|
57
|
+
p = argparse.ArgumentParser(description="Buro SDK manual smoke")
|
|
58
|
+
p.add_argument("--project", default="cli-login-test",
|
|
59
|
+
help="Project ref: 'slug' (personal) or 'team-slug/slug'.")
|
|
60
|
+
p.add_argument("--steps", type=int, default=40, help="Metric steps to log.")
|
|
61
|
+
p.add_argument("--name", default=None, help="Optional run name.")
|
|
62
|
+
p.add_argument("--delay", type=float, default=0.2, help="Seconds between steps.")
|
|
63
|
+
p.add_argument("--api-key", default=None,
|
|
64
|
+
help="Profile C: configure the key in code via buro.setup().")
|
|
65
|
+
p.add_argument("--api-url", default=None,
|
|
66
|
+
help="Server URL for buro.setup() (profile C) / override.")
|
|
67
|
+
args = p.parse_args()
|
|
68
|
+
|
|
69
|
+
# Profile C: explicit configuration in code. setup() wins over env + file.
|
|
70
|
+
if args.api_key or args.api_url:
|
|
71
|
+
buro.setup(api_key=args.api_key, api_url=args.api_url)
|
|
72
|
+
|
|
73
|
+
print(f"Credential source : {_credential_source(args.api_key)}")
|
|
74
|
+
print(f"Resolved api_url : {resolve_api_url()}")
|
|
75
|
+
print(f"Resolved api_key : {_mask(resolve_api_key())}")
|
|
76
|
+
|
|
77
|
+
if not resolve_api_key():
|
|
78
|
+
print(
|
|
79
|
+
"\nNo API key found in any source. Pick one:\n"
|
|
80
|
+
" - run `buro login --api-url <server>` (profile A), or\n"
|
|
81
|
+
" - export BURO_API_KEY=buro_key_... (profile B), or\n"
|
|
82
|
+
" - pass --api-key buro_key_... --api-url <server> (profile C)."
|
|
83
|
+
)
|
|
84
|
+
return 1
|
|
85
|
+
|
|
86
|
+
print(f"\nLogging to project {args.project!r} ...")
|
|
87
|
+
try:
|
|
88
|
+
buro.init(project=args.project, name=args.name,
|
|
89
|
+
config={"lr": 3e-4, "batch_size": 32, "optimizer": "adamw"})
|
|
90
|
+
try:
|
|
91
|
+
for step in range(args.steps):
|
|
92
|
+
loss = math.exp(-step / 12) + random.uniform(0, 0.05)
|
|
93
|
+
acc = min(0.99, 1 - math.exp(-step / 8) + random.uniform(-0.02, 0.02))
|
|
94
|
+
buro.log({"loss": loss, "accuracy": acc}, step=step)
|
|
95
|
+
print(f" step {step:>3} loss={loss:.4f} acc={acc:.4f}")
|
|
96
|
+
time.sleep(args.delay)
|
|
97
|
+
finally:
|
|
98
|
+
buro.finish()
|
|
99
|
+
except Exception as e: # noqa: BLE001 — surface a readable message for manual testing
|
|
100
|
+
print(f"\nFailed: {type(e).__name__}: {e}")
|
|
101
|
+
print("If this looks like auth: the key may be invalid/revoked, or the "
|
|
102
|
+
"server URL is wrong.")
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
print("\nDone. Open the web UI to see the run and its charts.")
|
|
106
|
+
return 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "buro"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Experiment tracker and lab journal made for humans — and sexy human-agent interaction for AI research"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"httpx>=0.27",
|
|
9
|
+
"lz4>=4.3",
|
|
10
|
+
"pathspec>=0.12",
|
|
11
|
+
"psutil>=6.0",
|
|
12
|
+
"typer>=0.12",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
buro = "buro.cli:main"
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
gpu = ["pynvml>=12.0"]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.3",
|
|
22
|
+
"respx>=0.22",
|
|
23
|
+
"numpy>=1.26",
|
|
24
|
+
"pillow>=10.0",
|
|
25
|
+
"psycopg2-binary>=2.9",
|
|
26
|
+
"mutmut>=2.5,<3",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["hatchling"]
|
|
31
|
+
build-backend = "hatchling.build"
|
|
32
|
+
|
|
33
|
+
[tool.hatch.version]
|
|
34
|
+
path = "src/buro/__init__.py"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/buro"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
|
|
42
|
+
[tool.mutmut]
|
|
43
|
+
paths_to_mutate = ["src/buro/"]
|
|
44
|
+
tests_dir = ["tests/"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Assert a GitHub Release tag matches the built buro distribution version.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
check_release_version.py <tag> <dist_dir>
|
|
6
|
+
|
|
7
|
+
<tag> GitHub Release tag, expected form ``sdk-v<version>``.
|
|
8
|
+
<dist_dir> Directory of artifacts from ``uv build`` (one wheel + one sdist).
|
|
9
|
+
|
|
10
|
+
Exits 0 when the tag's version equals the single version embedded in the
|
|
11
|
+
distribution artifacts; prints a diagnostic and exits non-zero otherwise.
|
|
12
|
+
The artifact filenames are the source of truth: this verifies what hatchling
|
|
13
|
+
actually produced, independent of how the version was configured.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
TAG_PREFIX = "sdk-v"
|
|
21
|
+
SDIST_SUFFIX = ".tar.gz"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def version_from_tag(tag: str) -> str:
|
|
25
|
+
"""``sdk-v0.0.1`` -> ``0.0.1``. Raise ValueError on any other shape."""
|
|
26
|
+
if not tag.startswith(TAG_PREFIX):
|
|
27
|
+
raise ValueError(f"tag {tag!r} does not start with {TAG_PREFIX!r}")
|
|
28
|
+
version = tag[len(TAG_PREFIX):]
|
|
29
|
+
if not version:
|
|
30
|
+
raise ValueError(f"tag {tag!r} has an empty version after {TAG_PREFIX!r}")
|
|
31
|
+
return version
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def versions_from_dist(dist_dir: Path) -> set[str]:
|
|
35
|
+
"""Parse the buro version out of every artifact filename in ``dist_dir``.
|
|
36
|
+
|
|
37
|
+
Wheel: buro-<ver>-py3-none-any.whl (version is the 2nd '-' field)
|
|
38
|
+
Sdist: buro-<ver>.tar.gz (strip 'buro-' prefix + '.tar.gz')
|
|
39
|
+
"""
|
|
40
|
+
versions: set[str] = set()
|
|
41
|
+
for whl in dist_dir.glob("buro-*.whl"):
|
|
42
|
+
versions.add(whl.name.split("-")[1])
|
|
43
|
+
for sdist in dist_dir.glob(f"buro-*{SDIST_SUFFIX}"):
|
|
44
|
+
versions.add(sdist.name[len("buro-"):-len(SDIST_SUFFIX)])
|
|
45
|
+
if not versions:
|
|
46
|
+
raise ValueError(f"no buro artifacts found in {dist_dir}")
|
|
47
|
+
return versions
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main(argv: list[str]) -> int:
|
|
51
|
+
if len(argv) != 2:
|
|
52
|
+
print("usage: check_release_version.py <tag> <dist_dir>", file=sys.stderr)
|
|
53
|
+
return 2
|
|
54
|
+
tag, dist_dir = argv
|
|
55
|
+
try:
|
|
56
|
+
expected = version_from_tag(tag)
|
|
57
|
+
built = versions_from_dist(Path(dist_dir))
|
|
58
|
+
except ValueError as exc:
|
|
59
|
+
print(f"::error::{exc}", file=sys.stderr)
|
|
60
|
+
return 1
|
|
61
|
+
if built != {expected}:
|
|
62
|
+
print(
|
|
63
|
+
f"::error::release tag {tag!r} -> version {expected!r} does not "
|
|
64
|
+
f"match built artifact version(s) {sorted(built)!r}",
|
|
65
|
+
file=sys.stderr,
|
|
66
|
+
)
|
|
67
|
+
return 1
|
|
68
|
+
print(f"OK: release tag {tag} matches built version {expected}")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
raise SystemExit(main(sys.argv[1:]))
|