labnotes 0.2.8__tar.gz → 0.2.9__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.8 → labnotes-0.2.9}/PKG-INFO +1 -1
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/load_experiment.py +8 -2
- labnotes-0.2.9/src/labnotes/param_names.py +9 -0
- labnotes-0.2.9/src/labnotes/run_columns.py +7 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/read_experiment_config.py +10 -4
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/browse_app.py +2 -16
- labnotes-0.2.9/src/labnotes/tui/experiment_screen.py +143 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/experiments_screen.py +2 -10
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/format_cell.py +4 -2
- labnotes-0.2.9/src/labnotes/tui/guard_view.py +35 -0
- labnotes-0.2.9/src/labnotes/tui/load_view.py +27 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/native_rows.py +3 -3
- labnotes-0.2.9/src/labnotes/tui/run_cells.py +12 -0
- labnotes-0.2.8/src/labnotes/tui/run_screen.py → labnotes-0.2.9/src/labnotes/tui/run_panel.py +5 -8
- labnotes-0.2.9/src/labnotes/tui/runs_browser.py +132 -0
- labnotes-0.2.9/src/labnotes/tui/table_columns.py +8 -0
- labnotes-0.2.9/src/labnotes/tui/table_rows.py +12 -0
- labnotes-0.2.9/src/labnotes/tui/view_pane.py +54 -0
- labnotes-0.2.9/src/labnotes/view/__init__.py +22 -0
- labnotes-0.2.9/src/labnotes/view/changed.py +19 -0
- labnotes-0.2.9/src/labnotes/view/experiment.py +20 -0
- labnotes-0.2.9/src/labnotes/view_specs.py +10 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/testing-conventions.toml +3 -15
- labnotes-0.2.8/src/labnotes/tui/experiment_screen.py +0 -154
- labnotes-0.2.8/src/labnotes/tui/group_cells.py +0 -36
- labnotes-0.2.8/src/labnotes/tui/invocation_columns.py +0 -9
- labnotes-0.2.8/src/labnotes/tui/invocation_rows.py +0 -32
- labnotes-0.2.8/src/labnotes/tui/run_cells.py +0 -22
- labnotes-0.2.8/src/labnotes/view/__init__.py +0 -3
- {labnotes-0.2.8 → labnotes-0.2.9}/.gitignore +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/CHANGELOG.md +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/MIGRATIONS.md +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/README.md +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/pyproject.toml +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/__init__.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/__main__.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/cli.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/command_line_group.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/config_migration.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/context.md +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/copy_experiment.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/create_app.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/create_note.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/data_html.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/experiment_definition.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/experiments.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/format_started.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/format_value.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/get_state.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/grid_cell.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/init.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/is_non_negative_number.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/latest_log.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/load_config.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/load_run.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/log.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/log_path.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/memory_interval.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/models.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/note_path.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/output_html.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/parse_param_value.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/parse_params.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/parse_subset.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/pivot_runs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/read_config.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/read_path.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/retries.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/run_output.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/cgroup_memory.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/check_args.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/check_hook_placeholders.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/check_subset.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/claim_run.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/command_flags.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/container_cgroup.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/container_memory.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/continue_run.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/docker_output.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/exit_signal.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/fill_dirs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/fill_experiment_env.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/finalize_lost_runs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/first_invocation.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/flag_name.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/group_args.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/hold_experiment.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/in_subset.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/invocation_hook.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/invocation_meta.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/override_flags.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/placeholders.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/project_snapshot.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/record_memory.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/run_git.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/run_locked.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/untracked_diff.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runs_view_command.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/save_note.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/server.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/set_state.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/state_path.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/list_resumable_runs.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_memory.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_run_file.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/runs_columns.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/check_view_response.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/config.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/run_app.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/runs_view.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/view_column.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/view_process.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/__init__.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/format_bytes.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/process_alive.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/run_usage.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/tree_rss.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/tree_usage.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/view/answer.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/view/build.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/view/respond.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/view/serve.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/tests/conftest.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/tests/e2e/conftest.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/tests/integration/conftest.py +0 -0
- {labnotes-0.2.8 → labnotes-0.2.9}/uv.lock +0 -0
|
@@ -19,9 +19,11 @@ from labnotes.parse_run_view import parse_run_view
|
|
|
19
19
|
from labnotes.pivot_runs import pivot_runs
|
|
20
20
|
from labnotes.python_blocks import python_blocks
|
|
21
21
|
from labnotes.render_markdown import render_markdown
|
|
22
|
+
from labnotes.run_columns import run_columns
|
|
22
23
|
from labnotes.runs_view_command import runs_view_command
|
|
23
24
|
from labnotes.sort_href import sort_href
|
|
24
25
|
from labnotes.sql_quote import quote_string
|
|
26
|
+
from labnotes.view_specs import view_specs
|
|
25
27
|
from labnotes.summarize_config import summarize_config
|
|
26
28
|
from labnotes.utils.process_alive import process_alive
|
|
27
29
|
|
|
@@ -49,7 +51,7 @@ async def load_experiment(
|
|
|
49
51
|
varies = cast(dict[str, list[object]], summary["varies"])
|
|
50
52
|
source = experiment_runs(name, list(varies))
|
|
51
53
|
summary = await check_counts(tables, source, summary)
|
|
52
|
-
shown =
|
|
54
|
+
shown = run_columns(config)
|
|
53
55
|
choices = {
|
|
54
56
|
key: {format_value(value): json.dumps(value) for value in values}
|
|
55
57
|
for key, values in varies.items()
|
|
@@ -70,7 +72,9 @@ async def load_experiment(
|
|
|
70
72
|
pid, created if isinstance(created, int | float) else None
|
|
71
73
|
)
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
for run in runs:
|
|
76
|
+
run["labnotes_running"] = is_running(run)
|
|
77
|
+
runs_live = any(run["labnotes_running"] for run in runs)
|
|
74
78
|
nested = await nests_runs(tables, source)
|
|
75
79
|
groups = (
|
|
76
80
|
group_runs(runs, list(choices), await count_groups(tables, source))
|
|
@@ -92,6 +96,7 @@ async def load_experiment(
|
|
|
92
96
|
return {
|
|
93
97
|
"name": row["name"],
|
|
94
98
|
"display_name": row["display_name"],
|
|
99
|
+
"config": config,
|
|
95
100
|
"config_source": row["source"],
|
|
96
101
|
"counts_set": "counts" in (config or {}),
|
|
97
102
|
**summary,
|
|
@@ -115,6 +120,7 @@ async def load_experiment(
|
|
|
115
120
|
"run_view": view,
|
|
116
121
|
"run_choices": choices,
|
|
117
122
|
"runs_view": runs_view_command(config),
|
|
123
|
+
"views": view_specs(config),
|
|
118
124
|
"sort_hrefs": {
|
|
119
125
|
column: sort_href(view, column) for column in ["status", *shown]
|
|
120
126
|
},
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from labnotes.summarize_config import summarize_config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def param_names(config: Mapping[str, object] | None) -> list[str]:
|
|
8
|
+
summary = summarize_config(None if config is None else dict(config))
|
|
9
|
+
return [key for key in cast(dict[str, object], summary["varies"]) if key != "id"]
|
|
@@ -30,7 +30,7 @@ SETTINGS = {
|
|
|
30
30
|
"from",
|
|
31
31
|
}
|
|
32
32
|
HOOKS = ("setup", "teardown")
|
|
33
|
-
|
|
33
|
+
MAX_VIEWS = 8
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def read_experiment_config(
|
|
@@ -107,12 +107,18 @@ def read_experiment_config(
|
|
|
107
107
|
views = values.get("views", {})
|
|
108
108
|
if not isinstance(views, dict):
|
|
109
109
|
raise ValueError(f"{path}: views must be a table, like [views]")
|
|
110
|
-
unknown = sorted(views.keys() - VIEWS)
|
|
111
|
-
if unknown:
|
|
112
|
-
raise ValueError(f"{path}: unknown keys in views: {', '.join(unknown)}")
|
|
113
110
|
runs = views.get("runs")
|
|
114
111
|
if runs is not None and (not isinstance(runs, str) or not runs.strip()):
|
|
115
112
|
raise ValueError(f"{path}: views.runs must be a command")
|
|
113
|
+
shown = {name: spec for name, spec in views.items() if name != "runs"}
|
|
114
|
+
if len(shown) > MAX_VIEWS:
|
|
115
|
+
raise ValueError(f"{path}: views holds at most {MAX_VIEWS} views besides runs")
|
|
116
|
+
for name, spec in shown.items():
|
|
117
|
+
source, _, cls = spec.rpartition(":") if isinstance(spec, str) else ("", "", "")
|
|
118
|
+
if not source.strip() or not cls.strip():
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f'{path}: views.{name} must be "path/to/file.py:ClassName"'
|
|
121
|
+
)
|
|
116
122
|
return ExperimentConfig(
|
|
117
123
|
command,
|
|
118
124
|
repeats,
|
|
@@ -5,8 +5,6 @@ from labnotes.models import LabnotesConfig
|
|
|
5
5
|
from labnotes.tables.open_tables import open_tables
|
|
6
6
|
from labnotes.tui.experiment_screen import ExperimentScreen
|
|
7
7
|
from labnotes.tui.experiments_screen import ExperimentsScreen
|
|
8
|
-
from labnotes.tui.runs_view import Request
|
|
9
|
-
from labnotes.tui.view_process import ViewProcess
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
class BrowseApp(App[BaseException | None]):
|
|
@@ -17,23 +15,11 @@ class BrowseApp(App[BaseException | None]):
|
|
|
17
15
|
self.config = config
|
|
18
16
|
self.experiment_name = name
|
|
19
17
|
self.title = config.root.name
|
|
20
|
-
self.views: dict[str, ViewProcess] = {}
|
|
21
18
|
|
|
22
19
|
def on_mount(self) -> None:
|
|
23
20
|
tables = open_tables(self.config, run_files=True)
|
|
24
|
-
self.push_screen(ExperimentsScreen(tables, self.config
|
|
21
|
+
self.push_screen(ExperimentsScreen(tables, self.config))
|
|
25
22
|
if self.experiment_name is not None:
|
|
26
23
|
self.push_screen(
|
|
27
|
-
ExperimentScreen(
|
|
28
|
-
tables, self.config, self.experiment_name, self.view_request
|
|
29
|
-
)
|
|
24
|
+
ExperimentScreen(tables, self.config, self.experiment_name)
|
|
30
25
|
)
|
|
31
|
-
|
|
32
|
-
def view_request(self, command: str) -> Request:
|
|
33
|
-
if command not in self.views:
|
|
34
|
-
self.views[command] = ViewProcess(command, self.config.root)
|
|
35
|
-
return self.views[command].request
|
|
36
|
-
|
|
37
|
-
async def on_unmount(self) -> None:
|
|
38
|
-
for process in self.views.values():
|
|
39
|
-
await process.close()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from types import MappingProxyType
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from dirsql import DirSQL
|
|
6
|
+
from starlette.datastructures import QueryParams
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.screen import Screen
|
|
10
|
+
from textual.widget import Widget
|
|
11
|
+
from textual.widgets import ContentSwitcher, Footer, Header
|
|
12
|
+
from textual.worker import Worker, WorkerState
|
|
13
|
+
|
|
14
|
+
from labnotes.load_experiment import load_experiment
|
|
15
|
+
from labnotes.models import LabnotesConfig
|
|
16
|
+
from labnotes.tui.native_rows import native_rows
|
|
17
|
+
from labnotes.tui.reload_on_change import reload_on_change
|
|
18
|
+
from labnotes.tui.stopped_title import stopped_title
|
|
19
|
+
from labnotes.tui.view_pane import ViewPane
|
|
20
|
+
from labnotes.view.experiment import Experiment
|
|
21
|
+
from labnotes.watch_rows import watch_rows
|
|
22
|
+
|
|
23
|
+
WATCHED = {"runs", "experiment_configs"}
|
|
24
|
+
BUILT_IN = "labnotes.tui.runs_browser:RunsBrowser"
|
|
25
|
+
MAX_VIEWS = 8
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExperimentScreen(Screen[None]):
|
|
29
|
+
BINDINGS = [Binding("escape", "back", "Back")]
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
tables: DirSQL,
|
|
34
|
+
config: LabnotesConfig,
|
|
35
|
+
name: str,
|
|
36
|
+
) -> None:
|
|
37
|
+
super().__init__()
|
|
38
|
+
self.tables = tables
|
|
39
|
+
self.config = config
|
|
40
|
+
self.experiment_name = name
|
|
41
|
+
self.found = False
|
|
42
|
+
self.shown: Experiment | None = None
|
|
43
|
+
self.left: dict[str, Widget] = {}
|
|
44
|
+
|
|
45
|
+
def compose(self) -> ComposeResult:
|
|
46
|
+
yield Header()
|
|
47
|
+
with ContentSwitcher(initial="view-1"):
|
|
48
|
+
yield ViewPane("runs", BUILT_IN, self.config.root, id="view-1")
|
|
49
|
+
yield Footer()
|
|
50
|
+
|
|
51
|
+
def on_mount(self) -> None:
|
|
52
|
+
self.sub_title = self.experiment_name
|
|
53
|
+
self.run_worker(
|
|
54
|
+
reload_on_change(
|
|
55
|
+
watch_rows(self.tables, WATCHED, self.experiment_name),
|
|
56
|
+
self.load,
|
|
57
|
+
self.stopped,
|
|
58
|
+
),
|
|
59
|
+
exit_on_error=False,
|
|
60
|
+
)
|
|
61
|
+
self.run_worker(self.watch_files(), exit_on_error=False)
|
|
62
|
+
|
|
63
|
+
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
|
64
|
+
if event.state == WorkerState.ERROR:
|
|
65
|
+
self.app.exit(event.worker.error)
|
|
66
|
+
|
|
67
|
+
def stopped(self, error: BaseException | None) -> None:
|
|
68
|
+
self.sub_title = stopped_title(self.sub_title, error)
|
|
69
|
+
|
|
70
|
+
async def load(self) -> None:
|
|
71
|
+
loaded: dict[str, Any] | None = await load_experiment(
|
|
72
|
+
self.tables,
|
|
73
|
+
self.config.root,
|
|
74
|
+
self.experiment_name,
|
|
75
|
+
QueryParams(),
|
|
76
|
+
)
|
|
77
|
+
if loaded is None and not self.found:
|
|
78
|
+
raise ValueError(f"No experiment named {self.experiment_name}.")
|
|
79
|
+
if loaded is None:
|
|
80
|
+
self.sub_title = f"{self.experiment_name} was removed."
|
|
81
|
+
self.shown = None
|
|
82
|
+
self.deliver(None)
|
|
83
|
+
return
|
|
84
|
+
if not self.found:
|
|
85
|
+
await self.add_views(loaded["views"])
|
|
86
|
+
self.found = True
|
|
87
|
+
self.sub_title = str(loaded["display_name"])
|
|
88
|
+
self.shown = Experiment(
|
|
89
|
+
self.config.root,
|
|
90
|
+
self.experiment_name,
|
|
91
|
+
native_rows(loaded),
|
|
92
|
+
MappingProxyType(loaded["config"] or {}),
|
|
93
|
+
)
|
|
94
|
+
self.deliver(self.shown)
|
|
95
|
+
|
|
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
|
+
async def watch_files(self) -> None:
|
|
120
|
+
async for row in watch_rows(self.tables, {"run_files"}, self.experiment_name):
|
|
121
|
+
if self.shown is not None:
|
|
122
|
+
self.deliver(self.shown, frozenset({str(row["path"])}))
|
|
123
|
+
|
|
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
|
+
def action_back(self) -> None:
|
|
140
|
+
if self.query_one(ContentSwitcher).current == "view-1":
|
|
141
|
+
self.app.pop_screen()
|
|
142
|
+
else:
|
|
143
|
+
self.action_show_view(1)
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
from collections.abc import Callable
|
|
2
|
-
|
|
3
1
|
from dirsql import DirSQL
|
|
4
2
|
from textual.app import ComposeResult
|
|
5
3
|
from textual.screen import Screen
|
|
@@ -12,7 +10,6 @@ from labnotes.tui.experiment_screen import ExperimentScreen
|
|
|
12
10
|
from labnotes.tui.fill_table import fill_table
|
|
13
11
|
from labnotes.tui.reload_on_change import reload_on_change
|
|
14
12
|
from labnotes.tui.runs_table import RunsTable
|
|
15
|
-
from labnotes.tui.runs_view import Request
|
|
16
13
|
from labnotes.tui.stopped_title import stopped_title
|
|
17
14
|
from labnotes.watch_rows import watch_rows
|
|
18
15
|
|
|
@@ -20,13 +17,10 @@ WATCHED = {"files", "runs", "experiment_configs"}
|
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
class ExperimentsScreen(Screen[None]):
|
|
23
|
-
def __init__(
|
|
24
|
-
self, tables: DirSQL, config: LabnotesConfig, views: Callable[[str], Request]
|
|
25
|
-
) -> None:
|
|
20
|
+
def __init__(self, tables: DirSQL, config: LabnotesConfig) -> None:
|
|
26
21
|
super().__init__()
|
|
27
22
|
self.tables = tables
|
|
28
23
|
self.config = config
|
|
29
|
-
self.views = views
|
|
30
24
|
|
|
31
25
|
def compose(self) -> ComposeResult:
|
|
32
26
|
yield Header()
|
|
@@ -63,7 +57,5 @@ class ExperimentsScreen(Screen[None]):
|
|
|
63
57
|
|
|
64
58
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
65
59
|
self.app.push_screen(
|
|
66
|
-
ExperimentScreen(
|
|
67
|
-
self.tables, self.config, str(event.row_key.value), self.views
|
|
68
|
-
)
|
|
60
|
+
ExperimentScreen(self.tables, self.config, str(event.row_key.value))
|
|
69
61
|
)
|
|
@@ -7,13 +7,15 @@ from labnotes.tui.dim import dim
|
|
|
7
7
|
from labnotes.tui.format_elapsed import format_elapsed
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def format_cell(key: str, value: object
|
|
10
|
+
def format_cell(key: str, value: object) -> object:
|
|
11
11
|
if key == "status" and value == "passed":
|
|
12
12
|
return Text("✓", style="green")
|
|
13
13
|
if key == "status" and value == "failed":
|
|
14
14
|
return Text("✕", style="red")
|
|
15
|
+
if key == "status" and value == "running":
|
|
16
|
+
return Text("◐", style="yellow")
|
|
15
17
|
if key == "status" and value == "unfinished":
|
|
16
|
-
return
|
|
18
|
+
return dim("–")
|
|
17
19
|
if key == "duration" and isinstance(value, int) and not isinstance(value, bool):
|
|
18
20
|
return format_elapsed(value)
|
|
19
21
|
if key == "started" and isinstance(value, str):
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from collections.abc import Callable, Iterable
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from textual.message import Message
|
|
5
|
+
from textual.widget import Widget
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def guard_view(view: type[Widget], failed: Callable[[Exception], None]) -> type[Widget]:
|
|
9
|
+
"""Subclass a view so errors in its compose and handlers reach `failed`.
|
|
10
|
+
|
|
11
|
+
Textual ends the app on any error a widget raises. A view is someone else's
|
|
12
|
+
code, so its errors stay in its pane instead.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def compose(self: Widget) -> Iterable[Widget]:
|
|
16
|
+
try:
|
|
17
|
+
yield from view.compose(self)
|
|
18
|
+
except Exception as error:
|
|
19
|
+
failed(error)
|
|
20
|
+
|
|
21
|
+
# _dispatch_message is private to Textual, but every handler runs under it.
|
|
22
|
+
async def _dispatch_message(self: Widget, message: Message) -> None:
|
|
23
|
+
try:
|
|
24
|
+
await view._dispatch_message(self, message)
|
|
25
|
+
except Exception as error:
|
|
26
|
+
failed(error)
|
|
27
|
+
|
|
28
|
+
namespace = {
|
|
29
|
+
"__module__": view.__module__,
|
|
30
|
+
"__qualname__": view.__qualname__,
|
|
31
|
+
"compose": compose,
|
|
32
|
+
"_dispatch_message": _dispatch_message,
|
|
33
|
+
}
|
|
34
|
+
# type() builds a subclass of view, so it is a Widget subclass.
|
|
35
|
+
return cast(type[Widget], type(view.__name__, (view,), namespace))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import sys
|
|
3
|
+
from importlib.machinery import SourceFileLoader
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
|
|
7
|
+
from textual.widget import Widget
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_view(spec: str, root: Path) -> type[Widget]:
|
|
11
|
+
"""Import the widget class `spec` names: `file.py:Class` from `root`, or `module:Class`."""
|
|
12
|
+
source, _, name = spec.rpartition(":")
|
|
13
|
+
if source.endswith(".py"):
|
|
14
|
+
path = root / source
|
|
15
|
+
module = ModuleType(f"labnotes_view_{path.stem}")
|
|
16
|
+
module.__file__ = str(path)
|
|
17
|
+
# Dataclasses and pickling look a class's module up by name.
|
|
18
|
+
sys.modules[module.__name__] = module
|
|
19
|
+
SourceFileLoader(module.__name__, str(path)).exec_module(module)
|
|
20
|
+
else:
|
|
21
|
+
module = importlib.import_module(source)
|
|
22
|
+
view = getattr(module, name, None)
|
|
23
|
+
if view is None:
|
|
24
|
+
raise ImportError(f"{source} has no {name}")
|
|
25
|
+
if not isinstance(view, type) or not issubclass(view, Widget):
|
|
26
|
+
raise TypeError(f"{spec} is not a Textual widget")
|
|
27
|
+
return view
|
|
@@ -11,7 +11,7 @@ def native_row(run: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
11
11
|
if isinstance(row.get(key), str):
|
|
12
12
|
row[key] = json.loads(row[key])
|
|
13
13
|
if run["labnotes_unfinished"]:
|
|
14
|
-
row["status"] = "unfinished"
|
|
14
|
+
row["status"] = "running" if run["labnotes_running"] else "unfinished"
|
|
15
15
|
elif run["labnotes_completed"]:
|
|
16
16
|
row["status"] = "passed"
|
|
17
17
|
else:
|
|
@@ -20,5 +20,5 @@ def native_row(run: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
20
20
|
return row
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def native_rows(experiment: Mapping[str, Any]) ->
|
|
24
|
-
return
|
|
23
|
+
def native_rows(experiment: Mapping[str, Any]) -> tuple[dict[str, Any], ...]:
|
|
24
|
+
return tuple(native_row(run) for run in experiment["runs_list"])
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
|
|
3
|
+
from labnotes.tui.format_cell import format_cell
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def run_cells(
|
|
7
|
+
row: Mapping[str, object], columns: Sequence[Mapping[str, str]]
|
|
8
|
+
) -> list[object]:
|
|
9
|
+
return [
|
|
10
|
+
format_cell(column["key"], row[column["key"]]) if column["key"] in row else ""
|
|
11
|
+
for column in columns
|
|
12
|
+
]
|
labnotes-0.2.8/src/labnotes/tui/run_screen.py → labnotes-0.2.9/src/labnotes/tui/run_panel.py
RENAMED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
3
|
from textual.app import ComposeResult
|
|
4
|
-
from textual.
|
|
5
|
-
from textual.
|
|
6
|
-
from textual.widgets import Footer, Header
|
|
4
|
+
from textual.containers import Vertical
|
|
5
|
+
from textual.widgets import Static
|
|
7
6
|
|
|
8
7
|
from labnotes.tui.log_blocks import LogBlocks
|
|
9
8
|
from labnotes.tui.run_output import RunOutput
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
class
|
|
13
|
-
|
|
11
|
+
class RunPanel(Vertical):
|
|
12
|
+
DEFAULT_CSS = "RunPanel #run-id { padding: 0 1; text-style: bold; }"
|
|
14
13
|
|
|
15
14
|
def __init__(self, run_id: str, folder: Path) -> None:
|
|
16
15
|
super().__init__()
|
|
@@ -18,13 +17,11 @@ class RunScreen(Screen[None]):
|
|
|
18
17
|
self.folder = folder
|
|
19
18
|
|
|
20
19
|
def compose(self) -> ComposeResult:
|
|
21
|
-
yield
|
|
20
|
+
yield Static(self.run_id, id="run-id", markup=False)
|
|
22
21
|
yield LogBlocks(id="blocks")
|
|
23
22
|
yield RunOutput(id="output")
|
|
24
|
-
yield Footer()
|
|
25
23
|
|
|
26
24
|
def on_mount(self) -> None:
|
|
27
|
-
self.sub_title = self.run_id
|
|
28
25
|
self.follow()
|
|
29
26
|
self.set_interval(0.25, self.follow)
|
|
30
27
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.widget import Widget
|
|
8
|
+
from textual.widgets import DataTable, Static
|
|
9
|
+
|
|
10
|
+
from labnotes.runs_view_command import runs_view_command
|
|
11
|
+
from labnotes.tui.fill_table import fill_table
|
|
12
|
+
from labnotes.tui.run_output import RunOutput
|
|
13
|
+
from labnotes.tui.run_panel import RunPanel
|
|
14
|
+
from labnotes.tui.runs_table import RunsTable
|
|
15
|
+
from labnotes.tui.runs_view import RunsView
|
|
16
|
+
from labnotes.tui.table_columns import table_columns
|
|
17
|
+
from labnotes.tui.table_rows import table_rows
|
|
18
|
+
from labnotes.tui.view_process import ViewProcess
|
|
19
|
+
from labnotes.view.changed import Changed
|
|
20
|
+
from labnotes.view.experiment import Experiment
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RunsBrowser(Widget):
|
|
24
|
+
"""Show an experiment's runs, then a run's output: the built-in view."""
|
|
25
|
+
|
|
26
|
+
DEFAULT_CSS = """
|
|
27
|
+
RunsBrowser RunsTable { height: 1fr; }
|
|
28
|
+
RunsBrowser #view-status { color: $text-warning; }
|
|
29
|
+
"""
|
|
30
|
+
BINDINGS = [
|
|
31
|
+
Binding("enter", "open_row", "Open", priority=True),
|
|
32
|
+
Binding("escape", "close_run", "Back"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
super().__init__()
|
|
37
|
+
self.process: ViewProcess | None = None
|
|
38
|
+
self.view: RunsView | None = None
|
|
39
|
+
self.experiment = Experiment(Path(), "", ())
|
|
40
|
+
self.viewing: str | None = None
|
|
41
|
+
|
|
42
|
+
def compose(self) -> ComposeResult:
|
|
43
|
+
yield RunsTable(id="runs", cursor_type="row")
|
|
44
|
+
yield Static(id="view-status")
|
|
45
|
+
|
|
46
|
+
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool:
|
|
47
|
+
if action == "open_row":
|
|
48
|
+
return self.viewing is None
|
|
49
|
+
if action == "close_run":
|
|
50
|
+
return self.viewing is not None
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
def on_labnotes_changed(self, message: Changed) -> None:
|
|
54
|
+
if message.paths:
|
|
55
|
+
if self.view is not None:
|
|
56
|
+
for path in sorted(message.paths):
|
|
57
|
+
self.view.touch(path)
|
|
58
|
+
self.ask(self.view)
|
|
59
|
+
return
|
|
60
|
+
experiment = message.experiment
|
|
61
|
+
if experiment is None:
|
|
62
|
+
fill_table(self.query_one(DataTable), [], [])
|
|
63
|
+
return
|
|
64
|
+
self.experiment = experiment
|
|
65
|
+
command = runs_view_command(dict(experiment.config))
|
|
66
|
+
if command is not None and self.view is None:
|
|
67
|
+
self.process = ViewProcess(command, experiment.root)
|
|
68
|
+
self.view = RunsView(self.process.request)
|
|
69
|
+
if self.view is not None:
|
|
70
|
+
rows = {str(row["folder"]): dict(row) for row in experiment.runs}
|
|
71
|
+
self.view.update(table_columns(experiment.config), rows)
|
|
72
|
+
self.ask(self.view)
|
|
73
|
+
self.redraw()
|
|
74
|
+
|
|
75
|
+
async def on_unmount(self) -> None:
|
|
76
|
+
if self.process is not None:
|
|
77
|
+
await self.process.close()
|
|
78
|
+
|
|
79
|
+
def ask(self, view: RunsView) -> None:
|
|
80
|
+
request = view.next_request()
|
|
81
|
+
if request is not None:
|
|
82
|
+
self.run_worker(self.answer(view, request), exit_on_error=False)
|
|
83
|
+
|
|
84
|
+
async def answer(self, view: RunsView, request: dict[str, Any]) -> None:
|
|
85
|
+
if await view.exchange(request):
|
|
86
|
+
self.redraw()
|
|
87
|
+
else:
|
|
88
|
+
self.show_status()
|
|
89
|
+
self.ask(view)
|
|
90
|
+
|
|
91
|
+
def redraw(self) -> None:
|
|
92
|
+
rows: Mapping[str, Mapping[str, Any]]
|
|
93
|
+
if self.view is None:
|
|
94
|
+
columns = table_columns(self.experiment.config)
|
|
95
|
+
rows = {str(row["folder"]): row for row in self.experiment.runs}
|
|
96
|
+
else:
|
|
97
|
+
columns, rows = self.view.table()
|
|
98
|
+
fill_table(
|
|
99
|
+
self.query_one(DataTable),
|
|
100
|
+
[column["label"] for column in columns],
|
|
101
|
+
table_rows(self.experiment.runs, columns, rows),
|
|
102
|
+
)
|
|
103
|
+
self.show_status()
|
|
104
|
+
|
|
105
|
+
def show_status(self) -> None:
|
|
106
|
+
text = "" if self.view is None else self.view.status()
|
|
107
|
+
status = self.query_one("#view-status", Static)
|
|
108
|
+
status.update(text)
|
|
109
|
+
status.display = bool(text) and self.viewing is None
|
|
110
|
+
|
|
111
|
+
async def action_open_row(self) -> None:
|
|
112
|
+
table = self.query_one(DataTable)
|
|
113
|
+
if not table.row_count:
|
|
114
|
+
return
|
|
115
|
+
folder = str(
|
|
116
|
+
table.coordinate_to_cell_key(table.cursor_coordinate).row_key.value
|
|
117
|
+
)
|
|
118
|
+
ids = {str(row["folder"]): str(row["id"]) for row in self.experiment.runs}
|
|
119
|
+
panel = RunPanel(ids[folder], self.experiment.root / folder)
|
|
120
|
+
self.viewing = folder
|
|
121
|
+
table.display = False
|
|
122
|
+
self.show_status()
|
|
123
|
+
await self.mount(panel)
|
|
124
|
+
panel.query_one(RunOutput).focus()
|
|
125
|
+
|
|
126
|
+
def action_close_run(self) -> None:
|
|
127
|
+
self.viewing = None
|
|
128
|
+
self.query_one(RunPanel).remove()
|
|
129
|
+
table = self.query_one(DataTable)
|
|
130
|
+
table.display = True
|
|
131
|
+
table.focus()
|
|
132
|
+
self.show_status()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
|
|
3
|
+
from labnotes.run_columns import run_columns
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def table_columns(config: Mapping[str, object]) -> list[dict[str, str]]:
|
|
7
|
+
keys = run_columns(config)
|
|
8
|
+
return [{"key": "status", "label": ""}, *({"key": k, "label": k} for k in keys)]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
|
|
3
|
+
from labnotes.tui.run_cells import run_cells
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def table_rows(
|
|
7
|
+
runs: Sequence[Mapping[str, object]],
|
|
8
|
+
columns: Sequence[Mapping[str, str]],
|
|
9
|
+
rows: Mapping[str, Mapping[str, object]],
|
|
10
|
+
) -> list[tuple[str, list[object]]]:
|
|
11
|
+
folders = [str(run["folder"]) for run in runs]
|
|
12
|
+
return [(folder, run_cells(rows[folder], columns)) for folder in folders]
|