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.
- runboard_ml-0.2.0/LICENSE +21 -0
- runboard_ml-0.2.0/PKG-INFO +187 -0
- runboard_ml-0.2.0/README.md +149 -0
- runboard_ml-0.2.0/pyproject.toml +50 -0
- runboard_ml-0.2.0/runboard/__init__.py +2 -0
- runboard_ml-0.2.0/runboard/__main__.py +3 -0
- runboard_ml-0.2.0/runboard/access.py +219 -0
- runboard_ml-0.2.0/runboard/backends/__init__.py +12 -0
- runboard_ml-0.2.0/runboard/backends/base.py +69 -0
- runboard_ml-0.2.0/runboard/backends/kaggle.py +261 -0
- runboard_ml-0.2.0/runboard/backends/local.py +114 -0
- runboard_ml-0.2.0/runboard/backends/remote.py +306 -0
- runboard_ml-0.2.0/runboard/cli.py +202 -0
- runboard_ml-0.2.0/runboard/code.py +150 -0
- runboard_ml-0.2.0/runboard/demo/evaluate.py +57 -0
- runboard_ml-0.2.0/runboard/demo/plan.yaml +26 -0
- runboard_ml-0.2.0/runboard/demo/runboard.yaml +46 -0
- runboard_ml-0.2.0/runboard/demo/runs.yaml +13 -0
- runboard_ml-0.2.0/runboard/demo/train.py +86 -0
- runboard_ml-0.2.0/runboard/errors.py +167 -0
- runboard_ml-0.2.0/runboard/export.py +325 -0
- runboard_ml-0.2.0/runboard/home.py +153 -0
- runboard_ml-0.2.0/runboard/hypotheses.py +373 -0
- runboard_ml-0.2.0/runboard/localrun.py +36 -0
- runboard_ml-0.2.0/runboard/planfile.py +195 -0
- runboard_ml-0.2.0/runboard/prereg.py +211 -0
- runboard_ml-0.2.0/runboard/proc.py +69 -0
- runboard_ml-0.2.0/runboard/progress.py +157 -0
- runboard_ml-0.2.0/runboard/project.py +707 -0
- runboard_ml-0.2.0/runboard/qr.py +154 -0
- runboard_ml-0.2.0/runboard/rb.py +190 -0
- runboard_ml-0.2.0/runboard/research.py +226 -0
- runboard_ml-0.2.0/runboard/runsfile.py +185 -0
- runboard_ml-0.2.0/runboard/runtime.py +516 -0
- runboard_ml-0.2.0/runboard/server.py +1020 -0
- runboard_ml-0.2.0/runboard/static/icon.svg +1 -0
- runboard_ml-0.2.0/runboard/static/index.html +443 -0
- runboard_ml-0.2.0/runboard/static/js/01-i18n.js +160 -0
- runboard_ml-0.2.0/runboard/static/js/02-core.js +202 -0
- runboard_ml-0.2.0/runboard/static/js/03-charts.js +377 -0
- runboard_ml-0.2.0/runboard/static/js/04-pages.js +318 -0
- runboard_ml-0.2.0/runboard/static/js/05-plan.js +296 -0
- runboard_ml-0.2.0/runboard/static/js/05-templates.js +53 -0
- runboard_ml-0.2.0/runboard/static/js/06-code.js +98 -0
- runboard_ml-0.2.0/runboard/static/js/06-project.js +100 -0
- runboard_ml-0.2.0/runboard/static/js/07-system.js +106 -0
- runboard_ml-0.2.0/runboard/static/js/08-wizard.js +275 -0
- runboard_ml-0.2.0/runboard/static/js/09-hypotheses.js +72 -0
- runboard_ml-0.2.0/runboard/static/js/10-main.js +84 -0
- runboard_ml-0.2.0/runboard/static/mini.html +148 -0
- runboard_ml-0.2.0/runboard/static/pair.html +56 -0
- runboard_ml-0.2.0/runboard/templates/check.py +147 -0
- runboard_ml-0.2.0/runboard/templates/convert.py +113 -0
- runboard_ml-0.2.0/runboard/templates/evaluate.py +162 -0
- runboard_ml-0.2.0/runboard/templates/runboard_train.py +136 -0
- runboard_ml-0.2.0/runboard/templates/speed.py +66 -0
- runboard_ml-0.2.0/runboard/templates/split.py +99 -0
- runboard_ml-0.2.0/runboard/templates/subsets.py +80 -0
- runboard_ml-0.2.0/runboard/templating.py +260 -0
- runboard_ml-0.2.0/runboard/watch.py +101 -0
- runboard_ml-0.2.0/runboard/wizard.py +281 -0
- runboard_ml-0.2.0/runboard_ml.egg-info/PKG-INFO +187 -0
- runboard_ml-0.2.0/runboard_ml.egg-info/SOURCES.txt +81 -0
- runboard_ml-0.2.0/runboard_ml.egg-info/dependency_links.txt +1 -0
- runboard_ml-0.2.0/runboard_ml.egg-info/entry_points.txt +2 -0
- runboard_ml-0.2.0/runboard_ml.egg-info/requires.txt +14 -0
- runboard_ml-0.2.0/runboard_ml.egg-info/top_level.txt +1 -0
- runboard_ml-0.2.0/setup.cfg +4 -0
- runboard_ml-0.2.0/tests/test_code.py +155 -0
- runboard_ml-0.2.0/tests/test_core.py +240 -0
- runboard_ml-0.2.0/tests/test_demo.py +57 -0
- runboard_ml-0.2.0/tests/test_export.py +114 -0
- runboard_ml-0.2.0/tests/test_fixes.py +198 -0
- runboard_ml-0.2.0/tests/test_plan.py +244 -0
- runboard_ml-0.2.0/tests/test_plan_v2.py +193 -0
- runboard_ml-0.2.0/tests/test_prereg.py +237 -0
- runboard_ml-0.2.0/tests/test_remote.py +217 -0
- runboard_ml-0.2.0/tests/test_research.py +120 -0
- runboard_ml-0.2.0/tests/test_runtime.py +311 -0
- runboard_ml-0.2.0/tests/test_security.py +146 -0
- runboard_ml-0.2.0/tests/test_templates.py +311 -0
- runboard_ml-0.2.0/tests/test_ui.py +496 -0
- 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) · [](https://github.com/alperentoker/runboard-ml/actions/workflows/tests.yml)
|
|
50
|
+
|
|
51
|
+

|
|
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) · [](https://github.com/alperentoker/runboard-ml/actions/workflows/tests.yml)
|
|
12
|
+
|
|
13
|
+

|
|
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,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"]
|