labnotes 0.2.17__tar.gz → 0.2.19__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.2.17 → labnotes-0.2.19}/PKG-INFO +1 -1
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/daemon/server.py +2 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/experiments.py +79 -16
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/models.py +2 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/retries.py +1 -1
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/describe_event.py +4 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/prepare_experiment.py +6 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/run_experiment.py +18 -8
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/start_experiment.py +12 -1
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/list_resumable_runs.py +6 -1
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/leave_prompt.py +18 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/run_app.py +129 -8
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/run_tui.py +9 -2
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/runs_browser.py +14 -20
- {labnotes-0.2.17 → labnotes-0.2.19}/.gitignore +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/CHANGELOG.md +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/MIGRATIONS.md +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/README.md +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/conftest.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/pyproject.toml +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/__init__.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/__main__.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/cli.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/command_line_group.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/config_migration.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/context.md +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/copy_experiment.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/create_app.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/create_note.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/daemon/client.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/daemon/command.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/daemon/ensure.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/daemon/marker.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/data_html.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/experiment_definition.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/format_started.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/format_value.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/get_state.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/grid_cell.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/init.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/is_non_negative_number.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/latest_log.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/load_config.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/load_run.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/log.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/log_path.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/memory_interval.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/note_path.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/output_html.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/param_names.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/parse_param_value.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/parse_params.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/parse_subset.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/pivot_runs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/read_config.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/read_path.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/run_columns.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/run_output.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/cgroup_memory.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/check_args.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/check_hook_placeholders.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/check_subset.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/claim_run.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/command_flags.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/container_cgroup.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/container_memory.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/continue_run.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/docker_output.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/exit_signal.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/fill_dirs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/fill_experiment_env.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/finalize_lost_runs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/first_invocation.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/flag_name.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/group_args.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/hold_experiment.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/in_subset.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/invocation_hook.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/invocation_meta.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/override_flags.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/placeholders.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/project_snapshot.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/record_memory.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/run_git.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/run_locked.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/run_state.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/untracked_diff.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/runs_view_command.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/save_note.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/server.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/set_state.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/state_path.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/read_memory.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/read_run_file.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/runs_columns.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/check_view_response.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/config.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/format_cell.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/guard_view.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/load_view.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/native_rows.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/run_panel.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/runs_view.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/table_columns.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/table_rows.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/view_column.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/view_pane.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/view_process.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/tui/view_switcher.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/utils/__init__.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/utils/format_bytes.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/utils/process_alive.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/utils/run_usage.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/utils/tree_rss.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/utils/tree_usage.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/view/__init__.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/view/answer.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/view/build.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/view/changed.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/view/experiment.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/view/respond.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/view/serve.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/view_specs.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/testing-conventions.toml +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/tests/conftest.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/tests/e2e/conftest.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/tests/integration/conftest.py +0 -0
- {labnotes-0.2.17 → labnotes-0.2.19}/uv.lock +0 -0
|
@@ -61,6 +61,8 @@ class ExperimentDaemon:
|
|
|
61
61
|
cast(str | None, options.get("teardown")),
|
|
62
62
|
cast(str | None, options.get("command_line")),
|
|
63
63
|
cast(str | None, options.get("run_id")),
|
|
64
|
+
cast(int | None, options.get("retries")),
|
|
65
|
+
bool(options.get("replace_capped")),
|
|
64
66
|
)
|
|
65
67
|
path = str(selected.run_state.path)
|
|
66
68
|
invocation = Invocation()
|
|
@@ -7,7 +7,9 @@ import click
|
|
|
7
7
|
from labnotes.command_line_group import COMMAND_LINE
|
|
8
8
|
from labnotes.create_experiment import create_experiment
|
|
9
9
|
from labnotes.daemon.command import command as daemon_command
|
|
10
|
+
from labnotes.daemon.ensure import ensure_daemon
|
|
10
11
|
from labnotes.runner.start_experiment import start_experiment
|
|
12
|
+
from labnotes.runner.run_state import RunState
|
|
11
13
|
from labnotes.utils.format_bytes import format_bytes
|
|
12
14
|
from labnotes.load_config import load_config
|
|
13
15
|
from labnotes.parse_params import parse_params
|
|
@@ -58,7 +60,23 @@ class PassthroughCommand(click.Command):
|
|
|
58
60
|
help="With run: how many runs at once. Overrides experiment.toml and"
|
|
59
61
|
" .labnotes.toml; 1 if none sets it.",
|
|
60
62
|
)
|
|
61
|
-
@click.option(
|
|
63
|
+
@click.option(
|
|
64
|
+
"--run-id",
|
|
65
|
+
help="With run: restart only this failed run by id, even if out of retries.",
|
|
66
|
+
)
|
|
67
|
+
@click.option(
|
|
68
|
+
"--retries",
|
|
69
|
+
type=click.IntRange(min=0),
|
|
70
|
+
help="With run: how many times a failed run is tried again, on exit 75 or,"
|
|
71
|
+
" with continues, by a later run. Overrides experiment.toml and"
|
|
72
|
+
" .labnotes.toml; 3 if none sets it.",
|
|
73
|
+
)
|
|
74
|
+
@click.option(
|
|
75
|
+
"--replace-capped",
|
|
76
|
+
is_flag=True,
|
|
77
|
+
help="With run: start a fresh run for a cell whose run is out of retries."
|
|
78
|
+
" Unset, that cell stays short.",
|
|
79
|
+
)
|
|
62
80
|
@click.option(
|
|
63
81
|
"--headless",
|
|
64
82
|
is_flag=True,
|
|
@@ -122,6 +140,8 @@ def experiments(
|
|
|
122
140
|
output_format: str | None,
|
|
123
141
|
concurrency: int | None,
|
|
124
142
|
run_id: str | None,
|
|
143
|
+
retries: int | None,
|
|
144
|
+
replace_capped: bool,
|
|
125
145
|
headless: bool,
|
|
126
146
|
experiment_config: Path | None,
|
|
127
147
|
timeout: int | None,
|
|
@@ -169,24 +189,61 @@ def experiments(
|
|
|
169
189
|
only = parse_subset(ctx.args)
|
|
170
190
|
except ValueError as error:
|
|
171
191
|
raise click.UsageError(str(error)) from error
|
|
172
|
-
start = start_experiment(
|
|
173
|
-
config,
|
|
174
|
-
name,
|
|
175
|
-
experiment_config,
|
|
176
|
-
concurrency,
|
|
177
|
-
timeout,
|
|
178
|
-
only,
|
|
179
|
-
passthrough or None,
|
|
180
|
-
setup,
|
|
181
|
-
teardown,
|
|
182
|
-
ctx.meta.get(COMMAND_LINE),
|
|
183
|
-
run_id,
|
|
184
|
-
)
|
|
185
192
|
try:
|
|
186
193
|
if not headless and sys.stdout.isatty() and sys.stderr.isatty():
|
|
187
|
-
|
|
194
|
+
asyncio.run(ensure_daemon(config, name))
|
|
195
|
+
reply = daemon_command(
|
|
196
|
+
config,
|
|
197
|
+
name,
|
|
198
|
+
{
|
|
199
|
+
"command": "restart" if run_id else "start",
|
|
200
|
+
"options": {
|
|
201
|
+
"experiment_config": str(experiment_config)
|
|
202
|
+
if experiment_config
|
|
203
|
+
else None,
|
|
204
|
+
"concurrency": concurrency,
|
|
205
|
+
"timeout": timeout,
|
|
206
|
+
"only": only,
|
|
207
|
+
"args": passthrough or None,
|
|
208
|
+
"setup": setup,
|
|
209
|
+
"teardown": teardown,
|
|
210
|
+
"command_line": ctx.meta.get(COMMAND_LINE),
|
|
211
|
+
"run_id": run_id,
|
|
212
|
+
"retries": retries,
|
|
213
|
+
"replace_capped": replace_capped,
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
if not reply.get("ok"):
|
|
218
|
+
raise ValueError(
|
|
219
|
+
str(reply.get("error", "Daemon rejected start"))
|
|
220
|
+
)
|
|
221
|
+
run_tui(
|
|
222
|
+
None,
|
|
223
|
+
name,
|
|
224
|
+
config,
|
|
225
|
+
experiment_config,
|
|
226
|
+
daemon_journal=RunState(Path(str(reply["journal"]))),
|
|
227
|
+
)
|
|
228
|
+
return
|
|
188
229
|
else:
|
|
189
|
-
results = run_headless(
|
|
230
|
+
results = run_headless(
|
|
231
|
+
start_experiment(
|
|
232
|
+
config,
|
|
233
|
+
name,
|
|
234
|
+
experiment_config,
|
|
235
|
+
concurrency,
|
|
236
|
+
timeout,
|
|
237
|
+
only,
|
|
238
|
+
passthrough or None,
|
|
239
|
+
setup,
|
|
240
|
+
teardown,
|
|
241
|
+
ctx.meta.get(COMMAND_LINE),
|
|
242
|
+
run_id,
|
|
243
|
+
retries,
|
|
244
|
+
replace_capped,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
190
247
|
except ValueError as error:
|
|
191
248
|
raise click.ClickException(str(error)) from error
|
|
192
249
|
superseded = {r["retry_of"] for r in results if "retry_of" in r}
|
|
@@ -204,6 +261,8 @@ def experiments(
|
|
|
204
261
|
output_format is not None
|
|
205
262
|
or concurrency is not None
|
|
206
263
|
or run_id is not None
|
|
264
|
+
or retries is not None
|
|
265
|
+
or replace_capped
|
|
207
266
|
or experiment_config is not None
|
|
208
267
|
or timeout is not None
|
|
209
268
|
or setup is not None
|
|
@@ -226,6 +285,10 @@ def experiments(
|
|
|
226
285
|
raise click.UsageError("-- ARGS needs run.")
|
|
227
286
|
if run_id is not None:
|
|
228
287
|
raise click.UsageError("--run-id needs run.")
|
|
288
|
+
if retries is not None:
|
|
289
|
+
raise click.UsageError("--retries needs run.")
|
|
290
|
+
if replace_capped:
|
|
291
|
+
raise click.UsageError("--replace-capped needs run.")
|
|
229
292
|
if ctx.args:
|
|
230
293
|
raise click.UsageError(f"Unexpected {ctx.args[0]}. --PARAM=VALUE needs run.")
|
|
231
294
|
if action not in (None, "runs"):
|
|
@@ -33,6 +33,10 @@ def describe_event(event: dict[str, object]) -> str:
|
|
|
33
33
|
case "gave up":
|
|
34
34
|
tried = f"after {event['retries']} retries"
|
|
35
35
|
return f"{event['run']} gave up (exit {RETRY_EXIT_CODE}) {tried}."
|
|
36
|
+
case "capped":
|
|
37
|
+
cells = len(event["runs"]) # type: ignore[arg-type]
|
|
38
|
+
noun = "cell" if cells == 1 else "cells"
|
|
39
|
+
return f"{cells} {noun} gave up after {event['retries']} retries."
|
|
36
40
|
case "retry cancelled":
|
|
37
41
|
return f"{event['run']} won't retry."
|
|
38
42
|
case "idle":
|
|
@@ -17,6 +17,8 @@ async def prepare_experiment(
|
|
|
17
17
|
args: list[str] | None = None,
|
|
18
18
|
setup: str | None = None,
|
|
19
19
|
teardown: str | None = None,
|
|
20
|
+
retries: int | None = None,
|
|
21
|
+
replace_capped: bool = False,
|
|
20
22
|
) -> tuple[ExperimentConfig, int]:
|
|
21
23
|
if config.experiment not in (None, name):
|
|
22
24
|
raise ValueError(
|
|
@@ -39,6 +41,10 @@ async def prepare_experiment(
|
|
|
39
41
|
experiment = dataclasses.replace(experiment, setup=setup)
|
|
40
42
|
if teardown is not None:
|
|
41
43
|
experiment = dataclasses.replace(experiment, teardown=teardown)
|
|
44
|
+
if retries is not None:
|
|
45
|
+
experiment = dataclasses.replace(experiment, retries=retries)
|
|
46
|
+
if replace_capped:
|
|
47
|
+
experiment = dataclasses.replace(experiment, replace_capped=True)
|
|
42
48
|
if only:
|
|
43
49
|
check_subset(experiment.params, only)
|
|
44
50
|
if args:
|
|
@@ -84,6 +84,17 @@ async def run_experiment(
|
|
|
84
84
|
for r in counted
|
|
85
85
|
if r["counted"] or str(r.get("id")) in held_ids
|
|
86
86
|
]
|
|
87
|
+
resumable: list[Entry] = []
|
|
88
|
+
capped: list[Entry] = []
|
|
89
|
+
tries: dict[str, int] = {}
|
|
90
|
+
if experiment.continues:
|
|
91
|
+
for row in await list_resumable_runs(tables, name, experiment.counts):
|
|
92
|
+
entry = (str(row["id"]), json.loads(str(row["params"])))
|
|
93
|
+
tries[entry[0]] = int(str(row["invocations"]))
|
|
94
|
+
over = tries[entry[0]] > experiment.retries
|
|
95
|
+
(capped if over else resumable).append(entry)
|
|
96
|
+
if not experiment.replace_capped:
|
|
97
|
+
finished += [params for _, params in capped]
|
|
87
98
|
planned: set[str] = set()
|
|
88
99
|
|
|
89
100
|
def taken(run: str) -> bool:
|
|
@@ -112,12 +123,6 @@ async def run_experiment(
|
|
|
112
123
|
def release(run: str) -> None:
|
|
113
124
|
os.close(locks.pop(run))
|
|
114
125
|
|
|
115
|
-
resumable: list[Entry] = []
|
|
116
|
-
if experiment.continues:
|
|
117
|
-
resumable = [
|
|
118
|
-
(str(row["id"]), json.loads(str(row["params"])))
|
|
119
|
-
for row in await list_resumable_runs(tables, name, experiment.counts)
|
|
120
|
-
]
|
|
121
126
|
plan: list[Entry] = []
|
|
122
127
|
target_folder: Path | None = None
|
|
123
128
|
if run_id is not None:
|
|
@@ -155,6 +160,8 @@ async def run_experiment(
|
|
|
155
160
|
run = new_stamped_run_id(taken) if resumed is None else resumed[0]
|
|
156
161
|
planned.add(run)
|
|
157
162
|
plan.append((run, params))
|
|
163
|
+
# A resumed run's earlier invocations spend the same retries as exit 75 does.
|
|
164
|
+
earlier = {run: tries[run] for run, _ in plan if run in tries and run != run_id}
|
|
158
165
|
hooks = {
|
|
159
166
|
"setup": experiment.setup,
|
|
160
167
|
"teardown": experiment.teardown,
|
|
@@ -180,6 +187,9 @@ async def run_experiment(
|
|
|
180
187
|
)
|
|
181
188
|
for row in held:
|
|
182
189
|
emit({"event": "held", "run": str(row["id"])})
|
|
190
|
+
gave_up = [run for run, params in capped if in_subset(params, only or {})]
|
|
191
|
+
if gave_up and run_id is None and not experiment.replace_capped:
|
|
192
|
+
emit({"event": "capped", "runs": gave_up, "retries": experiment.retries})
|
|
183
193
|
running: dict[str, set[asyncio.subprocess.Process]] = {}
|
|
184
194
|
trees: dict[str, set[psutil.Process]] = {}
|
|
185
195
|
stopped: set[str] = set()
|
|
@@ -240,11 +250,11 @@ async def run_experiment(
|
|
|
240
250
|
fill()
|
|
241
251
|
|
|
242
252
|
def retry_later(run: str) -> None:
|
|
243
|
-
tried = attempts.get(run, 0)
|
|
253
|
+
tried = attempts.get(run, earlier.get(run, 0))
|
|
244
254
|
if tried >= experiment.retries:
|
|
245
255
|
emit({"event": "gave up", "run": run, "retries": tried})
|
|
246
256
|
return
|
|
247
|
-
seconds = retry_seconds * 2**tried
|
|
257
|
+
seconds = retry_seconds * 2 ** (tried - earlier.get(run, 0))
|
|
248
258
|
|
|
249
259
|
def due() -> None:
|
|
250
260
|
del waiting[run]
|
|
@@ -32,12 +32,23 @@ def start_experiment(
|
|
|
32
32
|
teardown: str | None,
|
|
33
33
|
command_line: str | None = None,
|
|
34
34
|
run_id: str | None = None,
|
|
35
|
+
retries: int | None = None,
|
|
36
|
+
replace_capped: bool = False,
|
|
35
37
|
) -> ExperimentStart:
|
|
36
38
|
state = RunState.create(config, name)
|
|
37
39
|
|
|
38
40
|
async def start(report: Report, listen: Listen) -> list[dict[str, object]]:
|
|
39
41
|
experiment, limit = await prepare_experiment(
|
|
40
|
-
config,
|
|
42
|
+
config,
|
|
43
|
+
name,
|
|
44
|
+
experiment_config,
|
|
45
|
+
concurrency,
|
|
46
|
+
only,
|
|
47
|
+
args,
|
|
48
|
+
setup,
|
|
49
|
+
teardown,
|
|
50
|
+
retries,
|
|
51
|
+
replace_capped,
|
|
41
52
|
)
|
|
42
53
|
await finalize_lost_runs(config)
|
|
43
54
|
with project_snapshot(config.root) as snapshot:
|
|
@@ -8,8 +8,13 @@ async def list_resumable_runs(
|
|
|
8
8
|
tables: DirSQL, name: str, counts: str
|
|
9
9
|
) -> list[dict[str, object]]:
|
|
10
10
|
# Ended badly: a nonzero or missing exit code, not merely uncounted.
|
|
11
|
+
# Invocations stopped by hand don't spend retries.
|
|
11
12
|
return await tables.query(
|
|
12
|
-
|
|
13
|
+
"SELECT id, params, CASE WHEN json_type(run, '$.invocations') = 'array'"
|
|
14
|
+
" THEN (SELECT count(*) FROM json_each(run, '$.invocations')"
|
|
15
|
+
" WHERE json_extract(value, '$.ended_by') IS NOT 'stop')"
|
|
16
|
+
" ELSE json_extract(run, '$.ended_by') IS NOT 'stop' END AS invocations"
|
|
17
|
+
f" FROM runs WHERE experiment = {quote_string(name)}"
|
|
13
18
|
f" AND NOT coalesce(({counts}), false) AND NOT {UNFINISHED}"
|
|
14
19
|
" AND (exit_code IS NULL OR exit_code <> 0)"
|
|
15
20
|
" AND NOT coalesce(run ->> 'excluded', false) ORDER BY created, id"
|
|
@@ -25,3 +25,21 @@ class LeavePrompt(ModalScreen[str | None]):
|
|
|
25
25
|
"Stop them (enter or s) / cancel (esc).",
|
|
26
26
|
id="prompt",
|
|
27
27
|
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DetachedLeavePrompt(LeavePrompt):
|
|
31
|
+
"""Choose explicitly whether a detached daemon should keep its cells."""
|
|
32
|
+
|
|
33
|
+
BINDINGS = [
|
|
34
|
+
Binding("s", "dismiss('stop')", "Stop runs"),
|
|
35
|
+
Binding("enter,l", "dismiss('leave')", "Leave running"),
|
|
36
|
+
Binding("escape", "dismiss", "Cancel"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
def compose(self) -> ComposeResult:
|
|
40
|
+
runs = "run" if self.count == 1 else "runs"
|
|
41
|
+
yield Static(
|
|
42
|
+
f"{self.count} {runs} still going. "
|
|
43
|
+
"Stop them (s) / leave running (enter or l) / cancel (esc).",
|
|
44
|
+
id="prompt",
|
|
45
|
+
)
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
1
3
|
import signal
|
|
4
|
+
import socket
|
|
2
5
|
import time
|
|
3
6
|
import tomllib
|
|
4
7
|
from collections.abc import Mapping, Sequence
|
|
@@ -16,18 +19,21 @@ from textual.widget import Widget
|
|
|
16
19
|
from textual.widgets import ContentSwitcher, DataTable, Footer, Header, Input, Static
|
|
17
20
|
|
|
18
21
|
from labnotes.compose_experiment import compose_experiment
|
|
22
|
+
from labnotes.daemon.command import command as daemon_command
|
|
23
|
+
from labnotes.daemon.client import daemon_socket
|
|
19
24
|
from labnotes.load_experiment import load_experiment
|
|
20
25
|
from labnotes.models import Controls, LabnotesConfig, Start
|
|
21
26
|
from labnotes.retries import RETRY_EXIT_CODE
|
|
22
27
|
from labnotes.runner.describe_event import describe_event
|
|
23
28
|
from labnotes.runner.param_args import param_args
|
|
24
29
|
from labnotes.runner.start_experiment import ExperimentStart
|
|
30
|
+
from labnotes.runner.run_state import RunState
|
|
25
31
|
from labnotes.runner.stop_then_kill import GRACE_SECONDS, stop_then_kill
|
|
26
32
|
from labnotes.tables.open_tables import open_tables
|
|
27
33
|
from labnotes.tui.config import RUN_COLUMNS
|
|
28
34
|
from labnotes.tui.dim import dim
|
|
29
35
|
from labnotes.tui.format_elapsed import format_elapsed
|
|
30
|
-
from labnotes.tui.leave_prompt import LeavePrompt
|
|
36
|
+
from labnotes.tui.leave_prompt import DetachedLeavePrompt, LeavePrompt
|
|
31
37
|
from labnotes.tui.log_blocks import LogBlocks
|
|
32
38
|
from labnotes.tui.native_rows import native_rows
|
|
33
39
|
from labnotes.tui.reload_on_change import reload_on_change
|
|
@@ -55,6 +61,7 @@ class RunLiveView(Widget):
|
|
|
55
61
|
"""
|
|
56
62
|
BINDINGS = [
|
|
57
63
|
Binding("x", "kill_run", "Stop run"),
|
|
64
|
+
Binding("r", "restart_run", "Restart"),
|
|
58
65
|
Binding("slash", "filter", "Filter", key_display="/"),
|
|
59
66
|
Binding("escape", "leave", "Back"),
|
|
60
67
|
Binding("plus,equals_sign", "concurrency(1)", "More at once", key_display="+"),
|
|
@@ -294,6 +301,10 @@ class RunLiveView(Widget):
|
|
|
294
301
|
def action_concurrency(self, change: int) -> None:
|
|
295
302
|
self.controls.set_concurrency(self.concurrency + change)
|
|
296
303
|
|
|
304
|
+
def action_restart_run(self) -> None:
|
|
305
|
+
if (run := self.cursor_run()) is not None and isinstance(self.app, RunApp):
|
|
306
|
+
self.app.restart_run(run)
|
|
307
|
+
|
|
297
308
|
def action_filter(self) -> None:
|
|
298
309
|
field = self.query_one(Input)
|
|
299
310
|
field.display = True
|
|
@@ -354,11 +365,12 @@ class RunApp(ViewSwitcher, App[None]):
|
|
|
354
365
|
|
|
355
366
|
def __init__(
|
|
356
367
|
self,
|
|
357
|
-
start: Start,
|
|
368
|
+
start: Start | None,
|
|
358
369
|
title: str,
|
|
359
370
|
columns: Sequence[str],
|
|
360
371
|
config: LabnotesConfig | None = None,
|
|
361
372
|
experiment_config: Path | None = None,
|
|
373
|
+
daemon_journal: RunState | None = None,
|
|
362
374
|
) -> None:
|
|
363
375
|
super().__init__()
|
|
364
376
|
self.start = start
|
|
@@ -377,8 +389,12 @@ class RunApp(ViewSwitcher, App[None]):
|
|
|
377
389
|
self.shown: Experiment | None = None
|
|
378
390
|
self.left: dict[str, Widget] = {}
|
|
379
391
|
self.controls = self.body.controls
|
|
380
|
-
self.
|
|
392
|
+
self.daemon_journal = daemon_journal
|
|
393
|
+
self.run_state = daemon_journal or (
|
|
394
|
+
start.run_state if isinstance(start, ExperimentStart) else None
|
|
395
|
+
)
|
|
381
396
|
self.watching_state = False
|
|
397
|
+
self.attached: socket.socket | None = None
|
|
382
398
|
|
|
383
399
|
def compose(self) -> ComposeResult:
|
|
384
400
|
yield Header()
|
|
@@ -412,7 +428,93 @@ class RunApp(ViewSwitcher, App[None]):
|
|
|
412
428
|
exit_on_error=False,
|
|
413
429
|
)
|
|
414
430
|
self.run_worker(self.watch_files(tables), exit_on_error=False)
|
|
415
|
-
self.
|
|
431
|
+
if self.daemon_journal is not None:
|
|
432
|
+
journal = self.daemon_journal
|
|
433
|
+
try:
|
|
434
|
+
self.attach_daemon()
|
|
435
|
+
except (OSError, ValueError) as error:
|
|
436
|
+
self.error = error
|
|
437
|
+
self.exit()
|
|
438
|
+
return
|
|
439
|
+
self.listen(
|
|
440
|
+
Controls(
|
|
441
|
+
lambda: self.queue_command({"command": "stop_all"}),
|
|
442
|
+
lambda count: self.queue_command(
|
|
443
|
+
{
|
|
444
|
+
"command": "set_concurrency",
|
|
445
|
+
"count": count,
|
|
446
|
+
"journal": str(journal.path),
|
|
447
|
+
}
|
|
448
|
+
),
|
|
449
|
+
lambda run: self.queue_command({"command": "stop_one", "run": run}),
|
|
450
|
+
lambda: None,
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
self.set_interval(0.25, self.refresh_state)
|
|
454
|
+
self.refresh_state()
|
|
455
|
+
else:
|
|
456
|
+
self.run_worker(self.drive())
|
|
457
|
+
|
|
458
|
+
def attach_daemon(self) -> None:
|
|
459
|
+
assert self.config is not None
|
|
460
|
+
path = daemon_socket(self.config, self.experiment_name)
|
|
461
|
+
if path is None:
|
|
462
|
+
raise ValueError(f"No daemon is running for {self.experiment_name}.")
|
|
463
|
+
client = socket.socket(socket.AF_UNIX)
|
|
464
|
+
try:
|
|
465
|
+
client.settimeout(10)
|
|
466
|
+
client.connect(str(path))
|
|
467
|
+
client.sendall(b'{"command":"attach"}\n')
|
|
468
|
+
with client.makefile("r") as replies:
|
|
469
|
+
reply = json.loads(replies.readline())
|
|
470
|
+
if not reply.get("ok"):
|
|
471
|
+
raise ValueError(str(reply.get("error", "Daemon rejected attach")))
|
|
472
|
+
except Exception:
|
|
473
|
+
client.close()
|
|
474
|
+
raise
|
|
475
|
+
self.attached = client
|
|
476
|
+
|
|
477
|
+
def on_unmount(self) -> None:
|
|
478
|
+
if self.attached is not None:
|
|
479
|
+
self.attached.close()
|
|
480
|
+
|
|
481
|
+
def queue_command(self, message: dict[str, object]) -> None:
|
|
482
|
+
self.run_worker(self.send_command(message), exit_on_error=False)
|
|
483
|
+
|
|
484
|
+
async def send_command(self, message: dict[str, object]) -> None:
|
|
485
|
+
assert self.config is not None
|
|
486
|
+
try:
|
|
487
|
+
reply = await asyncio.to_thread(
|
|
488
|
+
daemon_command, self.config, self.experiment_name, message
|
|
489
|
+
)
|
|
490
|
+
if not reply.get("ok"):
|
|
491
|
+
raise ValueError(str(reply.get("error", "Daemon rejected command")))
|
|
492
|
+
except ValueError as error:
|
|
493
|
+
if self.body_ready:
|
|
494
|
+
self.body.show(str(error))
|
|
495
|
+
|
|
496
|
+
def restart_run(self, run: str) -> None:
|
|
497
|
+
if self.daemon_journal is None:
|
|
498
|
+
return
|
|
499
|
+
if run not in self.body.failed:
|
|
500
|
+
self.body.show(f"{run} has not failed.")
|
|
501
|
+
return
|
|
502
|
+
self.run_worker(self.restart_daemon_run(run), exit_on_error=False)
|
|
503
|
+
|
|
504
|
+
async def restart_daemon_run(self, run: str) -> None:
|
|
505
|
+
assert self.config is not None
|
|
506
|
+
try:
|
|
507
|
+
reply = await asyncio.to_thread(
|
|
508
|
+
daemon_command,
|
|
509
|
+
self.config,
|
|
510
|
+
self.experiment_name,
|
|
511
|
+
{"command": "restart", "options": {"run_id": run}},
|
|
512
|
+
)
|
|
513
|
+
if not reply.get("ok"):
|
|
514
|
+
raise ValueError(str(reply.get("error", "Daemon rejected restart")))
|
|
515
|
+
self.body.show(f"Restarted {run}.")
|
|
516
|
+
except ValueError as error:
|
|
517
|
+
self.body.show(f"Could not restart {run}: {error}")
|
|
416
518
|
|
|
417
519
|
def configured_views(self) -> dict[str, str]:
|
|
418
520
|
if self.config is None:
|
|
@@ -453,6 +555,7 @@ class RunApp(ViewSwitcher, App[None]):
|
|
|
453
555
|
self.deliver(self.shown, frozenset({str(row["path"])}))
|
|
454
556
|
|
|
455
557
|
async def drive(self) -> None:
|
|
558
|
+
assert self.start is not None
|
|
456
559
|
try:
|
|
457
560
|
self.results = await self.start(self.show_event, self.listen)
|
|
458
561
|
except Exception as error:
|
|
@@ -463,8 +566,9 @@ class RunApp(ViewSwitcher, App[None]):
|
|
|
463
566
|
self.controls = controls
|
|
464
567
|
if self.body_ready:
|
|
465
568
|
self.body.controls = controls
|
|
466
|
-
|
|
467
|
-
|
|
569
|
+
if self.daemon_journal is None:
|
|
570
|
+
stop_then_kill(self.stop_runs, signal.SIGTERM)
|
|
571
|
+
stop_then_kill(self.stop_runs, signal.SIGHUP)
|
|
468
572
|
|
|
469
573
|
def show_event(self, event: dict[str, object]) -> None:
|
|
470
574
|
if self.run_state is None:
|
|
@@ -488,6 +592,14 @@ class RunApp(ViewSwitcher, App[None]):
|
|
|
488
592
|
self.controls.interrupt()
|
|
489
593
|
|
|
490
594
|
def action_interrupt(self) -> None:
|
|
595
|
+
if self.daemon_journal is not None:
|
|
596
|
+
if self.body_ready and self.body.started:
|
|
597
|
+
self.push_screen(
|
|
598
|
+
DetachedLeavePrompt(len(self.body.started)), self.answer
|
|
599
|
+
)
|
|
600
|
+
else:
|
|
601
|
+
self.exit()
|
|
602
|
+
return
|
|
491
603
|
if self.body_ready and self.body.started:
|
|
492
604
|
self.push_screen(LeavePrompt(len(self.body.started)), self.answer)
|
|
493
605
|
elif self.body_ready and self.body.idle:
|
|
@@ -500,8 +612,17 @@ class RunApp(ViewSwitcher, App[None]):
|
|
|
500
612
|
|
|
501
613
|
def answer(self, choice: str | None) -> None:
|
|
502
614
|
if choice == "stop":
|
|
503
|
-
self.
|
|
504
|
-
|
|
615
|
+
if self.daemon_journal is None:
|
|
616
|
+
self.controls.interrupt()
|
|
617
|
+
self.set_timer(GRACE_SECONDS, self.controls.interrupt)
|
|
618
|
+
else:
|
|
619
|
+
self.run_worker(self.stop_daemon_and_exit(), exit_on_error=False)
|
|
620
|
+
elif choice == "leave" and self.daemon_journal is not None:
|
|
621
|
+
self.exit()
|
|
622
|
+
|
|
623
|
+
async def stop_daemon_and_exit(self) -> None:
|
|
624
|
+
await self.send_command({"command": "stop_all"})
|
|
625
|
+
self.exit()
|
|
505
626
|
|
|
506
627
|
def action_leave(self) -> None:
|
|
507
628
|
if self.query_one(ContentSwitcher).current != "view-1":
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
3
|
from labnotes.models import LabnotesConfig, Start
|
|
4
|
+
from labnotes.runner.run_state import RunState
|
|
4
5
|
from labnotes.tui.config import RUN_COLUMNS
|
|
5
6
|
from labnotes.tui.run_app import RunApp
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def run_tui(
|
|
9
|
-
start: Start,
|
|
10
|
+
start: Start | None,
|
|
10
11
|
title: str,
|
|
11
12
|
config: LabnotesConfig | None = None,
|
|
12
13
|
experiment_config: Path | None = None,
|
|
14
|
+
daemon_journal: RunState | None = None,
|
|
13
15
|
) -> list[dict[str, object]]:
|
|
14
16
|
app = RunApp(
|
|
15
|
-
start,
|
|
17
|
+
start,
|
|
18
|
+
title,
|
|
19
|
+
RUN_COLUMNS,
|
|
20
|
+
config=config,
|
|
21
|
+
experiment_config=experiment_config,
|
|
22
|
+
daemon_journal=daemon_journal,
|
|
16
23
|
)
|
|
17
24
|
app.run()
|
|
18
25
|
if app.error is not None:
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import os
|
|
3
|
-
import sys
|
|
4
3
|
from collections.abc import Mapping
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
from typing import Any, cast
|
|
@@ -11,6 +10,8 @@ from textual.widget import Widget
|
|
|
11
10
|
from textual.widgets import DataTable, Static
|
|
12
11
|
|
|
13
12
|
from labnotes.runs_view_command import runs_view_command
|
|
13
|
+
from labnotes.daemon.command import command as daemon_command
|
|
14
|
+
from labnotes.daemon.ensure import ensure_daemon
|
|
14
15
|
from labnotes.models import LabnotesConfig
|
|
15
16
|
from labnotes.runner.claim_run import claim_run
|
|
16
17
|
from labnotes.tui.fill_table import fill_table
|
|
@@ -144,28 +145,21 @@ class RunsBrowser(Widget):
|
|
|
144
145
|
|
|
145
146
|
async def restart(self, run_id: str) -> None:
|
|
146
147
|
config = cast(LabnotesConfig, getattr(self.app, "config"))
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
"
|
|
156
|
-
|
|
157
|
-
run_id,
|
|
158
|
-
stdout=asyncio.subprocess.PIPE,
|
|
159
|
-
stderr=asyncio.subprocess.PIPE,
|
|
160
|
-
)
|
|
161
|
-
stdout, stderr = await process.communicate()
|
|
162
|
-
if process.returncode == 0:
|
|
148
|
+
try:
|
|
149
|
+
await ensure_daemon(config, self.experiment.name)
|
|
150
|
+
reply = await asyncio.to_thread(
|
|
151
|
+
daemon_command,
|
|
152
|
+
config,
|
|
153
|
+
self.experiment.name,
|
|
154
|
+
{"command": "restart", "options": {"run_id": run_id}},
|
|
155
|
+
)
|
|
156
|
+
if not reply.get("ok"):
|
|
157
|
+
raise ValueError(str(reply.get("error", "Daemon rejected restart")))
|
|
163
158
|
self.restart_status = (
|
|
164
159
|
f"Restarted {run_id}. Select the run to view its output."
|
|
165
160
|
)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
self.restart_status = f"Could not restart {run_id}: {detail}"
|
|
161
|
+
except ValueError as error:
|
|
162
|
+
self.restart_status = f"Could not restart {run_id}: {error}"
|
|
169
163
|
self.show_status()
|
|
170
164
|
|
|
171
165
|
async def action_open_row(self) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|