runboard-ml 0.2.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.
Files changed (83) hide show
  1. runboard_ml-0.2.0/LICENSE +21 -0
  2. runboard_ml-0.2.0/PKG-INFO +187 -0
  3. runboard_ml-0.2.0/README.md +149 -0
  4. runboard_ml-0.2.0/pyproject.toml +50 -0
  5. runboard_ml-0.2.0/runboard/__init__.py +2 -0
  6. runboard_ml-0.2.0/runboard/__main__.py +3 -0
  7. runboard_ml-0.2.0/runboard/access.py +219 -0
  8. runboard_ml-0.2.0/runboard/backends/__init__.py +12 -0
  9. runboard_ml-0.2.0/runboard/backends/base.py +69 -0
  10. runboard_ml-0.2.0/runboard/backends/kaggle.py +261 -0
  11. runboard_ml-0.2.0/runboard/backends/local.py +114 -0
  12. runboard_ml-0.2.0/runboard/backends/remote.py +306 -0
  13. runboard_ml-0.2.0/runboard/cli.py +202 -0
  14. runboard_ml-0.2.0/runboard/code.py +150 -0
  15. runboard_ml-0.2.0/runboard/demo/evaluate.py +57 -0
  16. runboard_ml-0.2.0/runboard/demo/plan.yaml +26 -0
  17. runboard_ml-0.2.0/runboard/demo/runboard.yaml +46 -0
  18. runboard_ml-0.2.0/runboard/demo/runs.yaml +13 -0
  19. runboard_ml-0.2.0/runboard/demo/train.py +86 -0
  20. runboard_ml-0.2.0/runboard/errors.py +167 -0
  21. runboard_ml-0.2.0/runboard/export.py +325 -0
  22. runboard_ml-0.2.0/runboard/home.py +153 -0
  23. runboard_ml-0.2.0/runboard/hypotheses.py +373 -0
  24. runboard_ml-0.2.0/runboard/localrun.py +36 -0
  25. runboard_ml-0.2.0/runboard/planfile.py +195 -0
  26. runboard_ml-0.2.0/runboard/prereg.py +211 -0
  27. runboard_ml-0.2.0/runboard/proc.py +69 -0
  28. runboard_ml-0.2.0/runboard/progress.py +157 -0
  29. runboard_ml-0.2.0/runboard/project.py +707 -0
  30. runboard_ml-0.2.0/runboard/qr.py +154 -0
  31. runboard_ml-0.2.0/runboard/rb.py +190 -0
  32. runboard_ml-0.2.0/runboard/research.py +226 -0
  33. runboard_ml-0.2.0/runboard/runsfile.py +185 -0
  34. runboard_ml-0.2.0/runboard/runtime.py +516 -0
  35. runboard_ml-0.2.0/runboard/server.py +1020 -0
  36. runboard_ml-0.2.0/runboard/static/icon.svg +1 -0
  37. runboard_ml-0.2.0/runboard/static/index.html +443 -0
  38. runboard_ml-0.2.0/runboard/static/js/01-i18n.js +160 -0
  39. runboard_ml-0.2.0/runboard/static/js/02-core.js +202 -0
  40. runboard_ml-0.2.0/runboard/static/js/03-charts.js +377 -0
  41. runboard_ml-0.2.0/runboard/static/js/04-pages.js +318 -0
  42. runboard_ml-0.2.0/runboard/static/js/05-plan.js +296 -0
  43. runboard_ml-0.2.0/runboard/static/js/05-templates.js +53 -0
  44. runboard_ml-0.2.0/runboard/static/js/06-code.js +98 -0
  45. runboard_ml-0.2.0/runboard/static/js/06-project.js +100 -0
  46. runboard_ml-0.2.0/runboard/static/js/07-system.js +106 -0
  47. runboard_ml-0.2.0/runboard/static/js/08-wizard.js +275 -0
  48. runboard_ml-0.2.0/runboard/static/js/09-hypotheses.js +72 -0
  49. runboard_ml-0.2.0/runboard/static/js/10-main.js +84 -0
  50. runboard_ml-0.2.0/runboard/static/mini.html +148 -0
  51. runboard_ml-0.2.0/runboard/static/pair.html +56 -0
  52. runboard_ml-0.2.0/runboard/templates/check.py +147 -0
  53. runboard_ml-0.2.0/runboard/templates/convert.py +113 -0
  54. runboard_ml-0.2.0/runboard/templates/evaluate.py +162 -0
  55. runboard_ml-0.2.0/runboard/templates/runboard_train.py +136 -0
  56. runboard_ml-0.2.0/runboard/templates/speed.py +66 -0
  57. runboard_ml-0.2.0/runboard/templates/split.py +99 -0
  58. runboard_ml-0.2.0/runboard/templates/subsets.py +80 -0
  59. runboard_ml-0.2.0/runboard/templating.py +260 -0
  60. runboard_ml-0.2.0/runboard/watch.py +101 -0
  61. runboard_ml-0.2.0/runboard/wizard.py +281 -0
  62. runboard_ml-0.2.0/runboard_ml.egg-info/PKG-INFO +187 -0
  63. runboard_ml-0.2.0/runboard_ml.egg-info/SOURCES.txt +81 -0
  64. runboard_ml-0.2.0/runboard_ml.egg-info/dependency_links.txt +1 -0
  65. runboard_ml-0.2.0/runboard_ml.egg-info/entry_points.txt +2 -0
  66. runboard_ml-0.2.0/runboard_ml.egg-info/requires.txt +14 -0
  67. runboard_ml-0.2.0/runboard_ml.egg-info/top_level.txt +1 -0
  68. runboard_ml-0.2.0/setup.cfg +4 -0
  69. runboard_ml-0.2.0/tests/test_code.py +155 -0
  70. runboard_ml-0.2.0/tests/test_core.py +240 -0
  71. runboard_ml-0.2.0/tests/test_demo.py +57 -0
  72. runboard_ml-0.2.0/tests/test_export.py +114 -0
  73. runboard_ml-0.2.0/tests/test_fixes.py +198 -0
  74. runboard_ml-0.2.0/tests/test_plan.py +244 -0
  75. runboard_ml-0.2.0/tests/test_plan_v2.py +193 -0
  76. runboard_ml-0.2.0/tests/test_prereg.py +237 -0
  77. runboard_ml-0.2.0/tests/test_remote.py +217 -0
  78. runboard_ml-0.2.0/tests/test_research.py +120 -0
  79. runboard_ml-0.2.0/tests/test_runtime.py +311 -0
  80. runboard_ml-0.2.0/tests/test_security.py +146 -0
  81. runboard_ml-0.2.0/tests/test_templates.py +311 -0
  82. runboard_ml-0.2.0/tests/test_ui.py +496 -0
  83. runboard_ml-0.2.0/tests/test_wizard.py +75 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alperen Toker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: runboard-ml
