labnotes 0.2.9__tar.gz → 0.2.11__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.9 → labnotes-0.2.11}/PKG-INFO +1 -1
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/experiments.py +1 -1
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/experiment_screen.py +3 -42
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/run_app.py +185 -60
- labnotes-0.2.11/src/labnotes/tui/run_output.py +106 -0
- labnotes-0.2.11/src/labnotes/tui/run_tui.py +20 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/view_process.py +1 -1
- labnotes-0.2.11/src/labnotes/tui/view_switcher.py +53 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/testing-conventions.toml +12 -0
- labnotes-0.2.9/src/labnotes/tui/run_output.py +0 -80
- labnotes-0.2.9/src/labnotes/tui/run_tui.py +0 -11
- {labnotes-0.2.9 → labnotes-0.2.11}/.gitignore +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/CHANGELOG.md +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/MIGRATIONS.md +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/README.md +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/pyproject.toml +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/__init__.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/__main__.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/cli.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/command_line_group.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/config_migration.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/context.md +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/copy_experiment.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/create_app.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/create_note.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/data_html.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/experiment_definition.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/format_started.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/format_value.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/get_state.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/grid_cell.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/init.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/is_non_negative_number.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/latest_log.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/load_config.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/load_run.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/log.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/log_path.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/memory_interval.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/models.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/note_path.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/output_html.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/param_names.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/parse_param_value.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/parse_params.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/parse_subset.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/pivot_runs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/read_config.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/read_path.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/retries.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/run_columns.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/run_output.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/cgroup_memory.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/check_args.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/check_hook_placeholders.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/check_subset.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/claim_run.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/command_flags.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/container_cgroup.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/container_memory.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/continue_run.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/docker_output.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/exit_signal.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/fill_dirs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/fill_experiment_env.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/finalize_lost_runs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/first_invocation.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/flag_name.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/group_args.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/hold_experiment.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/in_subset.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/invocation_hook.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/invocation_meta.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/override_flags.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/placeholders.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/project_snapshot.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/record_memory.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/run_git.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/run_locked.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/untracked_diff.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/runs_view_command.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/save_note.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/server.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/set_state.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/state_path.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/list_resumable_runs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/read_memory.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/read_run_file.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/runs_columns.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/check_view_response.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/config.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/format_cell.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/guard_view.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/load_view.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/native_rows.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/run_panel.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/runs_browser.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/runs_view.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/table_columns.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/table_rows.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/view_column.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/tui/view_pane.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/utils/__init__.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/utils/format_bytes.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/utils/process_alive.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/utils/run_usage.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/utils/tree_rss.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/utils/tree_usage.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/view/__init__.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/view/answer.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/view/build.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/view/changed.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/view/experiment.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/view/respond.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/view/serve.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/view_specs.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/tests/conftest.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/tests/e2e/conftest.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/tests/integration/conftest.py +0 -0
- {labnotes-0.2.9 → labnotes-0.2.11}/uv.lock +0 -0
|
@@ -168,7 +168,7 @@ def experiments(
|
|
|
168
168
|
)
|
|
169
169
|
try:
|
|
170
170
|
if sys.stdout.isatty() and sys.stderr.isatty():
|
|
171
|
-
results = run_tui(start, name)
|
|
171
|
+
results = run_tui(start, name, config, experiment_config)
|
|
172
172
|
else:
|
|
173
173
|
results = run_headless(start)
|
|
174
174
|
except ValueError as error:
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from collections.abc import Mapping
|
|
2
1
|
from types import MappingProxyType
|
|
3
2
|
from typing import Any
|
|
4
3
|
|
|
@@ -17,15 +16,15 @@ from labnotes.tui.native_rows import native_rows
|
|
|
17
16
|
from labnotes.tui.reload_on_change import reload_on_change
|
|
18
17
|
from labnotes.tui.stopped_title import stopped_title
|
|
19
18
|
from labnotes.tui.view_pane import ViewPane
|
|
19
|
+
from labnotes.tui.view_switcher import ViewSwitcher
|
|
20
20
|
from labnotes.view.experiment import Experiment
|
|
21
21
|
from labnotes.watch_rows import watch_rows
|
|
22
22
|
|
|
23
23
|
WATCHED = {"runs", "experiment_configs"}
|
|
24
24
|
BUILT_IN = "labnotes.tui.runs_browser:RunsBrowser"
|
|
25
|
-
MAX_VIEWS = 8
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
class ExperimentScreen(Screen[None]):
|
|
27
|
+
class ExperimentScreen(ViewSwitcher, Screen[None]):
|
|
29
28
|
BINDINGS = [Binding("escape", "back", "Back")]
|
|
30
29
|
|
|
31
30
|
def __init__(
|
|
@@ -82,7 +81,7 @@ class ExperimentScreen(Screen[None]):
|
|
|
82
81
|
self.deliver(None)
|
|
83
82
|
return
|
|
84
83
|
if not self.found:
|
|
85
|
-
await self.add_views(loaded["views"])
|
|
84
|
+
await self.add_views(loaded["views"], self.config.root)
|
|
86
85
|
self.found = True
|
|
87
86
|
self.sub_title = str(loaded["display_name"])
|
|
88
87
|
self.shown = Experiment(
|
|
@@ -93,49 +92,11 @@ class ExperimentScreen(Screen[None]):
|
|
|
93
92
|
)
|
|
94
93
|
self.deliver(self.shown)
|
|
95
94
|
|
|
96
|
-
async def add_views(self, specs: Mapping[str, str]) -> None:
|
|
97
|
-
names = list(specs)[:MAX_VIEWS]
|
|
98
|
-
if not names:
|
|
99
|
-
return
|
|
100
|
-
panes = [
|
|
101
|
-
ViewPane(name, specs[name], self.config.root, id=f"view-{number}")
|
|
102
|
-
for number, name in enumerate(names, 2)
|
|
103
|
-
]
|
|
104
|
-
for pane in panes:
|
|
105
|
-
pane.display = False
|
|
106
|
-
await self.query_one(ContentSwitcher).mount_all(panes)
|
|
107
|
-
# Screen bindings are fixed per class; each screen's view names differ.
|
|
108
|
-
for number, name in enumerate(["runs", *names], 1):
|
|
109
|
-
self._bindings.bind(str(number), f"show_view({number})", name)
|
|
110
|
-
self._bindings.bind("0", "show_view(1)")
|
|
111
|
-
self.refresh_bindings()
|
|
112
|
-
|
|
113
|
-
def deliver(
|
|
114
|
-
self, experiment: Experiment | None, paths: frozenset[str] = frozenset()
|
|
115
|
-
) -> None:
|
|
116
|
-
for pane in self.query(ViewPane):
|
|
117
|
-
pane.deliver(experiment, paths)
|
|
118
|
-
|
|
119
95
|
async def watch_files(self) -> None:
|
|
120
96
|
async for row in watch_rows(self.tables, {"run_files"}, self.experiment_name):
|
|
121
97
|
if self.shown is not None:
|
|
122
98
|
self.deliver(self.shown, frozenset({str(row["path"])}))
|
|
123
99
|
|
|
124
|
-
def action_show_view(self, number: int) -> None:
|
|
125
|
-
switcher = self.query_one(ContentSwitcher)
|
|
126
|
-
if switcher.current is not None and self.focused is not None:
|
|
127
|
-
self.left[switcher.current] = self.focused
|
|
128
|
-
switcher.current = f"view-{number}"
|
|
129
|
-
self.call_after_refresh(self.refocus, switcher.current)
|
|
130
|
-
|
|
131
|
-
def refocus(self, current: str) -> None:
|
|
132
|
-
widget = self.left.get(current)
|
|
133
|
-
if widget is not None and widget.focusable:
|
|
134
|
-
widget.focus()
|
|
135
|
-
return
|
|
136
|
-
self.set_focus(None)
|
|
137
|
-
self.focus_next()
|
|
138
|
-
|
|
139
100
|
def action_back(self) -> None:
|
|
140
101
|
if self.query_one(ContentSwitcher).current == "view-1":
|
|
141
102
|
self.app.pop_screen()
|
|
@@ -1,38 +1,58 @@
|
|
|
1
1
|
import signal
|
|
2
2
|
import time
|
|
3
|
-
|
|
3
|
+
import tomllib
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
4
5
|
from pathlib import Path
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Any, cast
|
|
5
8
|
|
|
6
9
|
from rich.text import Text
|
|
10
|
+
from starlette.datastructures import QueryParams
|
|
7
11
|
from textual import work
|
|
8
12
|
from textual.app import App, ComposeResult
|
|
9
13
|
from textual.binding import Binding
|
|
10
|
-
from textual.
|
|
14
|
+
from textual.css.query import NoMatches
|
|
15
|
+
from textual.widget import Widget
|
|
16
|
+
from textual.widgets import ContentSwitcher, DataTable, Footer, Header, Input, Static
|
|
11
17
|
|
|
12
|
-
from labnotes.
|
|
13
|
-
from labnotes.
|
|
18
|
+
from labnotes.compose_experiment import compose_experiment
|
|
19
|
+
from labnotes.load_experiment import load_experiment
|
|
20
|
+
from labnotes.models import Controls, LabnotesConfig, Start
|
|
14
21
|
from labnotes.retries import RETRY_EXIT_CODE
|
|
15
|
-
from labnotes.utils.run_usage import run_usage
|
|
16
22
|
from labnotes.runner.describe_event import describe_event
|
|
17
|
-
from labnotes.runner.stop_then_kill import GRACE_SECONDS, stop_then_kill
|
|
18
23
|
from labnotes.runner.param_args import param_args
|
|
24
|
+
from labnotes.runner.stop_then_kill import GRACE_SECONDS, stop_then_kill
|
|
25
|
+
from labnotes.tables.open_tables import open_tables
|
|
26
|
+
from labnotes.tui.config import RUN_COLUMNS
|
|
19
27
|
from labnotes.tui.dim import dim
|
|
20
28
|
from labnotes.tui.format_elapsed import format_elapsed
|
|
21
29
|
from labnotes.tui.leave_prompt import LeavePrompt
|
|
22
30
|
from labnotes.tui.log_blocks import LogBlocks
|
|
31
|
+
from labnotes.tui.native_rows import native_rows
|
|
32
|
+
from labnotes.tui.reload_on_change import reload_on_change
|
|
23
33
|
from labnotes.tui.run_output import RunOutput
|
|
24
34
|
from labnotes.tui.runs_table import RunsTable
|
|
35
|
+
from labnotes.tui.view_pane import ViewPane
|
|
36
|
+
from labnotes.tui.view_switcher import ViewSwitcher
|
|
37
|
+
from labnotes.utils.format_bytes import format_bytes
|
|
38
|
+
from labnotes.utils.run_usage import run_usage
|
|
39
|
+
from labnotes.view.experiment import Experiment
|
|
40
|
+
from labnotes.view_specs import view_specs
|
|
41
|
+
from labnotes.watch_rows import watch_rows
|
|
25
42
|
|
|
26
43
|
PENDING, PASSED, FAILED = "○", "✓", "✗"
|
|
27
44
|
SPINNER = "|/-\\"
|
|
28
45
|
SKIPPED = "–"
|
|
29
46
|
VIEWING_ACTIONS = {"leave", "quit", "interrupt", "command_palette"}
|
|
47
|
+
BUILT_IN = "labnotes.tui.run_app:RunLiveView"
|
|
30
48
|
|
|
31
49
|
|
|
32
|
-
class
|
|
33
|
-
|
|
50
|
+
class RunLiveView(Widget):
|
|
51
|
+
DEFAULT_CSS = """
|
|
52
|
+
RunLiveView { height: 1fr; }
|
|
53
|
+
RunLiveView RunsTable { height: 1fr; }
|
|
54
|
+
"""
|
|
34
55
|
BINDINGS = [
|
|
35
|
-
Binding("ctrl+c", "interrupt", "Quit"),
|
|
36
56
|
Binding("x", "kill_run", "Stop run"),
|
|
37
57
|
Binding("slash", "filter", "Filter", key_display="/"),
|
|
38
58
|
Binding("escape", "leave", "Back"),
|
|
@@ -40,13 +60,9 @@ class RunApp(App[None]):
|
|
|
40
60
|
Binding("minus", "concurrency(-1)", "Fewer at once", key_display="-"),
|
|
41
61
|
]
|
|
42
62
|
|
|
43
|
-
def __init__(self,
|
|
63
|
+
def __init__(self, columns: Sequence[str] = ()) -> None:
|
|
44
64
|
super().__init__()
|
|
45
|
-
self.columns = tuple(columns)
|
|
46
|
-
self.start = start
|
|
47
|
-
self.title = title
|
|
48
|
-
self.results: list[dict[str, object]] = []
|
|
49
|
-
self.error: Exception | None = None
|
|
65
|
+
self.columns = tuple(columns or RUN_COLUMNS)
|
|
50
66
|
self.controls = Controls(
|
|
51
67
|
lambda: None, lambda count: None, lambda run: None, lambda: None
|
|
52
68
|
)
|
|
@@ -67,13 +83,11 @@ class RunApp(App[None]):
|
|
|
67
83
|
self.unmeasured: dict[str, None] = {}
|
|
68
84
|
|
|
69
85
|
def compose(self) -> ComposeResult:
|
|
70
|
-
yield Header()
|
|
71
86
|
yield RunsTable(id="runs", cursor_type="row")
|
|
72
87
|
yield LogBlocks(id="blocks")
|
|
73
88
|
yield RunOutput(id="output")
|
|
74
89
|
yield Input(id="filter", placeholder="Filter params")
|
|
75
90
|
yield Static(id="status")
|
|
76
|
-
yield Footer()
|
|
77
91
|
|
|
78
92
|
def on_mount(self) -> None:
|
|
79
93
|
table = self.query_one(DataTable)
|
|
@@ -81,32 +95,16 @@ class RunApp(App[None]):
|
|
|
81
95
|
table.add_column(column, key=column)
|
|
82
96
|
self.query_one(Input).display = False
|
|
83
97
|
self.query_one(RunOutput).display = False
|
|
84
|
-
self.query_one(Header).mount(Static(id="concurrency"))
|
|
85
98
|
table.set_interval(0.1, self.spin)
|
|
86
|
-
|
|
87
|
-
def on_ready(self) -> None:
|
|
88
99
|
self.set_interval(1, self.tick)
|
|
89
100
|
self.set_interval(0.25, self.follow)
|
|
90
|
-
self.run_worker(self.drive())
|
|
91
|
-
|
|
92
|
-
async def drive(self) -> None:
|
|
93
|
-
try:
|
|
94
|
-
self.results = await self.start(self.show_event, self.listen)
|
|
95
|
-
except Exception as error:
|
|
96
|
-
self.error = error
|
|
97
|
-
self.exit()
|
|
98
|
-
|
|
99
|
-
def listen(self, controls: Controls) -> None:
|
|
100
|
-
self.controls = controls
|
|
101
|
-
stop_then_kill(self.stop_runs, signal.SIGTERM)
|
|
102
|
-
stop_then_kill(self.stop_runs, signal.SIGHUP)
|
|
103
101
|
|
|
104
102
|
def show_event(self, event: dict[str, object]) -> None:
|
|
105
103
|
table = self.query_one(DataTable)
|
|
106
104
|
run = str(event.get("run"))
|
|
107
105
|
match event["event"]:
|
|
108
106
|
case "planned":
|
|
109
|
-
self.title = str(event["title"])
|
|
107
|
+
self.app.title = str(event["title"])
|
|
110
108
|
for planned in event["runs"]: # type: ignore[attr-defined]
|
|
111
109
|
args = " ".join(param_args(planned["params"]))
|
|
112
110
|
self.cells[planned["run"]] = self.waiting_cells(
|
|
@@ -219,7 +217,7 @@ class RunApp(App[None]):
|
|
|
219
217
|
table.update_cell(run, column, cell, update_width=True)
|
|
220
218
|
|
|
221
219
|
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool:
|
|
222
|
-
if isinstance(self.screen, LeavePrompt):
|
|
220
|
+
if isinstance(self.app.screen, LeavePrompt):
|
|
223
221
|
return False
|
|
224
222
|
if action == "leave" and self.viewing is None:
|
|
225
223
|
return self.query_one(Input).display
|
|
@@ -227,7 +225,9 @@ class RunApp(App[None]):
|
|
|
227
225
|
|
|
228
226
|
def show_concurrency(self, event: dict[str, object]) -> None:
|
|
229
227
|
self.concurrency = int(event["concurrency"]) # type: ignore[call-overload]
|
|
230
|
-
self.query_one("#concurrency", Static).update(
|
|
228
|
+
self.app.query_one("#concurrency", Static).update(
|
|
229
|
+
f"{self.concurrency} at a time"
|
|
230
|
+
)
|
|
231
231
|
|
|
232
232
|
def tick(self) -> None:
|
|
233
233
|
now = time.monotonic()
|
|
@@ -271,28 +271,6 @@ class RunApp(App[None]):
|
|
|
271
271
|
def show(self, text: str) -> None:
|
|
272
272
|
self.query_one("#status", Static).update(text)
|
|
273
273
|
|
|
274
|
-
def stop_runs(self) -> None:
|
|
275
|
-
if self.idle:
|
|
276
|
-
self.controls.finish()
|
|
277
|
-
else:
|
|
278
|
-
self.controls.interrupt()
|
|
279
|
-
|
|
280
|
-
def action_interrupt(self) -> None:
|
|
281
|
-
if self.started:
|
|
282
|
-
self.push_screen(LeavePrompt(len(self.started)), self.answer)
|
|
283
|
-
elif self.idle:
|
|
284
|
-
self.controls.finish()
|
|
285
|
-
else:
|
|
286
|
-
self.exit()
|
|
287
|
-
|
|
288
|
-
async def action_quit(self) -> None:
|
|
289
|
-
self.action_interrupt()
|
|
290
|
-
|
|
291
|
-
def answer(self, choice: str | None) -> None:
|
|
292
|
-
if choice == "stop":
|
|
293
|
-
self.controls.interrupt()
|
|
294
|
-
self.set_timer(GRACE_SECONDS, self.controls.interrupt)
|
|
295
|
-
|
|
296
274
|
def cursor_run(self) -> str | None:
|
|
297
275
|
table = self.query_one(DataTable)
|
|
298
276
|
if not table.row_count:
|
|
@@ -322,7 +300,7 @@ class RunApp(App[None]):
|
|
|
322
300
|
self.query_one(RunOutput).reset()
|
|
323
301
|
self.query_one(DataTable).display = False
|
|
324
302
|
self.query_one(RunOutput).display = True
|
|
325
|
-
self.sub_title = run
|
|
303
|
+
self.app.sub_title = run
|
|
326
304
|
self.show("")
|
|
327
305
|
self.refresh_bindings()
|
|
328
306
|
self.follow()
|
|
@@ -338,7 +316,7 @@ class RunApp(App[None]):
|
|
|
338
316
|
self.query_one(LogBlocks).display = False
|
|
339
317
|
self.query_one(RunOutput).display = False
|
|
340
318
|
self.query_one(DataTable).display = True
|
|
341
|
-
self.sub_title = ""
|
|
319
|
+
self.app.sub_title = ""
|
|
342
320
|
self.refresh_bindings()
|
|
343
321
|
if self.idle:
|
|
344
322
|
self.show_summary()
|
|
@@ -359,3 +337,150 @@ class RunApp(App[None]):
|
|
|
359
337
|
event.input.display = bool(event.value)
|
|
360
338
|
self.query_one(DataTable).focus()
|
|
361
339
|
self.refresh_bindings()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class RunApp(ViewSwitcher, App[None]):
|
|
343
|
+
CSS = "#concurrency { dock: right; width: auto; padding: 0 1; }"
|
|
344
|
+
BINDINGS = [Binding("ctrl+c", "interrupt", "Quit")]
|
|
345
|
+
|
|
346
|
+
def __init__(
|
|
347
|
+
self,
|
|
348
|
+
start: Start,
|
|
349
|
+
title: str,
|
|
350
|
+
columns: Sequence[str],
|
|
351
|
+
config: LabnotesConfig | None = None,
|
|
352
|
+
experiment_config: Path | None = None,
|
|
353
|
+
) -> None:
|
|
354
|
+
super().__init__()
|
|
355
|
+
self.start = start
|
|
356
|
+
self.title = title
|
|
357
|
+
self.config = config
|
|
358
|
+
self.experiment_config = experiment_config
|
|
359
|
+
self.experiment_name = title
|
|
360
|
+
self.view_specs = self.configured_views()
|
|
361
|
+
self.results: list[dict[str, object]] = []
|
|
362
|
+
self.error: Exception | None = None
|
|
363
|
+
self.columns = tuple(columns)
|
|
364
|
+
self.body = RunLiveView(columns)
|
|
365
|
+
self.body.id = "view-1"
|
|
366
|
+
self.body_ready = True
|
|
367
|
+
self.loaded_views = False
|
|
368
|
+
self.shown: Experiment | None = None
|
|
369
|
+
self.left: dict[str, Widget] = {}
|
|
370
|
+
self.controls = self.body.controls
|
|
371
|
+
|
|
372
|
+
def compose(self) -> ComposeResult:
|
|
373
|
+
yield Header()
|
|
374
|
+
with ContentSwitcher(initial="view-1"):
|
|
375
|
+
if self.view_specs:
|
|
376
|
+
assert self.config is not None
|
|
377
|
+
yield ViewPane("runs", BUILT_IN, self.config.root, id="view-1")
|
|
378
|
+
else:
|
|
379
|
+
yield self.body
|
|
380
|
+
yield Footer()
|
|
381
|
+
|
|
382
|
+
async def on_mount(self) -> None:
|
|
383
|
+
await self.query_one(Header).mount(Static(id="concurrency"))
|
|
384
|
+
if self.view_specs:
|
|
385
|
+
assert self.config is not None
|
|
386
|
+
self._bindings.bind("escape", "leave", "Back")
|
|
387
|
+
try:
|
|
388
|
+
self.body = self.query_one(RunLiveView)
|
|
389
|
+
except NoMatches:
|
|
390
|
+
# ViewPane already displays the built-in view's loading error.
|
|
391
|
+
self.body_ready = False
|
|
392
|
+
tables = open_tables(self.config, run_files=True)
|
|
393
|
+
self.run_worker(
|
|
394
|
+
reload_on_change(
|
|
395
|
+
watch_rows(
|
|
396
|
+
tables, {"runs", "experiment_configs"}, self.experiment_name
|
|
397
|
+
),
|
|
398
|
+
lambda: self.load_data(tables),
|
|
399
|
+
lambda error: None,
|
|
400
|
+
),
|
|
401
|
+
exit_on_error=False,
|
|
402
|
+
)
|
|
403
|
+
self.run_worker(self.watch_files(tables), exit_on_error=False)
|
|
404
|
+
self.run_worker(self.drive())
|
|
405
|
+
|
|
406
|
+
def configured_views(self) -> dict[str, str]:
|
|
407
|
+
if self.config is None:
|
|
408
|
+
return {}
|
|
409
|
+
folder = self.config.experiments.replace("{experiment}", self.experiment_name)
|
|
410
|
+
path = self.experiment_config or self.config.root / folder / "experiment.toml"
|
|
411
|
+
try:
|
|
412
|
+
source = path.read_text()
|
|
413
|
+
layer = tomllib.loads(source)
|
|
414
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
415
|
+
# The runner reports missing or invalid config through its normal path.
|
|
416
|
+
layer = {}
|
|
417
|
+
return view_specs(compose_experiment(self.config.defaults, layer))
|
|
418
|
+
|
|
419
|
+
async def load_data(self, tables: Any) -> None:
|
|
420
|
+
assert self.config is not None
|
|
421
|
+
loaded = await load_experiment(
|
|
422
|
+
tables, self.config.root, self.experiment_name, QueryParams()
|
|
423
|
+
)
|
|
424
|
+
if loaded is None:
|
|
425
|
+
return
|
|
426
|
+
if not self.loaded_views:
|
|
427
|
+
await self.add_views(
|
|
428
|
+
cast(Mapping[str, str], loaded["views"]), self.config.root
|
|
429
|
+
)
|
|
430
|
+
self.loaded_views = True
|
|
431
|
+
self.shown = Experiment(
|
|
432
|
+
self.config.root,
|
|
433
|
+
self.experiment_name,
|
|
434
|
+
native_rows(loaded),
|
|
435
|
+
MappingProxyType(cast(dict[str, Any], loaded["config"] or {})),
|
|
436
|
+
)
|
|
437
|
+
self.deliver(self.shown)
|
|
438
|
+
|
|
439
|
+
async def watch_files(self, tables: Any) -> None:
|
|
440
|
+
async for row in watch_rows(tables, {"run_files"}, self.experiment_name):
|
|
441
|
+
if self.shown is not None:
|
|
442
|
+
self.deliver(self.shown, frozenset({str(row["path"])}))
|
|
443
|
+
|
|
444
|
+
async def drive(self) -> None:
|
|
445
|
+
try:
|
|
446
|
+
self.results = await self.start(self.show_event, self.listen)
|
|
447
|
+
except Exception as error:
|
|
448
|
+
self.error = error
|
|
449
|
+
self.exit()
|
|
450
|
+
|
|
451
|
+
def listen(self, controls: Controls) -> None:
|
|
452
|
+
self.controls = controls
|
|
453
|
+
if self.body_ready:
|
|
454
|
+
self.body.controls = controls
|
|
455
|
+
stop_then_kill(self.stop_runs, signal.SIGTERM)
|
|
456
|
+
stop_then_kill(self.stop_runs, signal.SIGHUP)
|
|
457
|
+
|
|
458
|
+
def show_event(self, event: dict[str, object]) -> None:
|
|
459
|
+
if self.body_ready:
|
|
460
|
+
self.body.show_event(event)
|
|
461
|
+
|
|
462
|
+
def stop_runs(self) -> None:
|
|
463
|
+
if self.body_ready and self.body.idle:
|
|
464
|
+
self.controls.finish()
|
|
465
|
+
else:
|
|
466
|
+
self.controls.interrupt()
|
|
467
|
+
|
|
468
|
+
def action_interrupt(self) -> None:
|
|
469
|
+
if self.body_ready and self.body.started:
|
|
470
|
+
self.push_screen(LeavePrompt(len(self.body.started)), self.answer)
|
|
471
|
+
elif self.body_ready and self.body.idle:
|
|
472
|
+
self.controls.finish()
|
|
473
|
+
else:
|
|
474
|
+
self.exit()
|
|
475
|
+
|
|
476
|
+
async def action_quit(self) -> None:
|
|
477
|
+
self.action_interrupt()
|
|
478
|
+
|
|
479
|
+
def answer(self, choice: str | None) -> None:
|
|
480
|
+
if choice == "stop":
|
|
481
|
+
self.controls.interrupt()
|
|
482
|
+
self.set_timer(GRACE_SECONDS, self.controls.interrupt)
|
|
483
|
+
|
|
484
|
+
def action_leave(self) -> None:
|
|
485
|
+
if self.query_one(ContentSwitcher).current != "view-1":
|
|
486
|
+
self.action_show_view(1)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import codecs
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
from rich.style import Style
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from rich.cells import cell_len
|
|
9
|
+
from textual.geometry import Size
|
|
10
|
+
from textual.strip import Strip
|
|
11
|
+
from textual.widgets import Log
|
|
12
|
+
|
|
13
|
+
from labnotes.latest_log import latest_log
|
|
14
|
+
|
|
15
|
+
STREAMS = ("stdout", "stderr")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RunOutput(Log):
|
|
19
|
+
def __init__(self, id: str | None = None) -> None:
|
|
20
|
+
super().__init__(id=id)
|
|
21
|
+
self.reset()
|
|
22
|
+
|
|
23
|
+
def reset(self) -> None:
|
|
24
|
+
self.offsets = dict.fromkeys(STREAMS, 0)
|
|
25
|
+
self.paths: dict[str, Path] = {}
|
|
26
|
+
self.decoders = {
|
|
27
|
+
stream: codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
28
|
+
for stream in STREAMS
|
|
29
|
+
}
|
|
30
|
+
self.last_stream: str | None = None
|
|
31
|
+
self.unfinished = ""
|
|
32
|
+
self.clear()
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def mid_line(self) -> bool:
|
|
36
|
+
return bool(self.unfinished)
|
|
37
|
+
|
|
38
|
+
def follow(self, folder: Path) -> None:
|
|
39
|
+
for stream in STREAMS:
|
|
40
|
+
path = latest_log(folder, stream)
|
|
41
|
+
if not path.exists():
|
|
42
|
+
continue
|
|
43
|
+
if self.paths.setdefault(stream, path) != path:
|
|
44
|
+
self.paths[stream] = path
|
|
45
|
+
self.offsets[stream] = 0
|
|
46
|
+
with path.open("rb") as log:
|
|
47
|
+
log.seek(self.offsets[stream])
|
|
48
|
+
data = log.read()
|
|
49
|
+
self.offsets[stream] += len(data)
|
|
50
|
+
text = self.decoders[stream].decode(data)
|
|
51
|
+
if not text:
|
|
52
|
+
continue
|
|
53
|
+
# A stream's unfinished line is redrawn as it grows, but the
|
|
54
|
+
# other stream's output starts below it.
|
|
55
|
+
if self.mid_line and stream == self.last_stream:
|
|
56
|
+
self.drop_unfinished()
|
|
57
|
+
text = self.unfinished + text
|
|
58
|
+
*complete, self.unfinished = text.split("\n")
|
|
59
|
+
if complete:
|
|
60
|
+
self.write_lines(complete)
|
|
61
|
+
if self.unfinished:
|
|
62
|
+
self.write_lines([self.unfinished])
|
|
63
|
+
self.last_stream = stream
|
|
64
|
+
|
|
65
|
+
def write_lines(self, lines: Iterable[str], scroll_end: bool | None = None) -> Self:
|
|
66
|
+
new_lines = list(lines)
|
|
67
|
+
if not new_lines:
|
|
68
|
+
return self
|
|
69
|
+
at_end = self.is_vertical_scroll_end
|
|
70
|
+
self._lines.extend(new_lines)
|
|
71
|
+
self._width = max(self._width, max(map(len, new_lines)))
|
|
72
|
+
self.virtual_size = Size(self._width, len(self._lines))
|
|
73
|
+
self.refresh()
|
|
74
|
+
if (
|
|
75
|
+
self.is_mounted
|
|
76
|
+
and (self.auto_scroll if scroll_end is None else scroll_end)
|
|
77
|
+
and at_end
|
|
78
|
+
):
|
|
79
|
+
self.scroll_end(animate=False, immediate=True, x_axis=False)
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def drop_unfinished(self) -> None:
|
|
83
|
+
self._lines.pop()
|
|
84
|
+
self._render_line_cache.discard(len(self._lines))
|
|
85
|
+
self.virtual_size = Size(self._width, len(self._lines))
|
|
86
|
+
self.refresh()
|
|
87
|
+
|
|
88
|
+
def _render_line_strip(self, y: int, rich_style: Style) -> Strip:
|
|
89
|
+
selection = self.text_selection
|
|
90
|
+
if selection is None and y in self._render_line_cache:
|
|
91
|
+
return self._render_line_cache[y]
|
|
92
|
+
line_text = Text.from_ansi(self._lines[y], style=rich_style)
|
|
93
|
+
if selection is not None:
|
|
94
|
+
if (span := selection.get_span(y)) is not None:
|
|
95
|
+
start, end = span
|
|
96
|
+
if end == -1:
|
|
97
|
+
end = len(line_text)
|
|
98
|
+
line_text.stylize(
|
|
99
|
+
self.screen.get_component_rich_style("screen--selection"),
|
|
100
|
+
start,
|
|
101
|
+
end,
|
|
102
|
+
)
|
|
103
|
+
line = Strip(line_text.render(self.app.console), cell_len(line_text.plain))
|
|
104
|
+
if selection is None:
|
|
105
|
+
self._render_line_cache[y] = line
|
|
106
|
+
return line
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from labnotes.models import LabnotesConfig, Start
|
|
4
|
+
from labnotes.tui.config import RUN_COLUMNS
|
|
5
|
+
from labnotes.tui.run_app import RunApp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_tui(
|
|
9
|
+
start: Start,
|
|
10
|
+
title: str,
|
|
11
|
+
config: LabnotesConfig | None = None,
|
|
12
|
+
experiment_config: Path | None = None,
|
|
13
|
+
) -> list[dict[str, object]]:
|
|
14
|
+
app = RunApp(
|
|
15
|
+
start, title, RUN_COLUMNS, config=config, experiment_config=experiment_config
|
|
16
|
+
)
|
|
17
|
+
app.run()
|
|
18
|
+
if app.error is not None:
|
|
19
|
+
raise app.error
|
|
20
|
+
return app.results
|
|
@@ -13,7 +13,7 @@ GRACE_SECONDS = 1.0
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class ViewProcess:
|
|
16
|
-
"""Hold one view worker for
|
|
16
|
+
"""Hold one view worker for a screen's life, one request at a time."""
|
|
17
17
|
|
|
18
18
|
def __init__(self, command: str, root: Path) -> None:
|
|
19
19
|
self.command = command
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from textual.widget import Widget
|
|
5
|
+
from textual.widgets import ContentSwitcher
|
|
6
|
+
|
|
7
|
+
from labnotes.tui.view_pane import ViewPane
|
|
8
|
+
from labnotes.view.experiment import Experiment
|
|
9
|
+
|
|
10
|
+
MAX_VIEWS = 8
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ViewSwitcher:
|
|
14
|
+
"""Mount and switch the same view panes in browse and run screens."""
|
|
15
|
+
|
|
16
|
+
left: dict[str, Widget]
|
|
17
|
+
|
|
18
|
+
async def add_views(self, specs: Mapping[str, str], root: Path) -> None:
|
|
19
|
+
names = list(specs)[:MAX_VIEWS]
|
|
20
|
+
if not names:
|
|
21
|
+
return
|
|
22
|
+
panes = [
|
|
23
|
+
ViewPane(name, specs[name], root, id=f"view-{number}")
|
|
24
|
+
for number, name in enumerate(names, 2)
|
|
25
|
+
]
|
|
26
|
+
for pane in panes:
|
|
27
|
+
pane.display = False
|
|
28
|
+
await self.query_one(ContentSwitcher).mount_all(panes) # type: ignore[attr-defined]
|
|
29
|
+
for number, name in enumerate(["runs", *names], 1):
|
|
30
|
+
self._bindings.bind(str(number), f"show_view({number})", name) # type: ignore[attr-defined]
|
|
31
|
+
self._bindings.bind("0", "show_view(1)") # type: ignore[attr-defined]
|
|
32
|
+
self.refresh_bindings() # type: ignore[attr-defined]
|
|
33
|
+
|
|
34
|
+
def deliver(
|
|
35
|
+
self, experiment: Experiment | None, paths: frozenset[str] = frozenset()
|
|
36
|
+
) -> None:
|
|
37
|
+
for pane in self.query(ViewPane): # type: ignore[attr-defined]
|
|
38
|
+
pane.deliver(experiment, paths)
|
|
39
|
+
|
|
40
|
+
def action_show_view(self, number: int) -> None:
|
|
41
|
+
switcher = self.query_one(ContentSwitcher) # type: ignore[attr-defined]
|
|
42
|
+
if switcher.current is not None and self.focused is not None: # type: ignore[attr-defined]
|
|
43
|
+
self.left[switcher.current] = self.focused # type: ignore[attr-defined]
|
|
44
|
+
switcher.current = f"view-{number}"
|
|
45
|
+
self.call_after_refresh(self.refocus, switcher.current) # type: ignore[attr-defined]
|
|
46
|
+
|
|
47
|
+
def refocus(self, current: str) -> None:
|
|
48
|
+
widget = self.left.get(current)
|
|
49
|
+
if widget is not None and widget.focusable:
|
|
50
|
+
widget.focus()
|
|
51
|
+
return
|
|
52
|
+
self.set_focus(None) # type: ignore[attr-defined]
|
|
53
|
+
self.screen.focus_next() # type: ignore[attr-defined]
|
|
@@ -40,8 +40,20 @@ rules = ["mutation"]
|
|
|
40
40
|
lines = [221]
|
|
41
41
|
reason = "the leave action name is the string literal from the Binding, which CPython interns, so is equals =="
|
|
42
42
|
|
|
43
|
+
[[python.exempt]]
|
|
44
|
+
path = "src/labnotes/tui/run_app.py"
|
|
45
|
+
rules = ["mutation"]
|
|
46
|
+
lines = [485]
|
|
47
|
+
reason = "the switcher contains only view-1 through view-9; every non-built-in view id sorts above view-1, so > equals !="
|
|
48
|
+
|
|
43
49
|
[[python.exempt]]
|
|
44
50
|
path = "src/labnotes/runner/hold_experiment.py"
|
|
45
51
|
rules = ["mutation"]
|
|
46
52
|
lines = [18]
|
|
47
53
|
reason = "LOCK_EX and LOCK_NB are disjoint bits, so | equals + and ^"
|
|
54
|
+
|
|
55
|
+
[[python.exempt]]
|
|
56
|
+
path = "src/labnotes/tui/run_output.py"
|
|
57
|
+
rules = ["mutation"]
|
|
58
|
+
lines = [96]
|
|
59
|
+
reason = "a selection span ends at -1 or a non-negative offset, so <= equals ==; -1 is a CPython cached small int, so is equals =="
|