labnotes 0.0.82__tar.gz → 0.0.84__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.82 → labnotes-0.0.84}/PKG-INFO +6 -2
- {labnotes-0.0.82 → labnotes-0.0.84}/README.md +5 -1
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/context.md +9 -7
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/experiments.py +16 -4
- labnotes-0.0.84/src/labnotes/modes.py +2 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/read_config.py +1 -1
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/read_experiment_config.py +1 -1
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/run_experiment.py +1 -1
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/summarize_config.py +2 -2
- labnotes-0.0.84/src/labnotes/tui/browse_app.py +25 -0
- labnotes-0.0.84/src/labnotes/tui/browse_tui.py +8 -0
- labnotes-0.0.84/src/labnotes/tui/experiment_screen.py +80 -0
- labnotes-0.0.84/src/labnotes/tui/experiments_screen.py +47 -0
- labnotes-0.0.84/src/labnotes/tui/group_cells.py +26 -0
- labnotes-0.0.84/src/labnotes/tui/invocation_columns.py +9 -0
- labnotes-0.0.84/src/labnotes/tui/invocation_rows.py +31 -0
- labnotes-0.0.84/src/labnotes/tui/run_cells.py +29 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/testing-conventions.toml +26 -2
- {labnotes-0.0.82 → labnotes-0.0.84}/tests/integration/conftest.py +1 -1
- labnotes-0.0.82/src/labnotes/modes.py +0 -2
- {labnotes-0.0.82 → labnotes-0.0.84}/.gitignore +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/CHANGELOG.md +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/MIGRATIONS.md +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/pyproject.toml +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/daemon_app.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/remote_start.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/init.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/models.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/server.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tui/run_app.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.82 → labnotes-0.0.84}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labnotes
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.84
|
|
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>
|
|
@@ -68,7 +68,11 @@ labnotes experiments foo # the same, plus config
|
|
|
68
68
|
labnotes experiments foo runs # foo's runs, oldest first
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Each takes `--format table`
|
|
71
|
+
Each takes `--format table` or `--format json`, which print rows and exit.
|
|
72
|
+
Without `--format`, `labnotes experiments` and `labnotes experiments foo` open
|
|
73
|
+
a browser in a terminal: the experiment list, Enter to open one, Escape to go
|
|
74
|
+
back. An experiment shows its runs grouped by invocation; Space opens one.
|
|
75
|
+
Piped, they print a table.
|
|
72
76
|
|
|
73
77
|
`labnotes experiments new [NAME]` creates an experiment folder with a starter
|
|
74
78
|
`experiment.toml`, picking a random name if you give none. `new` itself can't
|
|
@@ -44,7 +44,11 @@ labnotes experiments foo # the same, plus config
|
|
|
44
44
|
labnotes experiments foo runs # foo's runs, oldest first
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Each takes `--format table`
|
|
47
|
+
Each takes `--format table` or `--format json`, which print rows and exit.
|
|
48
|
+
Without `--format`, `labnotes experiments` and `labnotes experiments foo` open
|
|
49
|
+
a browser in a terminal: the experiment list, Enter to open one, Escape to go
|
|
50
|
+
back. An experiment shows its runs grouped by invocation; Space opens one.
|
|
51
|
+
Piped, they print a table.
|
|
48
52
|
|
|
49
53
|
`labnotes experiments new [NAME]` creates an experiment folder with a starter
|
|
50
54
|
`experiment.toml`, picking a random name if you give none. `new` itself can't
|
|
@@ -29,12 +29,14 @@ Keys come from folder names, so renaming a folder renames its rows on the next
|
|
|
29
29
|
read. Paths are relative to the root, and times are UTC ISO 8601.
|
|
30
30
|
|
|
31
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
|
|
32
|
+
labnotes experiments --format json # name, last_run, created, files, runs
|
|
33
|
+
labnotes experiments foo --format json # the same, plus config
|
|
34
|
+
labnotes experiments foo runs --format json # foo's runs, oldest first
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Always pass `--format json` (to parse) or `--format table`. Without `--format`,
|
|
38
|
+
`labnotes experiments` and `labnotes experiments foo` open an interactive
|
|
39
|
+
browser when they find a terminal, and wait for keys that never come.
|
|
38
40
|
|
|
39
41
|
`labnotes experiments new [NAME]` creates an experiment folder with a starter
|
|
40
42
|
`experiment.toml`, picking a random name if you give none. `new` itself can't
|
|
@@ -58,11 +60,11 @@ and as `display_name` in `experiments`. The folder name stays the key, so
|
|
|
58
60
|
renaming the experiment means editing `name`.
|
|
59
61
|
Each combination of `[params]` runs `repeats` times (1 if omitted), round-robin, on every
|
|
60
62
|
`run`, whatever is already on disk. That is `mode = "adhoc"`, the default.
|
|
61
|
-
`mode = "
|
|
63
|
+
`mode = "matrix"` counts what is there: only runs that count fill a combination,
|
|
62
64
|
so running again fills in what failed or never ran. `mode` under
|
|
63
65
|
`[experiments]` in `.labnotes.toml` sets it for every experiment that doesn't
|
|
64
66
|
set its own.
|
|
65
|
-
Each run gets a new run folder. In `
|
|
67
|
+
Each run gets a new run folder. In `matrix` mode it is named for the UTC time it was planned
|
|
66
68
|
(`runs/20261020-000429-brave-fox`). In `adhoc` mode a `run` command that plans one run names
|
|
67
69
|
its folder the same way. One that plans more gets one folder named that way, and its runs nest
|
|
68
70
|
inside it (`runs/20261020-000429-brave-fox/quiet-river`); the run id is that path under `runs/`. Either way, folder listings sort by start. `command` runs in a shell from the root, with
|
|
@@ -91,7 +93,7 @@ A script run by labnotes should:
|
|
|
91
93
|
- stop any background process it starts before it exits; the run ends when `command` exits.
|
|
92
94
|
- flush its output as it goes (`print(..., flush=True)`, or `PYTHONUNBUFFERED=1`), so the live view shows it as it happens.
|
|
93
95
|
|
|
94
|
-
Runs can take a long time. Start `run` in the background, then check on it with `labnotes experiments foo runs`; a run with no `ended` is still going.
|
|
96
|
+
Runs can take a long time. Start `run` in the background, then check on it with `labnotes experiments foo runs --format json`; a run with no `ended` is still going.
|
|
95
97
|
|
|
96
98
|
In a terminal, `run` shows a live table of runs (status and time) and stays
|
|
97
99
|
open when they finish. Failed runs show their status in red; `f` hides or shows them, and `r` on one
|
|
@@ -11,6 +11,7 @@ from labnotes.print_rows import print_rows
|
|
|
11
11
|
from labnotes.read_config import CONFIG_FILE
|
|
12
12
|
from labnotes.runner.run_headless import run_headless
|
|
13
13
|
from labnotes.tables.experiment_rows import experiment_rows
|
|
14
|
+
from labnotes.tui.browse_tui import browse_tui
|
|
14
15
|
from labnotes.tui.run_tui import run_tui
|
|
15
16
|
|
|
16
17
|
|
|
@@ -24,8 +25,8 @@ from labnotes.tui.run_tui import run_tui
|
|
|
24
25
|
"--format",
|
|
25
26
|
"output_format",
|
|
26
27
|
type=click.Choice(["table", "json"]),
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
help="Print rows and exit. Unset, a terminal browses them instead;"
|
|
29
|
+
" anything else gets table.",
|
|
29
30
|
)
|
|
30
31
|
@click.option(
|
|
31
32
|
"--concurrency",
|
|
@@ -44,7 +45,7 @@ def experiments(
|
|
|
44
45
|
ctx: click.Context,
|
|
45
46
|
name: str | None,
|
|
46
47
|
action: str | None,
|
|
47
|
-
output_format: str,
|
|
48
|
+
output_format: str | None,
|
|
48
49
|
concurrency: int | None,
|
|
49
50
|
experiment_config: Path | None,
|
|
50
51
|
) -> None:
|
|
@@ -75,10 +76,21 @@ def experiments(
|
|
|
75
76
|
return
|
|
76
77
|
if action not in (None, "runs"):
|
|
77
78
|
raise click.UsageError(f"Unknown action {action}. Use runs or run.")
|
|
79
|
+
if (
|
|
80
|
+
action is None
|
|
81
|
+
and output_format is None
|
|
82
|
+
and sys.stdout.isatty()
|
|
83
|
+
and sys.stderr.isatty()
|
|
84
|
+
):
|
|
85
|
+
try:
|
|
86
|
+
browse_tui(config, name)
|
|
87
|
+
except ValueError as error:
|
|
88
|
+
raise click.ClickException(str(error)) from error
|
|
89
|
+
return
|
|
78
90
|
try:
|
|
79
91
|
rows = asyncio.run(experiment_rows(config, name, action))
|
|
80
92
|
except ValueError as error:
|
|
81
93
|
raise click.ClickException(str(error)) from error
|
|
82
94
|
if rows is None:
|
|
83
95
|
raise click.ClickException(f"No experiment named {name}.")
|
|
84
|
-
print_rows(rows, output_format)
|
|
96
|
+
print_rows(rows, output_format or "table")
|
|
@@ -24,7 +24,7 @@ def read_config(path: Path) -> LabnotesConfig:
|
|
|
24
24
|
experiments = values.get("experiments", {})
|
|
25
25
|
mode = experiments.get("mode", DEFAULT_MODE)
|
|
26
26
|
if mode not in MODES:
|
|
27
|
-
raise ValueError(f"{path}: [experiments] mode must be
|
|
27
|
+
raise ValueError(f"{path}: [experiments] mode must be matrix or adhoc")
|
|
28
28
|
concurrency = experiments.get("concurrency", 1)
|
|
29
29
|
if not is_positive_int(concurrency):
|
|
30
30
|
raise ValueError(
|
|
@@ -44,7 +44,7 @@ def read_experiment_config(
|
|
|
44
44
|
raise ValueError(f"{path}: counts must be a SQL condition")
|
|
45
45
|
mode = values.get("mode", mode)
|
|
46
46
|
if mode not in MODES:
|
|
47
|
-
raise ValueError(f"{path}: mode must be
|
|
47
|
+
raise ValueError(f"{path}: mode must be matrix or adhoc")
|
|
48
48
|
concurrency = values.get("concurrency", concurrency)
|
|
49
49
|
if not is_positive_int(concurrency):
|
|
50
50
|
raise ValueError(f"{path}: concurrency must be a positive integer")
|
|
@@ -37,7 +37,7 @@ async def run_experiment(
|
|
|
37
37
|
existing = await experiment_run_ids(tables, name)
|
|
38
38
|
finished: list[dict[str, object]] = []
|
|
39
39
|
match experiment.mode:
|
|
40
|
-
case "
|
|
40
|
+
case "matrix":
|
|
41
41
|
counted = await list_experiment_runs(tables, name, experiment.counts)
|
|
42
42
|
finished = [json.loads(str(r["params"])) for r in counted if r["counted"]]
|
|
43
43
|
planned: set[str] = set()
|
|
@@ -15,13 +15,13 @@ def summarize_config(config: dict[str, object] | None, mode: str) -> dict[str, o
|
|
|
15
15
|
cells = prod(len(values) for values in varies.values())
|
|
16
16
|
command = config.get("command")
|
|
17
17
|
counts = config.get("counts")
|
|
18
|
-
|
|
18
|
+
matrix = config.get("mode", mode) == "matrix"
|
|
19
19
|
return {
|
|
20
20
|
"fixed": {},
|
|
21
21
|
"varies": varies,
|
|
22
22
|
"cell_count": cells,
|
|
23
23
|
"runs_per_cell": repeats,
|
|
24
24
|
"completed_run": counts if isinstance(counts, str) else DEFAULT_COUNTS,
|
|
25
|
-
"mode": "
|
|
25
|
+
"mode": "matrix" if matrix else "adhoc",
|
|
26
26
|
"other": {} if command is None else {"command": command},
|
|
27
27
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from textual.app import App
|
|
2
|
+
from textual.binding import Binding
|
|
3
|
+
|
|
4
|
+
from labnotes.models import LabnotesConfig
|
|
5
|
+
from labnotes.tables.open_tables import open_tables
|
|
6
|
+
from labnotes.tui.experiment_screen import ExperimentScreen
|
|
7
|
+
from labnotes.tui.experiments_screen import ExperimentsScreen
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BrowseApp(App[BaseException | None]):
|
|
11
|
+
BINDINGS = [Binding("q", "quit", "Quit")]
|
|
12
|
+
|
|
13
|
+
def __init__(self, config: LabnotesConfig, name: str | None) -> None:
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.config = config
|
|
16
|
+
self.experiment_name = name
|
|
17
|
+
self.title = config.root.name
|
|
18
|
+
|
|
19
|
+
def on_mount(self) -> None:
|
|
20
|
+
tables = open_tables(self.config)
|
|
21
|
+
self.push_screen(ExperimentsScreen(tables, self.config))
|
|
22
|
+
if self.experiment_name is not None:
|
|
23
|
+
self.push_screen(
|
|
24
|
+
ExperimentScreen(tables, self.config, self.experiment_name)
|
|
25
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from dirsql import DirSQL
|
|
4
|
+
from starlette.datastructures import QueryParams
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.screen import Screen
|
|
8
|
+
from textual.widgets import DataTable, Footer, Header
|
|
9
|
+
from textual.worker import Worker, WorkerState
|
|
10
|
+
|
|
11
|
+
from labnotes.load_experiment import load_experiment
|
|
12
|
+
from labnotes.models import LabnotesConfig
|
|
13
|
+
from labnotes.tui.invocation_columns import invocation_columns
|
|
14
|
+
from labnotes.tui.invocation_rows import invocation_rows
|
|
15
|
+
from labnotes.tui.runs_table import RunsTable
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExperimentScreen(Screen[None]):
|
|
19
|
+
BINDINGS = [
|
|
20
|
+
Binding("space", "open_invocation", "Open invocation"),
|
|
21
|
+
Binding("escape", "app.pop_screen", "Back"),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
def __init__(self, tables: DirSQL, config: LabnotesConfig, name: str) -> None:
|
|
25
|
+
super().__init__()
|
|
26
|
+
self.tables = tables
|
|
27
|
+
self.config = config
|
|
28
|
+
self.experiment_name = name
|
|
29
|
+
self.experiment: dict[str, Any] = {}
|
|
30
|
+
self.opened: set[str] = set()
|
|
31
|
+
|
|
32
|
+
def compose(self) -> ComposeResult:
|
|
33
|
+
yield Header()
|
|
34
|
+
yield RunsTable(id="runs", cursor_type="row")
|
|
35
|
+
yield Footer()
|
|
36
|
+
|
|
37
|
+
def on_mount(self) -> None:
|
|
38
|
+
self.sub_title = self.experiment_name
|
|
39
|
+
self.run_worker(self.load(), exit_on_error=False)
|
|
40
|
+
|
|
41
|
+
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
|
42
|
+
if event.state == WorkerState.ERROR:
|
|
43
|
+
self.app.exit(event.worker.error)
|
|
44
|
+
|
|
45
|
+
async def load(self) -> None:
|
|
46
|
+
loaded = await load_experiment(
|
|
47
|
+
self.tables,
|
|
48
|
+
self.config.root,
|
|
49
|
+
self.config.mode,
|
|
50
|
+
self.experiment_name,
|
|
51
|
+
QueryParams(),
|
|
52
|
+
)
|
|
53
|
+
if loaded is None:
|
|
54
|
+
raise ValueError(f"No experiment named {self.experiment_name}.")
|
|
55
|
+
self.experiment = loaded
|
|
56
|
+
self.sub_title = str(loaded["display_name"])
|
|
57
|
+
self.query_one(DataTable).add_columns(*invocation_columns(loaded))
|
|
58
|
+
self.redraw()
|
|
59
|
+
|
|
60
|
+
def redraw(self) -> None:
|
|
61
|
+
table = self.query_one(DataTable)
|
|
62
|
+
row = table.cursor_row
|
|
63
|
+
table.clear()
|
|
64
|
+
for key, cells in invocation_rows(self.experiment, self.opened):
|
|
65
|
+
table.add_row(*cells, key=key)
|
|
66
|
+
table.move_cursor(row=row)
|
|
67
|
+
|
|
68
|
+
def action_open_invocation(self) -> None:
|
|
69
|
+
table = self.query_one(DataTable)
|
|
70
|
+
if not table.row_count:
|
|
71
|
+
return
|
|
72
|
+
key = str(table.coordinate_to_cell_key(table.cursor_coordinate).row_key.value)
|
|
73
|
+
groups = {
|
|
74
|
+
group["name"]
|
|
75
|
+
for group in self.experiment["run_groups"] or ()
|
|
76
|
+
if not group["flat"]
|
|
77
|
+
}
|
|
78
|
+
if key in groups:
|
|
79
|
+
self.opened ^= {key}
|
|
80
|
+
self.redraw()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from dirsql import DirSQL
|
|
2
|
+
from textual.app import ComposeResult
|
|
3
|
+
from textual.screen import Screen
|
|
4
|
+
from textual.widgets import DataTable, Footer, Header
|
|
5
|
+
from textual.worker import Worker, WorkerState
|
|
6
|
+
|
|
7
|
+
from labnotes.models import LabnotesConfig
|
|
8
|
+
from labnotes.tables.summarize_experiments import summarize_experiments
|
|
9
|
+
from labnotes.tui.experiment_screen import ExperimentScreen
|
|
10
|
+
from labnotes.tui.runs_table import RunsTable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExperimentsScreen(Screen[None]):
|
|
14
|
+
def __init__(self, tables: DirSQL, config: LabnotesConfig) -> None:
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.tables = tables
|
|
17
|
+
self.config = config
|
|
18
|
+
|
|
19
|
+
def compose(self) -> ComposeResult:
|
|
20
|
+
yield Header()
|
|
21
|
+
yield RunsTable(id="experiments", cursor_type="row")
|
|
22
|
+
yield Footer()
|
|
23
|
+
|
|
24
|
+
def on_mount(self) -> None:
|
|
25
|
+
self.run_worker(self.load(), exit_on_error=False)
|
|
26
|
+
|
|
27
|
+
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
|
28
|
+
if event.state == WorkerState.ERROR:
|
|
29
|
+
self.app.exit(event.worker.error)
|
|
30
|
+
|
|
31
|
+
async def load(self) -> None:
|
|
32
|
+
rows = await summarize_experiments(self.tables)
|
|
33
|
+
if not rows:
|
|
34
|
+
self.sub_title = "No experiments."
|
|
35
|
+
return
|
|
36
|
+
table = self.query_one(DataTable)
|
|
37
|
+
table.add_columns(*rows[0])
|
|
38
|
+
for row in rows:
|
|
39
|
+
table.add_row(
|
|
40
|
+
*("" if value is None else str(value) for value in row.values()),
|
|
41
|
+
key=str(row["name"]),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
45
|
+
self.app.push_screen(
|
|
46
|
+
ExperimentScreen(self.tables, self.config, str(event.row_key.value))
|
|
47
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
|
|
6
|
+
from labnotes.format_value import format_value
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def group_cells(
|
|
10
|
+
group: Mapping[str, Any], columns: Sequence[str], live: bool
|
|
11
|
+
) -> list[object]:
|
|
12
|
+
status = Text.assemble(
|
|
13
|
+
(f"{group['passed']}✓", "green"), " ", (f"{group['failed']}✕", "red")
|
|
14
|
+
)
|
|
15
|
+
if group["unfinished"]:
|
|
16
|
+
mark, style = ("◐", "yellow") if live else ("–", "dim")
|
|
17
|
+
status.append(f" {group['unfinished']}{mark}", style=style)
|
|
18
|
+
cells: list[object] = [status, group["name"], str(len(group["runs"]))]
|
|
19
|
+
for column in columns[1:]:
|
|
20
|
+
if column == "started":
|
|
21
|
+
cells.append(group["started"] or "")
|
|
22
|
+
elif column in group["shared"]:
|
|
23
|
+
cells.append(format_value(group["shared"][column]))
|
|
24
|
+
else:
|
|
25
|
+
cells.append("")
|
|
26
|
+
return cells
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def invocation_columns(experiment: Mapping[str, Any]) -> list[str]:
|
|
6
|
+
columns = ["", *experiment["run_columns"]]
|
|
7
|
+
if experiment["run_groups"] is not None:
|
|
8
|
+
columns.insert(2, "runs")
|
|
9
|
+
return columns
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from collections.abc import Collection, Mapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from labnotes.tui.group_cells import group_cells
|
|
5
|
+
from labnotes.tui.run_cells import run_cells
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def invocation_rows(
|
|
9
|
+
experiment: Mapping[str, Any], opened: Collection[str]
|
|
10
|
+
) -> list[tuple[str, list[object]]]:
|
|
11
|
+
columns = experiment["run_columns"]
|
|
12
|
+
live = experiment["runs_live"]
|
|
13
|
+
groups = experiment["run_groups"]
|
|
14
|
+
if groups is None:
|
|
15
|
+
return [
|
|
16
|
+
(str(run["id"]), run_cells(run, columns, False, live))
|
|
17
|
+
for run in experiment["runs_list"]
|
|
18
|
+
]
|
|
19
|
+
rows = []
|
|
20
|
+
for group in groups:
|
|
21
|
+
if group["flat"]:
|
|
22
|
+
[run, *_] = group["runs"]
|
|
23
|
+
rows.append((str(run["id"]), run_cells(run, columns, True, live)))
|
|
24
|
+
continue
|
|
25
|
+
rows.append((group["name"], group_cells(group, columns, live)))
|
|
26
|
+
if group["name"] in opened:
|
|
27
|
+
rows += [
|
|
28
|
+
(str(run["id"]), run_cells(run, columns, True, live))
|
|
29
|
+
for run in group["runs"]
|
|
30
|
+
]
|
|
31
|
+
return rows
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
|
|
5
|
+
from labnotes.format_duration import format_duration
|
|
6
|
+
from labnotes.format_value import format_value
|
|
7
|
+
from labnotes.tui.dim import dim
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_cells(
|
|
11
|
+
run: Mapping[str, object], columns: Sequence[str], nested: bool, live: bool
|
|
12
|
+
) -> list[object]:
|
|
13
|
+
if run["labnotes_unfinished"]:
|
|
14
|
+
status = Text("◐", style="yellow") if live else dim("–")
|
|
15
|
+
elif run["labnotes_completed"]:
|
|
16
|
+
status = Text("✓", style="green")
|
|
17
|
+
else:
|
|
18
|
+
status = Text("✕", style="red")
|
|
19
|
+
cells: list[object] = [status]
|
|
20
|
+
for column in columns:
|
|
21
|
+
value = run[column]
|
|
22
|
+
if column == "id" and nested:
|
|
23
|
+
child = str(value).partition("/")[2]
|
|
24
|
+
cells += [f" {child}" if child else str(value), ""]
|
|
25
|
+
elif column == "duration":
|
|
26
|
+
cells.append(format_duration(value)) # type: ignore[arg-type]
|
|
27
|
+
else:
|
|
28
|
+
cells.append(format_value(value))
|
|
29
|
+
return cells
|
|
@@ -14,13 +14,13 @@ reason = "last_stream is always the same str object taken from STREAMS, so is no
|
|
|
14
14
|
path = "src/labnotes/runner/run_experiment.py"
|
|
15
15
|
rules = ["mutation"]
|
|
16
16
|
lines = [47]
|
|
17
|
-
reason = "mode is validated to
|
|
17
|
+
reason = "mode is validated to matrix or adhoc; \"matrix\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
|
|
18
18
|
|
|
19
19
|
[[python.exempt]]
|
|
20
20
|
path = "src/labnotes/load_experiment.py"
|
|
21
21
|
rules = ["mutation"]
|
|
22
22
|
lines = [59]
|
|
23
|
-
reason = "summarize_config returns mode
|
|
23
|
+
reason = "summarize_config returns mode matrix or adhoc; \"matrix\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
|
|
24
24
|
|
|
25
25
|
[[python.exempt]]
|
|
26
26
|
path = "src/labnotes/daemon/exit_when_idle.py"
|
|
@@ -45,3 +45,27 @@ path = "src/labnotes/run_control.py"
|
|
|
45
45
|
rules = ["mutation"]
|
|
46
46
|
lines = [38]
|
|
47
47
|
reason = "argument counts are small ints, which CPython caches, so is not equals !="
|
|
48
|
+
|
|
49
|
+
[[python.exempt]]
|
|
50
|
+
path = "src/labnotes/tui/run_cells.py"
|
|
51
|
+
rules = ["mutation"]
|
|
52
|
+
lines = [22, 25]
|
|
53
|
+
reason = "run columns come from string literals in load_experiment, which CPython interns, so is equals =="
|
|
54
|
+
|
|
55
|
+
[[python.exempt]]
|
|
56
|
+
path = "src/labnotes/tui/group_cells.py"
|
|
57
|
+
rules = ["mutation"]
|
|
58
|
+
lines = [20]
|
|
59
|
+
reason = "run columns come from string literals in load_experiment, which CPython interns, so is equals =="
|
|
60
|
+
|
|
61
|
+
[[python.exempt]]
|
|
62
|
+
path = "src/labnotes/tui/experiment_screen.py"
|
|
63
|
+
rules = ["mutation"]
|
|
64
|
+
lines = [42]
|
|
65
|
+
reason = "WorkerState members are singletons, so is equals =="
|
|
66
|
+
|
|
67
|
+
[[python.exempt]]
|
|
68
|
+
path = "src/labnotes/tui/experiments_screen.py"
|
|
69
|
+
rules = ["mutation"]
|
|
70
|
+
lines = [28, 37]
|
|
71
|
+
reason = "WorkerState members are singletons, so is equals ==; every row of one query has the same keys, so any row names the columns"
|
|
@@ -13,7 +13,7 @@ from playwright.async_api import Page, async_playwright
|
|
|
13
13
|
from labnotes.daemon.socket_path import socket_path
|
|
14
14
|
|
|
15
15
|
CONFIG = (
|
|
16
|
-
'name = "Alpha study"\ncommand = "go"\nrepeats = 2\nmode = "
|
|
16
|
+
'name = "Alpha study"\ncommand = "go"\nrepeats = 2\nmode = "matrix"\n'
|
|
17
17
|
'[params]\nsource-language = ["en", "fr"]\n'
|
|
18
18
|
)
|
|
19
19
|
|
|
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
|
|
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
|
|
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
|