labnotes 0.0.105__tar.gz → 0.0.107__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.105 → labnotes-0.0.107}/PKG-INFO +1 -1
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/context.md +12 -6
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/describe_status.py +5 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/watch_daemon.py +21 -2
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/experiments.py +8 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/execute_run.py +17 -1
- labnotes-0.0.107/src/labnotes/runner/exit_signal.py +11 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/run_experiment.py +1 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/experiment_rows.py +6 -1
- labnotes-0.0.107/src/labnotes/tui/config.py +1 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/run_app.py +58 -20
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/run_tui.py +2 -1
- labnotes-0.0.107/src/labnotes/utils/__init__.py +0 -0
- labnotes-0.0.107/src/labnotes/utils/format_bytes.py +5 -0
- labnotes-0.0.107/src/labnotes/utils/run_usage.py +31 -0
- labnotes-0.0.107/src/labnotes/utils/tree_rss.py +27 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/testing-conventions.toml +12 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/.gitignore +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/CHANGELOG.md +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/MIGRATIONS.md +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/README.md +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/pyproject.toml +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/daemon_app.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/remote_start.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/init.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/log.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/log_path.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/models.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/retries.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/server.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/run_screen.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.105 → labnotes-0.0.107}/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.107
|
|
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>
|
|
@@ -75,8 +75,11 @@ script so it writes its outputs there.
|
|
|
75
75
|
Each param is appended as `--name value`, with the name as written (use
|
|
76
76
|
`source-language` for a `--source-language` flag), booleans as 0 or 1, and lists
|
|
77
77
|
and tables as JSON. labnotes writes `run.json` (params, created, ended, exit
|
|
78
|
-
code), `stdout.log` and `stderr.log` to the
|
|
79
|
-
run folder.
|
|
78
|
+
code, `ended_by`, `signal`), `stdout.log` and `stderr.log` to the
|
|
79
|
+
run folder. `ended_by` is `"timeout"`, `"stop"` (labnotes stopped it),
|
|
80
|
+
`"signal"` (a signal labnotes didn't send, like the out-of-memory killer) or
|
|
81
|
+
null (the script exited itself). `signal` is the signal number read from the
|
|
82
|
+
exit code (negative, or 128 + N from the shell), else null.
|
|
80
83
|
A run counts when it exited 0 and its `run.json` doesn't hold
|
|
81
84
|
`"excluded": true`, so excluding a run by hand means adding that key; the next
|
|
82
85
|
`run` replaces it. Other keys you add to `run.json` are kept and show in `run`.
|
|
@@ -109,9 +112,10 @@ shows a waiting retry as `retry in 30s` and the end as `gave up (exit 75)`;
|
|
|
109
112
|
`run status --json` reports `retrying`, `retried`, `gave up` and
|
|
110
113
|
`retry cancelled` events.
|
|
111
114
|
|
|
112
|
-
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.
|
|
115
|
+
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. Each run lists `size`, the bytes in its run folder, and a running run lists `memory`, the resident bytes of its command and everything it started.
|
|
113
116
|
|
|
114
|
-
In a terminal, `run` shows a live table of runs (status
|
|
117
|
+
In a terminal, `run` shows a live table of runs (status, time, memory and
|
|
118
|
+
folder size) and stays
|
|
115
119
|
open when they finish. Failed runs show their status in red; `f` hides or shows them, and `r` on one
|
|
116
120
|
runs it again under a new id, with `retry_of` set. `j` and `k` move the cursor, `g` and `G` jump to the
|
|
117
121
|
first and last row. Runs not started yet show grayed as `pending`, and as
|
|
@@ -133,7 +137,8 @@ going. Elsewhere, it prints a line per event instead.
|
|
|
133
137
|
- `-e PATH` reads another file instead of `experiment.toml`.
|
|
134
138
|
- `--timeout SECONDS` stops any one run still going after that many seconds:
|
|
135
139
|
SIGTERM, then SIGKILL 5 seconds later. Its
|
|
136
|
-
`run.json` gets `"error": "timed out after ..."`
|
|
140
|
+
`run.json` gets `"error": "timed out after ..."` and `"ended_by": "timeout"`
|
|
141
|
+
next to its exit code.
|
|
137
142
|
Unset, runs have no time limit.
|
|
138
143
|
- `run` hands its runs to the project's labnotes daemon, starting it if
|
|
139
144
|
needed, and shows or waits for them. The daemon listens on a socket in
|
|
@@ -146,7 +151,8 @@ going. Elsewhere, it prints a line per event instead.
|
|
|
146
151
|
- `labnotes run stop` stops every run the daemon has going: SIGTERM, then
|
|
147
152
|
SIGKILL 5 seconds later.
|
|
148
153
|
- `labnotes run status` prints what the daemon has going, a line per event so
|
|
149
|
-
far plus the latest line each running run logged
|
|
154
|
+
far plus the latest line each running run logged and what each running run
|
|
155
|
+
uses (`usage` events with `--json`). `-f` keeps following, and
|
|
150
156
|
waits for a daemon when none is up. `--json` prints one JSON object per line.
|
|
151
157
|
Following doesn't keep the daemon up.
|
|
152
158
|
- `labnotes run control NAME ACTION [ARG]` sends one live-view control to a
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from labnotes.utils.format_bytes import format_bytes
|
|
1
2
|
from labnotes.runner.describe_event import describe_event
|
|
2
3
|
|
|
3
4
|
|
|
@@ -5,6 +6,10 @@ def describe_status(event: dict[str, object]) -> str:
|
|
|
5
6
|
match event:
|
|
6
7
|
case {"event": "output"}:
|
|
7
8
|
said = f"{event['run']}: {event['line']}"
|
|
9
|
+
case {"event": "usage"}:
|
|
10
|
+
memory = format_bytes(event["memory"]) or "?" # type: ignore[arg-type]
|
|
11
|
+
size = format_bytes(event["size"]) or "?" # type: ignore[arg-type]
|
|
12
|
+
said = f"{event['run']} uses {memory} of memory, {size} on disk."
|
|
8
13
|
case {"event": "done", "error": None}:
|
|
9
14
|
said = "Done."
|
|
10
15
|
case {"event": "done"}:
|
|
@@ -6,9 +6,11 @@ import httpx2
|
|
|
6
6
|
|
|
7
7
|
from labnotes.daemon.daemon_client import daemon_client
|
|
8
8
|
from labnotes.daemon.run_logs import RunLogs
|
|
9
|
+
from labnotes.utils.run_usage import run_usage
|
|
9
10
|
|
|
10
11
|
Event = dict[str, object]
|
|
11
12
|
TAIL_SECONDS = 1.0
|
|
13
|
+
USAGE_SECONDS = 30.0
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
async def watch_daemon(root: Path, follow: bool, emit: Callable[[Event], None]) -> None:
|
|
@@ -23,7 +25,21 @@ async def watch_daemon(root: Path, follow: bool, emit: Callable[[Event], None])
|
|
|
23
25
|
await asyncio.sleep(TAIL_SECONDS)
|
|
24
26
|
tail()
|
|
25
27
|
|
|
28
|
+
async def sample() -> None:
|
|
29
|
+
for (experiment, run), folder in list(logs.folders.items()):
|
|
30
|
+
if (experiment, run) not in logs.folders:
|
|
31
|
+
continue
|
|
32
|
+
usage = await run_usage(folder, True)
|
|
33
|
+
if (experiment, run) in logs.folders:
|
|
34
|
+
emit({"event": "usage", "experiment": experiment, "run": run, **usage})
|
|
35
|
+
|
|
36
|
+
async def keep_sampling() -> None:
|
|
37
|
+
while True:
|
|
38
|
+
await asyncio.sleep(USAGE_SECONDS)
|
|
39
|
+
await sample()
|
|
40
|
+
|
|
26
41
|
tailing = asyncio.create_task(keep_tailing()) if follow else None
|
|
42
|
+
sampling = None
|
|
27
43
|
try:
|
|
28
44
|
async with (
|
|
29
45
|
daemon_client(root) as client,
|
|
@@ -36,8 +52,10 @@ async def watch_daemon(root: Path, follow: bool, emit: Callable[[Event], None])
|
|
|
36
52
|
match event:
|
|
37
53
|
case {"event": "replayed"}:
|
|
38
54
|
tail()
|
|
55
|
+
await sample()
|
|
39
56
|
if not follow:
|
|
40
57
|
return
|
|
58
|
+
sampling = asyncio.create_task(keep_sampling())
|
|
41
59
|
continue
|
|
42
60
|
case {"event": "started", "experiment": str(name), "run": str(run)}:
|
|
43
61
|
logs.add(name, run, Path(str(event["folder"])))
|
|
@@ -48,5 +66,6 @@ async def watch_daemon(root: Path, follow: bool, emit: Callable[[Event], None])
|
|
|
48
66
|
except httpx2.TransportError:
|
|
49
67
|
pass
|
|
50
68
|
finally:
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
for task in (tailing, sampling):
|
|
70
|
+
if task is not None:
|
|
71
|
+
task.cancel()
|
|
@@ -6,6 +6,7 @@ import click
|
|
|
6
6
|
|
|
7
7
|
from labnotes.create_experiment import create_experiment
|
|
8
8
|
from labnotes.daemon.remote_start import remote_start
|
|
9
|
+
from labnotes.utils.format_bytes import format_bytes
|
|
9
10
|
from labnotes.load_config import load_config
|
|
10
11
|
from labnotes.print_rows import print_rows
|
|
11
12
|
from labnotes.read_config import CONFIG_FILE
|
|
@@ -14,6 +15,8 @@ from labnotes.tables.experiment_rows import experiment_rows
|
|
|
14
15
|
from labnotes.tui.browse_tui import browse_tui
|
|
15
16
|
from labnotes.tui.run_tui import run_tui
|
|
16
17
|
|
|
18
|
+
BYTES = ("memory", "size")
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
@click.command(
|
|
19
22
|
help="List experiments, show one, list its runs, or run it."
|
|
@@ -103,4 +106,9 @@ def experiments(
|
|
|
103
106
|
raise click.ClickException(str(error)) from error
|
|
104
107
|
if rows is None:
|
|
105
108
|
raise click.ClickException(f"No experiment named {name}.")
|
|
109
|
+
if output_format != "json":
|
|
110
|
+
rows = [
|
|
111
|
+
row | {key: format_bytes(row[key]) for key in BYTES if key in row} # type: ignore[arg-type]
|
|
112
|
+
for row in rows
|
|
113
|
+
]
|
|
106
114
|
print_rows(rows, output_format or "table")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import contextlib
|
|
2
3
|
import os
|
|
3
4
|
import time
|
|
4
5
|
from collections.abc import Callable
|
|
@@ -7,6 +8,7 @@ from pathlib import Path
|
|
|
7
8
|
import psutil
|
|
8
9
|
|
|
9
10
|
from labnotes.log_path import LOG_ENV
|
|
11
|
+
from labnotes.runner.exit_signal import exit_signal
|
|
10
12
|
from labnotes.runner.shell_command import shell_command
|
|
11
13
|
from labnotes.runner.stop_then_kill import GRACE_SECONDS
|
|
12
14
|
from labnotes.runner.wait_for_tree import wait_for_tree
|
|
@@ -26,6 +28,7 @@ async def execute_run(
|
|
|
26
28
|
retry_of: str | None = None,
|
|
27
29
|
timeout: float | None = None,
|
|
28
30
|
stop: Callable[[], None] = lambda: None,
|
|
31
|
+
stopped: Callable[[], bool] = lambda: False,
|
|
29
32
|
) -> dict[str, object]:
|
|
30
33
|
run: dict[str, object] = {
|
|
31
34
|
"params": params,
|
|
@@ -33,6 +36,8 @@ async def execute_run(
|
|
|
33
36
|
"ended": None,
|
|
34
37
|
"exit_code": None,
|
|
35
38
|
"error": None,
|
|
39
|
+
"ended_by": None,
|
|
40
|
+
"signal": None,
|
|
36
41
|
}
|
|
37
42
|
if retry_of is not None:
|
|
38
43
|
run["retry_of"] = retry_of
|
|
@@ -58,6 +63,10 @@ async def execute_run(
|
|
|
58
63
|
run["error"] = str(error)
|
|
59
64
|
else:
|
|
60
65
|
running.add(process)
|
|
66
|
+
run["pid"] = process.pid
|
|
67
|
+
with contextlib.suppress(psutil.Error):
|
|
68
|
+
run["pid_created"] = psutil.Process(process.pid).create_time()
|
|
69
|
+
write_run_json(folder, run)
|
|
61
70
|
exited = asyncio.ensure_future(process.wait())
|
|
62
71
|
kill_later = None
|
|
63
72
|
try:
|
|
@@ -70,7 +79,14 @@ async def execute_run(
|
|
|
70
79
|
kill_later = asyncio.get_running_loop().call_later(
|
|
71
80
|
GRACE_SECONDS, stop
|
|
72
81
|
)
|
|
73
|
-
run["exit_code"] = await exited
|
|
82
|
+
run["exit_code"] = code = await exited
|
|
83
|
+
run["signal"] = exit_signal(code)
|
|
84
|
+
if kill_later is not None:
|
|
85
|
+
run["ended_by"] = "timeout"
|
|
86
|
+
elif stopping.is_set() or stopped():
|
|
87
|
+
run["ended_by"] = "stop"
|
|
88
|
+
elif run["signal"] is not None:
|
|
89
|
+
run["ended_by"] = "signal"
|
|
74
90
|
# The shell can exit on SIGTERM before what it started. Once
|
|
75
91
|
# stopping, the run stays killable until everything signalled
|
|
76
92
|
# is gone. A normal exit ends the run even if helpers linger.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import signal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def exit_signal(exit_code: int | None) -> int | None:
|
|
5
|
+
if exit_code is None:
|
|
6
|
+
return None
|
|
7
|
+
if exit_code < 0:
|
|
8
|
+
return -exit_code
|
|
9
|
+
# A shell reports a child killed by signal N as exit 128 + N.
|
|
10
|
+
number = exit_code - 128
|
|
11
|
+
return number if 0 < number < signal.NSIG else None
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
3
|
from labnotes.models import LabnotesConfig
|
|
4
|
+
from labnotes.utils.run_usage import run_usage
|
|
4
5
|
from labnotes.summarize_config import summarize_config
|
|
5
6
|
from labnotes.tables.list_experiment_runs import list_experiment_runs
|
|
6
7
|
from labnotes.tables.open_tables import open_tables
|
|
@@ -21,7 +22,11 @@ async def experiment_rows(
|
|
|
21
22
|
summary = summarize_config(
|
|
22
23
|
json.loads(str(stored)) if stored else None, config.mode
|
|
23
24
|
)
|
|
24
|
-
|
|
25
|
+
runs = await list_experiment_runs(
|
|
25
26
|
tables, name, str(summary["completed_run"])
|
|
26
27
|
)
|
|
28
|
+
for run in runs:
|
|
29
|
+
going = all(run[key] is None for key in ("ended", "exit_code", "error"))
|
|
30
|
+
run |= await run_usage(config.root / str(run["folder"]), going)
|
|
31
|
+
return runs
|
|
27
32
|
return rows
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
RUN_COLUMNS = ("run", "params", "status", "time", "memory", "size")
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import signal
|
|
3
3
|
import time
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from rich.text import Text
|
|
8
|
+
from textual import work
|
|
7
9
|
from textual.app import App, ComposeResult
|
|
8
10
|
from textual.binding import Binding
|
|
9
11
|
from textual.widgets import DataTable, Footer, Header, Input, Static
|
|
10
12
|
|
|
13
|
+
from labnotes.utils.format_bytes import format_bytes
|
|
11
14
|
from labnotes.models import Controls, Start
|
|
12
15
|
from labnotes.retries import RETRY_EXIT_CODE
|
|
16
|
+
from labnotes.utils.run_usage import run_usage
|
|
13
17
|
from labnotes.runner.describe_event import describe_event
|
|
14
18
|
from labnotes.runner.stop_then_kill import GRACE_SECONDS, stop_then_kill
|
|
15
19
|
from labnotes.runner.param_args import param_args
|
|
@@ -20,7 +24,6 @@ from labnotes.tui.log_blocks import LogBlocks
|
|
|
20
24
|
from labnotes.tui.run_output import RunOutput
|
|
21
25
|
from labnotes.tui.runs_table import RunsTable
|
|
22
26
|
|
|
23
|
-
COLUMNS = ("run", "params", "status", "time")
|
|
24
27
|
PENDING, PASSED, FAILED = "○", "✓", "✗"
|
|
25
28
|
SPINNER = "|/-\\"
|
|
26
29
|
SKIPPED, DESELECTED = "–", "⊘"
|
|
@@ -50,8 +53,9 @@ class RunApp(App[None]):
|
|
|
50
53
|
Binding("minus", "concurrency(-1)", "Fewer at once", key_display="-"),
|
|
51
54
|
]
|
|
52
55
|
|
|
53
|
-
def __init__(self, start: Start, title: str) -> None:
|
|
56
|
+
def __init__(self, start: Start, title: str, columns: Sequence[str]) -> None:
|
|
54
57
|
super().__init__()
|
|
58
|
+
self.columns = tuple(columns)
|
|
55
59
|
self.start = start
|
|
56
60
|
self.title = title
|
|
57
61
|
self.results: list[dict[str, object]] = []
|
|
@@ -73,7 +77,7 @@ class RunApp(App[None]):
|
|
|
73
77
|
self.frame = 0
|
|
74
78
|
self.started: dict[str, float] = {}
|
|
75
79
|
self.pending: set[str] = set()
|
|
76
|
-
self.cells: dict[str,
|
|
80
|
+
self.cells: dict[str, dict[str, object]] = {}
|
|
77
81
|
self.filter = ""
|
|
78
82
|
self.failed: set[str] = set()
|
|
79
83
|
self.retried: set[str] = set()
|
|
@@ -83,6 +87,8 @@ class RunApp(App[None]):
|
|
|
83
87
|
self.viewing: str | None = None
|
|
84
88
|
self.grabbed: str | None = None
|
|
85
89
|
self.grabbed_from: list[str] = []
|
|
90
|
+
self.sampling = False
|
|
91
|
+
self.unmeasured: dict[str, None] = {}
|
|
86
92
|
|
|
87
93
|
def compose(self) -> ComposeResult:
|
|
88
94
|
yield Header()
|
|
@@ -95,7 +101,7 @@ class RunApp(App[None]):
|
|
|
95
101
|
|
|
96
102
|
def on_mount(self) -> None:
|
|
97
103
|
table = self.query_one(DataTable)
|
|
98
|
-
for column in
|
|
104
|
+
for column in self.columns:
|
|
99
105
|
table.add_column(column, key=column)
|
|
100
106
|
self.query_one(Input).display = False
|
|
101
107
|
self.query_one(RunOutput).display = False
|
|
@@ -127,8 +133,9 @@ class RunApp(App[None]):
|
|
|
127
133
|
self.title = str(event["title"])
|
|
128
134
|
for planned in event["runs"]: # type: ignore[attr-defined]
|
|
129
135
|
args = " ".join(param_args(planned["params"]))
|
|
130
|
-
cells
|
|
131
|
-
|
|
136
|
+
self.cells[planned["run"]] = self.waiting_cells(
|
|
137
|
+
planned["run"], args
|
|
138
|
+
)
|
|
132
139
|
self.pending.add(planned["run"])
|
|
133
140
|
self.redraw(table.cursor_row)
|
|
134
141
|
self.show_concurrency(event)
|
|
@@ -145,13 +152,13 @@ class RunApp(App[None]):
|
|
|
145
152
|
if run == self.next_run:
|
|
146
153
|
self.show("")
|
|
147
154
|
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
148
|
-
|
|
149
|
-
run.rpartition("/")[2],
|
|
150
|
-
args,
|
|
151
|
-
SPINNER[self.frame],
|
|
152
|
-
format_elapsed(0),
|
|
153
|
-
|
|
154
|
-
for column, cell in
|
|
155
|
+
shown = {
|
|
156
|
+
"run": run.rpartition("/")[2],
|
|
157
|
+
"params": args,
|
|
158
|
+
"status": SPINNER[self.frame],
|
|
159
|
+
"time": format_elapsed(0),
|
|
160
|
+
}
|
|
161
|
+
for column, cell in shown.items():
|
|
155
162
|
self.set_cell(run, column, cell)
|
|
156
163
|
case "ended":
|
|
157
164
|
outcome = PASSED
|
|
@@ -166,6 +173,8 @@ class RunApp(App[None]):
|
|
|
166
173
|
self.tick()
|
|
167
174
|
del self.started[run]
|
|
168
175
|
self.redraw(table.cursor_row)
|
|
176
|
+
self.unmeasured[run] = None
|
|
177
|
+
self.measure()
|
|
169
178
|
case "queued":
|
|
170
179
|
self.reposition(event, run)
|
|
171
180
|
self.next_run = run
|
|
@@ -201,9 +210,7 @@ class RunApp(App[None]):
|
|
|
201
210
|
self.retried.add(run)
|
|
202
211
|
again = str(event["retry"])
|
|
203
212
|
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
204
|
-
self.cells[again] =
|
|
205
|
-
*map(dim, (again.rpartition("/")[2], args, PENDING, ""))
|
|
206
|
-
]
|
|
213
|
+
self.cells[again] = self.waiting_cells(again, args)
|
|
207
214
|
self.pending.add(again)
|
|
208
215
|
self.redraw(table.cursor_row)
|
|
209
216
|
self.show(describe_event(event))
|
|
@@ -244,14 +251,20 @@ class RunApp(App[None]):
|
|
|
244
251
|
table.clear()
|
|
245
252
|
for run, cells in self.cells.items():
|
|
246
253
|
hidden = self.hiding and run in self.failed
|
|
247
|
-
if self.filter in str(cells
|
|
248
|
-
table.add_row(*cells, key=run)
|
|
254
|
+
if self.filter in str(cells.get("params", "")) and not hidden:
|
|
255
|
+
table.add_row(*(cells.get(c, "") for c in self.columns), key=run)
|
|
249
256
|
table.move_cursor(row=row)
|
|
250
257
|
|
|
258
|
+
def waiting_cells(self, run: str, args: str) -> dict[str, object]:
|
|
259
|
+
shown = {"run": run.rpartition("/")[2], "params": args, "status": PENDING}
|
|
260
|
+
return {
|
|
261
|
+
column: dim(shown.get(column, "")) for column in {*self.columns, *shown}
|
|
262
|
+
}
|
|
263
|
+
|
|
251
264
|
def set_cell(self, run: str, column: str, cell: object) -> None:
|
|
252
|
-
self.cells[run][
|
|
265
|
+
self.cells[run][column] = cell
|
|
253
266
|
table = self.query_one(DataTable)
|
|
254
|
-
if run in table.rows:
|
|
267
|
+
if run in table.rows and column in self.columns:
|
|
255
268
|
table.update_cell(run, column, cell, update_width=True)
|
|
256
269
|
|
|
257
270
|
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool:
|
|
@@ -275,6 +288,31 @@ class RunApp(App[None]):
|
|
|
275
288
|
wait = format_elapsed(max(0, due - now))
|
|
276
289
|
outcome = f"{FAILED} exited {RETRY_EXIT_CODE}, retry in {wait}"
|
|
277
290
|
self.set_cell(run, "status", Text(outcome, style="yellow"))
|
|
291
|
+
if self.started:
|
|
292
|
+
self.measure()
|
|
293
|
+
|
|
294
|
+
def measure(self) -> None:
|
|
295
|
+
if not self.sampling:
|
|
296
|
+
self.sampling = True
|
|
297
|
+
self.sample()
|
|
298
|
+
|
|
299
|
+
@work
|
|
300
|
+
async def sample(self) -> None:
|
|
301
|
+
try:
|
|
302
|
+
for run in list(self.started):
|
|
303
|
+
usage = await run_usage(self.folders[run], True)
|
|
304
|
+
if run in self.started:
|
|
305
|
+
self.show_usage(run, usage)
|
|
306
|
+
while self.unmeasured:
|
|
307
|
+
run = next(iter(self.unmeasured))
|
|
308
|
+
del self.unmeasured[run]
|
|
309
|
+
self.show_usage(run, await run_usage(self.folders[run], False))
|
|
310
|
+
finally:
|
|
311
|
+
self.sampling = False
|
|
312
|
+
|
|
313
|
+
def show_usage(self, run: str, usage: dict[str, int | None]) -> None:
|
|
314
|
+
self.set_cell(run, "memory", format_bytes(usage["memory"]))
|
|
315
|
+
self.set_cell(run, "size", format_bytes(usage["size"]))
|
|
278
316
|
|
|
279
317
|
def spin(self) -> None:
|
|
280
318
|
self.frame = (self.frame + 1) % len(SPINNER)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from labnotes.models import Start
|
|
2
|
+
from labnotes.tui.config import RUN_COLUMNS
|
|
2
3
|
from labnotes.tui.run_app import RunApp
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
def run_tui(start: Start, title: str) -> list[dict[str, object]]:
|
|
6
|
-
app = RunApp(start, title)
|
|
7
|
+
app = RunApp(start, title, RUN_COLUMNS)
|
|
7
8
|
app.run()
|
|
8
9
|
if app.error is not None:
|
|
9
10
|
raise app.error
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from dirsql import DirSQL
|
|
4
|
+
|
|
5
|
+
from labnotes.utils.tree_rss import tree_rss
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def field(name: str) -> str:
|
|
9
|
+
return f"(SELECT content ->> '{name}' FROM './run.json' WHERE json_valid(content))"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
USAGE = (
|
|
13
|
+
"SELECT (SELECT SUM(size) FROM './**') AS size,"
|
|
14
|
+
f" {field('pid')} AS pid, {field('pid_created')} AS created,"
|
|
15
|
+
f" coalesce({field('ended')}, {field('exit_code')}, {field('error')}) AS done"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def run_usage(folder: Path, running: bool) -> dict[str, int | None]:
|
|
20
|
+
# Run folders are often gitignored (DVC, large outputs); size them anyway.
|
|
21
|
+
[row] = await DirSQL(str(folder), tables=[], no_ignore=True).query(USAGE)
|
|
22
|
+
pid, created = row["pid"], row["created"]
|
|
23
|
+
memory = None
|
|
24
|
+
if (
|
|
25
|
+
running
|
|
26
|
+
and row["done"] is None
|
|
27
|
+
and isinstance(pid, int)
|
|
28
|
+
and isinstance(created, int | float)
|
|
29
|
+
):
|
|
30
|
+
memory = tree_rss(pid, created)
|
|
31
|
+
return {"memory": memory, "size": row["size"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
|
|
5
|
+
# run.json round-trips create_time through JSON, and psutil derives it from
|
|
6
|
+
# boot time, so allow a little slack when matching.
|
|
7
|
+
SAME_START_SECONDS = 0.01
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def tree_rss(pid: int, created: float) -> int | None:
|
|
11
|
+
try:
|
|
12
|
+
root = psutil.Process(pid)
|
|
13
|
+
if abs(root.create_time() - created) > SAME_START_SECONDS:
|
|
14
|
+
return None
|
|
15
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
16
|
+
return None
|
|
17
|
+
try:
|
|
18
|
+
tree = [root, *root.children(recursive=True)]
|
|
19
|
+
except psutil.NoSuchProcess:
|
|
20
|
+
return None
|
|
21
|
+
except psutil.AccessDenied:
|
|
22
|
+
tree = [root]
|
|
23
|
+
sizes = []
|
|
24
|
+
for process in tree:
|
|
25
|
+
with contextlib.suppress(psutil.NoSuchProcess, psutil.AccessDenied):
|
|
26
|
+
sizes.append(process.memory_info().rss)
|
|
27
|
+
return sum(sizes) if sizes else None
|
|
@@ -69,3 +69,15 @@ path = "src/labnotes/runner/run_experiment.py"
|
|
|
69
69
|
rules = ["mutation"]
|
|
70
70
|
lines = [184, 263]
|
|
71
71
|
reason = "a retry is only scheduled below the budget and each one adds one, so tried never passes retries: >= equals ==; both sides are small ints, which CPython caches, so is equals =="
|
|
72
|
+
|
|
73
|
+
[[python.exempt]]
|
|
74
|
+
path = "src/labnotes/runner/exit_signal.py"
|
|
75
|
+
rules = ["mutation"]
|
|
76
|
+
lines = [10]
|
|
77
|
+
reason = "exit_code is non-negative here; when bit 7 is set, - 128 equals ^ 128, and when it is clear ^ 128 lands above NSIG like a non-positive difference"
|
|
78
|
+
|
|
79
|
+
[[python.exempt]]
|
|
80
|
+
path = "src/labnotes/experiments.py"
|
|
81
|
+
rules = ["mutation"]
|
|
82
|
+
lines = [109]
|
|
83
|
+
reason = "click hands back the \"json\" choice literal itself, so is not equals !="
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|