labnotes 0.0.122__tar.gz → 0.0.124__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.122 → labnotes-0.0.124}/PKG-INFO +1 -1
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/context.md +5 -10
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/daemon_app.py +2 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/remote_start.py +2 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/experiments.py +24 -2
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/models.py +0 -4
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/run_control.py +1 -3
- labnotes-0.0.124/src/labnotes/runner/check_args.py +15 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/describe_event.py +0 -10
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/execute_run.py +5 -2
- labnotes-0.0.124/src/labnotes/runner/flag_name.py +4 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/prepare_experiment.py +4 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/run_experiment.py +3 -51
- labnotes-0.0.124/src/labnotes/runner/shell_command.py +12 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/run_app.py +9 -140
- {labnotes-0.0.122 → labnotes-0.0.124}/testing-conventions.toml +6 -0
- labnotes-0.0.122/src/labnotes/runner/shell_command.py +0 -9
- {labnotes-0.0.122 → labnotes-0.0.124}/.gitignore +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/CHANGELOG.md +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/MIGRATIONS.md +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/README.md +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/pyproject.toml +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.122/src/labnotes/utils → labnotes-0.0.124/src/labnotes/daemon}/__init__.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/finalize_lost_runs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/last_stderr.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/grid_cell.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/init.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/is_non_negative_number.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/log.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/log_path.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/memory_interval.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/parse_subset.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/pivot_runs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/retries.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.122/src/labnotes/tui → labnotes-0.0.124/src/labnotes/runner}/__init__.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/check_subset.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/exit_signal.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/in_subset.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/invocation_meta.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/record_memory.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/runs_view_command.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/server.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/read_memory.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/read_run_file.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/runs_columns.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.122/src/labnotes/runner → labnotes-0.0.124/src/labnotes/tui}/__init__.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/check_view_response.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/config.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/format_cell.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/native_rows.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/run_screen.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/runs_view.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/view_column.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/tui/view_process.py +0 -0
- {labnotes-0.0.122/src/labnotes/daemon → labnotes-0.0.124/src/labnotes/utils}/__init__.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/utils/format_bytes.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/utils/process_alive.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/utils/run_usage.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/utils/tree_rss.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/utils/tree_usage.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/view/__init__.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/view/answer.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/view/build.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/view/respond.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/view/serve.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.122 → labnotes-0.0.124}/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.124
|
|
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>
|
|
@@ -132,14 +132,10 @@ sets the seconds between samples; 0 turns it off.
|
|
|
132
132
|
|
|
133
133
|
In a terminal, `run` shows a live table of runs (status, time, memory and
|
|
134
134
|
folder size) and stays
|
|
135
|
-
open when they finish. Failed runs show their status in red
|
|
136
|
-
runs it again under a new id, with `retry_of` set. `j` and `k` move the cursor, `g` and `G` jump to the
|
|
135
|
+
open when they finish. Failed runs show their status in red. `j` and `k` move the cursor, `g` and `G` jump to the
|
|
137
136
|
first and last row. Runs not started yet show grayed as `pending`, and as
|
|
138
|
-
`skipped` once stopped.
|
|
139
|
-
|
|
140
|
-
drops it, Escape puts it back. Space deselects a pending run so it won't run; Space again puts it back at
|
|
141
|
-
the end of the queue. `/` filters rows by params text; Enter keeps the filter,
|
|
142
|
-
Escape clears it. `D` deselects every pending run the filter shows. `x` stops the
|
|
137
|
+
`skipped` once stopped. `/` filters rows by params text; Enter keeps the filter,
|
|
138
|
+
Escape clears it. `x` stops the
|
|
143
139
|
running run under the cursor; `x` again kills it. `+` and
|
|
144
140
|
`-` change how many run at once; lowering it lets running runs
|
|
145
141
|
finish. Enter on a run follows its `stdout.log` and `stderr.log` together, as
|
|
@@ -180,9 +176,8 @@ going. Elsewhere, it prints a line per event instead.
|
|
|
180
176
|
waits for a daemon when none is up. `--json` prints one JSON object per line.
|
|
181
177
|
Following doesn't keep the daemon up.
|
|
182
178
|
- `labnotes run control NAME ACTION [ARG]` sends one live-view control to a
|
|
183
|
-
running experiment: `kill
|
|
184
|
-
|
|
185
|
-
`set_concurrency` to a number, `interrupt` or `finish`.
|
|
179
|
+
running experiment: `kill` a run, `set_concurrency` to a number,
|
|
180
|
+
`interrupt` or `finish`.
|
|
186
181
|
- A second `run` of the same experiment fails while the first is going.
|
|
187
182
|
- It exits 0 even if runs fail; the summary line counts failures. A run that
|
|
188
183
|
was retried counts as retried, not failed. If it can't
|
|
@@ -79,6 +79,7 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
|
|
|
79
79
|
None if path is None else Path(str(path)),
|
|
80
80
|
body.get("concurrency"), # type: ignore[arg-type]
|
|
81
81
|
body.get("only"), # type: ignore[arg-type]
|
|
82
|
+
body.get("args"), # type: ignore[arg-type]
|
|
82
83
|
)
|
|
83
84
|
except ValueError as error:
|
|
84
85
|
return refuse(400, str(error))
|
|
@@ -105,6 +106,7 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
|
|
|
105
106
|
listen,
|
|
106
107
|
body.get("timeout"), # type: ignore[arg-type]
|
|
107
108
|
only=body.get("only"), # type: ignore[arg-type]
|
|
109
|
+
args=body.get("args"), # type: ignore[arg-type]
|
|
108
110
|
)
|
|
109
111
|
# Clients hear why the session ended; the daemon keeps serving.
|
|
110
112
|
except Exception as failure: # noqa: BLE001
|
|
@@ -17,6 +17,7 @@ def remote_start(
|
|
|
17
17
|
concurrency: int | None,
|
|
18
18
|
timeout: float | None = None,
|
|
19
19
|
only: dict[str, str] | None = None,
|
|
20
|
+
args: list[str] | None = None,
|
|
20
21
|
) -> Start:
|
|
21
22
|
async def start(report: Report, listen: Listen) -> list[dict[str, object]]:
|
|
22
23
|
try:
|
|
@@ -48,6 +49,7 @@ def remote_start(
|
|
|
48
49
|
"concurrency": concurrency,
|
|
49
50
|
"timeout": timeout,
|
|
50
51
|
"only": only,
|
|
52
|
+
"args": args,
|
|
51
53
|
},
|
|
52
54
|
)
|
|
53
55
|
if reply.status_code != 202:
|
|
@@ -17,12 +17,25 @@ from labnotes.tui.browse_tui import browse_tui
|
|
|
17
17
|
from labnotes.tui.run_tui import run_tui
|
|
18
18
|
|
|
19
19
|
BYTES = ("memory", "size")
|
|
20
|
+
PASSTHROUGH = "labnotes.passthrough"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PassthroughCommand(click.Command):
|
|
24
|
+
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
|
|
25
|
+
if "--" in args:
|
|
26
|
+
at = args.index("--")
|
|
27
|
+
ctx.meta[PASSTHROUGH] = args[at + 1 :]
|
|
28
|
+
args = args[:at]
|
|
29
|
+
return super().parse_args(ctx, args)
|
|
20
30
|
|
|
21
31
|
|
|
22
32
|
@click.command(
|
|
33
|
+
cls=PassthroughCommand,
|
|
23
34
|
help="List experiments, show one, list its runs, or run it."
|
|
24
35
|
" `new [NAME]` creates one. With run, --PARAM=VALUE runs only the"
|
|
25
|
-
" matrix cells where that param has that value
|
|
36
|
+
" matrix cells where that param has that value, and anything after --"
|
|
37
|
+
" is appended to every run's command and recorded in run.json, so never"
|
|
38
|
+
" pass secrets there.",
|
|
26
39
|
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
|
|
27
40
|
)
|
|
28
41
|
@click.argument("name", required=False)
|
|
@@ -63,6 +76,7 @@ def experiments(
|
|
|
63
76
|
experiment_config: Path | None,
|
|
64
77
|
timeout: int | None,
|
|
65
78
|
) -> None:
|
|
79
|
+
passthrough: list[str] | None = ctx.meta.get(PASSTHROUGH)
|
|
66
80
|
config = load_config(ctx)
|
|
67
81
|
if config is None:
|
|
68
82
|
raise click.ClickException(
|
|
@@ -82,7 +96,13 @@ def experiments(
|
|
|
82
96
|
except ValueError as error:
|
|
83
97
|
raise click.UsageError(str(error)) from error
|
|
84
98
|
start = remote_start(
|
|
85
|
-
config,
|
|
99
|
+
config,
|
|
100
|
+
name,
|
|
101
|
+
experiment_config,
|
|
102
|
+
concurrency,
|
|
103
|
+
timeout,
|
|
104
|
+
only,
|
|
105
|
+
passthrough or None,
|
|
86
106
|
)
|
|
87
107
|
try:
|
|
88
108
|
if sys.stdout.isatty() and sys.stderr.isatty():
|
|
@@ -101,6 +121,8 @@ def experiments(
|
|
|
101
121
|
passed = len(results) - failed - retried
|
|
102
122
|
click.echo(f"{passed} passed, {failed} failed.{again}")
|
|
103
123
|
return
|
|
124
|
+
if passthrough is not None:
|
|
125
|
+
raise click.UsageError("-- ARGS needs run.")
|
|
104
126
|
if ctx.args:
|
|
105
127
|
raise click.UsageError(f"Unexpected {ctx.args[0]}. --PARAM=VALUE needs run.")
|
|
106
128
|
if action not in (None, "runs"):
|
|
@@ -14,13 +14,9 @@ Report = Callable[[dict[str, object]], None]
|
|
|
14
14
|
@dataclass(frozen=True)
|
|
15
15
|
class Controls:
|
|
16
16
|
interrupt: Callable[[], None]
|
|
17
|
-
run_next: Callable[[str], None]
|
|
18
17
|
set_concurrency: Callable[[int], None]
|
|
19
|
-
toggle: Callable[[str], None]
|
|
20
18
|
kill: Callable[[str], None]
|
|
21
|
-
retry: Callable[[str], None]
|
|
22
19
|
finish: Callable[[], None]
|
|
23
|
-
move: Callable[[str, int], None]
|
|
24
20
|
|
|
25
21
|
|
|
26
22
|
Listen = Callable[[Controls], None]
|
|
@@ -18,9 +18,7 @@ PARAMS = {
|
|
|
18
18
|
name="control",
|
|
19
19
|
help=(
|
|
20
20
|
"Send one control to an experiment's runs, as the live view's keys do. "
|
|
21
|
-
"kill
|
|
22
|
-
"in the queue, from 0; set_concurrency a number; "
|
|
23
|
-
"interrupt and finish nothing."
|
|
21
|
+
"kill takes a run; set_concurrency a number; interrupt and finish nothing."
|
|
24
22
|
),
|
|
25
23
|
)
|
|
26
24
|
@click.argument("name")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
|
|
3
|
+
from labnotes.runner.flag_name import flag_name
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def check_args(params: dict[str, list[object]], command: str, args: list[str]) -> None:
|
|
7
|
+
taken = {flag_name(token) for token in shlex.split(command)}
|
|
8
|
+
for arg in args:
|
|
9
|
+
name = flag_name(arg)
|
|
10
|
+
if name is None:
|
|
11
|
+
continue
|
|
12
|
+
if name in params:
|
|
13
|
+
raise ValueError(f"--{name} is a param. Pick it with --{name}=VALUE.")
|
|
14
|
+
if name in taken:
|
|
15
|
+
raise ValueError(f"--{name} is already in the command.")
|
|
@@ -14,16 +14,6 @@ def describe_event(event: dict[str, object]) -> str:
|
|
|
14
14
|
case "ended":
|
|
15
15
|
outcome = event["error"] or f"exited {event['exit_code']}"
|
|
16
16
|
return f"{event['run']} {outcome}"
|
|
17
|
-
case "queued":
|
|
18
|
-
return f"{event['run']} runs next."
|
|
19
|
-
case "moved":
|
|
20
|
-
pending: list[str] = event["pending"] # type: ignore[assignment]
|
|
21
|
-
place = pending.index(str(event["run"])) + 1
|
|
22
|
-
return f"{event['run']} moved to {place} of {len(pending)} in the queue."
|
|
23
|
-
case "deselected":
|
|
24
|
-
return f"{event['run']} deselected."
|
|
25
|
-
case "reselected":
|
|
26
|
-
return f"{event['run']} back in the queue."
|
|
27
17
|
case "stopping run":
|
|
28
18
|
return f"Stopping {event['run']}."
|
|
29
19
|
case "killing run":
|
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
import contextlib
|
|
3
3
|
import os
|
|
4
4
|
import time
|
|
5
|
-
from collections.abc import Callable
|
|
5
|
+
from collections.abc import Callable, Sequence
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
import psutil
|
|
@@ -31,10 +31,11 @@ async def execute_run(
|
|
|
31
31
|
stop: Callable[[], None] = lambda: None,
|
|
32
32
|
stopped: Callable[[], bool] = lambda: False,
|
|
33
33
|
meta: dict[str, object] | None = None,
|
|
34
|
+
args: Sequence[str] = (),
|
|
34
35
|
*,
|
|
35
36
|
memory_interval: float,
|
|
36
37
|
) -> dict[str, object]:
|
|
37
|
-
resolved = shell_command(command, folder, params)
|
|
38
|
+
resolved = shell_command(command, folder, params, args)
|
|
38
39
|
run: dict[str, object] = {
|
|
39
40
|
"params": params,
|
|
40
41
|
"command": resolved,
|
|
@@ -45,6 +46,8 @@ async def execute_run(
|
|
|
45
46
|
"ended_by": None,
|
|
46
47
|
"signal": None,
|
|
47
48
|
}
|
|
49
|
+
if args:
|
|
50
|
+
run["args"] = list(args)
|
|
48
51
|
if retry_of is not None:
|
|
49
52
|
run["retry_of"] = retry_of
|
|
50
53
|
if meta is not None:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
3
|
from labnotes.models import ExperimentConfig, LabnotesConfig
|
|
4
|
+
from labnotes.runner.check_args import check_args
|
|
4
5
|
from labnotes.runner.check_subset import check_subset
|
|
5
6
|
from labnotes.runner.read_experiment_config import read_experiment_config
|
|
6
7
|
from labnotes.tables.file_text import file_text
|
|
@@ -12,6 +13,7 @@ async def prepare_experiment(
|
|
|
12
13
|
experiment_config: Path | None,
|
|
13
14
|
concurrency: int | None,
|
|
14
15
|
only: dict[str, str] | None = None,
|
|
16
|
+
args: list[str] | None = None,
|
|
15
17
|
) -> tuple[ExperimentConfig, int]:
|
|
16
18
|
if config.experiment not in (None, name):
|
|
17
19
|
raise ValueError(
|
|
@@ -33,4 +35,6 @@ async def prepare_experiment(
|
|
|
33
35
|
)
|
|
34
36
|
if only:
|
|
35
37
|
check_subset(experiment.params, only)
|
|
38
|
+
if args:
|
|
39
|
+
check_args(experiment.params, experiment.command, args)
|
|
36
40
|
return experiment, experiment.concurrency if concurrency is None else concurrency
|
|
@@ -38,6 +38,7 @@ async def run_experiment(
|
|
|
38
38
|
timeout: float | None = None,
|
|
39
39
|
retry_seconds: float = RETRY_SECONDS,
|
|
40
40
|
only: dict[str, str] | None = None,
|
|
41
|
+
args: list[str] | None = None,
|
|
41
42
|
) -> list[dict[str, object]]:
|
|
42
43
|
meta = None
|
|
43
44
|
if experiment.meta is not None:
|
|
@@ -128,43 +129,6 @@ async def run_experiment(
|
|
|
128
129
|
report({"event": "stopping run", "run": run})
|
|
129
130
|
signal_run(run, signal.SIGTERM)
|
|
130
131
|
|
|
131
|
-
held: list[Entry] = []
|
|
132
|
-
|
|
133
|
-
def find(entries: list[Entry], run: str) -> Entry | None:
|
|
134
|
-
return next((entry for entry in entries if entry[0] == run), None)
|
|
135
|
-
|
|
136
|
-
def arrange(event: str, run: str) -> None:
|
|
137
|
-
pending = [id_ for id_, _ in plan]
|
|
138
|
-
deselected = [id_ for id_, _ in held]
|
|
139
|
-
report({"event": event, "run": run, "pending": pending, "held": deselected})
|
|
140
|
-
|
|
141
|
-
def run_next(run: str) -> None:
|
|
142
|
-
entry = find(plan, run)
|
|
143
|
-
if entry is not None:
|
|
144
|
-
plan.remove(entry)
|
|
145
|
-
plan.insert(0, entry)
|
|
146
|
-
arrange("queued", run)
|
|
147
|
-
|
|
148
|
-
def move(run: str, place: int) -> None:
|
|
149
|
-
entry = find(plan, run)
|
|
150
|
-
if entry is not None:
|
|
151
|
-
plan.remove(entry)
|
|
152
|
-
plan.insert(max(0, place), entry)
|
|
153
|
-
arrange("moved", run)
|
|
154
|
-
|
|
155
|
-
def toggle(run: str) -> None:
|
|
156
|
-
if stopping.is_set():
|
|
157
|
-
return
|
|
158
|
-
if (entry := find(plan, run)) is not None:
|
|
159
|
-
plan.remove(entry)
|
|
160
|
-
held.append(entry)
|
|
161
|
-
arrange("deselected", run)
|
|
162
|
-
elif (entry := find(held, run)) is not None:
|
|
163
|
-
held.remove(entry)
|
|
164
|
-
plan.append(entry)
|
|
165
|
-
arrange("reselected", run)
|
|
166
|
-
fill()
|
|
167
|
-
|
|
168
132
|
ended: dict[str, dict[str, object]] = {}
|
|
169
133
|
parents: dict[str, str] = {}
|
|
170
134
|
attempts: dict[str, int] = {}
|
|
@@ -179,15 +143,6 @@ async def run_experiment(
|
|
|
179
143
|
report({"event": "retried", "run": run, "retry": again, "params": ended[run]})
|
|
180
144
|
fill()
|
|
181
145
|
|
|
182
|
-
def retry(run: str) -> None:
|
|
183
|
-
if stopping.is_set() or run not in ended:
|
|
184
|
-
return
|
|
185
|
-
if run in waiting:
|
|
186
|
-
waiting.pop(run).cancel()
|
|
187
|
-
requeue(run, attempts.get(run, 0) + 1)
|
|
188
|
-
else:
|
|
189
|
-
requeue(run, 0)
|
|
190
|
-
|
|
191
146
|
def retry_later(run: str) -> None:
|
|
192
147
|
tried = attempts.get(run, 0)
|
|
193
148
|
if tried >= experiment.retries:
|
|
@@ -266,6 +221,7 @@ async def run_experiment(
|
|
|
266
221
|
stop=lambda: kill(run),
|
|
267
222
|
stopped=lambda: run in stopped,
|
|
268
223
|
meta=meta,
|
|
224
|
+
args=args or (),
|
|
269
225
|
memory_interval=experiment.memory_interval,
|
|
270
226
|
)
|
|
271
227
|
del running[run], trees[run]
|
|
@@ -277,11 +233,7 @@ async def run_experiment(
|
|
|
277
233
|
):
|
|
278
234
|
retry_later(run)
|
|
279
235
|
|
|
280
|
-
listen(
|
|
281
|
-
Controls(
|
|
282
|
-
interrupt, run_next, set_concurrency, toggle, kill, retry, finish, move
|
|
283
|
-
)
|
|
284
|
-
)
|
|
236
|
+
listen(Controls(interrupt, set_concurrency, kill, finish))
|
|
285
237
|
fill()
|
|
286
238
|
await done.wait()
|
|
287
239
|
await asyncio.gather(*started)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from labnotes.runner.param_args import param_args
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def shell_command(
|
|
9
|
+
command: str, folder: Path, params: dict[str, object], args: Sequence[str] = ()
|
|
10
|
+
) -> str:
|
|
11
|
+
filled = command.replace("{run_dir}", shlex.quote(str(folder)))
|
|
12
|
+
return f"{filled} {shlex.join([*param_args(params), *args])}".rstrip()
|
|
@@ -26,27 +26,15 @@ from labnotes.tui.runs_table import RunsTable
|
|
|
26
26
|
|
|
27
27
|
PENDING, PASSED, FAILED = "○", "✓", "✗"
|
|
28
28
|
SPINNER = "|/-\\"
|
|
29
|
-
SKIPPED
|
|
29
|
+
SKIPPED = "–"
|
|
30
30
|
VIEWING_ACTIONS = {"leave", "quit", "interrupt", "command_palette"}
|
|
31
|
-
GRABBED_ACTIONS = {"shift", "drop", "put_back"}
|
|
32
|
-
GRABBING_ACTIONS = {*GRABBED_ACTIONS, "grab", "quit", "interrupt", "command_palette"}
|
|
33
31
|
|
|
34
32
|
|
|
35
33
|
class RunApp(App[None]):
|
|
36
34
|
CSS = "#concurrency { dock: right; width: auto; padding: 0 1; }"
|
|
37
35
|
BINDINGS = [
|
|
38
36
|
Binding("ctrl+c", "interrupt", "Quit"),
|
|
39
|
-
Binding("n", "run_next", "Run next"),
|
|
40
|
-
Binding("m", "grab", "Move"),
|
|
41
|
-
Binding("j,down", "shift(1)", priority=True),
|
|
42
|
-
Binding("k,up", "shift(-1)", priority=True),
|
|
43
|
-
Binding("enter", "drop", "Drop", priority=True),
|
|
44
|
-
Binding("escape", "put_back", "Put back"),
|
|
45
|
-
Binding("space", "toggle_run", "Deselect"),
|
|
46
|
-
Binding("D", "deselect_shown", "Deselect shown"),
|
|
47
37
|
Binding("x", "kill_run", "Stop run"),
|
|
48
|
-
Binding("f", "hide_failed", "Hide failed"),
|
|
49
|
-
Binding("r", "retry", "Retry"),
|
|
50
38
|
Binding("slash", "filter", "Filter", key_display="/"),
|
|
51
39
|
Binding("escape", "leave", "Back"),
|
|
52
40
|
Binding("plus,equals_sign", "concurrency(1)", "More at once", key_display="+"),
|
|
@@ -61,18 +49,10 @@ class RunApp(App[None]):
|
|
|
61
49
|
self.results: list[dict[str, object]] = []
|
|
62
50
|
self.error: Exception | None = None
|
|
63
51
|
self.controls = Controls(
|
|
64
|
-
lambda: None,
|
|
65
|
-
lambda run: None,
|
|
66
|
-
lambda count: None,
|
|
67
|
-
lambda run: None,
|
|
68
|
-
lambda run: None,
|
|
69
|
-
lambda run: None,
|
|
70
|
-
lambda: None,
|
|
71
|
-
lambda run, place: None,
|
|
52
|
+
lambda: None, lambda count: None, lambda run: None, lambda: None
|
|
72
53
|
)
|
|
73
54
|
self.concurrency = 1
|
|
74
55
|
self.idle = False
|
|
75
|
-
self.next_run: str | None = None
|
|
76
56
|
self.ended: set[str] = set()
|
|
77
57
|
self.frame = 0
|
|
78
58
|
self.started: dict[str, float] = {}
|
|
@@ -82,11 +62,8 @@ class RunApp(App[None]):
|
|
|
82
62
|
self.failed: set[str] = set()
|
|
83
63
|
self.retried: set[str] = set()
|
|
84
64
|
self.waiting: dict[str, float] = {}
|
|
85
|
-
self.hiding = False
|
|
86
65
|
self.folders: dict[str, Path] = {}
|
|
87
66
|
self.viewing: str | None = None
|
|
88
|
-
self.grabbed: str | None = None
|
|
89
|
-
self.grabbed_from: list[str] = []
|
|
90
67
|
self.sampling = False
|
|
91
68
|
self.unmeasured: dict[str, None] = {}
|
|
92
69
|
|
|
@@ -143,14 +120,10 @@ class RunApp(App[None]):
|
|
|
143
120
|
case "adhoc", [only]:
|
|
144
121
|
self.view(only["run"]) # type: ignore[index]
|
|
145
122
|
case "started":
|
|
146
|
-
if run == self.grabbed:
|
|
147
|
-
self.action_put_back()
|
|
148
123
|
self.idle = False
|
|
149
124
|
self.pending.discard(run)
|
|
150
125
|
self.started[run] = time.monotonic()
|
|
151
126
|
self.folders[run] = Path(str(event["folder"]))
|
|
152
|
-
if run == self.next_run:
|
|
153
|
-
self.show("")
|
|
154
127
|
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
155
128
|
shown = {
|
|
156
129
|
"run": run.rpartition("/")[2],
|
|
@@ -175,21 +148,6 @@ class RunApp(App[None]):
|
|
|
175
148
|
self.redraw(table.cursor_row)
|
|
176
149
|
self.unmeasured[run] = None
|
|
177
150
|
self.measure()
|
|
178
|
-
case "queued":
|
|
179
|
-
self.reposition(event, run)
|
|
180
|
-
self.next_run = run
|
|
181
|
-
case "moved":
|
|
182
|
-
self.reposition(event, run)
|
|
183
|
-
case "deselected":
|
|
184
|
-
self.pending.discard(run)
|
|
185
|
-
self.set_cell(run, "status", dim(DESELECTED))
|
|
186
|
-
self.reorder(event, table.cursor_row)
|
|
187
|
-
self.show(describe_event(event))
|
|
188
|
-
case "reselected":
|
|
189
|
-
self.pending.add(run)
|
|
190
|
-
self.set_cell(run, "status", dim(PENDING))
|
|
191
|
-
self.reorder(event, table.cursor_row)
|
|
192
|
-
self.show(describe_event(event))
|
|
193
151
|
case "retrying":
|
|
194
152
|
self.waiting[run] = time.monotonic() + float(event["seconds"]) # type: ignore[arg-type]
|
|
195
153
|
self.tick()
|
|
@@ -233,25 +191,11 @@ class RunApp(App[None]):
|
|
|
233
191
|
again = f" {retried} retried." if retried else ""
|
|
234
192
|
self.show(f"{passed} passed, {failed} failed.{again} Ctrl-C to quit.")
|
|
235
193
|
|
|
236
|
-
def reposition(self, event: dict[str, object], run: str) -> None:
|
|
237
|
-
table = self.query_one(DataTable)
|
|
238
|
-
self.reorder(event, table.cursor_row)
|
|
239
|
-
table.move_cursor(row=table.get_row_index(run))
|
|
240
|
-
self.show(describe_event(event))
|
|
241
|
-
|
|
242
|
-
def reorder(self, event: dict[str, object], row: int) -> None:
|
|
243
|
-
self.release()
|
|
244
|
-
waiting: list[str] = [*event["pending"], *event["held"]] # type: ignore[misc]
|
|
245
|
-
for run in waiting:
|
|
246
|
-
self.cells[run] = self.cells.pop(run)
|
|
247
|
-
self.redraw(row)
|
|
248
|
-
|
|
249
194
|
def redraw(self, row: int) -> None:
|
|
250
195
|
table = self.query_one(DataTable)
|
|
251
196
|
table.clear()
|
|
252
197
|
for run, cells in self.cells.items():
|
|
253
|
-
|
|
254
|
-
if self.filter in str(cells.get("params", "")) and not hidden:
|
|
198
|
+
if self.filter in str(cells.get("params", "")):
|
|
255
199
|
table.add_row(
|
|
256
200
|
*(cells.get(c, "") for c in self.columns),
|
|
257
201
|
key=run,
|
|
@@ -274,10 +218,8 @@ class RunApp(App[None]):
|
|
|
274
218
|
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool:
|
|
275
219
|
if isinstance(self.screen, LeavePrompt):
|
|
276
220
|
return False
|
|
277
|
-
if self.
|
|
278
|
-
return
|
|
279
|
-
if action in GRABBED_ACTIONS:
|
|
280
|
-
return False
|
|
221
|
+
if action == "leave" and self.viewing is None:
|
|
222
|
+
return self.query_one(Input).display
|
|
281
223
|
return self.viewing is None or action in VIEWING_ACTIONS
|
|
282
224
|
|
|
283
225
|
def show_concurrency(self, event: dict[str, object]) -> None:
|
|
@@ -324,7 +266,6 @@ class RunApp(App[None]):
|
|
|
324
266
|
self.set_cell(run, "status", SPINNER[self.frame])
|
|
325
267
|
|
|
326
268
|
def show(self, text: str) -> None:
|
|
327
|
-
self.next_run = None
|
|
328
269
|
self.query_one("#status", Static).update(text)
|
|
329
270
|
|
|
330
271
|
def stop_runs(self) -> None:
|
|
@@ -358,84 +299,10 @@ class RunApp(App[None]):
|
|
|
358
299
|
cell = table.coordinate_to_cell_key(table.cursor_coordinate)
|
|
359
300
|
return str(cell.row_key.value)
|
|
360
301
|
|
|
361
|
-
def action_run_next(self) -> None:
|
|
362
|
-
if (run := self.cursor_run()) is not None:
|
|
363
|
-
self.controls.run_next(run)
|
|
364
|
-
|
|
365
|
-
def queue(self) -> list[str]:
|
|
366
|
-
return [run for run in self.cells if run in self.pending]
|
|
367
|
-
|
|
368
|
-
def action_grab(self) -> None:
|
|
369
|
-
if self.grabbed is not None:
|
|
370
|
-
self.action_drop()
|
|
371
|
-
return
|
|
372
|
-
run = self.cursor_run()
|
|
373
|
-
if run not in self.pending:
|
|
374
|
-
return
|
|
375
|
-
self.grabbed = run
|
|
376
|
-
self.grabbed_from = list(self.cells)
|
|
377
|
-
self.refresh_bindings()
|
|
378
|
-
self.show(f"Moving {run}. j/k moves it, Enter drops it, Escape puts it back.")
|
|
379
|
-
|
|
380
|
-
def action_shift(self, step: int) -> None:
|
|
381
|
-
run = str(self.grabbed)
|
|
382
|
-
queue = self.queue()
|
|
383
|
-
place = queue.index(run) + step
|
|
384
|
-
if place not in range(len(queue)):
|
|
385
|
-
return
|
|
386
|
-
order = list(self.cells)
|
|
387
|
-
here, there = order.index(run), order.index(queue[place])
|
|
388
|
-
order[here], order[there] = order[there], order[here]
|
|
389
|
-
self.cells = {key: self.cells[key] for key in order}
|
|
390
|
-
self.redraw_on(run)
|
|
391
|
-
|
|
392
|
-
def action_drop(self) -> None:
|
|
393
|
-
run = str(self.grabbed)
|
|
394
|
-
self.release()
|
|
395
|
-
self.show("")
|
|
396
|
-
self.controls.move(run, self.queue().index(run))
|
|
397
|
-
|
|
398
|
-
def action_put_back(self) -> None:
|
|
399
|
-
run = str(self.grabbed)
|
|
400
|
-
self.release()
|
|
401
|
-
places = {key: place for place, key in enumerate(self.grabbed_from)}
|
|
402
|
-
ordered = sorted(self.cells, key=lambda key: places.get(key, len(places)))
|
|
403
|
-
self.cells = {key: self.cells[key] for key in ordered}
|
|
404
|
-
self.redraw_on(run)
|
|
405
|
-
self.show("")
|
|
406
|
-
|
|
407
|
-
def release(self) -> None:
|
|
408
|
-
self.grabbed = None
|
|
409
|
-
self.refresh_bindings()
|
|
410
|
-
|
|
411
|
-
def redraw_on(self, run: str) -> None:
|
|
412
|
-
table = self.query_one(DataTable)
|
|
413
|
-
self.redraw(table.cursor_row)
|
|
414
|
-
table.move_cursor(row=table.get_row_index(run))
|
|
415
|
-
|
|
416
|
-
def action_toggle_run(self) -> None:
|
|
417
|
-
if (run := self.cursor_run()) is not None:
|
|
418
|
-
self.controls.toggle(run)
|
|
419
|
-
|
|
420
|
-
def action_hide_failed(self) -> None:
|
|
421
|
-
self.hiding = not self.hiding
|
|
422
|
-
self.redraw(self.query_one(DataTable).cursor_row)
|
|
423
|
-
self.show("Failed runs hidden." if self.hiding else "Failed runs shown.")
|
|
424
|
-
|
|
425
|
-
def action_retry(self) -> None:
|
|
426
|
-
if (run := self.cursor_run()) is not None and run in self.failed:
|
|
427
|
-
self.controls.retry(run)
|
|
428
|
-
|
|
429
302
|
def action_kill_run(self) -> None:
|
|
430
303
|
if (run := self.cursor_run()) is not None:
|
|
431
304
|
self.controls.kill(run)
|
|
432
305
|
|
|
433
|
-
def action_deselect_shown(self) -> None:
|
|
434
|
-
shown = [str(key.value) for key in self.query_one(DataTable).rows]
|
|
435
|
-
for run in shown:
|
|
436
|
-
if run in self.pending:
|
|
437
|
-
self.controls.toggle(run)
|
|
438
|
-
|
|
439
306
|
def action_concurrency(self, change: int) -> None:
|
|
440
307
|
self.controls.set_concurrency(self.concurrency + change)
|
|
441
308
|
|
|
@@ -443,10 +310,10 @@ class RunApp(App[None]):
|
|
|
443
310
|
field = self.query_one(Input)
|
|
444
311
|
field.display = True
|
|
445
312
|
field.focus()
|
|
313
|
+
self.refresh_bindings()
|
|
446
314
|
|
|
447
315
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
448
|
-
|
|
449
|
-
self.view(str(event.row_key.value))
|
|
316
|
+
self.view(str(event.row_key.value))
|
|
450
317
|
|
|
451
318
|
def view(self, run: str) -> None:
|
|
452
319
|
self.viewing = run
|
|
@@ -481,6 +348,7 @@ class RunApp(App[None]):
|
|
|
481
348
|
field.value = ""
|
|
482
349
|
field.display = False
|
|
483
350
|
self.query_one(DataTable).focus()
|
|
351
|
+
self.refresh_bindings()
|
|
484
352
|
|
|
485
353
|
def on_input_changed(self, event: Input.Changed) -> None:
|
|
486
354
|
self.filter = event.value
|
|
@@ -489,3 +357,4 @@ class RunApp(App[None]):
|
|
|
489
357
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
490
358
|
event.input.display = bool(event.value)
|
|
491
359
|
self.query_one(DataTable).focus()
|
|
360
|
+
self.refresh_bindings()
|
|
@@ -81,3 +81,9 @@ path = "src/labnotes/experiments.py"
|
|
|
81
81
|
rules = ["mutation"]
|
|
82
82
|
lines = [120]
|
|
83
83
|
reason = "click hands back the \"json\" choice literal itself, so is not equals !="
|
|
84
|
+
|
|
85
|
+
[[python.exempt]]
|
|
86
|
+
path = "src/labnotes/tui/run_app.py"
|
|
87
|
+
rules = ["mutation"]
|
|
88
|
+
lines = [221]
|
|
89
|
+
reason = "the leave action name is the string literal from the Binding, which CPython interns, so is equals =="
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import shlex
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from labnotes.runner.param_args import param_args
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def shell_command(command: str, folder: Path, params: dict[str, object]) -> str:
|
|
8
|
-
filled = command.replace("{run_dir}", shlex.quote(str(folder)))
|
|
9
|
-
return f"{filled} {shlex.join(param_args(params))}".rstrip()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|