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.
Files changed (59) hide show
  1. buro-0.0.1/.gitignore +9 -0
  2. buro-0.0.1/.mutmut-cache +0 -0
  3. buro-0.0.1/Makefile +7 -0
  4. buro-0.0.1/PKG-INFO +81 -0
  5. buro-0.0.1/README.md +60 -0
  6. buro-0.0.1/docs/publishing.md +37 -0
  7. buro-0.0.1/docs/wandb-compatibility.md +87 -0
  8. buro-0.0.1/examples/MANUAL_TESTING.md +172 -0
  9. buro-0.0.1/examples/log_demo.py +110 -0
  10. buro-0.0.1/pyproject.toml +44 -0
  11. buro-0.0.1/scripts/check_release_version.py +73 -0
  12. buro-0.0.1/scripts/conftest.py +10 -0
  13. buro-0.0.1/scripts/mutation_check.py +179 -0
  14. buro-0.0.1/src/buro/__init__.py +201 -0
  15. buro-0.0.1/src/buro/_compat.py +37 -0
  16. buro-0.0.1/src/buro/buffer.py +44 -0
  17. buro-0.0.1/src/buro/cli.py +130 -0
  18. buro-0.0.1/src/buro/client.py +189 -0
  19. buro-0.0.1/src/buro/code_capture.py +201 -0
  20. buro-0.0.1/src/buro/code_snapshot.py +335 -0
  21. buro-0.0.1/src/buro/code_snapshot_uploader.py +159 -0
  22. buro-0.0.1/src/buro/config.py +41 -0
  23. buro-0.0.1/src/buro/credentials.py +42 -0
  24. buro-0.0.1/src/buro/errors.py +42 -0
  25. buro-0.0.1/src/buro/log_capture.py +168 -0
  26. buro-0.0.1/src/buro/media.py +145 -0
  27. buro-0.0.1/src/buro/run.py +414 -0
  28. buro-0.0.1/src/buro/settings.py +69 -0
  29. buro-0.0.1/src/buro/slug_ref.py +33 -0
  30. buro-0.0.1/src/buro/system_metrics.py +96 -0
  31. buro-0.0.1/src/buro/wal.py +35 -0
  32. buro-0.0.1/tests/conftest.py +0 -0
  33. buro-0.0.1/tests/e2e/__init__.py +0 -0
  34. buro-0.0.1/tests/e2e/conftest.py +244 -0
  35. buro-0.0.1/tests/e2e/fake_server.py +39 -0
  36. buro-0.0.1/tests/e2e/project_tree.py +66 -0
  37. buro-0.0.1/tests/e2e/test_capture_scenarios.py +217 -0
  38. buro-0.0.1/tests/e2e/test_lifecycle_scenarios.py +237 -0
  39. buro-0.0.1/tests/test___init__.py +61 -0
  40. buro-0.0.1/tests/test__compat.py +51 -0
  41. buro-0.0.1/tests/test_buffer.py +58 -0
  42. buro-0.0.1/tests/test_check_release_version.py +71 -0
  43. buro-0.0.1/tests/test_cli.py +270 -0
  44. buro-0.0.1/tests/test_client.py +335 -0
  45. buro-0.0.1/tests/test_code_capture.py +260 -0
  46. buro-0.0.1/tests/test_code_snapshot.py +1102 -0
  47. buro-0.0.1/tests/test_code_snapshot_uploader.py +408 -0
  48. buro-0.0.1/tests/test_config.py +33 -0
  49. buro-0.0.1/tests/test_credentials.py +57 -0
  50. buro-0.0.1/tests/test_integration.py +487 -0
  51. buro-0.0.1/tests/test_log_capture.py +385 -0
  52. buro-0.0.1/tests/test_media.py +154 -0
  53. buro-0.0.1/tests/test_run.py +1102 -0
  54. buro-0.0.1/tests/test_settings.py +91 -0
  55. buro-0.0.1/tests/test_slug_init.py +221 -0
  56. buro-0.0.1/tests/test_system_metrics.py +31 -0
  57. buro-0.0.1/tests/test_wal.py +39 -0
  58. buro-0.0.1/tests/test_wandb_compat.py +68 -0
  59. buro-0.0.1/uv.lock +841 -0
buro-0.0.1/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ .ruff_cache/
Binary file
buro-0.0.1/Makefile ADDED
@@ -0,0 +1,7 @@
1
+ .PHONY: mutation-check
2
+
3
+ mutation-check:
4
+ @if [ -z "$(FILE)" ]; then \
5
+ echo "Usage: make mutation-check FILE=<source-file>"; exit 3; \
6
+ fi
7
+ @uv run python scripts/mutation_check.py "$(FILE)"
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:]))