3
+ Version: 0.2.0
4
+ Summary: Pre-registered ML experiments: lock the plan, run on your GPU, Kaggle, SSH or SLURM, watch live, and get hypotheses decided with bootstrap intervals and paper-ready output.
5
+ Author: Alperen Toker
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/alperentoker/runboard-ml
8
+ Project-URL: Documentation, https://github.com/alperentoker/runboard-ml/blob/main/docs/manifest.md
9
+ Project-URL: Issues, https://github.com/alperentoker/runboard-ml/issues
10
+ Project-URL: Security, https://github.com/alperentoker/runboard-ml/security/policy
11
+ Keywords: pre-registration,reproducibility,hypothesis testing,bootstrap,machine learning,training,dashboard,kaggle,slurm,yolo
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: fastapi>=0.110
26
+ Requires-Dist: uvicorn>=0.29
27
+ Requires-Dist: pyyaml>=6
28
+ Provides-Extra: kaggle
29
+ Requires-Dist: kaggle>=2.2; extra == "kaggle"
30
+ Requires-Dist: six; extra == "kaggle"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest; extra == "dev"
33
+ Requires-Dist: httpx; extra == "dev"
34
+ Requires-Dist: kaggle>=2.2; extra == "dev"
35
+ Requires-Dist: six; extra == "dev"
36
+ Requires-Dist: numpy; extra == "dev"
37
+ Dynamic: license-file
38
+
39
+ # runboard
40
+
41
+ **Pre-registered machine-learning experiments, from the plan to the paper.** Write your questions and hypotheses,
42
+ lock the plan, run the experiments on your own GPU, Kaggle, a machine over SSH or a SLURM cluster, watch them live, and
43
+ get every hypothesis decided with a bootstrap confidence interval, plus the tables, methods and deviations sections
44
+ for the paper. One self-hosted dashboard for all your projects; everything stays in readable files in your repository.
45
+
46
+ **New here?** `pip install runboard-ml numpy`, then `runboard demo`: a five-minute pre-registered study on any CPU,
47
+ walked through in the [tutorial](https://github.com/alperentoker/runboard-ml/blob/main/docs/tutorial.md).
48
+
49
+ [Türkçe](https://github.com/alperentoker/runboard-ml/blob/main/README.tr.md) · [![tests](https://github.com/alperentoker/runboard-ml/actions/workflows/tests.yml/badge.svg)](https://github.com/alperentoker/runboard-ml/actions/workflows/tests.yml)
50
+
51
+ ![Hypotheses decided with confidence intervals](https://raw.githubusercontent.com/alperentoker/runboard-ml/main/docs/images/tutorial-7-verdicts.png)
52
+
53
+ <table><tr>
54
+ <td width="50%">
55
+
56
+ <picture>
57
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/alperentoker/runboard-ml/main/docs/images/run-dark.png">
58
+ <img alt="A run's page: live progress, results and training curves" src="https://raw.githubusercontent.com/alperentoker/runboard-ml/main/docs/images/run-light.png">
59
+ </picture>
60
+
61
+ <sub>A run, live</sub>
62
+ </td>
63
+ <td width="50%">
64
+
65
+ <picture>
66
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/alperentoker/runboard-ml/main/docs/images/plan-dark.png">
67
+ <img alt="Editing the run plan: every change is previewed as the lines it changes" src="https://raw.githubusercontent.com/alperentoker/runboard-ml/main/docs/images/plan-light.png">
68
+ </picture>
69
+
70
+ <sub>Editing the plan: a preview of the exact lines that change</sub>
71
+ </td>
72
+ </tr></table>
73
+
74
+ - **Hypotheses a machine can check** — `R7.aerial >= 0.9 * R5.aerial`, `collapse(R4, ground, aerial) > ...`, seed groups,
75
+ `log_fit(...)`: each comparison is decided on paired bootstrap replicates as *supported*, *refuted* or
76
+ *indistinguishable*, with its 95 % interval.
77
+ - **A lock that means something** — `plan.lock.json` records when, at which commit and the fingerprints of the plan,
78
+ the runs and the scripts; every later change needs a reason and is listed as a deviation. A sealed test set is
79
+ evaluated once per model, on the record.
80
+ - **Research wizard** — questions, experiment designs (specialists, data-size curves, architectures, ablations,
81
+ seeds), the evaluation protocol and hypotheses checked as you type become a ready project.
82
+ - **Code templates** — split, leakage check, conversion, data-size subsets, evaluation and speed scripts, written into
83
+ your project as ordinary files that runboard never overwrites once you change them.
84
+ - **Paper output** — Markdown and LaTeX tables, the hypotheses, a methods draft, the deviations section, a
85
+ reproducibility checklist, a registered-report page and an archive for Zenodo.
86
+ - **Live runs anywhere** — your GPU, Kaggle (private kernels, weekly quota, private datasets), a machine over SSH and a
87
+ SLURM cluster (your own SSH keys; runboard never sees a password); logs, second-by-second progress, charts, a queue.
88
+ - **Provenance** — every run records the commit, uncommitted changes and the fingerprints of what it started from.
89
+ - **Tablet view**, a Code page, English and Türkçe.
90
+
91
+ Secrets never leave your machine: the Kaggle token stays in `~/.kaggle`, paired devices and all state live in
92
+ `~/.runboard`, never inside a project repository.
93
+
94
+ ## Install
95
+
96
+ ```bash
97
+ pip install runboard-ml # or: pip install "runboard-ml[kaggle]" for Kaggle
98
+ runboard # http://localhost:8000 — this computer only, no password
99
+ ```
100
+
101
+ The package is called `runboard-ml` on PyPI (`runboard` belongs to another project); the command and the Python module
102
+ are `runboard`. Do not install both in one environment: they would share the module name. Python 3.11 or newer.
103
+
104
+ Then, in the dashboard:
105
+
106
+ 1. **System → Connections → Kaggle** (only if you want cloud GPUs): the panel walks you through creating a token on
107
+ kaggle.com and pasting it once. It is saved to `~/.kaggle/access_token` on this machine, tested, and never shown again.
108
+ 2. **New project**: pick a folder and choose *Pre-registered study (YOLO)*, *YOLO training (no code)* or *My own training
109
+ script*, then where to run (this computer, Kaggle, SSH, SLURM). runboard writes `runboard.yaml`, the runs file and,
110
+ for YOLO, the training and evaluation scripts into the folder; it never changes your own files.
111
+
112
+ A folder that already has a `runboard.yaml` can be added from the same wizard or with `runboard add PATH`.
113
+
114
+ ## Tablet access
115
+
116
+ By default runboard listens only on this computer (`127.0.0.1`) and asks for nothing. To follow runs from a tablet on
117
+ the same Wi-Fi, turn on **System → Tablet access** (the dashboard restarts on all network interfaces), press
118
+ **Pair a device** and scan the QR code with the tablet, or open the address shown and type the 6-digit code. The code
119
+ works once and for two minutes. Paired devices are listed there and can be removed at any time.
120
+
121
+ A paired device can follow, start and stop runs. Entering the Kaggle token, adding projects, changing code and the
122
+ project file, the folder browser and startup settings stay on this computer. Outside your home network, use a VPN such
123
+ as Tailscale and add its machine name to `allowed_hosts` in `~/.runboard/config.yaml`. See
124
+ [SECURITY.md](https://github.com/alperentoker/runboard-ml/blob/main/SECURITY.md).
125
+
126
+ ## A project by hand
127
+
128
+ The wizard covers the common cases. Under the hood a project is any folder with a `runboard.yaml` (and, for a
129
+ pre-registered study, a `plan.yaml`). runboard does not own your code or configs; the manifest just tells it where
130
+ things are:
131
+
132
+ ```yaml
133
+ version: 2
134
+ name: My detector
135
+ runs:
136
+ file: configs/runs.yaml # your runs, in your own format: a mapping of run id -> fields
137
+ section: runs
138
+ defaults: train
139
+ name: "{id}-{model|stem}-s{seed}"
140
+ backends:
141
+ local: {command: '"{python}" train.py --run {id}', output_dir: runs/local}
142
+ kaggle: {files: {train.py: train.py, runs.yaml: configs/runs.yaml}, command: "python train.py --run {id}"}
143
+ results:
144
+ report: "{out}/summary.json"
145
+ metrics: {primary: map50_95}
146
+ ```
147
+
148
+ See [the manifest reference](https://github.com/alperentoker/runboard-ml/blob/main/docs/manifest.md) for every option,
149
+ including `plan.yaml`, criteria, evaluation files, the lock and the backends, and
150
+ [the design notes](https://github.com/alperentoker/runboard-ml/blob/main/docs/design-v2.md) for how verdicts are
151
+ computed.
152
+
153
+ ## Reporting progress
154
+
155
+ Print one line per event from your training script, or use `rb.py` (stdlib only; `runboard demo` and the wizard put a
156
+ copy next to your scripts):
157
+
158
+ ```python
159
+ import rb
160
+ rb.ultralytics(model) # Ultralytics: one line adds everything below
161
+ rb.start(epochs=100, batches=798)
162
+ rb.beat(epoch=3, batch=400, batches=798, its=4.9)
163
+ rb.epoch(3, epochs=100, secs=171, metrics={"map": 48.2}, best=48.2, eta=time.time() + 3600)
164
+ ```
165
+
166
+ Each call prints `@@rb {json}`; your own log lines are untouched. Projects with existing plain-text logs can describe
167
+ them with regular expressions instead (`progress.patterns`).
168
+
169
+ ## Run it permanently
170
+
171
+ *System → Start runboard when Windows starts* (Windows), or a systemd/launchd service running `runboard serve`.
172
+ `runboard serve` also starts a small watchdog: if the dashboard stops answering for 20 s it is started again, and every
173
+ start, stop and unexpected error is written with its time to `~/.runboard/server.log`. `runboard stop` stops both and
174
+ keeps them stopped until the next `runboard serve` (set `watchdog: false` in `~/.runboard/config.yaml` to go without).
175
+ To reach it from outside your home network, put it behind a private network such as Tailscale rather than exposing the port.
176
+
177
+ ## Development
178
+
179
+ ```bash
180
+ git clone https://github.com/alperentoker/runboard-ml
181
+ cd runboard-ml
182
+ pip install -e .[dev]
183
+ pytest
184
+ ```
185
+
186
+ MIT licensed. Changes: [CHANGELOG.md](https://github.com/alperentoker/runboard-ml/blob/main/CHANGELOG.md) ·
187
+ plans: [ROADMAP.md](https://github.com/alperentoker/runboard-ml/blob/main/ROADMAP.md).
@@ -0,0 +1,149 @@
1
+ # runboard
2
+
3
+ **Pre-registered machine-learning experiments, from the plan to the paper.** Write your questions and hypotheses,
4
+ lock the plan, run the experiments on your own GPU, Kaggle, a machine over SSH or a SLURM cluster, watch them live, and
5
+ get every hypothesis decided with a bootstrap confidence interval, plus the tables, methods and deviations sections
6
+ for the paper. One self-hosted dashboard for all your projects; everything stays in readable files in your repository.
7
+
8
+ **New here?** `pip install runboard-ml numpy`, then `runboard demo`: a five-minute pre-registered study on any CPU,
9
+ walked through in the [tutorial](https://github.com/alperentoker/runboard-ml/blob/main/docs/tutorial.md).
10
+
11
+ [Türkçe](https://github.com/alperentoker/runboard-ml/blob/main/README.tr.md) · [![tests](https://github.com/alperentoker/runboard-ml/actions/workflows/tests.yml/badge.svg)](https://github.com/alperentoker/runboard-ml/actions/workflows/tests.yml)
12
+
13
+ ![Hypotheses decided with confidence intervals](https://raw.githubusercontent.com/alperentoker/runboard-ml/main/docs/images/tutorial-7-verdicts.png)
14
+
15
+ <table><tr>
16
+ <td width="50%">
17
+
18
+ <picture>
19
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/alperentoker/runboard-ml/main/docs/images/run-dark.png">
20
+ <img alt="A run's page: live progress, results and training curves" src="https://raw.githubusercontent.com/alperentoker/runboard-ml/main/docs/images/run-light.png">
21
+ </picture>
22
+
23
+ <sub>A run, live</sub>
24
+ </td>
25
+ <td width="50%">
26
+
27
+ <picture>
28
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/alperentoker/runboard-ml/main/docs/images/plan-dark.png">
29
+ <img alt="Editing the run plan: every change is previewed as the lines it changes" src="https://raw.githubusercontent.com/alperentoker/runboard-ml/main/docs/images/plan-light.png">
30
+ </picture>
31
+
32
+ <sub>Editing the plan: a preview of the exact lines that change</sub>
33
+ </td>
34
+ </tr></table>
35
+
36
+ - **Hypotheses a machine can check** — `R7.aerial >= 0.9 * R5.aerial`, `collapse(R4, ground, aerial) > ...`, seed groups,
37
+ `log_fit(...)`: each comparison is decided on paired bootstrap replicates as *supported*, *refuted* or
38
+ *indistinguishable*, with its 95 % interval.
39
+ - **A lock that means something** — `plan.lock.json` records when, at which commit and the fingerprints of the plan,
40
+ the runs and the scripts; every later change needs a reason and is listed as a deviation. A sealed test set is
41
+ evaluated once per model, on the record.
42
+ - **Research wizard** — questions, experiment designs (specialists, data-size curves, architectures, ablations,
43
+ seeds), the evaluation protocol and hypotheses checked as you type become a ready project.
44
+ - **Code templates** — split, leakage check, conversion, data-size subsets, evaluation and speed scripts, written into
45
+ your project as ordinary files that runboard never overwrites once you change them.
46
+ - **Paper output** — Markdown and LaTeX tables, the hypotheses, a methods draft, the deviations section, a
47
+ reproducibility checklist, a registered-report page and an archive for Zenodo.
48
+ - **Live runs anywhere** — your GPU, Kaggle (private kernels, weekly quota, private datasets), a machine over SSH and a
49
+ SLURM cluster (your own SSH keys; runboard never sees a password); logs, second-by-second progress, charts, a queue.
50
+ - **Provenance** — every run records the commit, uncommitted changes and the fingerprints of what it started from.
51
+ - **Tablet view**, a Code page, English and Türkçe.
52
+
53
+ Secrets never leave your machine: the Kaggle token stays in `~/.kaggle`, paired devices and all state live in
54
+ `~/.runboard`, never inside a project repository.
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ pip install runboard-ml # or: pip install "runboard-ml[kaggle]" for Kaggle
60
+ runboard # http://localhost:8000 — this computer only, no password
61
+ ```
62
+
63
+ The package is called `runboard-ml` on PyPI (`runboard` belongs to another project); the command and the Python module
64
+ are `runboard`. Do not install both in one environment: they would share the module name. Python 3.11 or newer.
65
+
66
+ Then, in the dashboard:
67
+
68
+ 1. **System → Connections → Kaggle** (only if you want cloud GPUs): the panel walks you through creating a token on
69
+ kaggle.com and pasting it once. It is saved to `~/.kaggle/access_token` on this machine, tested, and never shown again.
70
+ 2. **New project**: pick a folder and choose *Pre-registered study (YOLO)*, *YOLO training (no code)* or *My own training
71
+ script*, then where to run (this computer, Kaggle, SSH, SLURM). runboard writes `runboard.yaml`, the runs file and,
72
+ for YOLO, the training and evaluation scripts into the folder; it never changes your own files.
73
+
74
+ A folder that already has a `runboard.yaml` can be added from the same wizard or with `runboard add PATH`.
75
+
76
+ ## Tablet access
77
+
78
+ By default runboard listens only on this computer (`127.0.0.1`) and asks for nothing. To follow runs from a tablet on
79
+ the same Wi-Fi, turn on **System → Tablet access** (the dashboard restarts on all network interfaces), press
80
+ **Pair a device** and scan the QR code with the tablet, or open the address shown and type the 6-digit code. The code
81
+ works once and for two minutes. Paired devices are listed there and can be removed at any time.
82
+
83
+ A paired device can follow, start and stop runs. Entering the Kaggle token, adding projects, changing code and the
84
+ project file, the folder browser and startup settings stay on this computer. Outside your home network, use a VPN such
85
+ as Tailscale and add its machine name to `allowed_hosts` in `~/.runboard/config.yaml`. See
86
+ [SECURITY.md](https://github.com/alperentoker/runboard-ml/blob/main/SECURITY.md).
87
+
88
+ ## A project by hand
89
+
90
+ The wizard covers the common cases. Under the hood a project is any folder with a `runboard.yaml` (and, for a
91
+ pre-registered study, a `plan.yaml`). runboard does not own your code or configs; the manifest just tells it where
92
+ things are:
93
+
94
+ ```yaml
95
+ version: 2
96
+ name: My detector
97
+ runs:
98
+ file: configs/runs.yaml # your runs, in your own format: a mapping of run id -> fields
99
+ section: runs
100
+ defaults: train
101
+ name: "{id}-{model|stem}-s{seed}"
102
+ backends:
103
+ local: {command: '"{python}" train.py --run {id}', output_dir: runs/local}
104
+ kaggle: {files: {train.py: train.py, runs.yaml: configs/runs.yaml}, command: "python train.py --run {id}"}
105
+ results:
106
+ report: "{out}/summary.json"
107
+ metrics: {primary: map50_95}
108
+ ```
109
+
110
+ See [the manifest reference](https://github.com/alperentoker/runboard-ml/blob/main/docs/manifest.md) for every option,
111
+ including `plan.yaml`, criteria, evaluation files, the lock and the backends, and
112
+ [the design notes](https://github.com/alperentoker/runboard-ml/blob/main/docs/design-v2.md) for how verdicts are
113
+ computed.
114
+
115
+ ## Reporting progress
116
+
117
+ Print one line per event from your training script, or use `rb.py` (stdlib only; `runboard demo` and the wizard put a
118
+ copy next to your scripts):
119
+
120
+ ```python
121
+ import rb
122
+ rb.ultralytics(model) # Ultralytics: one line adds everything below
123
+ rb.start(epochs=100, batches=798)
124
+ rb.beat(epoch=3, batch=400, batches=798, its=4.9)
125
+ rb.epoch(3, epochs=100, secs=171, metrics={"map": 48.2}, best=48.2, eta=time.time() + 3600)
126
+ ```
127
+
128
+ Each call prints `@@rb {json}`; your own log lines are untouched. Projects with existing plain-text logs can describe
129
+ them with regular expressions instead (`progress.patterns`).
130
+
131
+ ## Run it permanently
132
+
133
+ *System → Start runboard when Windows starts* (Windows), or a systemd/launchd service running `runboard serve`.
134
+ `runboard serve` also starts a small watchdog: if the dashboard stops answering for 20 s it is started again, and every
135
+ start, stop and unexpected error is written with its time to `~/.runboard/server.log`. `runboard stop` stops both and
136
+ keeps them stopped until the next `runboard serve` (set `watchdog: false` in `~/.runboard/config.yaml` to go without).
137
+ To reach it from outside your home network, put it behind a private network such as Tailscale rather than exposing the port.
138
+
139
+ ## Development
140
+
141
+ ```bash
142
+ git clone https://github.com/alperentoker/runboard-ml
143
+ cd runboard-ml
144
+ pip install -e .[dev]
145
+ pytest
146
+ ```
147
+
148
+ MIT licensed. Changes: [CHANGELOG.md](https://github.com/alperentoker/runboard-ml/blob/main/CHANGELOG.md) ·
149
+ plans: [ROADMAP.md](https://github.com/alperentoker/runboard-ml/blob/main/ROADMAP.md).
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "runboard-ml" # "runboard" on PyPI belongs to another project; the command and the module are still runboard
7
+ version = "0.2.0"
8
+ description = "Pre-registered ML experiments: lock the plan, run on your GPU, Kaggle, SSH or SLURM, watch live, and get hypotheses decided with bootstrap intervals and paper-ready output."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Alperen Toker" }]
13
+ requires-python = ">=3.11" # the Kaggle client (kaggle>=2.2) needs 3.11
14
+ dependencies = ["fastapi>=0.110", "uvicorn>=0.29", "pyyaml>=6"]
15
+ keywords = ["pre-registration", "reproducibility", "hypothesis testing", "bootstrap", "machine learning", "training",
16
+ "dashboard", "kaggle", "slurm", "yolo"]
17
+ classifiers = [
18
+ "Development Status :: 4 - Beta",
19
+ "Environment :: Web Environment",
20
+ "Intended Audience :: Science/Research",
21
+ "Operating System :: OS Independent",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
27
+ "Topic :: Scientific/Engineering :: Information Analysis",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/alperentoker/runboard-ml"
32
+ Documentation = "https://github.com/alperentoker/runboard-ml/blob/main/docs/manifest.md"
33
+ Issues = "https://github.com/alperentoker/runboard-ml/issues"
34
+ Security = "https://github.com/alperentoker/runboard-ml/security/policy"
35
+
36
+ [project.optional-dependencies]
37
+ kaggle = ["kaggle>=2.2", "six"] # newer Kaggle clients import six without depending on it
38
+ dev = ["pytest", "httpx", "kaggle>=2.2", "six", "numpy"] # numpy: the demo and the evaluation helpers
39
+
40
+ [project.scripts]
41
+ runboard = "runboard.cli:main"
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
45
+
46
+ [tool.setuptools.packages.find]
47
+ include = ["runboard*"]
48
+
49
+ [tool.setuptools.package-data]
50
+ runboard = ["static/*", "static/js/*", "templates/*", "demo/*"]
@@ -0,0 +1,2 @@
1
+ """runboard: one self-hosted dashboard for ML training runs across projects and compute backends."""
2
+ __version__ = "0.2.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,219 @@
1
+ """Who may use the dashboard.
2
+
3
+ - This computer (a loopback connection) needs nothing: no password, no login screen.
4
+ - Other devices (a tablet on the same Wi-Fi) only when network access is switched on, and only after pairing once:
5
+ this computer shows a QR code / 6-digit code, the device gets a long random key in a cookie, and only a hash of that
6
+ key is stored (~/.runboard/devices.json). Paired devices can be removed at any time.
7
+ - Every request is also checked against DNS rebinding (Host header) and cross-site requests (Origin), so a web page
8
+ open in a browser cannot drive the dashboard.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import copy
13
+ import hashlib
14
+ import ipaddress
15
+ import secrets
16
+ import socket
17
+ import threading
18
+ import time
19
+ from http.cookies import SimpleCookie
20
+
21
+ from . import home
22
+
23
+ COOKIE = "rb_device"
24
+ PAIR_SECONDS = 120
25
+ PAIR_TRIES = 5
26
+ _lock = threading.Lock()
27
+ _pairing: dict | None = None
28
+
29
+
30
+ # ---------------------------------------------------------------- devices
31
+
32
+ def _devices_file():
33
+ return home.HOME / "devices.json"
34
+
35
+
36
+ _devices_cache: tuple = (None, [])
37
+
38
+
39
+ def devices() -> list[dict]:
40
+ """Paired devices; every request from a tablet asks, so the file is read again only when it changed."""
41
+ global _devices_cache
42
+ f = _devices_file()
43
+ try:
44
+ st = f.stat()
45
+ sig = (str(f), st.st_mtime_ns, st.st_size)
46
+ except FileNotFoundError:
47
+ return []
48
+ if _devices_cache[0] != sig:
49
+ _devices_cache = (sig, home.read_json(f, []))
50
+ return copy.deepcopy(_devices_cache[1])
51
+
52
+
53
+ def _hash(key: str) -> str:
54
+ return hashlib.sha256(key.encode()).hexdigest()
55
+
56
+
57
+ def add_device(name: str) -> str:
58
+ """Creates a device and returns its cookie value (the key itself is never stored)."""
59
+ did, key = secrets.token_hex(4), secrets.token_urlsafe(32)
60
+ with _lock:
61
+ ds = devices()
62
+ ds.append({"id": did, "name": name[:60] or "device", "hash": _hash(key),
63
+ "created": time.strftime("%Y-%m-%d %H:%M"), "last_seen": None})
64
+ home.write_json(_devices_file(), ds)
65
+ return f"{did}.{key}"
66
+
67
+
68
+ def remove_device(did: str) -> bool:
69
+ with _lock:
70
+ ds = devices()
71
+ keep = [d for d in ds if d["id"] != did]
72
+ home.write_json(_devices_file(), keep)
73
+ return len(keep) != len(ds)
74
+
75
+
76
+ def device_for(cookie_value: str | None) -> dict | None:
77
+ if not cookie_value or "." not in cookie_value:
78
+ return None
79
+ did, key = cookie_value.split(".", 1)
80
+ h = _hash(key)
81
+ for d in devices():
82
+ if d["id"] == did and secrets.compare_digest(d["hash"], h):
83
+ if not d.get("last_seen") or d["last_seen"][:16] != time.strftime("%Y-%m-%d %H:%M"):
84
+ with _lock:
85
+ ds = devices()
86
+ for x in ds:
87
+ if x["id"] == did:
88
+ x["last_seen"] = time.strftime("%Y-%m-%d %H:%M")
89
+ home.write_json(_devices_file(), ds)
90
+ return d
91
+ return None
92
+
93
+
94
+ def device_name(user_agent: str) -> str:
95
+ ua = user_agent or ""
96
+ for key, name in (("iPad", "iPad"), ("iPhone", "iPhone"), ("Android", "Android"), ("Macintosh", "Mac"),
97
+ ("Windows", "Windows"), ("Linux", "Linux")):
98
+ if key in ua:
99
+ return name + (" tablet" if key == "Android" and "Mobile" not in ua else "")
100
+ return "device"
101
+
102
+
103
+ # ---------------------------------------------------------------- pairing
104
+
105
+ def start_pairing() -> dict:
106
+ """A new one-time pairing: a token for the QR link and a 6-digit code to type by hand. The previous one ends."""
107
+ global _pairing
108
+ with _lock:
109
+ _pairing = {"token": secrets.token_urlsafe(16), "code": f"{secrets.randbelow(10**6):06d}",
110
+ "expires": time.time() + PAIR_SECONDS, "tries": 0}
111
+ return dict(_pairing)
112
+
113
+
114
+ def redeem(token: str | None = None, code: str | None = None) -> bool:
115
+ """True once for the current pairing's token or code; wrong codes count, and after PAIR_TRIES it is void."""
116
+ global _pairing
117
+ with _lock:
118
+ p = _pairing
119
+ if not p or time.time() > p["expires"]:
120
+ _pairing = None
121
+ return False
122
+ ok = (token is not None and secrets.compare_digest(token, p["token"])) or \
123
+ (code is not None and secrets.compare_digest(str(code).strip(), p["code"]))
124
+ if ok:
125
+ _pairing = None
126
+ return True
127
+ p["tries"] += 1
128
+ if p["tries"] >= PAIR_TRIES:
129
+ _pairing = None
130
+ return False
131
+
132
+
133
+ def pairing_state() -> dict | None:
134
+ p = _pairing
135
+ return {"seconds": max(0, round(p["expires"] - time.time()))} if p and time.time() < p["expires"] else None
136
+
137
+
138
+ # ---------------------------------------------------------------- request checks
139
+
140
+ def _local_addresses() -> set[str]:
141
+ out = {"127.0.0.1", "::1"}
142
+ try:
143
+ out.update(socket.gethostbyname_ex(socket.gethostname())[2])
144
+ except OSError:
145
+ pass
146
+ return out
147
+
148
+
149
+ LOCAL_ADDRESSES = _local_addresses()
150
+
151
+
152
+ def is_local_client(host: str | None) -> bool:
153
+ if not host:
154
+ return False
155
+ host = host.removeprefix("::ffff:")
156
+ return host in LOCAL_ADDRESSES or host.startswith("127.")
157
+
158
+
159
+ def host_allowed(host_header: str, extra: list[str]) -> bool:
160
+ """DNS rebinding guard: a request must name this machine by IP, as localhost, by its own name or by a name listed
161
+ in config.yaml (allowed_hosts, e.g. a Tailscale name). A web page's own domain is never accepted."""
162
+ host = host_header.strip().lower()
163
+ if host.startswith("["):
164
+ host = host[1:host.find("]")]
165
+ elif host.count(":") == 1:
166
+ host = host.split(":")[0]
167
+ try:
168
+ ipaddress.ip_address(host)
169
+ return True
170
+ except ValueError:
171
+ pass
172
+ names = {"localhost", socket.gethostname().lower()} | {h.lower() for h in extra}
173
+ return host in names or host.endswith(".localhost")
174
+
175
+
176
+ def cross_site(method: str, headers: dict[str, str]) -> bool:
177
+ """CSRF guard for anything that changes state: another site's page may not send it."""
178
+ if method in ("GET", "HEAD", "OPTIONS"):
179
+ return False
180
+ if headers.get("sec-fetch-site", "same-origin") not in ("same-origin", "none"):
181
+ return True
182
+ origin = headers.get("origin")
183
+ if origin and origin != "null":
184
+ return origin.split("://", 1)[-1].lower() != headers.get("host", "").lower()
185
+ return origin == "null"
186
+
187
+
188
+ def cookie_value(headers: dict[str, str]) -> str | None:
189
+ c = SimpleCookie()
190
+ try:
191
+ c.load(headers.get("cookie", ""))
192
+ except Exception:
193
+ return None
194
+ return c[COOKIE].value if COOKIE in c else None
195
+
196
+
197
+ def set_cookie_header(value: str) -> str:
198
+ return f"{COOKIE}={value}; Path=/; Max-Age=31536000; HttpOnly; SameSite=Strict"
199
+
200
+
201
+ CSP = ("default-src 'self'; script-src 'self'{scripts}; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; "
202
+ "media-src 'self' blob:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'")
203
+ SECURITY_HEADERS = [
204
+ (b"x-frame-options", b"DENY"),
205
+ (b"x-content-type-options", b"nosniff"),
206
+ (b"referrer-policy", b"no-referrer"),
207
+ (b"content-security-policy", CSP.format(scripts="").encode()),
208
+ ]
209
+
210
+
211
+ def page_csp(html: str) -> str:
212
+ """The policy for one page: only the page's own scripts run, named by their SHA-256. A script that got into the page
213
+ any other way (an injected tag or an onclick attribute) is refused by the browser."""
214
+ import base64
215
+ import re
216
+ hashes = [base64.b64encode(hashlib.sha256(m.encode("utf-8")).digest()).decode()
217
+ for m in re.findall(r"<script>(.*?)</script>", html, re.S)]
218
+ return CSP.format(scripts="".join(f" 'sha256-{h}'" for h in hashes))
219
+
@@ -0,0 +1,12 @@
1
+ """Compute backends. Each one launches a run, reports its status, streams its log and brings its outputs home.
2
+
3
+ Adding a backend: subclass Backend in a new module and register it in BACKENDS.
4
+ """
5
+ from .base import ACTIVE, DONE, Backend
6
+ from .kaggle import KaggleBackend
7
+ from .local import LocalBackend
8
+ from .remote import SlurmBackend, SSHBackend
9
+
10
+ BACKENDS: dict[str, Backend] = {b.name: b for b in (KaggleBackend(), LocalBackend(), SSHBackend(), SlurmBackend())}
11
+
12
+ __all__ = ["ACTIVE", "DONE", "Backend", "BACKENDS"]