labnotes 0.0.101__tar.gz → 0.0.103__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.101 → labnotes-0.0.103}/PKG-INFO +4 -2
- {labnotes-0.0.101 → labnotes-0.0.103}/README.md +3 -1
- labnotes-0.0.103/src/labnotes/compose_experiment.py +8 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/context.md +5 -1
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/daemon_app.py +1 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/remote_start.py +2 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/experiments.py +9 -1
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/load_run.py +1 -1
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/models.py +2 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/read_config.py +16 -3
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/execute_run.py +20 -2
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/prepare_experiment.py +6 -2
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/read_experiment_config.py +9 -2
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/run_experiment.py +3 -0
- labnotes-0.0.103/src/labnotes/tables/experiments_ddl.py +34 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/open_tables.py +3 -19
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/read_experiment.py +7 -2
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/templates/run.html +1 -1
- {labnotes-0.0.101 → labnotes-0.0.103}/.gitignore +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/CHANGELOG.md +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/MIGRATIONS.md +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/pyproject.toml +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/init.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/log.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/log_path.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/server.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/run_app.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/run_screen.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/testing-conventions.toml +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.101 → labnotes-0.0.103}/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.103
|
|
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>
|
|
@@ -51,7 +51,9 @@ labnotes reads the file from the working folder, or from the path given with
|
|
|
51
51
|
|
|
52
52
|
When `experiment.toml` sits next to `.labnotes.toml`, the root is the one
|
|
53
53
|
experiment, named after its folder. Paths then drop `{experiment}`: files
|
|
54
|
-
default to `files/` and runs to `runs/{run}`.
|
|
54
|
+
default to `files/` and runs to `runs/{run}`. An `[experiment]` table in
|
|
55
|
+
`.labnotes.toml` takes the same keys as `experiment.toml` and is the base
|
|
56
|
+
that `experiment.toml` layers over.
|
|
55
57
|
|
|
56
58
|
`labnotes experiments` reads three tables from those paths:
|
|
57
59
|
|
|
@@ -27,7 +27,9 @@ labnotes reads the file from the working folder, or from the path given with
|
|
|
27
27
|
|
|
28
28
|
When `experiment.toml` sits next to `.labnotes.toml`, the root is the one
|
|
29
29
|
experiment, named after its folder. Paths then drop `{experiment}`: files
|
|
30
|
-
default to `files/` and runs to `runs/{run}`.
|
|
30
|
+
default to `files/` and runs to `runs/{run}`. An `[experiment]` table in
|
|
31
|
+
`.labnotes.toml` takes the same keys as `experiment.toml` and is the base
|
|
32
|
+
that `experiment.toml` layers over.
|
|
31
33
|
|
|
32
34
|
`labnotes experiments` reads three tables from those paths:
|
|
33
35
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
def compose_experiment(
|
|
2
|
+
base: dict[str, object], layer: dict[str, object]
|
|
3
|
+
) -> dict[str, object]:
|
|
4
|
+
composed = {**base, **layer}
|
|
5
|
+
base_params, layer_params = base.get("params"), layer.get("params")
|
|
6
|
+
if isinstance(base_params, dict) and isinstance(layer_params, dict):
|
|
7
|
+
composed["params"] = {**base_params, **layer_params}
|
|
8
|
+
return composed
|
|
@@ -15,7 +15,7 @@ path = "{experiment}/runs/{run}" # one folder per run
|
|
|
15
15
|
|
|
16
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
17
|
|
|
18
|
-
When `experiment.toml` sits next to `.labnotes.toml`, the root is the one experiment, named after its folder. Paths then drop `{experiment}`: files default to `files/`, runs to `runs/{run}`, and `[experiments] path` is rejected.
|
|
18
|
+
When `experiment.toml` sits next to `.labnotes.toml`, the root is the one experiment, named after its folder. Paths then drop `{experiment}`: files default to `files/`, runs to `runs/{run}`, and `[experiments] path` is rejected. An `[experiment]` table in `.labnotes.toml` holds the same keys as `experiment.toml` and is the base each `experiment.toml` layers over (top-level keys replace, `params` merge by name). With no `[experiments] path`, it also makes the root the one experiment.
|
|
19
19
|
|
|
20
20
|
`labnotes experiments` reads three tables from those paths:
|
|
21
21
|
|
|
@@ -117,6 +117,10 @@ going. Elsewhere, it prints a line per event instead.
|
|
|
117
117
|
in `experiment.toml` sets it, then `concurrency` under `[experiments]` in
|
|
118
118
|
`.labnotes.toml`, then 1.
|
|
119
119
|
- `-e PATH` reads another file instead of `experiment.toml`.
|
|
120
|
+
- `--timeout SECONDS` stops any one run still going after that many seconds:
|
|
121
|
+
SIGTERM, then SIGKILL 5 seconds later. Its
|
|
122
|
+
`run.json` gets `"error": "timed out after ..."` next to its exit code.
|
|
123
|
+
Unset, runs have no time limit.
|
|
120
124
|
- `run` hands its runs to the project's labnotes daemon, starting it if
|
|
121
125
|
needed, and shows or waits for them. The daemon listens on a socket in
|
|
122
126
|
`/tmp/labnotes-<uid>/` and exits once nothing is running and no client has
|
|
@@ -102,6 +102,7 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
|
|
|
102
102
|
concurrency,
|
|
103
103
|
lambda event: record({**event, "experiment": name}),
|
|
104
104
|
listen,
|
|
105
|
+
body.get("timeout"), # type: ignore[arg-type]
|
|
105
106
|
)
|
|
106
107
|
# Clients hear why the session ended; the daemon keeps serving.
|
|
107
108
|
except Exception as failure: # noqa: BLE001
|
|
@@ -15,6 +15,7 @@ def remote_start(
|
|
|
15
15
|
name: str,
|
|
16
16
|
experiment_config: Path | None,
|
|
17
17
|
concurrency: int | None,
|
|
18
|
+
timeout: float | None = None,
|
|
18
19
|
) -> Start:
|
|
19
20
|
async def start(report: Report, listen: Listen) -> list[dict[str, object]]:
|
|
20
21
|
try:
|
|
@@ -44,6 +45,7 @@ def remote_start(
|
|
|
44
45
|
if experiment_config is None
|
|
45
46
|
else str(experiment_config.resolve()),
|
|
46
47
|
"concurrency": concurrency,
|
|
48
|
+
"timeout": timeout,
|
|
47
49
|
},
|
|
48
50
|
)
|
|
49
51
|
if reply.status_code != 202:
|
|
@@ -40,6 +40,13 @@ from labnotes.tui.run_tui import run_tui
|
|
|
40
40
|
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
41
41
|
help="With run: read this file instead of the experiment's experiment.toml.",
|
|
42
42
|
)
|
|
43
|
+
@click.option(
|
|
44
|
+
"--timeout",
|
|
45
|
+
type=click.IntRange(min=1),
|
|
46
|
+
metavar="SECONDS",
|
|
47
|
+
help="With run: stop any run still going after this many seconds."
|
|
48
|
+
" Unset, runs have no time limit.",
|
|
49
|
+
)
|
|
43
50
|
@click.pass_context
|
|
44
51
|
def experiments(
|
|
45
52
|
ctx: click.Context,
|
|
@@ -48,6 +55,7 @@ def experiments(
|
|
|
48
55
|
output_format: str | None,
|
|
49
56
|
concurrency: int | None,
|
|
50
57
|
experiment_config: Path | None,
|
|
58
|
+
timeout: int | None,
|
|
51
59
|
) -> None:
|
|
52
60
|
config = load_config(ctx)
|
|
53
61
|
if config is None:
|
|
@@ -63,7 +71,7 @@ def experiments(
|
|
|
63
71
|
click.echo(f"Created {path / 'experiment.toml'}")
|
|
64
72
|
return
|
|
65
73
|
case str(), "run":
|
|
66
|
-
start = remote_start(config, name, experiment_config, concurrency)
|
|
74
|
+
start = remote_start(config, name, experiment_config, concurrency, timeout)
|
|
67
75
|
try:
|
|
68
76
|
if sys.stdout.isatty() and sys.stderr.isatty():
|
|
69
77
|
results = run_tui(start, name)
|
|
@@ -10,7 +10,7 @@ async def load_run(
|
|
|
10
10
|
) -> dict[str, object] | None:
|
|
11
11
|
rows = await tables.query(
|
|
12
12
|
"SELECT runs.experiment, experiments.display_name, runs.id, runs.folder,"
|
|
13
|
-
" runs.created, runs.ended, runs.exit_code, runs.params"
|
|
13
|
+
" runs.created, runs.ended, runs.exit_code, runs.error, runs.params"
|
|
14
14
|
" FROM runs JOIN experiments ON experiments.name = runs.experiment"
|
|
15
15
|
f" WHERE runs.experiment = {quote_string(experiment)}"
|
|
16
16
|
f" AND runs.id = {quote_string(run)}"
|
|
@@ -49,6 +49,8 @@ class LabnotesConfig:
|
|
|
49
49
|
concurrency: int = 1
|
|
50
50
|
# Set when the config's folder is itself the one experiment.
|
|
51
51
|
experiment: str | None = None
|
|
52
|
+
# [experiment] in .labnotes.toml, under each experiment.toml.
|
|
53
|
+
defaults: dict[str, object] = field(default_factory=dict)
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
@dataclass(frozen=True)
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from labnotes.models import LabnotesConfig
|
|
5
5
|
from labnotes.is_positive_int import is_positive_int
|
|
6
6
|
from labnotes.modes import DEFAULT_MODE, MODES
|
|
7
|
+
from labnotes.runner.read_experiment_config import SETTINGS
|
|
7
8
|
from labnotes.read_path import DEFAULT_PATHS, read_path
|
|
8
9
|
|
|
9
10
|
CONFIG_FILE = ".labnotes.toml"
|
|
@@ -15,16 +16,27 @@ def read_config(path: Path) -> LabnotesConfig:
|
|
|
15
16
|
values = tomllib.load(file)
|
|
16
17
|
except tomllib.TOMLDecodeError as error:
|
|
17
18
|
raise ValueError(f"{path}: {error}") from error
|
|
18
|
-
unknown = sorted(values.keys() - DEFAULT_PATHS
|
|
19
|
+
unknown = sorted(values.keys() - {*DEFAULT_PATHS, "experiment"})
|
|
19
20
|
if unknown:
|
|
20
21
|
raise ValueError(f"{path}: unknown tables {', '.join(unknown)}")
|
|
22
|
+
experiments = values.get("experiments", {})
|
|
23
|
+
defaults = values.get("experiment", {})
|
|
24
|
+
if not isinstance(defaults, dict):
|
|
25
|
+
raise ValueError(f"{path}: experiment must be a table, like [experiment]")
|
|
26
|
+
unknown = sorted(defaults.keys() - SETTINGS)
|
|
27
|
+
if unknown:
|
|
28
|
+
raise ValueError(f"{path}: unknown keys in [experiment]: {', '.join(unknown)}")
|
|
21
29
|
root = path.resolve().parent
|
|
22
|
-
|
|
30
|
+
# An [experiments] path means many experiments, each taking [experiment] as
|
|
31
|
+
# its defaults; without one, [experiment] makes the root the experiment.
|
|
32
|
+
single = (root / "experiment.toml").is_file() or (
|
|
33
|
+
"experiment" in values
|
|
34
|
+
and not (isinstance(experiments, dict) and "path" in experiments)
|
|
35
|
+
)
|
|
23
36
|
paths = {
|
|
24
37
|
table: read_path(path, table, values.get(table, {}), single=single)
|
|
25
38
|
for table in DEFAULT_PATHS
|
|
26
39
|
}
|
|
27
|
-
experiments = values.get("experiments", {})
|
|
28
40
|
mode = experiments.get("mode", DEFAULT_MODE)
|
|
29
41
|
if mode not in MODES:
|
|
30
42
|
raise ValueError(f"{path}: [experiments] mode must be matrix or adhoc")
|
|
@@ -40,4 +52,5 @@ def read_config(path: Path) -> LabnotesConfig:
|
|
|
40
52
|
mode=mode,
|
|
41
53
|
concurrency=concurrency,
|
|
42
54
|
experiment=root.name if single else None,
|
|
55
|
+
defaults=defaults,
|
|
43
56
|
)
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import os
|
|
3
3
|
import time
|
|
4
|
+
from collections.abc import Callable
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
import psutil
|
|
7
8
|
|
|
8
9
|
from labnotes.log_path import LOG_ENV
|
|
9
10
|
from labnotes.runner.shell_command import shell_command
|
|
11
|
+
from labnotes.runner.stop_then_kill import GRACE_SECONDS
|
|
10
12
|
from labnotes.runner.wait_for_tree import wait_for_tree
|
|
11
13
|
from labnotes.runner.write_run_json import write_run_json
|
|
12
14
|
from labnotes.tables.iso_time import iso_time
|
|
15
|
+
from labnotes.tui.format_elapsed import format_elapsed
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
async def execute_run(
|
|
@@ -20,6 +23,8 @@ async def execute_run(
|
|
|
20
23
|
running: set[asyncio.subprocess.Process],
|
|
21
24
|
stopping: asyncio.Event,
|
|
22
25
|
tree: set[psutil.Process],
|
|
26
|
+
timeout: float | None = None,
|
|
27
|
+
stop: Callable[[], None] = lambda: None,
|
|
23
28
|
) -> dict[str, object]:
|
|
24
29
|
run: dict[str, object] = {
|
|
25
30
|
"params": params,
|
|
@@ -50,14 +55,27 @@ async def execute_run(
|
|
|
50
55
|
run["error"] = str(error)
|
|
51
56
|
else:
|
|
52
57
|
running.add(process)
|
|
58
|
+
exited = asyncio.ensure_future(process.wait())
|
|
59
|
+
kill_later = None
|
|
53
60
|
try:
|
|
54
|
-
|
|
61
|
+
if (
|
|
62
|
+
timeout is not None
|
|
63
|
+
and not (await asyncio.wait({exited}, timeout=timeout))[0]
|
|
64
|
+
):
|
|
65
|
+
run["error"] = f"timed out after {format_elapsed(timeout)}"
|
|
66
|
+
stop()
|
|
67
|
+
kill_later = asyncio.get_running_loop().call_later(
|
|
68
|
+
GRACE_SECONDS, stop
|
|
69
|
+
)
|
|
70
|
+
run["exit_code"] = await exited
|
|
55
71
|
# The shell can exit on SIGTERM before what it started. Once
|
|
56
72
|
# stopping, the run stays killable until everything signalled
|
|
57
73
|
# is gone. A normal exit ends the run even if helpers linger.
|
|
58
|
-
if stopping.is_set():
|
|
74
|
+
if stopping.is_set() or kill_later is not None:
|
|
59
75
|
await wait_for_tree(tree)
|
|
60
76
|
finally:
|
|
77
|
+
if kill_later is not None:
|
|
78
|
+
kill_later.cancel()
|
|
61
79
|
running.discard(process)
|
|
62
80
|
run["ended"] = iso_time(time.time())
|
|
63
81
|
write_run_json(folder, run)
|
|
@@ -19,9 +19,13 @@ async def prepare_experiment(
|
|
|
19
19
|
folder = config.experiments.replace("{experiment}", name)
|
|
20
20
|
experiment_config = config.root / folder / "experiment.toml"
|
|
21
21
|
source = await file_text(config.root, experiment_config)
|
|
22
|
-
if source is None:
|
|
22
|
+
if source is None and not config.defaults:
|
|
23
23
|
raise ValueError(f"No experiment.toml for {name} at {experiment_config}.")
|
|
24
24
|
experiment = read_experiment_config(
|
|
25
|
-
|
|
25
|
+
config.path if source is None else experiment_config,
|
|
26
|
+
source or "",
|
|
27
|
+
config.mode,
|
|
28
|
+
config.concurrency,
|
|
29
|
+
config.defaults,
|
|
26
30
|
)
|
|
27
31
|
return experiment, experiment.concurrency if concurrency is None else concurrency
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import tomllib
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
6
|
+
from labnotes.compose_experiment import compose_experiment
|
|
5
7
|
from labnotes.default_counts import DEFAULT_COUNTS
|
|
6
8
|
from labnotes.models import ExperimentConfig
|
|
7
9
|
from labnotes.is_positive_int import is_positive_int
|
|
@@ -11,12 +13,17 @@ SETTINGS = {"name", "command", "repeats", "params", "counts", "mode", "concurren
|
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def read_experiment_config(
|
|
14
|
-
path: Path,
|
|
16
|
+
path: Path,
|
|
17
|
+
source: str,
|
|
18
|
+
mode: str,
|
|
19
|
+
concurrency: int,
|
|
20
|
+
defaults: dict[str, object] | None = None,
|
|
15
21
|
) -> ExperimentConfig:
|
|
16
22
|
try:
|
|
17
|
-
|
|
23
|
+
layer = tomllib.loads(source)
|
|
18
24
|
except tomllib.TOMLDecodeError as error:
|
|
19
25
|
raise ValueError(f"{path}: {error}") from error
|
|
26
|
+
values: dict[str, Any] = compose_experiment(defaults or {}, layer)
|
|
20
27
|
unknown = sorted(values.keys() - SETTINGS)
|
|
21
28
|
if unknown:
|
|
22
29
|
raise ValueError(f"{path}: unknown keys {', '.join(unknown)}")
|
|
@@ -32,6 +32,7 @@ async def run_experiment(
|
|
|
32
32
|
concurrency: int,
|
|
33
33
|
report: Report,
|
|
34
34
|
listen: Listen,
|
|
35
|
+
timeout: float | None = None,
|
|
35
36
|
) -> list[dict[str, object]]:
|
|
36
37
|
tables = open_tables(config)
|
|
37
38
|
existing = await experiment_run_ids(tables, name)
|
|
@@ -203,6 +204,8 @@ async def run_experiment(
|
|
|
203
204
|
running[run],
|
|
204
205
|
stopping,
|
|
205
206
|
trees[run],
|
|
207
|
+
timeout=timeout,
|
|
208
|
+
stop=lambda: kill(run),
|
|
206
209
|
)
|
|
207
210
|
del running[run], trees[run]
|
|
208
211
|
report({"event": "ended", "run": run, **result})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from labnotes.models import LabnotesConfig
|
|
4
|
+
from labnotes.sql_quote import quote_string
|
|
5
|
+
|
|
6
|
+
# experiments is a view, so files and runs can't declare a foreign key to it.
|
|
7
|
+
EXPERIMENTS = """
|
|
8
|
+
CREATE TABLE experiment_configs (
|
|
9
|
+
experiment TEXT PRIMARY KEY, config TEXT, source TEXT
|
|
10
|
+
);
|
|
11
|
+
CREATE VIEW experiments AS
|
|
12
|
+
SELECT name, coalesce(config ->> 'name', name) AS display_name, config, source
|
|
13
|
+
FROM (
|
|
14
|
+
SELECT
|
|
15
|
+
names.name,
|
|
16
|
+
coalesce(experiment_configs.config, {defaults}) AS config,
|
|
17
|
+
experiment_configs.source
|
|
18
|
+
FROM (
|
|
19
|
+
SELECT experiment AS name FROM experiment_configs
|
|
20
|
+
UNION SELECT experiment FROM files
|
|
21
|
+
UNION SELECT experiment FROM runs{one}
|
|
22
|
+
) AS names
|
|
23
|
+
LEFT JOIN experiment_configs ON experiment_configs.experiment = names.name
|
|
24
|
+
);
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def experiments_ddl(config: LabnotesConfig) -> str:
|
|
29
|
+
defaults = json.dumps(config.defaults, default=str) if config.defaults else None
|
|
30
|
+
one = config.experiment
|
|
31
|
+
return EXPERIMENTS.format(
|
|
32
|
+
defaults="NULL" if defaults is None else quote_string(defaults),
|
|
33
|
+
one="" if one is None else f"\n UNION SELECT {quote_string(one)}",
|
|
34
|
+
)
|
|
@@ -4,6 +4,7 @@ from dirsql import DirSQL, Table
|
|
|
4
4
|
|
|
5
5
|
from labnotes.models import LabnotesConfig
|
|
6
6
|
from labnotes.tables.experiment_toml import experiment_toml
|
|
7
|
+
from labnotes.tables.experiments_ddl import experiments_ddl
|
|
7
8
|
from labnotes.tables.path_glob import path_glob
|
|
8
9
|
from labnotes.tables.read_blocks import read_blocks
|
|
9
10
|
from labnotes.tables.read_experiment import read_experiment
|
|
@@ -11,24 +12,6 @@ from labnotes.tables.read_file import read_file
|
|
|
11
12
|
from labnotes.tables.read_log import read_log
|
|
12
13
|
from labnotes.tables.read_run import read_run
|
|
13
14
|
|
|
14
|
-
# experiments is a view, so files and runs can't declare a foreign key to it.
|
|
15
|
-
EXPERIMENTS = """
|
|
16
|
-
CREATE TABLE experiment_configs (
|
|
17
|
-
experiment TEXT PRIMARY KEY, config TEXT, source TEXT
|
|
18
|
-
);
|
|
19
|
-
CREATE VIEW experiments AS
|
|
20
|
-
SELECT
|
|
21
|
-
names.name,
|
|
22
|
-
coalesce(experiment_configs.config ->> 'name', names.name) AS display_name,
|
|
23
|
-
experiment_configs.config,
|
|
24
|
-
experiment_configs.source
|
|
25
|
-
FROM (
|
|
26
|
-
SELECT experiment AS name FROM experiment_configs
|
|
27
|
-
UNION SELECT experiment FROM files
|
|
28
|
-
UNION SELECT experiment FROM runs
|
|
29
|
-
) AS names
|
|
30
|
-
LEFT JOIN experiment_configs ON experiment_configs.experiment = names.name;
|
|
31
|
-
"""
|
|
32
15
|
FILES = """
|
|
33
16
|
CREATE TABLE files (
|
|
34
17
|
experiment TEXT, filename TEXT, path TEXT PRIMARY KEY, ctime TEXT, mtime TEXT,
|
|
@@ -97,13 +80,14 @@ def open_tables(config: LabnotesConfig) -> DirSQL:
|
|
|
97
80
|
),
|
|
98
81
|
Table(
|
|
99
82
|
name="experiment_configs",
|
|
100
|
-
ddl=
|
|
83
|
+
ddl=experiments_ddl(config),
|
|
101
84
|
glob=path_glob(experiment_toml(config.experiments)),
|
|
102
85
|
on_file=partial(
|
|
103
86
|
read_experiment,
|
|
104
87
|
root,
|
|
105
88
|
config.experiments,
|
|
106
89
|
experiment=config.experiment,
|
|
90
|
+
defaults=config.defaults,
|
|
107
91
|
),
|
|
108
92
|
),
|
|
109
93
|
],
|
|
@@ -2,19 +2,24 @@ import json
|
|
|
2
2
|
import tomllib
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from labnotes.compose_experiment import compose_experiment
|
|
5
6
|
from labnotes.tables.experiment_toml import experiment_toml
|
|
6
7
|
from labnotes.tables.path_pattern import path_pattern
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def read_experiment(
|
|
10
|
-
root: Path,
|
|
11
|
+
root: Path,
|
|
12
|
+
template: str,
|
|
13
|
+
path: str,
|
|
14
|
+
experiment: str | None = None,
|
|
15
|
+
defaults: dict[str, object] | None = None,
|
|
11
16
|
) -> list[dict[str, object]]:
|
|
12
17
|
relative = Path(path).relative_to(root).as_posix()
|
|
13
18
|
found = path_pattern(experiment_toml(template)).fullmatch(relative)
|
|
14
19
|
if found is None:
|
|
15
20
|
return []
|
|
16
21
|
source = Path(path).read_text(encoding="utf-8")
|
|
17
|
-
config = tomllib.loads(source)
|
|
22
|
+
config = compose_experiment(defaults or {}, tomllib.loads(source))
|
|
18
23
|
return [
|
|
19
24
|
{
|
|
20
25
|
"experiment": experiment or found["experiment"],
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<section class="intro">
|
|
6
6
|
<h1 class="mono">{{ run.id }}</h1>
|
|
7
7
|
<p class="config-summary">
|
|
8
|
-
{% if run.ended is none %}<span class="chip run-state">running</span>{% elif run.exit_code is none %}<span class="chip run-state bad">ended</span>{% else %}<span class="chip run-state {{ 'good' if run.exit_code == 0 else 'bad' }}">exit {{ run.exit_code }}</span>{% endif %}
|
|
8
|
+
{% if run.ended is none %}<span class="chip run-state">running</span>{% elif run.error %}<span class="chip run-state bad">{{ run.error }}</span>{% elif run.exit_code is none %}<span class="chip run-state bad">ended</span>{% else %}<span class="chip run-state {{ 'good' if run.exit_code == 0 else 'bad' }}">exit {{ run.exit_code }}</span>{% endif %}
|
|
9
9
|
{% if run.created %}<span class="setting">started <code>{{ run.created }}</code></span>{% endif %}
|
|
10
10
|
{% for key, value in run.params.items() %}<span class="setting">{{ key }} <code>{{ value | value }}</code></span>{% endfor %}
|
|
11
11
|
</p>
|
|
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
|
|
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
|