labnotes 0.0.12__tar.gz → 0.0.14__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.
- {labnotes-0.0.12 → labnotes-0.0.14}/.gitignore +2 -0
- labnotes-0.0.14/PKG-INFO +100 -0
- labnotes-0.0.14/README.md +80 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/pyproject.toml +2 -1
- labnotes-0.0.14/src/labnotes/cli.py +42 -0
- labnotes-0.0.14/src/labnotes/context.md +47 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/context_guide.py +4 -2
- labnotes-0.0.14/src/labnotes/default_config_text.py +7 -0
- labnotes-0.0.14/src/labnotes/experiments.py +33 -0
- labnotes-0.0.14/src/labnotes/init.py +17 -0
- labnotes-0.0.14/src/labnotes/load_config.py +14 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/models.py +4 -2
- labnotes-0.0.14/src/labnotes/print_rows.py +19 -0
- labnotes-0.0.14/src/labnotes/read_config.py +27 -0
- labnotes-0.0.14/src/labnotes/read_path.py +34 -0
- labnotes-0.0.14/src/labnotes/server.py +64 -0
- labnotes-0.0.14/src/labnotes/tables/__init__.py +0 -0
- labnotes-0.0.14/src/labnotes/tables/experiment_rows.py +17 -0
- labnotes-0.0.14/src/labnotes/tables/file_prefix.py +9 -0
- labnotes-0.0.14/src/labnotes/tables/iso_time.py +5 -0
- labnotes-0.0.14/src/labnotes/tables/list_experiment_runs.py +10 -0
- labnotes-0.0.14/src/labnotes/tables/open_tables.py +63 -0
- labnotes-0.0.14/src/labnotes/tables/path_glob.py +8 -0
- labnotes-0.0.14/src/labnotes/tables/path_pattern.py +23 -0
- labnotes-0.0.14/src/labnotes/tables/read_experiment.py +15 -0
- labnotes-0.0.14/src/labnotes/tables/read_file.py +23 -0
- labnotes-0.0.14/src/labnotes/tables/read_run.py +27 -0
- labnotes-0.0.14/src/labnotes/tables/summarize_experiments.py +27 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/uv.lock +15 -0
- labnotes-0.0.12/PKG-INFO +0 -78
- labnotes-0.0.12/README.md +0 -59
- labnotes-0.0.12/src/labnotes/cli.py +0 -121
- labnotes-0.0.12/src/labnotes/context.md +0 -34
- labnotes-0.0.12/src/labnotes/dump_experiments.py +0 -15
- labnotes-0.0.12/src/labnotes/query_experiment.py +0 -24
- labnotes-0.0.12/src/labnotes/read_config.py +0 -23
- labnotes-0.0.12/src/labnotes/scope_runs.py +0 -14
- {labnotes-0.0.12 → labnotes-0.0.14}/CHANGELOG.md +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/MIGRATIONS.md +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/open_experiments.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/open_runs.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/read_design.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/read_note.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/run_columns.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/stop_idle_kernels.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/study_progress.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/summarize_design.py +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.12 → labnotes-0.0.14}/src/labnotes/templates/macros.html +0 -0
labnotes-0.0.14/PKG-INFO
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: labnotes
|
|
3
|
+
Version: 0.0.14
|
|
4
|
+
Summary: Browse experiment designs, notes, and run data from a folder.
|
|
5
|
+
Project-URL: Repository, https://github.com/thekevinscott/labnotes
|
|
6
|
+
Author-email: Kevin Scott <me@thekevinscott.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: click>=8.5.0
|
|
10
|
+
Requires-Dist: dirsql>=0.4.110
|
|
11
|
+
Requires-Dist: ipykernel>=7.4.0
|
|
12
|
+
Requires-Dist: jinja2>=3.1.6
|
|
13
|
+
Requires-Dist: jupyter-client>=8.10.0
|
|
14
|
+
Requires-Dist: markdown-it-py>=4.2.0
|
|
15
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
16
|
+
Requires-Dist: rich>=14
|
|
17
|
+
Requires-Dist: starlette>=1.8.0
|
|
18
|
+
Requires-Dist: uvicorn>=0.54.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# labnotes
|
|
22
|
+
|
|
23
|
+
Browse experiment designs, notes, and run data from a folder.
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
uvx labnotes server
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A project's root is the folder holding its `.labnotes.toml`. `labnotes init`
|
|
30
|
+
writes one with the default layout:
|
|
31
|
+
|
|
32
|
+
```toml
|
|
33
|
+
[experiments]
|
|
34
|
+
path = "{experiment}"
|
|
35
|
+
|
|
36
|
+
[files]
|
|
37
|
+
path = "{experiment}/files/"
|
|
38
|
+
|
|
39
|
+
[runs]
|
|
40
|
+
path = "{experiment}/runs/{run}"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Each path is relative to the root. `{experiment}` and `{run}` stand for folder
|
|
44
|
+
names: the experiments path must use `{experiment}`, and the runs path both.
|
|
45
|
+
labnotes reads the file from the working folder, or from the path given with
|
|
46
|
+
`-c`.
|
|
47
|
+
|
|
48
|
+
`labnotes experiments` reads three tables from those paths:
|
|
49
|
+
|
|
50
|
+
- `experiments`: `name`, and `config`, the experiment's `experiment.toml` as
|
|
51
|
+
JSON. An experiment is a folder matching the experiments path that holds an
|
|
52
|
+
`experiment.toml`, files or runs.
|
|
53
|
+
- `files`: `experiment`, `filename`, `path`, `ctime`, `mtime`.
|
|
54
|
+
- `runs`: `experiment`, `id`, `folder`, `created`, `ended`, `exit_code`,
|
|
55
|
+
`error`, `stdout`, `stderr`, and `params` as JSON, read from each run
|
|
56
|
+
folder's `run.json`.
|
|
57
|
+
|
|
58
|
+
Keys come from folder names, so renaming a folder renames its rows on the next
|
|
59
|
+
read. Paths are relative to the root, and times are UTC ISO 8601.
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
labnotes experiments # name, last_run, created, files, runs
|
|
63
|
+
labnotes experiments foo # the same, plus config
|
|
64
|
+
labnotes experiments foo runs # foo's runs, oldest first
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Each takes `--format table` (the default) or `--format json`.
|
|
68
|
+
|
|
69
|
+
`labnotes server` doesn't read `.labnotes.toml` yet. Give it the two folders
|
|
70
|
+
it uses with `--experiments-folder` and `--data-folder`:
|
|
71
|
+
|
|
72
|
+
- `--experiments-folder` holds one subfolder per experiment, each with a
|
|
73
|
+
`design.yaml` (with a `study` key) and any number of markdown notes.
|
|
74
|
+
- `--data-folder` holds the run data and a `.dirsql.toml` that declares a
|
|
75
|
+
`runs` table with a `study` column. An experiment's runs are the rows whose
|
|
76
|
+
`study` matches its design's.
|
|
77
|
+
|
|
78
|
+
That serves a local site at http://127.0.0.1:8000: a table of experiments,
|
|
79
|
+
and a page per experiment with its parameters, its notes, progress per cell of
|
|
80
|
+
the design grid, and every run so far. Both folders are watched, so a reload
|
|
81
|
+
shows new runs and edited notes.
|
|
82
|
+
|
|
83
|
+
`design.yaml` keys the site reads: `study`, `fixed`, `varies` (each key a list
|
|
84
|
+
of values), `runs_per_cell`, and `completed_run`, a SQL condition over the
|
|
85
|
+
`runs` table that marks a run as complete. Anything else shows up as-is.
|
|
86
|
+
|
|
87
|
+
`-h`/`--host` and `-p`/`--port` change where it listens; `-h 0.0.0.0` (or a
|
|
88
|
+
Tailscale IP) serves beyond localhost. Every other uvicorn option passes
|
|
89
|
+
through: `--reload`, `--workers`, `--proxy-headers`, `--root-path`, the
|
|
90
|
+
`--ssl-*` set, and the rest. `labnotes server --help` lists them. `LABNOTES_EXPERIMENTS_FOLDER` and
|
|
91
|
+
`LABNOTES_DATA_FOLDER` can stand in for the folder flags.
|
|
92
|
+
|
|
93
|
+
## Agent skill
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
npx skills add thekevinscott/labnotes
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The skill tells a coding agent to run `labnotes context`, which prints a usage
|
|
100
|
+
guide for the installed version.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# labnotes
|
|
2
|
+
|
|
3
|
+
Browse experiment designs, notes, and run data from a folder.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
uvx labnotes server
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
A project's root is the folder holding its `.labnotes.toml`. `labnotes init`
|
|
10
|
+
writes one with the default layout:
|
|
11
|
+
|
|
12
|
+
```toml
|
|
13
|
+
[experiments]
|
|
14
|
+
path = "{experiment}"
|
|
15
|
+
|
|
16
|
+
[files]
|
|
17
|
+
path = "{experiment}/files/"
|
|
18
|
+
|
|
19
|
+
[runs]
|
|
20
|
+
path = "{experiment}/runs/{run}"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Each path is relative to the root. `{experiment}` and `{run}` stand for folder
|
|
24
|
+
names: the experiments path must use `{experiment}`, and the runs path both.
|
|
25
|
+
labnotes reads the file from the working folder, or from the path given with
|
|
26
|
+
`-c`.
|
|
27
|
+
|
|
28
|
+
`labnotes experiments` reads three tables from those paths:
|
|
29
|
+
|
|
30
|
+
- `experiments`: `name`, and `config`, the experiment's `experiment.toml` as
|
|
31
|
+
JSON. An experiment is a folder matching the experiments path that holds an
|
|
32
|
+
`experiment.toml`, files or runs.
|
|
33
|
+
- `files`: `experiment`, `filename`, `path`, `ctime`, `mtime`.
|
|
34
|
+
- `runs`: `experiment`, `id`, `folder`, `created`, `ended`, `exit_code`,
|
|
35
|
+
`error`, `stdout`, `stderr`, and `params` as JSON, read from each run
|
|
36
|
+
folder's `run.json`.
|
|
37
|
+
|
|
38
|
+
Keys come from folder names, so renaming a folder renames its rows on the next
|
|
39
|
+
read. Paths are relative to the root, and times are UTC ISO 8601.
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
labnotes experiments # name, last_run, created, files, runs
|
|
43
|
+
labnotes experiments foo # the same, plus config
|
|
44
|
+
labnotes experiments foo runs # foo's runs, oldest first
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Each takes `--format table` (the default) or `--format json`.
|
|
48
|
+
|
|
49
|
+
`labnotes server` doesn't read `.labnotes.toml` yet. Give it the two folders
|
|
50
|
+
it uses with `--experiments-folder` and `--data-folder`:
|
|
51
|
+
|
|
52
|
+
- `--experiments-folder` holds one subfolder per experiment, each with a
|
|
53
|
+
`design.yaml` (with a `study` key) and any number of markdown notes.
|
|
54
|
+
- `--data-folder` holds the run data and a `.dirsql.toml` that declares a
|
|
55
|
+
`runs` table with a `study` column. An experiment's runs are the rows whose
|
|
56
|
+
`study` matches its design's.
|
|
57
|
+
|
|
58
|
+
That serves a local site at http://127.0.0.1:8000: a table of experiments,
|
|
59
|
+
and a page per experiment with its parameters, its notes, progress per cell of
|
|
60
|
+
the design grid, and every run so far. Both folders are watched, so a reload
|
|
61
|
+
shows new runs and edited notes.
|
|
62
|
+
|
|
63
|
+
`design.yaml` keys the site reads: `study`, `fixed`, `varies` (each key a list
|
|
64
|
+
of values), `runs_per_cell`, and `completed_run`, a SQL condition over the
|
|
65
|
+
`runs` table that marks a run as complete. Anything else shows up as-is.
|
|
66
|
+
|
|
67
|
+
`-h`/`--host` and `-p`/`--port` change where it listens; `-h 0.0.0.0` (or a
|
|
68
|
+
Tailscale IP) serves beyond localhost. Every other uvicorn option passes
|
|
69
|
+
through: `--reload`, `--workers`, `--proxy-headers`, `--root-path`, the
|
|
70
|
+
`--ssl-*` set, and the rest. `labnotes server --help` lists them. `LABNOTES_EXPERIMENTS_FOLDER` and
|
|
71
|
+
`LABNOTES_DATA_FOLDER` can stand in for the folder flags.
|
|
72
|
+
|
|
73
|
+
## Agent skill
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npx skills add thekevinscott/labnotes
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The skill tells a coding agent to run `labnotes context`, which prints a usage
|
|
80
|
+
guide for the installed version.
|
|
@@ -18,6 +18,7 @@ dependencies = [
|
|
|
18
18
|
"jupyter-client>=8.10.0",
|
|
19
19
|
"markdown-it-py>=4.2.0",
|
|
20
20
|
"pyyaml>=6.0.3",
|
|
21
|
+
"rich>=14",
|
|
21
22
|
"starlette>=1.8.0",
|
|
22
23
|
"uvicorn>=0.54.0",
|
|
23
24
|
]
|
|
@@ -51,7 +52,7 @@ exclude = ["*_test.py"]
|
|
|
51
52
|
packages = ["src/labnotes"]
|
|
52
53
|
|
|
53
54
|
[tool.pytest.ini_options]
|
|
54
|
-
testpaths = ["src/labnotes"]
|
|
55
|
+
testpaths = ["src/labnotes", "tests"]
|
|
55
56
|
python_files = "*_test.py"
|
|
56
57
|
|
|
57
58
|
[tool.ruff.lint]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from labnotes.context_guide import context_guide
|
|
6
|
+
from labnotes.experiments import experiments
|
|
7
|
+
from labnotes.init import init
|
|
8
|
+
from labnotes.load_config import load_config
|
|
9
|
+
from labnotes.read_config import CONFIG_FILE
|
|
10
|
+
from labnotes.server import server
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group(
|
|
14
|
+
invoke_without_command=True,
|
|
15
|
+
help="Manage experiments in a folder. `labnotes context` prints a guide for"
|
|
16
|
+
" coding agents.",
|
|
17
|
+
)
|
|
18
|
+
@click.version_option(package_name="labnotes")
|
|
19
|
+
@click.option(
|
|
20
|
+
"-c",
|
|
21
|
+
"--config",
|
|
22
|
+
"config_path",
|
|
23
|
+
type=click.Path(path_type=Path, dir_okay=False, exists=True),
|
|
24
|
+
help=f"Project config. Default: {CONFIG_FILE} in the working folder, if there.",
|
|
25
|
+
)
|
|
26
|
+
@click.pass_context
|
|
27
|
+
def main(ctx: click.Context, config_path: Path | None) -> None:
|
|
28
|
+
if ctx.invoked_subcommand is None:
|
|
29
|
+
click.echo(ctx.get_help())
|
|
30
|
+
ctx.exit()
|
|
31
|
+
ctx.obj = config_path or Path.cwd() / CONFIG_FILE
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@main.command(help="Print a guide for coding agents.")
|
|
35
|
+
@click.pass_context
|
|
36
|
+
def context(ctx: click.Context) -> None:
|
|
37
|
+
click.echo(context_guide(load_config(ctx), Path.cwd()), nl=False)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
main.add_command(experiments)
|
|
41
|
+
main.add_command(init)
|
|
42
|
+
main.add_command(server)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
## Usage
|
|
2
|
+
|
|
3
|
+
A project that uses labnotes has a `.labnotes.toml` at its root; the folder holding it is the root. labnotes reads it from the working folder, so run labnotes from the project root, or pass `-c PATH`. The lines above this guide name the one found and its paths, or say there is none. `labnotes init` writes one with the default layout:
|
|
4
|
+
|
|
5
|
+
```toml
|
|
6
|
+
[experiments]
|
|
7
|
+
path = "{experiment}" # one folder per experiment
|
|
8
|
+
|
|
9
|
+
[files]
|
|
10
|
+
path = "{experiment}/files/" # an experiment's notes and other files
|
|
11
|
+
|
|
12
|
+
[runs]
|
|
13
|
+
path = "{experiment}/runs/{run}" # one folder per run
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Paths are relative to the root. `{experiment}` and `{run}` stand for folder names; the experiments and files paths use `{experiment}`, the runs path both. A project laid out differently keeps its folders and changes the paths: look at where its experiments, notes and runs live, then write the templates that match.
|
|
17
|
+
|
|
18
|
+
`labnotes experiments` reads three tables from those paths:
|
|
19
|
+
|
|
20
|
+
- `experiments`: `name`, and `config`, the experiment's `experiment.toml` as
|
|
21
|
+
JSON. An experiment is a folder matching the experiments path that holds an
|
|
22
|
+
`experiment.toml`, files or runs.
|
|
23
|
+
- `files`: `experiment`, `filename`, `path`, `ctime`, `mtime`.
|
|
24
|
+
- `runs`: `experiment`, `id`, `folder`, `created`, `ended`, `exit_code`,
|
|
25
|
+
`error`, `stdout`, `stderr`, and `params` as JSON, read from each run
|
|
26
|
+
folder's `run.json`.
|
|
27
|
+
|
|
28
|
+
Keys come from folder names, so renaming a folder renames its rows on the next
|
|
29
|
+
read. Paths are relative to the root, and times are UTC ISO 8601.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
labnotes experiments # name, last_run, created, files, runs
|
|
33
|
+
labnotes experiments foo # the same, plus config
|
|
34
|
+
labnotes experiments foo runs # foo's runs, oldest first
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each takes `--format table` (the default) or `--format json`; use `--format json` when you parse the output.
|
|
38
|
+
|
|
39
|
+
`labnotes server` doesn't read `.labnotes.toml` yet: pass `--experiments-folder` and `--data-folder`, or set `LABNOTES_EXPERIMENTS_FOLDER` and `LABNOTES_DATA_FOLDER`. The experiments folder has one subfolder per experiment, each with a `design.yaml` and markdown notes. The data folder holds run data and a `.dirsql.toml` declaring a `runs` table with a `study` column.
|
|
40
|
+
|
|
41
|
+
The run data lives in the data folder. Query it with [dirsql](https://dirsql.dev) and the data folder's `.dirsql.toml`, which declares the `runs` table:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
uvx dirsql -c data/.dirsql.toml "SELECT * FROM runs LIMIT 1" --format json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`uvx labnotes server` serves the experiments and their runs as a local site.
|
|
@@ -16,7 +16,9 @@ def context_guide(config: LabnotesConfig | None, cwd: Path) -> str:
|
|
|
16
16
|
else:
|
|
17
17
|
project = (
|
|
18
18
|
f"Config: {config.path}\n"
|
|
19
|
-
f"
|
|
20
|
-
f"
|
|
19
|
+
f"Root: {config.root}\n"
|
|
20
|
+
f"Experiments: {config.experiments}\n"
|
|
21
|
+
f"Files: {config.files}\n"
|
|
22
|
+
f"Runs: {config.runs}"
|
|
21
23
|
)
|
|
22
24
|
return f"labnotes {version('labnotes')}\n\n{project}\n\n{guide}"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from labnotes.load_config import load_config
|
|
6
|
+
from labnotes.print_rows import print_rows
|
|
7
|
+
from labnotes.read_config import CONFIG_FILE
|
|
8
|
+
from labnotes.tables.experiment_rows import experiment_rows
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command(help="List experiments, show one, or list its runs.")
|
|
12
|
+
@click.argument("name", required=False)
|
|
13
|
+
@click.argument("action", required=False, type=click.Choice(["runs"]))
|
|
14
|
+
@click.option(
|
|
15
|
+
"--format",
|
|
16
|
+
"output_format",
|
|
17
|
+
type=click.Choice(["table", "json"]),
|
|
18
|
+
default="table",
|
|
19
|
+
show_default=True,
|
|
20
|
+
)
|
|
21
|
+
@click.pass_context
|
|
22
|
+
def experiments(
|
|
23
|
+
ctx: click.Context, name: str | None, action: str | None, output_format: str
|
|
24
|
+
) -> None:
|
|
25
|
+
config = load_config(ctx)
|
|
26
|
+
if config is None:
|
|
27
|
+
raise click.ClickException(
|
|
28
|
+
f"No {CONFIG_FILE} here. Run labnotes init, or pass -c PATH."
|
|
29
|
+
)
|
|
30
|
+
rows = asyncio.run(experiment_rows(config, name, action))
|
|
31
|
+
if rows is None:
|
|
32
|
+
raise click.ClickException(f"No experiment named {name}.")
|
|
33
|
+
print_rows(rows, output_format)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from labnotes.default_config_text import default_config_text
|
|
6
|
+
from labnotes.read_config import CONFIG_FILE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command(help=f"Write a {CONFIG_FILE} with the default layout here.")
|
|
10
|
+
def init() -> None:
|
|
11
|
+
path = Path.cwd() / CONFIG_FILE
|
|
12
|
+
try:
|
|
13
|
+
with path.open("x", encoding="utf-8") as file:
|
|
14
|
+
file.write(default_config_text())
|
|
15
|
+
except FileExistsError as error:
|
|
16
|
+
raise click.ClickException(f"{path} already exists.") from error
|
|
17
|
+
click.echo(f"Wrote {path}")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from labnotes.models import LabnotesConfig
|
|
6
|
+
from labnotes.read_config import read_config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_config(ctx: click.Context) -> LabnotesConfig | None:
|
|
10
|
+
path: Path = ctx.find_root().obj
|
|
11
|
+
try:
|
|
12
|
+
return read_config(path) if path.is_file() else None
|
|
13
|
+
except ValueError as error:
|
|
14
|
+
raise click.ClickException(str(error)) from error
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def print_rows(rows: list[dict[str, object]], output_format: str) -> None:
|
|
9
|
+
match output_format:
|
|
10
|
+
case "json":
|
|
11
|
+
click.echo(json.dumps(rows, indent=2))
|
|
12
|
+
return
|
|
13
|
+
if not rows:
|
|
14
|
+
click.echo("No rows.")
|
|
15
|
+
return
|
|
16
|
+
table = Table(*rows[0].keys())
|
|
17
|
+
for row in rows:
|
|
18
|
+
table.add_row(*("" if value is None else str(value) for value in row.values()))
|
|
19
|
+
Console().print(table)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from labnotes.models import LabnotesConfig
|
|
5
|
+
from labnotes.read_path import DEFAULT_PATHS, read_path
|
|
6
|
+
|
|
7
|
+
CONFIG_FILE = ".labnotes.toml"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def read_config(path: Path) -> LabnotesConfig:
|
|
11
|
+
try:
|
|
12
|
+
with path.open("rb") as file:
|
|
13
|
+
values = tomllib.load(file)
|
|
14
|
+
except tomllib.TOMLDecodeError as error:
|
|
15
|
+
raise ValueError(f"{path}: {error}") from error
|
|
16
|
+
unknown = sorted(values.keys() - DEFAULT_PATHS.keys())
|
|
17
|
+
if unknown:
|
|
18
|
+
raise ValueError(f"{path}: unknown tables {', '.join(unknown)}")
|
|
19
|
+
path = path.resolve()
|
|
20
|
+
return LabnotesConfig(
|
|
21
|
+
path,
|
|
22
|
+
path.parent,
|
|
23
|
+
**{
|
|
24
|
+
table: read_path(path, table, values.get(table, {}))
|
|
25
|
+
for table in DEFAULT_PATHS
|
|
26
|
+
},
|
|
27
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path, PurePosixPath
|
|
3
|
+
|
|
4
|
+
DEFAULT_PATHS = {
|
|
5
|
+
"experiments": "{experiment}",
|
|
6
|
+
"files": "{experiment}/files/",
|
|
7
|
+
"runs": "{experiment}/runs/{run}",
|
|
8
|
+
}
|
|
9
|
+
PLACEHOLDERS = {
|
|
10
|
+
"experiments": {"experiment"},
|
|
11
|
+
"files": {"experiment"},
|
|
12
|
+
"runs": {"experiment", "run"},
|
|
13
|
+
}
|
|
14
|
+
PLACEHOLDER = re.compile(r"\{([^}]*)\}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read_path(config: Path, table: str, section: object) -> str:
|
|
18
|
+
if not isinstance(section, dict):
|
|
19
|
+
raise ValueError(f"{config}: {table} must be a table, like [{table}]")
|
|
20
|
+
unknown = sorted(section.keys() - {"path"})
|
|
21
|
+
if unknown:
|
|
22
|
+
raise ValueError(f"{config}: unknown keys in [{table}]: {', '.join(unknown)}")
|
|
23
|
+
template = section.get("path", DEFAULT_PATHS[table])
|
|
24
|
+
if not isinstance(template, str):
|
|
25
|
+
raise ValueError(f"{config}: [{table}] path must be a string")
|
|
26
|
+
if PurePosixPath(template).is_absolute() or template.startswith("~"):
|
|
27
|
+
raise ValueError(
|
|
28
|
+
f"{config}: [{table}] path must be relative to the config's folder"
|
|
29
|
+
)
|
|
30
|
+
required = PLACEHOLDERS[table]
|
|
31
|
+
if set(PLACEHOLDER.findall(template)) != required:
|
|
32
|
+
expected = " and ".join(f"{{{name}}}" for name in sorted(required))
|
|
33
|
+
raise ValueError(f"{config}: [{table}] path must use exactly {expected}")
|
|
34
|
+
return template
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from uvicorn.main import main as uvicorn_main
|
|
6
|
+
|
|
7
|
+
from labnotes.serve_app import DATA_FOLDER, EXPERIMENTS_FOLDER, KERNEL_IDLE_MINUTES
|
|
8
|
+
|
|
9
|
+
# labnotes owns these; the rest of uvicorn's options pass straight through.
|
|
10
|
+
OWN_OPTIONS = {"app", "factory", "app_dir", "version", "host", "port"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command(help="Serve the experiments and their run data as a local site.")
|
|
14
|
+
@click.option(
|
|
15
|
+
"--experiments-folder",
|
|
16
|
+
type=click.Path(path_type=Path),
|
|
17
|
+
envvar=EXPERIMENTS_FOLDER,
|
|
18
|
+
required=True,
|
|
19
|
+
help="One subfolder per experiment, each with a design.yaml and markdown notes.",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"--data-folder",
|
|
23
|
+
type=click.Path(path_type=Path),
|
|
24
|
+
envvar=DATA_FOLDER,
|
|
25
|
+
required=True,
|
|
26
|
+
help="Run data, with a .dirsql.toml declaring a runs table that has a study column.",
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"--kernel-idle-minutes",
|
|
30
|
+
type=click.IntRange(min=1),
|
|
31
|
+
default=30,
|
|
32
|
+
show_default=True,
|
|
33
|
+
envvar=KERNEL_IDLE_MINUTES,
|
|
34
|
+
help="Stop a note's Python kernel after this long without a run.",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"-h",
|
|
38
|
+
"--host",
|
|
39
|
+
default="127.0.0.1",
|
|
40
|
+
show_default=True,
|
|
41
|
+
help="Bind to this host. 0.0.0.0 or a Tailscale IP serves beyond localhost.",
|
|
42
|
+
)
|
|
43
|
+
@click.option("-p", "--port", type=int, default=8000, show_default=True)
|
|
44
|
+
@click.pass_context
|
|
45
|
+
def server(
|
|
46
|
+
ctx: click.Context,
|
|
47
|
+
experiments_folder: Path,
|
|
48
|
+
data_folder: Path,
|
|
49
|
+
kernel_idle_minutes: int,
|
|
50
|
+
**server_options: object,
|
|
51
|
+
) -> None:
|
|
52
|
+
# Through the environment so --reload and --workers child processes see them.
|
|
53
|
+
os.environ[EXPERIMENTS_FOLDER] = str(experiments_folder.resolve())
|
|
54
|
+
os.environ[DATA_FOLDER] = str(data_folder.resolve())
|
|
55
|
+
os.environ[KERNEL_IDLE_MINUTES] = str(kernel_idle_minutes)
|
|
56
|
+
ctx.invoke(
|
|
57
|
+
uvicorn_main,
|
|
58
|
+
app="labnotes.serve_app:serve_app",
|
|
59
|
+
factory=True,
|
|
60
|
+
**server_options,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
server.params += [p for p in uvicorn_main.params if p.name not in OWN_OPTIONS]
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from labnotes.models import LabnotesConfig
|
|
2
|
+
from labnotes.tables.list_experiment_runs import list_experiment_runs
|
|
3
|
+
from labnotes.tables.open_tables import open_tables
|
|
4
|
+
from labnotes.tables.summarize_experiments import summarize_experiments
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def experiment_rows(
|
|
8
|
+
config: LabnotesConfig, name: str | None, action: str | None
|
|
9
|
+
) -> list[dict[str, object]] | None:
|
|
10
|
+
tables = open_tables(config)
|
|
11
|
+
rows = await summarize_experiments(tables, name)
|
|
12
|
+
if name is not None and not rows:
|
|
13
|
+
return None
|
|
14
|
+
match name, action:
|
|
15
|
+
case str(), "runs":
|
|
16
|
+
return await list_experiment_runs(tables, name)
|
|
17
|
+
return rows
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from dirsql import DirSQL
|
|
2
|
+
|
|
3
|
+
from labnotes.sql_quote import quote_string
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def list_experiment_runs(tables: DirSQL, name: str) -> list[dict[str, object]]:
|
|
7
|
+
return await tables.query(
|
|
8
|
+
"SELECT id, created, ended, exit_code, error, params, folder FROM runs"
|
|
9
|
+
f" WHERE experiment = {quote_string(name)} ORDER BY created, id"
|
|
10
|
+
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
|
|
3
|
+
from dirsql import DirSQL, Table
|
|
4
|
+
|
|
5
|
+
from labnotes.models import LabnotesConfig
|
|
6
|
+
from labnotes.tables.path_glob import path_glob
|
|
7
|
+
from labnotes.tables.read_experiment import read_experiment
|
|
8
|
+
from labnotes.tables.read_file import read_file
|
|
9
|
+
from labnotes.tables.read_run import read_run
|
|
10
|
+
|
|
11
|
+
# experiments is a view, so files and runs can't declare a foreign key to it.
|
|
12
|
+
EXPERIMENTS = """
|
|
13
|
+
CREATE TABLE experiment_configs (experiment TEXT PRIMARY KEY, config TEXT);
|
|
14
|
+
CREATE VIEW experiments AS
|
|
15
|
+
SELECT names.name, experiment_configs.config
|
|
16
|
+
FROM (
|
|
17
|
+
SELECT experiment AS name FROM experiment_configs
|
|
18
|
+
UNION SELECT experiment FROM files
|
|
19
|
+
UNION SELECT experiment FROM runs
|
|
20
|
+
) AS names
|
|
21
|
+
LEFT JOIN experiment_configs ON experiment_configs.experiment = names.name;
|
|
22
|
+
"""
|
|
23
|
+
FILES = """
|
|
24
|
+
CREATE TABLE files (
|
|
25
|
+
experiment TEXT, filename TEXT, path TEXT PRIMARY KEY, ctime TEXT, mtime TEXT
|
|
26
|
+
)
|
|
27
|
+
"""
|
|
28
|
+
RUNS = """
|
|
29
|
+
CREATE TABLE runs (
|
|
30
|
+
experiment TEXT, id TEXT, folder TEXT, created TEXT, ended TEXT,
|
|
31
|
+
exit_code INTEGER, error TEXT, stdout TEXT, stderr TEXT, params TEXT,
|
|
32
|
+
PRIMARY KEY (experiment, id)
|
|
33
|
+
)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def open_tables(config: LabnotesConfig) -> DirSQL:
|
|
38
|
+
root = config.root
|
|
39
|
+
return DirSQL(
|
|
40
|
+
str(root),
|
|
41
|
+
tables=[
|
|
42
|
+
Table(
|
|
43
|
+
name="files",
|
|
44
|
+
ddl=FILES,
|
|
45
|
+
glob=path_glob(config.files),
|
|
46
|
+
on_file=partial(read_file, root, config.files),
|
|
47
|
+
),
|
|
48
|
+
Table(
|
|
49
|
+
name="runs",
|
|
50
|
+
ddl=RUNS,
|
|
51
|
+
glob=path_glob(f"{config.runs}/run.json"),
|
|
52
|
+
on_file=partial(read_run, root, config.runs),
|
|
53
|
+
),
|
|
54
|
+
Table(
|
|
55
|
+
name="experiment_configs",
|
|
56
|
+
ddl=EXPERIMENTS,
|
|
57
|
+
glob=path_glob(f"{config.experiments}/experiment.toml"),
|
|
58
|
+
on_file=partial(read_experiment, root, config.experiments),
|
|
59
|
+
),
|
|
60
|
+
],
|
|
61
|
+
# Run folders are often gitignored (DVC, large outputs); scan them anyway.
|
|
62
|
+
no_ignore=True,
|
|
63
|
+
)
|