labnotes 0.0.14__tar.gz → 0.0.15__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.14 → labnotes-0.0.15}/.gitignore +1 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/PKG-INFO +37 -1
- {labnotes-0.0.14 → labnotes-0.0.15}/README.md +33 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/pyproject.toml +8 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/context.md +33 -0
- labnotes-0.0.15/src/labnotes/experiments.py +76 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/models.py +13 -0
- labnotes-0.0.15/src/labnotes/runner/describe_event.py +17 -0
- labnotes-0.0.15/src/labnotes/runner/execute_run.py +49 -0
- labnotes-0.0.15/src/labnotes/runner/lock_path.py +7 -0
- labnotes-0.0.15/src/labnotes/runner/new_run_id.py +11 -0
- labnotes-0.0.15/src/labnotes/runner/param_args.py +16 -0
- labnotes-0.0.15/src/labnotes/runner/param_grid.py +6 -0
- labnotes-0.0.15/src/labnotes/runner/plan_runs.py +9 -0
- labnotes-0.0.15/src/labnotes/runner/read_experiment_config.py +35 -0
- labnotes-0.0.15/src/labnotes/runner/run_experiment.py +66 -0
- labnotes-0.0.15/src/labnotes/runner/run_folder.py +5 -0
- labnotes-0.0.15/src/labnotes/runner/run_headless.py +15 -0
- labnotes-0.0.15/src/labnotes/runner/shell_command.py +9 -0
- labnotes-0.0.15/src/labnotes/runner/start_experiment.py +32 -0
- labnotes-0.0.15/src/labnotes/runner/write_run_json.py +8 -0
- labnotes-0.0.15/src/labnotes/tables/__init__.py +0 -0
- labnotes-0.0.15/src/labnotes/tui/__init__.py +0 -0
- labnotes-0.0.15/src/labnotes/tui/format_elapsed.py +8 -0
- labnotes-0.0.15/src/labnotes/tui/run_app.py +90 -0
- labnotes-0.0.15/src/labnotes/tui/run_tui.py +8 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/uv.lock +67 -0
- labnotes-0.0.14/src/labnotes/experiments.py +0 -33
- {labnotes-0.0.14 → labnotes-0.0.15}/CHANGELOG.md +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/MIGRATIONS.md +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/init.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/open_experiments.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/open_runs.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/read_design.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/read_note.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/run_columns.py +0 -0
- {labnotes-0.0.14/src/labnotes/tables → labnotes-0.0.15/src/labnotes/runner}/__init__.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/server.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/stop_idle_kernels.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/study_progress.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/summarize_design.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.14 → labnotes-0.0.15}/src/labnotes/templates/macros.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labnotes
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.15
|
|
4
4
|
Summary: Browse experiment designs, notes, and run data from a folder.
|
|
5
5
|
Project-URL: Repository, https://github.com/thekevinscott/labnotes
|
|
6
6
|
Author-email: Kevin Scott <me@thekevinscott.com>
|
|
@@ -8,6 +8,8 @@ License-Expression: MIT
|
|
|
8
8
|
Requires-Python: >=3.12
|
|
9
9
|
Requires-Dist: click>=8.5.0
|
|
10
10
|
Requires-Dist: dirsql>=0.4.110
|
|
11
|
+
Requires-Dist: filelock>=4.2.0
|
|
12
|
+
Requires-Dist: haikunator>=2.1.0
|
|
11
13
|
Requires-Dist: ipykernel>=7.4.0
|
|
12
14
|
Requires-Dist: jinja2>=3.1.6
|
|
13
15
|
Requires-Dist: jupyter-client>=8.10.0
|
|
@@ -15,6 +17,7 @@ Requires-Dist: markdown-it-py>=4.2.0
|
|
|
15
17
|
Requires-Dist: pyyaml>=6.0.3
|
|
16
18
|
Requires-Dist: rich>=14
|
|
17
19
|
Requires-Dist: starlette>=1.8.0
|
|
20
|
+
Requires-Dist: textual>=8.2.8
|
|
18
21
|
Requires-Dist: uvicorn>=0.54.0
|
|
19
22
|
Description-Content-Type: text/markdown
|
|
20
23
|
|
|
@@ -66,6 +69,39 @@ labnotes experiments foo runs # foo's runs, oldest first
|
|
|
66
69
|
|
|
67
70
|
Each takes `--format table` (the default) or `--format json`.
|
|
68
71
|
|
|
72
|
+
`labnotes experiments foo run` runs `foo` from its `experiment.toml`, in the
|
|
73
|
+
experiments folder:
|
|
74
|
+
|
|
75
|
+
```toml
|
|
76
|
+
command = "uv run scripts/study.py --output {run_dir}"
|
|
77
|
+
repeats = 5
|
|
78
|
+
|
|
79
|
+
[params]
|
|
80
|
+
model = ["opus", "sonnet"]
|
|
81
|
+
effort = ["low", "high"]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Each combination of `[params]` runs `repeats` times, round-robin. Only runs
|
|
85
|
+
that exit 0 count, so running again fills in what failed or never ran.
|
|
86
|
+
Each run gets a new run folder. `command` runs in a shell from the root, with
|
|
87
|
+
`{run_dir}` replaced by the run folder's absolute path. Pass it to the
|
|
88
|
+
script so it writes its outputs there.
|
|
89
|
+
Each param is appended as `--name value`, with the name as written (use
|
|
90
|
+
`source-language` for a `--source-language` flag), booleans as 0 or 1, and lists
|
|
91
|
+
and tables as JSON. labnotes writes `run.json` (params, created, ended, exit
|
|
92
|
+
code), `stdout.log` and `stderr.log` to the run folder.
|
|
93
|
+
|
|
94
|
+
In a terminal, `run` shows a live table of runs (status and time) and stays
|
|
95
|
+
open when they finish; `q` quits. Elsewhere, or with `--no-tui`, it prints a
|
|
96
|
+
line per event instead.
|
|
97
|
+
|
|
98
|
+
- `--concurrency N` runs N at a time (default 1).
|
|
99
|
+
- `-e PATH` reads another file instead of `experiment.toml`.
|
|
100
|
+
- Ctrl-C stops the running scripts and records their end. A second Ctrl-C
|
|
101
|
+
kills them.
|
|
102
|
+
- A second `run` of the same experiment fails while the first holds the lock.
|
|
103
|
+
- It exits 1 if any run failed.
|
|
104
|
+
|
|
69
105
|
`labnotes server` doesn't read `.labnotes.toml` yet. Give it the two folders
|
|
70
106
|
it uses with `--experiments-folder` and `--data-folder`:
|
|
71
107
|
|
|
@@ -46,6 +46,39 @@ labnotes experiments foo runs # foo's runs, oldest first
|
|
|
46
46
|
|
|
47
47
|
Each takes `--format table` (the default) or `--format json`.
|
|
48
48
|
|
|
49
|
+
`labnotes experiments foo run` runs `foo` from its `experiment.toml`, in the
|
|
50
|
+
experiments folder:
|
|
51
|
+
|
|
52
|
+
```toml
|
|
53
|
+
command = "uv run scripts/study.py --output {run_dir}"
|
|
54
|
+
repeats = 5
|
|
55
|
+
|
|
56
|
+
[params]
|
|
57
|
+
model = ["opus", "sonnet"]
|
|
58
|
+
effort = ["low", "high"]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Each combination of `[params]` runs `repeats` times, round-robin. Only runs
|
|
62
|
+
that exit 0 count, so running again fills in what failed or never ran.
|
|
63
|
+
Each run gets a new run folder. `command` runs in a shell from the root, with
|
|
64
|
+
`{run_dir}` replaced by the run folder's absolute path. Pass it to the
|
|
65
|
+
script so it writes its outputs there.
|
|
66
|
+
Each param is appended as `--name value`, with the name as written (use
|
|
67
|
+
`source-language` for a `--source-language` flag), booleans as 0 or 1, and lists
|
|
68
|
+
and tables as JSON. labnotes writes `run.json` (params, created, ended, exit
|
|
69
|
+
code), `stdout.log` and `stderr.log` to the run folder.
|
|
70
|
+
|
|
71
|
+
In a terminal, `run` shows a live table of runs (status and time) and stays
|
|
72
|
+
open when they finish; `q` quits. Elsewhere, or with `--no-tui`, it prints a
|
|
73
|
+
line per event instead.
|
|
74
|
+
|
|
75
|
+
- `--concurrency N` runs N at a time (default 1).
|
|
76
|
+
- `-e PATH` reads another file instead of `experiment.toml`.
|
|
77
|
+
- Ctrl-C stops the running scripts and records their end. A second Ctrl-C
|
|
78
|
+
kills them.
|
|
79
|
+
- A second `run` of the same experiment fails while the first holds the lock.
|
|
80
|
+
- It exits 1 if any run failed.
|
|
81
|
+
|
|
49
82
|
`labnotes server` doesn't read `.labnotes.toml` yet. Give it the two folders
|
|
50
83
|
it uses with `--experiments-folder` and `--data-folder`:
|
|
51
84
|
|
|
@@ -13,6 +13,8 @@ authors = [{ name = "Kevin Scott", email = "me@thekevinscott.com" }]
|
|
|
13
13
|
dependencies = [
|
|
14
14
|
"click>=8.5.0",
|
|
15
15
|
"dirsql>=0.4.110",
|
|
16
|
+
"filelock>=4.2.0",
|
|
17
|
+
"haikunator>=2.1.0",
|
|
16
18
|
"ipykernel>=7.4.0",
|
|
17
19
|
"jinja2>=3.1.6",
|
|
18
20
|
"jupyter-client>=8.10.0",
|
|
@@ -20,6 +22,7 @@ dependencies = [
|
|
|
20
22
|
"pyyaml>=6.0.3",
|
|
21
23
|
"rich>=14",
|
|
22
24
|
"starlette>=1.8.0",
|
|
25
|
+
"textual>=8.2.8",
|
|
23
26
|
"uvicorn>=0.54.0",
|
|
24
27
|
]
|
|
25
28
|
|
|
@@ -62,3 +65,8 @@ select = ["E4", "E7", "E9", "F", "TID252"]
|
|
|
62
65
|
|
|
63
66
|
[tool.ruff.lint.flake8-tidy-imports]
|
|
64
67
|
ban-relative-imports = "all"
|
|
68
|
+
|
|
69
|
+
# haikunator ships no type hints.
|
|
70
|
+
[[tool.mypy.overrides]]
|
|
71
|
+
module = ["haikunator"]
|
|
72
|
+
ignore_missing_imports = true
|
|
@@ -36,6 +36,39 @@ labnotes experiments foo runs # foo's runs, oldest first
|
|
|
36
36
|
|
|
37
37
|
Each takes `--format table` (the default) or `--format json`; use `--format json` when you parse the output.
|
|
38
38
|
|
|
39
|
+
`labnotes experiments foo run` runs `foo` from its `experiment.toml`, in the
|
|
40
|
+
experiments folder:
|
|
41
|
+
|
|
42
|
+
```toml
|
|
43
|
+
command = "uv run scripts/study.py --output {run_dir}"
|
|
44
|
+
repeats = 5
|
|
45
|
+
|
|
46
|
+
[params]
|
|
47
|
+
model = ["opus", "sonnet"]
|
|
48
|
+
effort = ["low", "high"]
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Each combination of `[params]` runs `repeats` times, round-robin. Only runs
|
|
52
|
+
that exit 0 count, so running again fills in what failed or never ran.
|
|
53
|
+
Each run gets a new run folder. `command` runs in a shell from the root, with
|
|
54
|
+
`{run_dir}` replaced by the run folder's absolute path. Pass it to the
|
|
55
|
+
script so it writes its outputs there.
|
|
56
|
+
Each param is appended as `--name value`, with the name as written (use
|
|
57
|
+
`source-language` for a `--source-language` flag), booleans as 0 or 1, and lists
|
|
58
|
+
and tables as JSON. labnotes writes `run.json` (params, created, ended, exit
|
|
59
|
+
code), `stdout.log` and `stderr.log` to the run folder.
|
|
60
|
+
|
|
61
|
+
In a terminal, `run` shows a live table of runs (status and time) and stays
|
|
62
|
+
open when they finish; `q` quits. Elsewhere, or with `--no-tui`, it prints a
|
|
63
|
+
line per event instead.
|
|
64
|
+
|
|
65
|
+
- `--concurrency N` runs N at a time (default 1).
|
|
66
|
+
- `-e PATH` reads another file instead of `experiment.toml`.
|
|
67
|
+
- Ctrl-C stops the running scripts and records their end. A second Ctrl-C
|
|
68
|
+
kills them.
|
|
69
|
+
- A second `run` of the same experiment fails while the first holds the lock.
|
|
70
|
+
- It exits 1 if any run failed.
|
|
71
|
+
|
|
39
72
|
`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
73
|
|
|
41
74
|
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:
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
from functools import partial
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from labnotes.load_config import load_config
|
|
9
|
+
from labnotes.print_rows import print_rows
|
|
10
|
+
from labnotes.read_config import CONFIG_FILE
|
|
11
|
+
from labnotes.runner.run_headless import run_headless
|
|
12
|
+
from labnotes.runner.start_experiment import start_experiment
|
|
13
|
+
from labnotes.tables.experiment_rows import experiment_rows
|
|
14
|
+
from labnotes.tui.run_tui import run_tui
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command(help="List experiments, show one, list its runs, or run it.")
|
|
18
|
+
@click.argument("name", required=False)
|
|
19
|
+
@click.argument("action", required=False, type=click.Choice(["runs", "run"]))
|
|
20
|
+
@click.option(
|
|
21
|
+
"--format",
|
|
22
|
+
"output_format",
|
|
23
|
+
type=click.Choice(["table", "json"]),
|
|
24
|
+
default="table",
|
|
25
|
+
show_default=True,
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--concurrency",
|
|
29
|
+
type=click.IntRange(min=1),
|
|
30
|
+
default=1,
|
|
31
|
+
show_default=True,
|
|
32
|
+
help="With run: how many runs at once.",
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"-e",
|
|
36
|
+
"--experiment-config",
|
|
37
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
38
|
+
help="With run: read this file instead of the experiment's experiment.toml.",
|
|
39
|
+
)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--no-tui",
|
|
42
|
+
is_flag=True,
|
|
43
|
+
help="With run: print a line per event instead of the live view.",
|
|
44
|
+
)
|
|
45
|
+
@click.pass_context
|
|
46
|
+
def experiments(
|
|
47
|
+
ctx: click.Context,
|
|
48
|
+
name: str | None,
|
|
49
|
+
action: str | None,
|
|
50
|
+
output_format: str,
|
|
51
|
+
concurrency: int,
|
|
52
|
+
experiment_config: Path | None,
|
|
53
|
+
no_tui: bool,
|
|
54
|
+
) -> None:
|
|
55
|
+
config = load_config(ctx)
|
|
56
|
+
if config is None:
|
|
57
|
+
raise click.ClickException(
|
|
58
|
+
f"No {CONFIG_FILE} here. Run labnotes init, or pass -c PATH."
|
|
59
|
+
)
|
|
60
|
+
match name, action:
|
|
61
|
+
case str(), "run":
|
|
62
|
+
live = sys.stdout.isatty() and sys.stderr.isatty() and not no_tui
|
|
63
|
+
drive = partial(run_tui, title=name) if live else run_headless
|
|
64
|
+
try:
|
|
65
|
+
results = start_experiment(
|
|
66
|
+
config, name, experiment_config, concurrency, drive
|
|
67
|
+
)
|
|
68
|
+
except ValueError as error:
|
|
69
|
+
raise click.ClickException(str(error)) from error
|
|
70
|
+
failed = sum(result["exit_code"] != 0 for result in results)
|
|
71
|
+
click.echo(f"{len(results) - failed} passed, {failed} failed.")
|
|
72
|
+
ctx.exit(1 if failed else 0)
|
|
73
|
+
rows = asyncio.run(experiment_rows(config, name, action))
|
|
74
|
+
if rows is None:
|
|
75
|
+
raise click.ClickException(f"No experiment named {name}.")
|
|
76
|
+
print_rows(rows, output_format)
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
from collections.abc import Callable, Coroutine
|
|
1
2
|
from dataclasses import dataclass, field
|
|
2
3
|
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
Report = Callable[[dict[str, object]], None]
|
|
7
|
+
Listen = Callable[[Callable[[], None]], None]
|
|
8
|
+
Start = Callable[[Report, Listen], Coroutine[Any, Any, list[dict[str, object]]]]
|
|
3
9
|
|
|
4
10
|
|
|
5
11
|
@dataclass(frozen=True)
|
|
@@ -17,3 +23,10 @@ class LabnotesConfig:
|
|
|
17
23
|
experiments: str
|
|
18
24
|
files: str
|
|
19
25
|
runs: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ExperimentConfig:
|
|
30
|
+
command: str
|
|
31
|
+
repeats: int
|
|
32
|
+
params: dict[str, list[object]]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from labnotes.runner.param_args import param_args
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def describe_event(event: dict[str, object]) -> str:
|
|
5
|
+
match event["event"]:
|
|
6
|
+
case "planned":
|
|
7
|
+
return f"{event['runs']} runs to go, {event['concurrency']} at a time."
|
|
8
|
+
case "started":
|
|
9
|
+
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
10
|
+
return f"{event['run']} started {args}".rstrip()
|
|
11
|
+
case "ended":
|
|
12
|
+
outcome = event["error"] or f"exited {event['exit_code']}"
|
|
13
|
+
return f"{event['run']} {outcome}"
|
|
14
|
+
case "stopping":
|
|
15
|
+
return "Stopping runs. Ctrl-C again to kill them."
|
|
16
|
+
case _:
|
|
17
|
+
return "Killing runs."
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from labnotes.runner.shell_command import shell_command
|
|
6
|
+
from labnotes.runner.write_run_json import write_run_json
|
|
7
|
+
from labnotes.tables.iso_time import iso_time
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def execute_run(
|
|
11
|
+
root: Path,
|
|
12
|
+
folder: Path,
|
|
13
|
+
command: str,
|
|
14
|
+
params: dict[str, object],
|
|
15
|
+
running: set[asyncio.subprocess.Process],
|
|
16
|
+
) -> dict[str, object]:
|
|
17
|
+
run: dict[str, object] = {
|
|
18
|
+
"params": params,
|
|
19
|
+
"created": iso_time(time.time()),
|
|
20
|
+
"ended": None,
|
|
21
|
+
"exit_code": None,
|
|
22
|
+
"error": None,
|
|
23
|
+
}
|
|
24
|
+
write_run_json(folder, run)
|
|
25
|
+
with (
|
|
26
|
+
(folder / "stdout.log").open("wb") as stdout,
|
|
27
|
+
(folder / "stderr.log").open("wb") as stderr,
|
|
28
|
+
):
|
|
29
|
+
try:
|
|
30
|
+
process = await asyncio.create_subprocess_shell(
|
|
31
|
+
shell_command(command, folder, params),
|
|
32
|
+
cwd=root,
|
|
33
|
+
stdout=stdout,
|
|
34
|
+
stderr=stderr,
|
|
35
|
+
# Own process group, so Ctrl-C reaches labnotes first and the
|
|
36
|
+
# runner decides how each run stops.
|
|
37
|
+
start_new_session=True,
|
|
38
|
+
)
|
|
39
|
+
except OSError as error:
|
|
40
|
+
run["error"] = str(error)
|
|
41
|
+
else:
|
|
42
|
+
running.add(process)
|
|
43
|
+
try:
|
|
44
|
+
run["exit_code"] = await process.wait()
|
|
45
|
+
finally:
|
|
46
|
+
running.discard(process)
|
|
47
|
+
run["ended"] = iso_time(time.time())
|
|
48
|
+
write_run_json(folder, run)
|
|
49
|
+
return run
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from haikunator import Haikunator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def new_run_id(taken: Callable[[str], bool]) -> str:
|
|
7
|
+
name = Haikunator().haikunate(token_length=0)
|
|
8
|
+
candidate, suffix = name, 2
|
|
9
|
+
while taken(candidate):
|
|
10
|
+
candidate, suffix = f"{name}-{suffix}", suffix + 1
|
|
11
|
+
return candidate
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def param_args(params: dict[str, object]) -> list[str]:
|
|
5
|
+
args: list[str] = []
|
|
6
|
+
for name, value in params.items():
|
|
7
|
+
if isinstance(value, bool):
|
|
8
|
+
text = "1" if value else "0"
|
|
9
|
+
elif isinstance(value, str):
|
|
10
|
+
text = value
|
|
11
|
+
elif isinstance(value, list | dict):
|
|
12
|
+
text = json.dumps(value)
|
|
13
|
+
else:
|
|
14
|
+
text = str(value)
|
|
15
|
+
args += [f"--{name}", text]
|
|
16
|
+
return args
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tomllib
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from labnotes.models import ExperimentConfig
|
|
6
|
+
|
|
7
|
+
SETTINGS = {"command", "repeats", "params"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def read_experiment_config(path: Path) -> ExperimentConfig:
|
|
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() - SETTINGS)
|
|
17
|
+
if unknown:
|
|
18
|
+
raise ValueError(f"{path}: unknown keys {', '.join(unknown)}")
|
|
19
|
+
command = values.get("command")
|
|
20
|
+
if not isinstance(command, str) or not command.strip():
|
|
21
|
+
raise ValueError(f"{path}: command must be a string")
|
|
22
|
+
repeats = values.get("repeats", 1)
|
|
23
|
+
if isinstance(repeats, bool) or not isinstance(repeats, int) or repeats < 1:
|
|
24
|
+
raise ValueError(f"{path}: repeats must be a positive integer")
|
|
25
|
+
params = values.get("params", {})
|
|
26
|
+
if not isinstance(params, dict):
|
|
27
|
+
raise ValueError(f"{path}: params must be a table, like [params]")
|
|
28
|
+
for name, options in params.items():
|
|
29
|
+
if not isinstance(options, list) or not options:
|
|
30
|
+
raise ValueError(f"{path}: params.{name} must be a non-empty list")
|
|
31
|
+
try:
|
|
32
|
+
json.dumps(params)
|
|
33
|
+
except TypeError as error:
|
|
34
|
+
raise ValueError(f"{path}: params can't hold dates or times") from error
|
|
35
|
+
return ExperimentConfig(command, repeats, params)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
|
|
7
|
+
from labnotes.models import ExperimentConfig, LabnotesConfig, Listen, Report
|
|
8
|
+
from labnotes.runner.execute_run import execute_run
|
|
9
|
+
from labnotes.runner.new_run_id import new_run_id
|
|
10
|
+
from labnotes.runner.param_grid import param_grid
|
|
11
|
+
from labnotes.runner.plan_runs import plan_runs
|
|
12
|
+
from labnotes.runner.run_folder import run_folder
|
|
13
|
+
from labnotes.tables.list_experiment_runs import list_experiment_runs
|
|
14
|
+
from labnotes.tables.open_tables import open_tables
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def run_experiment(
|
|
18
|
+
config: LabnotesConfig,
|
|
19
|
+
name: str,
|
|
20
|
+
experiment: ExperimentConfig,
|
|
21
|
+
concurrency: int,
|
|
22
|
+
report: Report,
|
|
23
|
+
listen: Listen,
|
|
24
|
+
) -> list[dict[str, object]]:
|
|
25
|
+
runs = await list_experiment_runs(open_tables(config), name)
|
|
26
|
+
finished = [json.loads(str(run["params"])) for run in runs if run["exit_code"] == 0]
|
|
27
|
+
plan = plan_runs(param_grid(experiment.params), experiment.repeats, finished)
|
|
28
|
+
report({"event": "planned", "runs": len(plan), "concurrency": concurrency})
|
|
29
|
+
running: set[asyncio.subprocess.Process] = set()
|
|
30
|
+
results: list[dict[str, object]] = []
|
|
31
|
+
stopping = False
|
|
32
|
+
|
|
33
|
+
def signal_runs(signal_number: int) -> None:
|
|
34
|
+
for process in running:
|
|
35
|
+
with contextlib.suppress(ProcessLookupError):
|
|
36
|
+
os.killpg(process.pid, signal_number)
|
|
37
|
+
|
|
38
|
+
def interrupt() -> None:
|
|
39
|
+
nonlocal stopping
|
|
40
|
+
plan.clear()
|
|
41
|
+
if stopping:
|
|
42
|
+
report({"event": "killing"})
|
|
43
|
+
signal_runs(signal.SIGKILL)
|
|
44
|
+
else:
|
|
45
|
+
stopping = True
|
|
46
|
+
report({"event": "stopping"})
|
|
47
|
+
signal_runs(signal.SIGTERM)
|
|
48
|
+
|
|
49
|
+
async def work() -> None:
|
|
50
|
+
while plan:
|
|
51
|
+
params = plan.pop(0)
|
|
52
|
+
run = new_run_id(
|
|
53
|
+
lambda id_: run_folder(config.root, config.runs, name, id_).exists()
|
|
54
|
+
)
|
|
55
|
+
folder = run_folder(config.root, config.runs, name, run)
|
|
56
|
+
folder.mkdir(parents=True)
|
|
57
|
+
report({"event": "started", "run": run, "params": params})
|
|
58
|
+
result = await execute_run(
|
|
59
|
+
config.root, folder, experiment.command, params, running
|
|
60
|
+
)
|
|
61
|
+
report({"event": "ended", "run": run, **result})
|
|
62
|
+
results.append(result)
|
|
63
|
+
|
|
64
|
+
listen(interrupt)
|
|
65
|
+
await asyncio.gather(*(work() for _ in range(concurrency)))
|
|
66
|
+
return results
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import signal
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from labnotes.models import Start
|
|
8
|
+
from labnotes.runner.describe_event import describe_event
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_headless(start: Start) -> list[dict[str, object]]:
|
|
12
|
+
def listen(interrupt: Callable[[], None]) -> None:
|
|
13
|
+
asyncio.get_running_loop().add_signal_handler(signal.SIGINT, interrupt)
|
|
14
|
+
|
|
15
|
+
return asyncio.run(start(lambda event: click.echo(describe_event(event)), listen))
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from labnotes.runner.param_args import param_args
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def shell_command(command: str, folder: Path, params: dict[str, object]) -> str:
|
|
8
|
+
filled = command.replace("{run_dir}", shlex.quote(str(folder)))
|
|
9
|
+
return f"{filled} {shlex.join(param_args(params))}".rstrip()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from functools import partial
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from filelock import FileLock, Timeout
|
|
6
|
+
|
|
7
|
+
from labnotes.models import LabnotesConfig, Start
|
|
8
|
+
from labnotes.runner.lock_path import lock_path
|
|
9
|
+
from labnotes.runner.read_experiment_config import read_experiment_config
|
|
10
|
+
from labnotes.runner.run_experiment import run_experiment
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def start_experiment(
|
|
14
|
+
config: LabnotesConfig,
|
|
15
|
+
name: str,
|
|
16
|
+
experiment_config: Path | None,
|
|
17
|
+
concurrency: int,
|
|
18
|
+
drive: Callable[[Start], list[dict[str, object]]],
|
|
19
|
+
) -> list[dict[str, object]]:
|
|
20
|
+
if experiment_config is None:
|
|
21
|
+
folder = config.experiments.replace("{experiment}", name)
|
|
22
|
+
experiment_config = config.root / folder / "experiment.toml"
|
|
23
|
+
if not experiment_config.is_file():
|
|
24
|
+
raise ValueError(f"No experiment.toml for {name} at {experiment_config}.")
|
|
25
|
+
experiment = read_experiment_config(experiment_config)
|
|
26
|
+
lock = lock_path(config.root, config.runs, name)
|
|
27
|
+
lock.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
try:
|
|
29
|
+
with FileLock(lock, blocking=False):
|
|
30
|
+
return drive(partial(run_experiment, config, name, experiment, concurrency))
|
|
31
|
+
except Timeout as error:
|
|
32
|
+
raise ValueError(f"{name} is already running ({lock} is held).") from error
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
|
|
4
|
+
from textual.app import App, ComposeResult
|
|
5
|
+
from textual.binding import Binding
|
|
6
|
+
from textual.widgets import DataTable, Footer, Header, Static
|
|
7
|
+
|
|
8
|
+
from labnotes.models import Start
|
|
9
|
+
from labnotes.runner.describe_event import describe_event
|
|
10
|
+
from labnotes.runner.param_args import param_args
|
|
11
|
+
from labnotes.tui.format_elapsed import format_elapsed
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RunApp(App[None]):
|
|
15
|
+
BINDINGS = [
|
|
16
|
+
Binding("ctrl+c", "interrupt", "Stop runs"),
|
|
17
|
+
Binding("q", "quit", "Quit"),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
def __init__(self, start: Start, title: str) -> None:
|
|
21
|
+
super().__init__()
|
|
22
|
+
self.start = start
|
|
23
|
+
self.title = title
|
|
24
|
+
self.results: list[dict[str, object]] = []
|
|
25
|
+
self.interrupt: Callable[[], None] = lambda: None
|
|
26
|
+
self.done = False
|
|
27
|
+
self.started: dict[str, float] = {}
|
|
28
|
+
|
|
29
|
+
def compose(self) -> ComposeResult:
|
|
30
|
+
yield Header()
|
|
31
|
+
yield DataTable(id="runs", cursor_type="none")
|
|
32
|
+
yield Static(id="status")
|
|
33
|
+
yield Footer()
|
|
34
|
+
|
|
35
|
+
def on_mount(self) -> None:
|
|
36
|
+
table = self.query_one(DataTable)
|
|
37
|
+
for column in ("run", "params", "status", "time"):
|
|
38
|
+
table.add_column(column, key=column)
|
|
39
|
+
|
|
40
|
+
def on_ready(self) -> None:
|
|
41
|
+
self.set_interval(1, self.tick)
|
|
42
|
+
self.run_worker(self.drive())
|
|
43
|
+
|
|
44
|
+
async def drive(self) -> None:
|
|
45
|
+
self.results = await self.start(self.show_event, self.listen)
|
|
46
|
+
self.done = True
|
|
47
|
+
failed = sum(result["exit_code"] != 0 for result in self.results)
|
|
48
|
+
passed = len(self.results) - failed
|
|
49
|
+
self.show(f"{passed} passed, {failed} failed. q to quit.")
|
|
50
|
+
|
|
51
|
+
def listen(self, interrupt: Callable[[], None]) -> None:
|
|
52
|
+
self.interrupt = interrupt
|
|
53
|
+
|
|
54
|
+
def show_event(self, event: dict[str, object]) -> None:
|
|
55
|
+
table = self.query_one(DataTable)
|
|
56
|
+
run = str(event.get("run"))
|
|
57
|
+
match event["event"]:
|
|
58
|
+
case "started":
|
|
59
|
+
self.started[run] = time.monotonic()
|
|
60
|
+
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
61
|
+
table.add_row(run, args, "running", "0s", key=run)
|
|
62
|
+
case "ended":
|
|
63
|
+
outcome = event["error"] or f"exited {event['exit_code']}"
|
|
64
|
+
table.update_cell(run, "status", outcome, update_width=True)
|
|
65
|
+
self.tick()
|
|
66
|
+
del self.started[run]
|
|
67
|
+
case _:
|
|
68
|
+
self.show(describe_event(event))
|
|
69
|
+
|
|
70
|
+
def tick(self) -> None:
|
|
71
|
+
table = self.query_one(DataTable)
|
|
72
|
+
for run, began in self.started.items():
|
|
73
|
+
table.update_cell(
|
|
74
|
+
run, "time", format_elapsed(time.monotonic() - began), update_width=True
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
def show(self, text: str) -> None:
|
|
78
|
+
self.query_one("#status", Static).update(text)
|
|
79
|
+
|
|
80
|
+
def action_interrupt(self) -> None:
|
|
81
|
+
if self.done:
|
|
82
|
+
self.exit()
|
|
83
|
+
else:
|
|
84
|
+
self.interrupt()
|
|
85
|
+
|
|
86
|
+
async def action_quit(self) -> None:
|
|
87
|
+
if self.done:
|
|
88
|
+
self.exit()
|
|
89
|
+
else:
|
|
90
|
+
self.show("Runs are still going. Ctrl-C stops them.")
|
|
@@ -273,6 +273,15 @@ wheels = [
|
|
|
273
273
|
{ url = "https://files.pythonhosted.org/packages/4d/dd/8bc67e7d5ffc1d88aa5af511189dfb76dc4e1e5b808fab186146ef2663e5/executing-2.3.0-py3-none-any.whl", hash = "sha256:736e859c9f8701f11fcf516856f26f562e04776387824b43a35a1dfe21c84122", size = 29393, upload-time = "2026-10-10T14:06:02.777Z" },
|
|
274
274
|
]
|
|
275
275
|
|
|
276
|
+
[[package]]
|
|
277
|
+
name = "filelock"
|
|
278
|
+
version = "4.2.0"
|
|
279
|
+
source = { registry = "https://pypi.org/simple" }
|
|
280
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f4/a9/1af41b37c3279712b22cdc63aac78a52432202b6fe1f9666a2a3d2831fb4/filelock-4.2.0.tar.gz", hash = "sha256:7a60906c75227cf04d0c273afadc8219400f11aeb13cc69591d4f6cdc6c8036e", size = 569667, upload-time = "2026-10-14T20:57:13.11Z" }
|
|
281
|
+
wheels = [
|
|
282
|
+
{ url = "https://files.pythonhosted.org/packages/8e/a3/9bc26acff301fe1aaea1cc3d82a1d57e0a34df3e1cadbfa91ac2dbcdde5c/filelock-4.2.0-py3-none-any.whl", hash = "sha256:2ff5690882e8cdb00ef31fb3d01a3094c29f30985426c59495afb1733f3b7238", size = 135032, upload-time = "2026-10-14T20:57:11.349Z" },
|
|
283
|
+
]
|
|
284
|
+
|
|
276
285
|
[[package]]
|
|
277
286
|
name = "h11"
|
|
278
287
|
version = "0.16.0"
|
|
@@ -282,6 +291,15 @@ wheels = [
|
|
|
282
291
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
|
283
292
|
]
|
|
284
293
|
|
|
294
|
+
[[package]]
|
|
295
|
+
name = "haikunator"
|
|
296
|
+
version = "2.1.0"
|
|
297
|
+
source = { registry = "https://pypi.org/simple" }
|
|
298
|
+
sdist = { url = "https://files.pythonhosted.org/packages/af/58/6a000ee0ec34cac5c78669359a8b1db969f1f511454a140ad3d193714ba2/haikunator-2.1.0.zip", hash = "sha256:91ee3949a3a613cac037ddde0b16b17062e248376b11491436e49d5ddc75ff9b", size = 4933, upload-time = "2016-09-20T17:36:00.843Z" }
|
|
299
|
+
wheels = [
|
|
300
|
+
{ url = "https://files.pythonhosted.org/packages/43/fa/130968f1a1bb1461c287b9ff35c630460801783243acda2cbf3a4c5964a5/haikunator-2.1.0-py2.py3-none-any.whl", hash = "sha256:66f68b15345b279f78a5fffd4ab56cfb19a9dbb1f41b7f442472efd4cb83458e", size = 4595, upload-time = "2016-09-20T17:35:58.142Z" },
|
|
301
|
+
]
|
|
302
|
+
|
|
285
303
|
[[package]]
|
|
286
304
|
name = "httpcore2"
|
|
287
305
|
version = "2.13.1"
|
|
@@ -455,6 +473,8 @@ source = { editable = "." }
|
|
|
455
473
|
dependencies = [
|
|
456
474
|
{ name = "click" },
|
|
457
475
|
{ name = "dirsql" },
|
|
476
|
+
{ name = "filelock" },
|
|
477
|
+
{ name = "haikunator" },
|
|
458
478
|
{ name = "ipykernel" },
|
|
459
479
|
{ name = "jinja2" },
|
|
460
480
|
{ name = "jupyter-client" },
|
|
@@ -462,6 +482,7 @@ dependencies = [
|
|
|
462
482
|
{ name = "pyyaml" },
|
|
463
483
|
{ name = "rich" },
|
|
464
484
|
{ name = "starlette" },
|
|
485
|
+
{ name = "textual" },
|
|
465
486
|
{ name = "uvicorn" },
|
|
466
487
|
]
|
|
467
488
|
|
|
@@ -478,6 +499,8 @@ dev = [
|
|
|
478
499
|
requires-dist = [
|
|
479
500
|
{ name = "click", specifier = ">=8.5.0" },
|
|
480
501
|
{ name = "dirsql", specifier = ">=0.4.110" },
|
|
502
|
+
{ name = "filelock", specifier = ">=4.2.0" },
|
|
503
|
+
{ name = "haikunator", specifier = ">=2.1.0" },
|
|
481
504
|
{ name = "ipykernel", specifier = ">=7.4.0" },
|
|
482
505
|
{ name = "jinja2", specifier = ">=3.1.6" },
|
|
483
506
|
{ name = "jupyter-client", specifier = ">=8.10.0" },
|
|
@@ -485,6 +508,7 @@ requires-dist = [
|
|
|
485
508
|
{ name = "pyyaml", specifier = ">=6.0.3" },
|
|
486
509
|
{ name = "rich", specifier = ">=14" },
|
|
487
510
|
{ name = "starlette", specifier = ">=1.8.0" },
|
|
511
|
+
{ name = "textual", specifier = ">=8.2.8" },
|
|
488
512
|
{ name = "uvicorn", specifier = ">=0.54.0" },
|
|
489
513
|
]
|
|
490
514
|
|
|
@@ -598,6 +622,15 @@ wheels = [
|
|
|
598
622
|
{ url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" },
|
|
599
623
|
]
|
|
600
624
|
|
|
625
|
+
[[package]]
|
|
626
|
+
name = "linkify-it-py"
|
|
627
|
+
version = "2.2.0"
|
|
628
|
+
source = { registry = "https://pypi.org/simple" }
|
|
629
|
+
sdist = { url = "https://files.pythonhosted.org/packages/45/98/7a1a5f31fd5c7ba93e963b168e244b8e3dd705b3d2a718e3c3307583bf57/linkify_it_py-2.2.0.tar.gz", hash = "sha256:907acd2d17ac1fbb9ddb62c8957ccbd6158cac602231a15c3b0cd1e215f03cee", size = 32939, upload-time = "2026-08-29T07:07:08.305Z" }
|
|
630
|
+
wheels = [
|
|
631
|
+
{ url = "https://files.pythonhosted.org/packages/13/d4/1152d1c7ab42d8b908be64fd200ddc870dc9d4925e951198702084aa1a7d/linkify_it_py-2.2.0-py3-none-any.whl", hash = "sha256:3adc40eb5af300b2605fcfdb968c24e1d780a90f1f2221af7c15e5111e94d443", size = 21971, upload-time = "2026-08-29T07:07:07.164Z" },
|
|
632
|
+
]
|
|
633
|
+
|
|
601
634
|
[[package]]
|
|
602
635
|
name = "markdown-it-py"
|
|
603
636
|
version = "4.2.0"
|
|
@@ -610,6 +643,11 @@ wheels = [
|
|
|
610
643
|
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
|
611
644
|
]
|
|
612
645
|
|
|
646
|
+
[package.optional-dependencies]
|
|
647
|
+
linkify = [
|
|
648
|
+
{ name = "linkify-it-py" },
|
|
649
|
+
]
|
|
650
|
+
|
|
613
651
|
[[package]]
|
|
614
652
|
name = "markupsafe"
|
|
615
653
|
version = "3.0.4"
|
|
@@ -735,6 +773,18 @@ wheels = [
|
|
|
735
773
|
{ url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" },
|
|
736
774
|
]
|
|
737
775
|
|
|
776
|
+
[[package]]
|
|
777
|
+
name = "mdit-py-plugins"
|
|
778
|
+
version = "0.6.1"
|
|
779
|
+
source = { registry = "https://pypi.org/simple" }
|
|
780
|
+
dependencies = [
|
|
781
|
+
{ name = "markdown-it-py" },
|
|
782
|
+
]
|
|
783
|
+
sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" }
|
|
784
|
+
wheels = [
|
|
785
|
+
{ url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" },
|
|
786
|
+
]
|
|
787
|
+
|
|
738
788
|
[[package]]
|
|
739
789
|
name = "mdurl"
|
|
740
790
|
version = "0.1.2"
|
|
@@ -1145,6 +1195,23 @@ wheels = [
|
|
|
1145
1195
|
{ url = "https://files.pythonhosted.org/packages/c1/b0/5742e4ac7af5eb58ec3470a537a49d7aa507e5539413e504b3a65ef50ba8/starlette-1.8.0-py3-none-any.whl", hash = "sha256:dfdd6b29c26483288088d990eee59631dedadd66ce20d203402a7ca8e3c4656f", size = 79612, upload-time = "2026-10-13T07:54:38.019Z" },
|
|
1146
1196
|
]
|
|
1147
1197
|
|
|
1198
|
+
[[package]]
|
|
1199
|
+
name = "textual"
|
|
1200
|
+
version = "8.2.8"
|
|
1201
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1202
|
+
dependencies = [
|
|
1203
|
+
{ name = "markdown-it-py", extra = ["linkify"] },
|
|
1204
|
+
{ name = "mdit-py-plugins" },
|
|
1205
|
+
{ name = "platformdirs" },
|
|
1206
|
+
{ name = "pygments" },
|
|
1207
|
+
{ name = "rich" },
|
|
1208
|
+
{ name = "typing-extensions" },
|
|
1209
|
+
]
|
|
1210
|
+
sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" }
|
|
1211
|
+
wheels = [
|
|
1212
|
+
{ url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" },
|
|
1213
|
+
]
|
|
1214
|
+
|
|
1148
1215
|
[[package]]
|
|
1149
1216
|
name = "tornado"
|
|
1150
1217
|
version = "6.5.10"
|
|
@@ -1,33 +0,0 @@
|
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|