pandm 0.1.0__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.
- pandm-0.1.0/.github/workflows/ci.yml +31 -0
- pandm-0.1.0/.github/workflows/release.yml +68 -0
- pandm-0.1.0/.gitignore +16 -0
- pandm-0.1.0/PKG-INFO +103 -0
- pandm-0.1.0/README.md +87 -0
- pandm-0.1.0/docs/screenshot.png +0 -0
- pandm-0.1.0/examples/train_demo.py +70 -0
- pandm-0.1.0/justfile +33 -0
- pandm-0.1.0/pyproject.toml +34 -0
- pandm-0.1.0/pyrightconfig.json +4 -0
- pandm-0.1.0/src/pandm/__init__.py +6 -0
- pandm-0.1.0/src/pandm/cli.py +135 -0
- pandm-0.1.0/src/pandm/client.py +112 -0
- pandm-0.1.0/src/pandm/sdk.py +245 -0
- pandm-0.1.0/src/pandm/server/__init__.py +3 -0
- pandm-0.1.0/src/pandm/server/app.py +163 -0
- pandm-0.1.0/src/pandm/static/assets/index-C2Her4H1.js +53 -0
- pandm-0.1.0/src/pandm/static/assets/index-C_SZSafT.css +1 -0
- pandm-0.1.0/src/pandm/static/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- pandm-0.1.0/src/pandm/static/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- pandm-0.1.0/src/pandm/static/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- pandm-0.1.0/src/pandm/static/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- pandm-0.1.0/src/pandm/static/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- pandm-0.1.0/src/pandm/static/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- pandm-0.1.0/src/pandm/static/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- pandm-0.1.0/src/pandm/static/index.html +18 -0
- pandm-0.1.0/src/pandm/storage.py +302 -0
- pandm-0.1.0/tests/test_smoke.py +179 -0
- pandm-0.1.0/uv.lock +987 -0
- pandm-0.1.0/web/index.html +17 -0
- pandm-0.1.0/web/package.json +21 -0
- pandm-0.1.0/web/pnpm-lock.yaml +1698 -0
- pandm-0.1.0/web/src/App.vue +107 -0
- pandm-0.1.0/web/src/api.ts +56 -0
- pandm-0.1.0/web/src/colors.ts +26 -0
- pandm-0.1.0/web/src/components/Lightbox.vue +32 -0
- pandm-0.1.0/web/src/components/MediaGroup.vue +93 -0
- pandm-0.1.0/web/src/components/MediaPanel.vue +41 -0
- pandm-0.1.0/web/src/components/MetricChart.vue +163 -0
- pandm-0.1.0/web/src/components/MetricsPanel.vue +92 -0
- pandm-0.1.0/web/src/components/Sidebar.vue +82 -0
- pandm-0.1.0/web/src/components/TablePanel.vue +92 -0
- pandm-0.1.0/web/src/components/TopBar.vue +126 -0
- pandm-0.1.0/web/src/fmt.ts +61 -0
- pandm-0.1.0/web/src/main.ts +10 -0
- pandm-0.1.0/web/src/store.ts +0 -0
- pandm-0.1.0/web/src/styles/main.css +95 -0
- pandm-0.1.0/web/tsconfig.json +17 -0
- pandm-0.1.0/web/uno.config.ts +32 -0
- pandm-0.1.0/web/vite.config.ts +16 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v5
|
|
13
|
+
|
|
14
|
+
- uses: pnpm/action-setup@v4
|
|
15
|
+
with:
|
|
16
|
+
version: 10
|
|
17
|
+
- uses: actions/setup-node@v5
|
|
18
|
+
with:
|
|
19
|
+
node-version: 22
|
|
20
|
+
cache: pnpm
|
|
21
|
+
cache-dependency-path: web/pnpm-lock.yaml
|
|
22
|
+
- name: Build dashboard
|
|
23
|
+
run: pnpm install --frozen-lockfile && pnpm build
|
|
24
|
+
working-directory: web
|
|
25
|
+
|
|
26
|
+
- uses: astral-sh/setup-uv@v7
|
|
27
|
+
- run: uv sync
|
|
28
|
+
- name: Tests
|
|
29
|
+
run: uv run pytest tests -q
|
|
30
|
+
- name: Typecheck
|
|
31
|
+
run: uvx pyright
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publish to PyPI when a version tag is pushed:
|
|
4
|
+
# git tag v0.1.0 && git push origin v0.1.0
|
|
5
|
+
# Uses PyPI trusted publishing (OIDC) — no API token needed, but the
|
|
6
|
+
# publisher must be registered on pypi.org for this repo + workflow.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags: ['v*']
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v5
|
|
17
|
+
|
|
18
|
+
- uses: pnpm/action-setup@v4
|
|
19
|
+
with:
|
|
20
|
+
version: 10
|
|
21
|
+
- uses: actions/setup-node@v5
|
|
22
|
+
with:
|
|
23
|
+
node-version: 22
|
|
24
|
+
cache: pnpm
|
|
25
|
+
cache-dependency-path: web/pnpm-lock.yaml
|
|
26
|
+
- name: Build dashboard
|
|
27
|
+
run: pnpm install --frozen-lockfile && pnpm build
|
|
28
|
+
working-directory: web
|
|
29
|
+
|
|
30
|
+
- uses: astral-sh/setup-uv@v7
|
|
31
|
+
|
|
32
|
+
- name: Tag must match pyproject version
|
|
33
|
+
run: |
|
|
34
|
+
v="$(uv version --short)"
|
|
35
|
+
if [ "v$v" != "$GITHUB_REF_NAME" ]; then
|
|
36
|
+
echo "::error::tag $GITHUB_REF_NAME != pyproject version $v"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
- name: Dashboard must be bundled
|
|
41
|
+
run: test -f src/pandm/static/index.html
|
|
42
|
+
|
|
43
|
+
- run: uv build
|
|
44
|
+
|
|
45
|
+
- name: Smoke-test wheel
|
|
46
|
+
run: |
|
|
47
|
+
uv run --no-project --isolated --with dist/*.whl python -c "import pandm; print(pandm.__version__)"
|
|
48
|
+
uv run --no-project --isolated --with dist/*.whl pandm version
|
|
49
|
+
|
|
50
|
+
- uses: actions/upload-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: dist
|
|
53
|
+
path: dist/
|
|
54
|
+
|
|
55
|
+
publish:
|
|
56
|
+
needs: build
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
environment:
|
|
59
|
+
name: pypi
|
|
60
|
+
url: https://pypi.org/p/pandm
|
|
61
|
+
permissions:
|
|
62
|
+
id-token: write # trusted publishing
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/download-artifact@v4
|
|
65
|
+
with:
|
|
66
|
+
name: dist
|
|
67
|
+
path: dist/
|
|
68
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
pandm-0.1.0/.gitignore
ADDED
pandm-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pandm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Beautiful, local-first experiment tracking. A lightweight alternative to wandb / tensorboard.
|
|
5
|
+
Author-email: Jannchie <jannchie@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: fastapi>=0.110
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: pillow>=10
|
|
11
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
12
|
+
Requires-Dist: rich>=13
|
|
13
|
+
Requires-Dist: typer>=0.12
|
|
14
|
+
Requires-Dist: uvicorn[standard]>=0.29
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# pandm
|
|
18
|
+
|
|
19
|
+
pandm tracks ML experiments locally. The Python SDK writes metrics and images straight to a `.pandm/` directory next to your code — no account, no daemon, no cloud — and `pandm ui` serves a dashboard to compare runs. Unlike wandb there is nothing to sign up for, and unlike tensorboard the data is plain SQLite + PNG files you can query yourself. The same scripts report to a shared server over HTTP when you set one env var.
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pip install pandm
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import pandm
|
|
33
|
+
|
|
34
|
+
run = pandm.init(project="mnist", config={"lr": 1e-3, "batch_size": 64})
|
|
35
|
+
for step in range(1000):
|
|
36
|
+
loss, acc = train_step()
|
|
37
|
+
run.log({"train/loss": loss, "train/acc": acc}, step=step)
|
|
38
|
+
if step % 100 == 0:
|
|
39
|
+
run.log_image("samples", sample_grid, step=step) # PIL / numpy / torch / path
|
|
40
|
+
run.finish()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pandm ui # opens http://127.0.0.1:7878
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The dashboard overlays selected runs per metric, with smoothing, log scale, step/time axes, an image browser with a step slider, and a config/summary comparison table. It polls while runs are alive, so curves grow during training.
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
`step` is optional (an internal counter is used). Runs end as `finished` or `crashed`: uncaught exceptions are detected via `sys.excepthook` (and the context manager), and hard-killed processes (`kill -9`, OOM) are presumed crashed once their 15s heartbeat goes quiet for 60s — self-healing if the process was merely suspended.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
with pandm.init(project="mnist") as run:
|
|
55
|
+
run.log({"loss": 0.5})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
List or delete runs from the terminal:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
pandm ls
|
|
62
|
+
pandm delete <run_id>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Data lives in `./.pandm` by default; override with `--dir` or `PANDM_DIR`.
|
|
66
|
+
|
|
67
|
+
### Cloud mode
|
|
68
|
+
|
|
69
|
+
Run a server anywhere, then point training scripts at it — no code changes:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
pandm server --api-key my-secret # on the server (default port 7878)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
export PANDM_REMOTE=http://my-host:7878
|
|
77
|
+
export PANDM_API_KEY=my-secret
|
|
78
|
+
python train.py # same script, now reports over HTTP
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The API key protects write endpoints only; put the server behind a reverse proxy if reads need auth too. If the server becomes unreachable mid-run, the SDK warns and keeps training: it retries every 30s, replays run creation on recovery, and drops whatever was logged while offline.
|
|
82
|
+
|
|
83
|
+
## API
|
|
84
|
+
|
|
85
|
+
| | |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `pandm.init(project, name=None, config=None, *, directory=None, remote=None, api_key=None)` | start a run |
|
|
88
|
+
| `run.log(metrics, step=None)` | log scalar metrics |
|
|
89
|
+
| `run.log_image(key, image, step=None, caption=None)` | log an image |
|
|
90
|
+
| `run.finish(status="finished")` | end the run (also via `atexit`) |
|
|
91
|
+
| `GET /api/docs` | REST API reference on any running server |
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
uv sync && uv run pytest # python sdk + server
|
|
97
|
+
cd web && pnpm install && pnpm dev # dashboard dev server (proxies to :7878)
|
|
98
|
+
pnpm build # bundles the dashboard into src/pandm/static
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
pandm-0.1.0/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# pandm
|
|
2
|
+
|
|
3
|
+
pandm tracks ML experiments locally. The Python SDK writes metrics and images straight to a `.pandm/` directory next to your code — no account, no daemon, no cloud — and `pandm ui` serves a dashboard to compare runs. Unlike wandb there is nothing to sign up for, and unlike tensorboard the data is plain SQLite + PNG files you can query yourself. The same scripts report to a shared server over HTTP when you set one env var.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install pandm
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import pandm
|
|
17
|
+
|
|
18
|
+
run = pandm.init(project="mnist", config={"lr": 1e-3, "batch_size": 64})
|
|
19
|
+
for step in range(1000):
|
|
20
|
+
loss, acc = train_step()
|
|
21
|
+
run.log({"train/loss": loss, "train/acc": acc}, step=step)
|
|
22
|
+
if step % 100 == 0:
|
|
23
|
+
run.log_image("samples", sample_grid, step=step) # PIL / numpy / torch / path
|
|
24
|
+
run.finish()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
pandm ui # opens http://127.0.0.1:7878
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The dashboard overlays selected runs per metric, with smoothing, log scale, step/time axes, an image browser with a step slider, and a config/summary comparison table. It polls while runs are alive, so curves grow during training.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
`step` is optional (an internal counter is used). Runs end as `finished` or `crashed`: uncaught exceptions are detected via `sys.excepthook` (and the context manager), and hard-killed processes (`kill -9`, OOM) are presumed crashed once their 15s heartbeat goes quiet for 60s — self-healing if the process was merely suspended.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
with pandm.init(project="mnist") as run:
|
|
39
|
+
run.log({"loss": 0.5})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
List or delete runs from the terminal:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pandm ls
|
|
46
|
+
pandm delete <run_id>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Data lives in `./.pandm` by default; override with `--dir` or `PANDM_DIR`.
|
|
50
|
+
|
|
51
|
+
### Cloud mode
|
|
52
|
+
|
|
53
|
+
Run a server anywhere, then point training scripts at it — no code changes:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
pandm server --api-key my-secret # on the server (default port 7878)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
export PANDM_REMOTE=http://my-host:7878
|
|
61
|
+
export PANDM_API_KEY=my-secret
|
|
62
|
+
python train.py # same script, now reports over HTTP
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The API key protects write endpoints only; put the server behind a reverse proxy if reads need auth too. If the server becomes unreachable mid-run, the SDK warns and keeps training: it retries every 30s, replays run creation on recovery, and drops whatever was logged while offline.
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
| | |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `pandm.init(project, name=None, config=None, *, directory=None, remote=None, api_key=None)` | start a run |
|
|
72
|
+
| `run.log(metrics, step=None)` | log scalar metrics |
|
|
73
|
+
| `run.log_image(key, image, step=None, caption=None)` | log an image |
|
|
74
|
+
| `run.finish(status="finished")` | end the run (also via `atexit`) |
|
|
75
|
+
| `GET /api/docs` | REST API reference on any running server |
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
uv sync && uv run pytest # python sdk + server
|
|
81
|
+
cd web && pnpm install && pnpm dev # dashboard dev server (proxies to :7878)
|
|
82
|
+
pnpm build # bundles the dashboard into src/pandm/static
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Simulate a few training runs so the dashboard has something to show.
|
|
2
|
+
|
|
3
|
+
uv run python examples/train_demo.py
|
|
4
|
+
uv run pandm ui
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import random
|
|
11
|
+
|
|
12
|
+
from PIL import Image, ImageDraw, ImageOps
|
|
13
|
+
|
|
14
|
+
import pandm
|
|
15
|
+
|
|
16
|
+
PALETTES = [
|
|
17
|
+
((24, 26, 60), (139, 149, 246)),
|
|
18
|
+
((40, 18, 40), (239, 125, 155)),
|
|
19
|
+
((12, 40, 38), (72, 207, 173)),
|
|
20
|
+
((45, 30, 12), (245, 163, 95)),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fake_sample(step: int, total: int, palette_idx: int) -> Image.Image:
|
|
25
|
+
"""A 'denoising' image: noise fades out as training progresses."""
|
|
26
|
+
dark, bright = PALETTES[palette_idx % len(PALETTES)]
|
|
27
|
+
base = ImageOps.colorize(Image.linear_gradient("L").rotate(45 - palette_idx * 30, expand=False), dark, bright)
|
|
28
|
+
base = base.resize((256, 256))
|
|
29
|
+
sigma = 90 * (1 - step / total) + 6
|
|
30
|
+
noise = ImageOps.colorize(Image.effect_noise((256, 256), sigma), (0, 0, 0), (255, 255, 255))
|
|
31
|
+
img = Image.blend(base, noise, alpha=max(0.04, 0.8 * (1 - step / total)))
|
|
32
|
+
draw = ImageDraw.Draw(img)
|
|
33
|
+
draw.rectangle([8, 224, 116, 246], fill=(0, 0, 0, 160))
|
|
34
|
+
draw.text((14, 228), f"step {step}", fill=(235, 235, 240))
|
|
35
|
+
return img
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def simulate(project: str, name: str, lr: float, bs: int, palette_idx: int, steps: int = 400, seed: int = 0) -> None:
|
|
39
|
+
rng = random.Random(seed)
|
|
40
|
+
run = pandm.init(
|
|
41
|
+
project=project,
|
|
42
|
+
name=name,
|
|
43
|
+
config={"lr": lr, "batch_size": bs, "optimizer": "adamw", "model": "resnet18", "seed": seed},
|
|
44
|
+
)
|
|
45
|
+
base = 2.0 + rng.uniform(-0.2, 0.4)
|
|
46
|
+
speed = lr * rng.uniform(800, 1400)
|
|
47
|
+
for step in range(steps):
|
|
48
|
+
progress = step / steps
|
|
49
|
+
loss = base * math.exp(-speed * progress) + 0.08 + rng.gauss(0, 0.02) * (1.2 - progress)
|
|
50
|
+
acc = 1 - 0.9 * math.exp(-3.5 * progress) + rng.gauss(0, 0.008)
|
|
51
|
+
lr_t = lr * 0.5 * (1 + math.cos(math.pi * progress)) # cosine schedule
|
|
52
|
+
run.log({"train/loss": loss, "train/acc": min(acc, 0.999), "lr": lr_t}, step=step)
|
|
53
|
+
if step % 10 == 0:
|
|
54
|
+
val_loss = loss + 0.12 + rng.gauss(0, 0.03)
|
|
55
|
+
run.log({"val/loss": val_loss, "val/acc": min(acc - 0.04 + rng.gauss(0, 0.01), 0.999)}, step=step)
|
|
56
|
+
if step % 80 == 0 or step == steps - 1:
|
|
57
|
+
run.log_image("samples", fake_sample(step, steps, palette_idx), step=step, caption=f"epoch {step // 80}")
|
|
58
|
+
run.finish()
|
|
59
|
+
print(f" ✓ {project}/{name}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
print("simulating runs…")
|
|
64
|
+
simulate("mnist-diffusion", "baseline", lr=1e-3, bs=64, palette_idx=0, seed=1)
|
|
65
|
+
simulate("mnist-diffusion", "high-lr", lr=3e-3, bs=64, palette_idx=1, seed=2)
|
|
66
|
+
simulate("mnist-diffusion", "big-batch", lr=1e-3, bs=256, palette_idx=2, seed=3)
|
|
67
|
+
simulate("mnist-diffusion", "low-lr", lr=3e-4, bs=64, palette_idx=3, seed=4)
|
|
68
|
+
simulate("llm-finetune", "lora-r8", lr=2e-4, bs=8, palette_idx=1, steps=300, seed=5)
|
|
69
|
+
simulate("llm-finetune", "lora-r32", lr=2e-4, bs=8, palette_idx=2, steps=300, seed=6)
|
|
70
|
+
print("done — run `pandm ui` to view")
|
pandm-0.1.0/justfile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# 列出所有命令
|
|
2
|
+
default:
|
|
3
|
+
@just --list
|
|
4
|
+
|
|
5
|
+
# Python 单测
|
|
6
|
+
test:
|
|
7
|
+
uv run pytest tests -q
|
|
8
|
+
|
|
9
|
+
# pyright 类型检查
|
|
10
|
+
typecheck:
|
|
11
|
+
pyright
|
|
12
|
+
|
|
13
|
+
# 单测 + 类型检查
|
|
14
|
+
check: test typecheck
|
|
15
|
+
|
|
16
|
+
# 构建前端到 src/pandm/static(打包进 wheel 前必跑)
|
|
17
|
+
web-build:
|
|
18
|
+
pnpm -C web build
|
|
19
|
+
|
|
20
|
+
# 前端开发服务器(/api 代理到 127.0.0.1:7878)
|
|
21
|
+
web-dev:
|
|
22
|
+
pnpm -C web dev
|
|
23
|
+
|
|
24
|
+
# 生成演示数据
|
|
25
|
+
demo:
|
|
26
|
+
uv run python examples/train_demo.py
|
|
27
|
+
|
|
28
|
+
# 启动本地 dashboard
|
|
29
|
+
ui:
|
|
30
|
+
uv run pandm ui
|
|
31
|
+
|
|
32
|
+
# 一条龙:建前端 → 造数据 → 开 dashboard
|
|
33
|
+
play: web-build demo ui
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pandm"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Beautiful, local-first experiment tracking. A lightweight alternative to wandb / tensorboard."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Jannchie", email = "jannchie@gmail.com" }]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"fastapi>=0.110",
|
|
11
|
+
"uvicorn[standard]>=0.29",
|
|
12
|
+
"typer>=0.12",
|
|
13
|
+
"rich>=13",
|
|
14
|
+
"pillow>=10",
|
|
15
|
+
"httpx>=0.27",
|
|
16
|
+
"python-multipart>=0.0.9",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
pandm = "pandm.cli:app"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["hatchling"]
|
|
24
|
+
build-backend = "hatchling.build"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build]
|
|
27
|
+
# the built dashboard is gitignored but must ship with the package
|
|
28
|
+
artifacts = ["src/pandm/static/**"]
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/pandm"]
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = ["pytest>=8"]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""pandm CLI: `pandm ui` (local dashboard), `pandm server` (cloud mode), `pandm ls`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
import webbrowser
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .storage import LocalStore, resolve_dir
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(
|
|
19
|
+
help="pandm — beautiful, local-first experiment tracking.",
|
|
20
|
+
no_args_is_help=True,
|
|
21
|
+
add_completion=False,
|
|
22
|
+
)
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
DirOption = typer.Option(None, "--dir", "-d", help="Data directory (default: ./.pandm or $PANDM_DIR).")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _banner(url: str, data_dir: Path, mode: str) -> None:
|
|
29
|
+
console.print(
|
|
30
|
+
Panel.fit(
|
|
31
|
+
f"[bold]pandm[/bold] [dim]v{__version__}[/dim] · {mode}\n\n"
|
|
32
|
+
f" [bold cyan]{url}[/bold cyan]\n"
|
|
33
|
+
f" [dim]data: {data_dir}[/dim]",
|
|
34
|
+
border_style="bright_black",
|
|
35
|
+
padding=(1, 3),
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.command()
|
|
41
|
+
def ui(
|
|
42
|
+
directory: Optional[Path] = DirOption,
|
|
43
|
+
port: int = typer.Option(7878, "--port", "-p"),
|
|
44
|
+
host: str = typer.Option("127.0.0.1", "--host"),
|
|
45
|
+
open_browser: bool = typer.Option(True, "--open/--no-open", help="Open the dashboard in a browser."),
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Start the local dashboard."""
|
|
48
|
+
import uvicorn
|
|
49
|
+
|
|
50
|
+
from .server import create_app
|
|
51
|
+
|
|
52
|
+
data_dir = resolve_dir(directory)
|
|
53
|
+
url = f"http://{host}:{port}"
|
|
54
|
+
_banner(url, data_dir, "local dashboard")
|
|
55
|
+
if open_browser:
|
|
56
|
+
threading.Timer(0.8, webbrowser.open, args=[url]).start()
|
|
57
|
+
uvicorn.run(create_app(data_dir), host=host, port=port, log_level="warning")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.command()
|
|
61
|
+
def server(
|
|
62
|
+
directory: Optional[Path] = DirOption,
|
|
63
|
+
port: int = typer.Option(7878, "--port", "-p"),
|
|
64
|
+
host: str = typer.Option("0.0.0.0", "--host"),
|
|
65
|
+
api_key: Optional[str] = typer.Option(
|
|
66
|
+
None, "--api-key", envvar="PANDM_API_KEY", help="Require x-api-key on write endpoints."
|
|
67
|
+
),
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Start a pandm server for cloud deployment (SDKs report via PANDM_REMOTE)."""
|
|
70
|
+
import uvicorn
|
|
71
|
+
|
|
72
|
+
from .server import create_app
|
|
73
|
+
|
|
74
|
+
data_dir = resolve_dir(directory)
|
|
75
|
+
_banner(f"http://{host}:{port}", data_dir, "server mode" + (" · api-key on" if api_key else ""))
|
|
76
|
+
uvicorn.run(create_app(data_dir, api_key=api_key), host=host, port=port, log_level="info")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.command("ls")
|
|
80
|
+
def list_runs(
|
|
81
|
+
project: Optional[str] = typer.Option(None, "--project", "-P"),
|
|
82
|
+
directory: Optional[Path] = DirOption,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""List runs in the terminal."""
|
|
85
|
+
store = LocalStore(resolve_dir(directory))
|
|
86
|
+
runs = store.list_runs(project)
|
|
87
|
+
if not runs:
|
|
88
|
+
console.print("[dim]no runs yet — call pandm.init() in your training script[/dim]")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
import datetime as dt
|
|
92
|
+
|
|
93
|
+
status_style = {"running": "bold cyan", "finished": "green", "crashed": "red"}
|
|
94
|
+
table = Table(box=None, header_style="bold dim", pad_edge=False)
|
|
95
|
+
for col in ("ID", "NAME", "PROJECT", "STATUS", "CREATED", "DURATION"):
|
|
96
|
+
table.add_column(col)
|
|
97
|
+
for run in runs:
|
|
98
|
+
created = dt.datetime.fromtimestamp(run["created_at"]).strftime("%Y-%m-%d %H:%M")
|
|
99
|
+
end = run["finished_at"] or run["updated_at"]
|
|
100
|
+
mins, secs = divmod(int(max(0, end - run["created_at"])), 60)
|
|
101
|
+
hours, mins = divmod(mins, 60)
|
|
102
|
+
duration = f"{hours}h{mins:02d}m" if hours else (f"{mins}m{secs:02d}s" if mins else f"{secs}s")
|
|
103
|
+
table.add_row(
|
|
104
|
+
f"[dim]{run['id']}[/dim]",
|
|
105
|
+
run["name"],
|
|
106
|
+
run["project"],
|
|
107
|
+
f"[{status_style.get(run['status'], 'white')}]{run['status']}[/]",
|
|
108
|
+
created,
|
|
109
|
+
duration,
|
|
110
|
+
)
|
|
111
|
+
console.print(table)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command()
|
|
115
|
+
def delete(
|
|
116
|
+
run_id: str,
|
|
117
|
+
directory: Optional[Path] = DirOption,
|
|
118
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Delete a run and its media files."""
|
|
121
|
+
store = LocalStore(resolve_dir(directory))
|
|
122
|
+
run = store.get_run(run_id)
|
|
123
|
+
if run is None:
|
|
124
|
+
console.print(f"[red]run {run_id} not found[/red]")
|
|
125
|
+
raise typer.Exit(1)
|
|
126
|
+
if not yes and not typer.confirm(f"delete run {run['name']} ({run_id})?"):
|
|
127
|
+
raise typer.Exit(0)
|
|
128
|
+
store.delete_run(run_id)
|
|
129
|
+
console.print(f"[dim]deleted {run_id}[/dim]")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.command()
|
|
133
|
+
def version() -> None:
|
|
134
|
+
"""Print version."""
|
|
135
|
+
console.print(f"pandm v{__version__}")
|