labnotes 0.0.111__tar.gz → 0.0.113__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labnotes-0.0.111 → labnotes-0.0.113}/PKG-INFO +1 -1
- labnotes-0.0.113/src/labnotes/__init__.py +4 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/context.md +7 -0
- labnotes-0.0.113/src/labnotes/is_non_negative_number.py +10 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/load_experiment.py +2 -0
- labnotes-0.0.113/src/labnotes/memory_interval.py +2 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/models.py +2 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/execute_run.py +15 -1
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/read_experiment_config.py +27 -1
- labnotes-0.0.113/src/labnotes/runner/record_memory.py +18 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/run_experiment.py +1 -0
- labnotes-0.0.113/src/labnotes/runs_view_command.py +4 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/open_tables.py +35 -1
- labnotes-0.0.113/src/labnotes/tables/read_memory.py +34 -0
- labnotes-0.0.113/src/labnotes/tables/read_run_file.py +21 -0
- labnotes-0.0.113/src/labnotes/tui/browse_app.py +39 -0
- labnotes-0.0.113/src/labnotes/tui/check_view_response.py +43 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/experiment_screen.py +56 -4
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/experiments_screen.py +10 -2
- labnotes-0.0.113/src/labnotes/tui/format_cell.py +27 -0
- labnotes-0.0.113/src/labnotes/tui/group_cells.py +36 -0
- labnotes-0.0.113/src/labnotes/tui/invocation_columns.py +9 -0
- labnotes-0.0.113/src/labnotes/tui/invocation_rows.py +32 -0
- labnotes-0.0.113/src/labnotes/tui/native_rows.py +24 -0
- labnotes-0.0.113/src/labnotes/tui/run_cells.py +22 -0
- labnotes-0.0.113/src/labnotes/tui/runs_view.py +81 -0
- labnotes-0.0.113/src/labnotes/tui/view_column.py +15 -0
- labnotes-0.0.113/src/labnotes/tui/view_process.py +75 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/utils/process_alive.py +1 -1
- labnotes-0.0.113/src/labnotes/utils/tree_rss.py +6 -0
- labnotes-0.0.111/src/labnotes/utils/tree_rss.py → labnotes-0.0.113/src/labnotes/utils/tree_usage.py +2 -2
- labnotes-0.0.113/src/labnotes/view/__init__.py +3 -0
- labnotes-0.0.113/src/labnotes/view/answer.py +11 -0
- labnotes-0.0.113/src/labnotes/view/build.py +5 -0
- labnotes-0.0.113/src/labnotes/view/respond.py +13 -0
- labnotes-0.0.113/src/labnotes/view/serve.py +21 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/testing-conventions.toml +12 -0
- labnotes-0.0.111/src/labnotes/__init__.py +0 -3
- labnotes-0.0.111/src/labnotes/tui/browse_app.py +0 -25
- labnotes-0.0.111/src/labnotes/tui/group_cells.py +0 -27
- labnotes-0.0.111/src/labnotes/tui/invocation_columns.py +0 -9
- labnotes-0.0.111/src/labnotes/tui/invocation_rows.py +0 -32
- labnotes-0.0.111/src/labnotes/tui/run_cells.py +0 -29
- {labnotes-0.0.111 → labnotes-0.0.113}/.gitignore +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/CHANGELOG.md +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/MIGRATIONS.md +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/README.md +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/pyproject.toml +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/daemon_app.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/finalize_lost_runs.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/remote_start.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/init.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/log.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/log_path.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/retries.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/exit_signal.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/invocation_meta.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/server.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/config.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/run_app.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/run_screen.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/utils/__init__.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/utils/format_bytes.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/utils/run_usage.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.111 → labnotes-0.0.113}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labnotes
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.113
|
|
4
4
|
Summary: Browse experiment designs, notes, and run data from a folder.
|
|
5
5
|
Project-URL: Repository, https://github.com/thekevinscott/labnotes
|
|
6
6
|
Author-email: Kevin Scott <me@thekevinscott.com>
|
|
@@ -123,6 +123,13 @@ shows a waiting retry as `retry in 30s` and the end as `gave up (exit 75)`;
|
|
|
123
123
|
|
|
124
124
|
Runs can take a long time. Start `run` in the background, then check on it with `labnotes experiments foo runs --format json`; a run with no `ended` is still going. Each run lists `size`, the bytes in its run folder, and a running run lists `memory`, the resident bytes of its command and everything it started.
|
|
125
125
|
|
|
126
|
+
While a run goes, labnotes appends a memory sample to `memory.jsonl` in its run
|
|
127
|
+
folder every 5 seconds: one JSON line, `{"time": <unix seconds>, "rss": <bytes>,
|
|
128
|
+
"processes": <count>}`, for the command and everything it started. It keeps
|
|
129
|
+
every sample, so a run killed for memory still shows the climb.
|
|
130
|
+
`memory_interval` in `experiment.toml` (or `[experiment]` in `.labnotes.toml`)
|
|
131
|
+
sets the seconds between samples; 0 turns it off.
|
|
132
|
+
|
|
126
133
|
In a terminal, `run` shows a live table of runs (status, time, memory and
|
|
127
134
|
folder size) and stays
|
|
128
135
|
open when they finish. Failed runs show their status in red; `f` hides or shows them, and `r` on one
|
|
@@ -19,6 +19,7 @@ from labnotes.nests_runs import nests_runs
|
|
|
19
19
|
from labnotes.parse_run_view import parse_run_view
|
|
20
20
|
from labnotes.python_blocks import python_blocks
|
|
21
21
|
from labnotes.render_markdown import render_markdown
|
|
22
|
+
from labnotes.runs_view_command import runs_view_command
|
|
22
23
|
from labnotes.sort_href import sort_href
|
|
23
24
|
from labnotes.sql_quote import quote_string
|
|
24
25
|
from labnotes.summarize_config import summarize_config
|
|
@@ -88,6 +89,7 @@ async def load_experiment(
|
|
|
88
89
|
"run_columns": shown,
|
|
89
90
|
"run_view": view,
|
|
90
91
|
"run_choices": choices,
|
|
92
|
+
"runs_view": runs_view_command(config),
|
|
91
93
|
"sort_hrefs": {
|
|
92
94
|
column: sort_href(view, column) for column in ["status", *shown]
|
|
93
95
|
},
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Any, Protocol
|
|
5
5
|
|
|
6
6
|
from labnotes.default_counts import DEFAULT_COUNTS
|
|
7
|
+
from labnotes.memory_interval import DEFAULT_MEMORY_INTERVAL
|
|
7
8
|
from labnotes.modes import DEFAULT_MODE
|
|
8
9
|
from labnotes.retries import DEFAULT_RETRIES
|
|
9
10
|
|
|
@@ -66,3 +67,4 @@ class ExperimentConfig:
|
|
|
66
67
|
concurrency: int = 1
|
|
67
68
|
retries: int = DEFAULT_RETRIES
|
|
68
69
|
meta: str | None = None
|
|
70
|
+
memory_interval: float = DEFAULT_MEMORY_INTERVAL
|
|
@@ -9,6 +9,7 @@ import psutil
|
|
|
9
9
|
|
|
10
10
|
from labnotes.log_path import LOG_ENV
|
|
11
11
|
from labnotes.runner.exit_signal import exit_signal
|
|
12
|
+
from labnotes.runner.record_memory import record_memory
|
|
12
13
|
from labnotes.runner.shell_command import shell_command
|
|
13
14
|
from labnotes.runner.stop_then_kill import GRACE_SECONDS
|
|
14
15
|
from labnotes.runner.wait_for_tree import wait_for_tree
|
|
@@ -30,6 +31,8 @@ async def execute_run(
|
|
|
30
31
|
stop: Callable[[], None] = lambda: None,
|
|
31
32
|
stopped: Callable[[], bool] = lambda: False,
|
|
32
33
|
meta: dict[str, object] | None = None,
|
|
34
|
+
*,
|
|
35
|
+
memory_interval: float,
|
|
33
36
|
) -> dict[str, object]:
|
|
34
37
|
resolved = shell_command(command, folder, params)
|
|
35
38
|
run: dict[str, object] = {
|
|
@@ -69,9 +72,17 @@ async def execute_run(
|
|
|
69
72
|
else:
|
|
70
73
|
running.add(process)
|
|
71
74
|
run["pid"] = process.pid
|
|
75
|
+
created = None
|
|
72
76
|
with contextlib.suppress(psutil.Error):
|
|
73
|
-
run["pid_created"] = psutil.Process(process.pid).create_time()
|
|
77
|
+
run["pid_created"] = created = psutil.Process(process.pid).create_time()
|
|
74
78
|
write_run_json(folder, run)
|
|
79
|
+
sampler = None
|
|
80
|
+
if memory_interval > 0 and created is not None:
|
|
81
|
+
sampler = asyncio.create_task(
|
|
82
|
+
record_memory(
|
|
83
|
+
folder / "memory.jsonl", process.pid, created, memory_interval
|
|
84
|
+
)
|
|
85
|
+
)
|
|
75
86
|
exited = asyncio.ensure_future(process.wait())
|
|
76
87
|
kill_later = None
|
|
77
88
|
try:
|
|
@@ -100,6 +111,9 @@ async def execute_run(
|
|
|
100
111
|
finally:
|
|
101
112
|
if kill_later is not None:
|
|
102
113
|
kill_later.cancel()
|
|
114
|
+
if sampler is not None:
|
|
115
|
+
sampler.cancel()
|
|
116
|
+
await asyncio.wait({sampler})
|
|
103
117
|
running.discard(process)
|
|
104
118
|
run["ended"] = iso_time(time.time())
|
|
105
119
|
write_run_json(folder, run)
|
|
@@ -7,6 +7,8 @@ from labnotes.compose_experiment import compose_experiment
|
|
|
7
7
|
from labnotes.default_counts import DEFAULT_COUNTS
|
|
8
8
|
from labnotes.models import ExperimentConfig
|
|
9
9
|
from labnotes.is_non_negative_int import is_non_negative_int
|
|
10
|
+
from labnotes.is_non_negative_number import is_non_negative_number
|
|
11
|
+
from labnotes.memory_interval import DEFAULT_MEMORY_INTERVAL
|
|
10
12
|
from labnotes.is_positive_int import is_positive_int
|
|
11
13
|
from labnotes.modes import MODES
|
|
12
14
|
|
|
@@ -20,7 +22,10 @@ SETTINGS = {
|
|
|
20
22
|
"concurrency",
|
|
21
23
|
"retries",
|
|
22
24
|
"meta",
|
|
25
|
+
"memory_interval",
|
|
26
|
+
"views",
|
|
23
27
|
}
|
|
28
|
+
VIEWS = {"runs"}
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
def read_experiment_config(
|
|
@@ -73,6 +78,27 @@ def read_experiment_config(
|
|
|
73
78
|
meta = values.get("meta")
|
|
74
79
|
if meta is not None and (not isinstance(meta, str) or not meta.strip()):
|
|
75
80
|
raise ValueError(f"{path}: meta must be a string")
|
|
81
|
+
memory_interval = values.get("memory_interval", DEFAULT_MEMORY_INTERVAL)
|
|
82
|
+
if not is_non_negative_number(memory_interval):
|
|
83
|
+
raise ValueError(f"{path}: memory_interval must be 0 or more seconds")
|
|
84
|
+
views = values.get("views", {})
|
|
85
|
+
if not isinstance(views, dict):
|
|
86
|
+
raise ValueError(f"{path}: views must be a table, like [views]")
|
|
87
|
+
unknown = sorted(views.keys() - VIEWS)
|
|
88
|
+
if unknown:
|
|
89
|
+
raise ValueError(f"{path}: unknown keys in views: {', '.join(unknown)}")
|
|
90
|
+
runs = views.get("runs")
|
|
91
|
+
if runs is not None and (not isinstance(runs, str) or not runs.strip()):
|
|
92
|
+
raise ValueError(f"{path}: views.runs must be a command")
|
|
76
93
|
return ExperimentConfig(
|
|
77
|
-
command,
|
|
94
|
+
command,
|
|
95
|
+
repeats,
|
|
96
|
+
params,
|
|
97
|
+
name,
|
|
98
|
+
counts,
|
|
99
|
+
mode,
|
|
100
|
+
concurrency,
|
|
101
|
+
retries,
|
|
102
|
+
meta,
|
|
103
|
+
memory_interval,
|
|
78
104
|
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from labnotes.utils.tree_usage import tree_usage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def record_memory(path: Path, pid: int, created: float, interval: float) -> None:
|
|
10
|
+
with path.open("a", encoding="utf-8") as samples:
|
|
11
|
+
while True:
|
|
12
|
+
usage = tree_usage(pid, created)
|
|
13
|
+
if usage is not None:
|
|
14
|
+
rss, processes = usage
|
|
15
|
+
sample = {"time": time.time(), "rss": rss, "processes": processes}
|
|
16
|
+
samples.write(json.dumps(sample) + "\n")
|
|
17
|
+
samples.flush()
|
|
18
|
+
await asyncio.sleep(interval)
|
|
@@ -10,7 +10,9 @@ from labnotes.tables.read_blocks import read_blocks
|
|
|
10
10
|
from labnotes.tables.read_experiment import read_experiment
|
|
11
11
|
from labnotes.tables.read_file import read_file
|
|
12
12
|
from labnotes.tables.read_log import read_log
|
|
13
|
+
from labnotes.tables.read_memory import read_memory
|
|
13
14
|
from labnotes.tables.read_run import read_run
|
|
15
|
+
from labnotes.tables.read_run_file import read_run_file
|
|
14
16
|
|
|
15
17
|
FILES = """
|
|
16
18
|
CREATE TABLE files (
|
|
@@ -39,10 +41,33 @@ CREATE TABLE blocks (
|
|
|
39
41
|
PRIMARY KEY (experiment, run)
|
|
40
42
|
)
|
|
41
43
|
"""
|
|
44
|
+
MEMORY = """
|
|
45
|
+
CREATE TABLE memory (
|
|
46
|
+
experiment TEXT, run TEXT, line INTEGER, time REAL, rss INTEGER, processes INTEGER,
|
|
47
|
+
PRIMARY KEY (experiment, run, line)
|
|
48
|
+
)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
# Every file in a run folder, so a change anywhere in it is a watch event.
|
|
52
|
+
RUN_FILES = """
|
|
53
|
+
CREATE TABLE run_files (
|
|
54
|
+
experiment TEXT, path TEXT PRIMARY KEY, size INTEGER, mtime_ns INTEGER
|
|
55
|
+
)
|
|
56
|
+
"""
|
|
42
57
|
|
|
43
58
|
|
|
44
|
-
def open_tables(config: LabnotesConfig) -> DirSQL:
|
|
59
|
+
def open_tables(config: LabnotesConfig, run_files: bool = False) -> DirSQL:
|
|
45
60
|
root = config.root
|
|
61
|
+
extra = [
|
|
62
|
+
Table(
|
|
63
|
+
name="run_files",
|
|
64
|
+
ddl=RUN_FILES,
|
|
65
|
+
glob=path_glob(f"{config.runs}/**"),
|
|
66
|
+
on_file=partial(
|
|
67
|
+
read_run_file, root, config.runs, experiment=config.experiment
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
]
|
|
46
71
|
return DirSQL(
|
|
47
72
|
str(root),
|
|
48
73
|
tables=[
|
|
@@ -78,6 +103,14 @@ def open_tables(config: LabnotesConfig) -> DirSQL:
|
|
|
78
103
|
read_blocks, root, config.runs, experiment=config.experiment
|
|
79
104
|
),
|
|
80
105
|
),
|
|
106
|
+
Table(
|
|
107
|
+
name="memory",
|
|
108
|
+
ddl=MEMORY,
|
|
109
|
+
glob=path_glob(f"{config.runs}/memory.jsonl"),
|
|
110
|
+
on_file=partial(
|
|
111
|
+
read_memory, root, config.runs, experiment=config.experiment
|
|
112
|
+
),
|
|
113
|
+
),
|
|
81
114
|
Table(
|
|
82
115
|
name="experiment_configs",
|
|
83
116
|
ddl=experiments_ddl(config),
|
|
@@ -90,6 +123,7 @@ def open_tables(config: LabnotesConfig) -> DirSQL:
|
|
|
90
123
|
defaults=config.defaults,
|
|
91
124
|
),
|
|
92
125
|
),
|
|
126
|
+
*(extra if run_files else []),
|
|
93
127
|
],
|
|
94
128
|
# Run folders are often gitignored (DVC, large outputs); scan them anyway.
|
|
95
129
|
no_ignore=True,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from labnotes.tables.path_pattern import path_pattern
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def read_memory(
|
|
8
|
+
root: Path, template: str, path: str, experiment: str | None = None
|
|
9
|
+
) -> list[dict[str, object]]:
|
|
10
|
+
relative = Path(path).relative_to(root).as_posix()
|
|
11
|
+
found = path_pattern(f"{template}/memory.jsonl").fullmatch(relative)
|
|
12
|
+
if found is None:
|
|
13
|
+
return []
|
|
14
|
+
rows: list[dict[str, object]] = []
|
|
15
|
+
lines = Path(path).read_text(encoding="utf-8").splitlines()
|
|
16
|
+
for line, text in enumerate(lines):
|
|
17
|
+
# A run killed mid-write leaves a cut-off last line.
|
|
18
|
+
try:
|
|
19
|
+
sample = json.loads(text)
|
|
20
|
+
except json.JSONDecodeError:
|
|
21
|
+
continue
|
|
22
|
+
if not isinstance(sample, dict):
|
|
23
|
+
continue
|
|
24
|
+
rows.append(
|
|
25
|
+
{
|
|
26
|
+
"experiment": experiment or found["experiment"],
|
|
27
|
+
"run": found["run"],
|
|
28
|
+
"line": line,
|
|
29
|
+
"time": sample.get("time"),
|
|
30
|
+
"rss": sample.get("rss"),
|
|
31
|
+
"processes": sample.get("processes"),
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
return rows
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from labnotes.tables.path_pattern import path_pattern
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def read_run_file(
|
|
7
|
+
root: Path, template: str, path: str, experiment: str | None = None
|
|
8
|
+
) -> list[dict[str, object]]:
|
|
9
|
+
relative = Path(path).relative_to(root).as_posix()
|
|
10
|
+
found = path_pattern(f"{template}/**").fullmatch(relative)
|
|
11
|
+
if found is None:
|
|
12
|
+
return []
|
|
13
|
+
stat = Path(path).stat()
|
|
14
|
+
return [
|
|
15
|
+
{
|
|
16
|
+
"experiment": experiment or found["experiment"],
|
|
17
|
+
"path": relative,
|
|
18
|
+
"size": stat.st_size,
|
|
19
|
+
"mtime_ns": stat.st_mtime_ns,
|
|
20
|
+
}
|
|
21
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from textual.app import App
|
|
2
|
+
from textual.binding import Binding
|
|
3
|
+
|
|
4
|
+
from labnotes.models import LabnotesConfig
|
|
5
|
+
from labnotes.tables.open_tables import open_tables
|
|
6
|
+
from labnotes.tui.experiment_screen import ExperimentScreen
|
|
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
|
+
|
|
11
|
+
|
|
12
|
+
class BrowseApp(App[BaseException | None]):
|
|
13
|
+
BINDINGS = [Binding("ctrl+c", "quit", "Quit")]
|
|
14
|
+
|
|
15
|
+
def __init__(self, config: LabnotesConfig, name: str | None) -> None:
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.config = config
|
|
18
|
+
self.experiment_name = name
|
|
19
|
+
self.title = config.root.name
|
|
20
|
+
self.views: dict[str, ViewProcess] = {}
|
|
21
|
+
|
|
22
|
+
def on_mount(self) -> None:
|
|
23
|
+
tables = open_tables(self.config, run_files=True)
|
|
24
|
+
self.push_screen(ExperimentsScreen(tables, self.config, self.view_request))
|
|
25
|
+
if self.experiment_name is not None:
|
|
26
|
+
self.push_screen(
|
|
27
|
+
ExperimentScreen(
|
|
28
|
+
tables, self.config, self.experiment_name, self.view_request
|
|
29
|
+
)
|
|
30
|
+
)
|
|
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,43 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from labnotes.tui.view_column import view_column
|
|
6
|
+
|
|
7
|
+
SHAPE = "the response must be an object with columns and rows"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_view_response(
|
|
11
|
+
reply: str, request: Mapping[str, Any]
|
|
12
|
+
) -> tuple[list[dict[str, str]], dict[str, dict[str, Any]]]:
|
|
13
|
+
try:
|
|
14
|
+
response = json.loads(reply)
|
|
15
|
+
except json.JSONDecodeError as error:
|
|
16
|
+
raise ValueError(f"invalid JSON: {error}") from error
|
|
17
|
+
if not isinstance(response, dict) or response.keys() != {"columns", "rows"}:
|
|
18
|
+
raise ValueError(SHAPE)
|
|
19
|
+
if not isinstance(response["columns"], list):
|
|
20
|
+
raise ValueError("columns must be a list")
|
|
21
|
+
columns = [view_column(entry) for entry in response["columns"]]
|
|
22
|
+
keys = set()
|
|
23
|
+
for each in columns:
|
|
24
|
+
if each["key"] in keys:
|
|
25
|
+
raise ValueError(f"column {each['key']} is listed twice")
|
|
26
|
+
keys.add(each["key"])
|
|
27
|
+
rows = response["rows"]
|
|
28
|
+
if not isinstance(rows, dict):
|
|
29
|
+
raise ValueError("rows must be an object")
|
|
30
|
+
asked = request["rows"]
|
|
31
|
+
for folder in asked:
|
|
32
|
+
if folder not in rows:
|
|
33
|
+
raise ValueError(f"no row for {folder}")
|
|
34
|
+
for folder, row in rows.items():
|
|
35
|
+
if folder not in asked:
|
|
36
|
+
raise ValueError(f"a row for {folder}, which wasn't asked for")
|
|
37
|
+
if not isinstance(row, dict):
|
|
38
|
+
raise ValueError(f"{folder}: the row must be an object")
|
|
39
|
+
# Native values may ride along unshown, so {**row, ...} needs no filtering.
|
|
40
|
+
stray = sorted(row.keys() - keys - asked[folder].keys())
|
|
41
|
+
if stray:
|
|
42
|
+
raise ValueError(f"{folder}: unknown columns {', '.join(stray)}")
|
|
43
|
+
return columns, rows
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
1
2
|
from typing import Any
|
|
2
3
|
|
|
3
4
|
from dirsql import DirSQL
|
|
@@ -5,7 +6,7 @@ from starlette.datastructures import QueryParams
|
|
|
5
6
|
from textual.app import ComposeResult
|
|
6
7
|
from textual.binding import Binding
|
|
7
8
|
from textual.screen import Screen
|
|
8
|
-
from textual.widgets import DataTable, Footer, Header
|
|
9
|
+
from textual.widgets import DataTable, Footer, Header, Static
|
|
9
10
|
from textual.worker import Worker, WorkerState
|
|
10
11
|
|
|
11
12
|
from labnotes.load_experiment import load_experiment
|
|
@@ -13,9 +14,11 @@ from labnotes.models import LabnotesConfig
|
|
|
13
14
|
from labnotes.tui.fill_table import fill_table
|
|
14
15
|
from labnotes.tui.invocation_columns import invocation_columns
|
|
15
16
|
from labnotes.tui.invocation_rows import invocation_rows
|
|
17
|
+
from labnotes.tui.native_rows import native_rows
|
|
16
18
|
from labnotes.tui.reload_on_change import reload_on_change
|
|
17
19
|
from labnotes.tui.run_screen import RunScreen
|
|
18
20
|
from labnotes.tui.runs_table import RunsTable
|
|
21
|
+
from labnotes.tui.runs_view import Request, RunsView
|
|
19
22
|
from labnotes.tui.stopped_title import stopped_title
|
|
20
23
|
from labnotes.watch_rows import watch_rows
|
|
21
24
|
|
|
@@ -23,22 +26,35 @@ WATCHED = {"runs", "experiment_configs"}
|
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
class ExperimentScreen(Screen[None]):
|
|
29
|
+
DEFAULT_CSS = """
|
|
30
|
+
ExperimentScreen RunsTable { height: 1fr; }
|
|
31
|
+
ExperimentScreen #view-status { color: $text-warning; }
|
|
32
|
+
"""
|
|
26
33
|
BINDINGS = [
|
|
27
34
|
Binding("enter", "open_row", "Open", priority=True),
|
|
28
35
|
Binding("escape", "app.pop_screen", "Back"),
|
|
29
36
|
]
|
|
30
37
|
|
|
31
|
-
def __init__(
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
tables: DirSQL,
|
|
41
|
+
config: LabnotesConfig,
|
|
42
|
+
name: str,
|
|
43
|
+
views: Callable[[str], Request],
|
|
44
|
+
) -> None:
|
|
32
45
|
super().__init__()
|
|
33
46
|
self.tables = tables
|
|
34
47
|
self.config = config
|
|
35
48
|
self.experiment_name = name
|
|
49
|
+
self.views = views
|
|
50
|
+
self.view: RunsView | None = None
|
|
36
51
|
self.experiment: dict[str, Any] = {}
|
|
37
52
|
self.opened: set[str] = set()
|
|
38
53
|
|
|
39
54
|
def compose(self) -> ComposeResult:
|
|
40
55
|
yield Header()
|
|
41
56
|
yield RunsTable(id="runs", cursor_type="row")
|
|
57
|
+
yield Static(id="view-status")
|
|
42
58
|
yield Footer()
|
|
43
59
|
|
|
44
60
|
def on_mount(self) -> None:
|
|
@@ -75,14 +91,50 @@ class ExperimentScreen(Screen[None]):
|
|
|
75
91
|
return
|
|
76
92
|
self.experiment = loaded
|
|
77
93
|
self.sub_title = str(loaded["display_name"])
|
|
94
|
+
command = loaded["runs_view"]
|
|
95
|
+
if command is not None and self.view is None:
|
|
96
|
+
self.view = RunsView(self.views(str(command)))
|
|
97
|
+
self.run_worker(self.watch_files(self.view), exit_on_error=False)
|
|
98
|
+
if self.view is not None:
|
|
99
|
+
self.view.update(invocation_columns(loaded), native_rows(loaded))
|
|
100
|
+
self.ask(self.view)
|
|
78
101
|
self.redraw()
|
|
79
102
|
|
|
103
|
+
async def watch_files(self, view: RunsView) -> None:
|
|
104
|
+
async for row in watch_rows(self.tables, {"run_files"}, self.experiment_name):
|
|
105
|
+
view.touch(str(row["path"]))
|
|
106
|
+
self.ask(view)
|
|
107
|
+
|
|
108
|
+
def ask(self, view: RunsView) -> None:
|
|
109
|
+
request = view.next_request()
|
|
110
|
+
if request is not None:
|
|
111
|
+
self.run_worker(self.answer(view, request), exit_on_error=False)
|
|
112
|
+
|
|
113
|
+
async def answer(self, view: RunsView, request: dict[str, Any]) -> None:
|
|
114
|
+
if await view.exchange(request):
|
|
115
|
+
self.redraw()
|
|
116
|
+
else:
|
|
117
|
+
self.show_status()
|
|
118
|
+
self.ask(view)
|
|
119
|
+
|
|
80
120
|
def redraw(self) -> None:
|
|
121
|
+
if self.view is None:
|
|
122
|
+
columns = invocation_columns(self.experiment)
|
|
123
|
+
rows = native_rows(self.experiment)
|
|
124
|
+
else:
|
|
125
|
+
columns, rows = self.view.table()
|
|
81
126
|
fill_table(
|
|
82
127
|
self.query_one(DataTable),
|
|
83
|
-
|
|
84
|
-
invocation_rows(self.experiment, self.opened),
|
|
128
|
+
[column["label"] for column in columns],
|
|
129
|
+
invocation_rows(self.experiment, self.opened, columns, rows),
|
|
85
130
|
)
|
|
131
|
+
self.show_status()
|
|
132
|
+
|
|
133
|
+
def show_status(self) -> None:
|
|
134
|
+
text = "" if self.view is None else self.view.status()
|
|
135
|
+
status = self.query_one("#view-status", Static)
|
|
136
|
+
status.update(text)
|
|
137
|
+
status.display = bool(text)
|
|
86
138
|
|
|
87
139
|
def action_open_row(self) -> None:
|
|
88
140
|
table = self.query_one(DataTable)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
1
3
|
from dirsql import DirSQL
|
|
2
4
|
from textual.app import ComposeResult
|
|
3
5
|
from textual.screen import Screen
|
|
@@ -10,6 +12,7 @@ from labnotes.tui.experiment_screen import ExperimentScreen
|
|
|
10
12
|
from labnotes.tui.fill_table import fill_table
|
|
11
13
|
from labnotes.tui.reload_on_change import reload_on_change
|
|
12
14
|
from labnotes.tui.runs_table import RunsTable
|
|
15
|
+
from labnotes.tui.runs_view import Request
|
|
13
16
|
from labnotes.tui.stopped_title import stopped_title
|
|
14
17
|
from labnotes.watch_rows import watch_rows
|
|
15
18
|
|
|
@@ -17,10 +20,13 @@ WATCHED = {"files", "runs", "experiment_configs"}
|
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
class ExperimentsScreen(Screen[None]):
|
|
20
|
-
def __init__(
|
|
23
|
+
def __init__(
|
|
24
|
+
self, tables: DirSQL, config: LabnotesConfig, views: Callable[[str], Request]
|
|
25
|
+
) -> None:
|
|
21
26
|
super().__init__()
|
|
22
27
|
self.tables = tables
|
|
23
28
|
self.config = config
|
|
29
|
+
self.views = views
|
|
24
30
|
|
|
25
31
|
def compose(self) -> ComposeResult:
|
|
26
32
|
yield Header()
|
|
@@ -57,5 +63,7 @@ class ExperimentsScreen(Screen[None]):
|
|
|
57
63
|
|
|
58
64
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
59
65
|
self.app.push_screen(
|
|
60
|
-
ExperimentScreen(
|
|
66
|
+
ExperimentScreen(
|
|
67
|
+
self.tables, self.config, str(event.row_key.value), self.views
|
|
68
|
+
)
|
|
61
69
|
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from datetime import UTC, datetime
|
|
2
|
+
|
|
3
|
+
from rich.text import Text
|
|
4
|
+
|
|
5
|
+
from labnotes.format_value import format_value
|
|
6
|
+
from labnotes.tui.dim import dim
|
|
7
|
+
from labnotes.tui.format_elapsed import format_elapsed
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def format_cell(key: str, value: object, live: bool) -> object:
|
|
11
|
+
if key == "status" and value == "passed":
|
|
12
|
+
return Text("✓", style="green")
|
|
13
|
+
if key == "status" and value == "failed":
|
|
14
|
+
return Text("✕", style="red")
|
|
15
|
+
if key == "status" and value == "unfinished":
|
|
16
|
+
return Text("◐", style="yellow") if live else dim("–")
|
|
17
|
+
if key == "duration" and isinstance(value, int) and not isinstance(value, bool):
|
|
18
|
+
return format_elapsed(value)
|
|
19
|
+
if key == "started" and isinstance(value, str):
|
|
20
|
+
try:
|
|
21
|
+
started = datetime.fromisoformat(value)
|
|
22
|
+
except ValueError:
|
|
23
|
+
return value
|
|
24
|
+
if started.tzinfo is not None:
|
|
25
|
+
started = started.astimezone(UTC)
|
|
26
|
+
return started.strftime("%Y-%m-%d %H:%M:%S")
|
|
27
|
+
return format_value(value)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
|
|
6
|
+
from labnotes.format_value import format_value
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def group_cells(
|
|
10
|
+
group: Mapping[str, Any],
|
|
11
|
+
columns: Sequence[Mapping[str, str]],
|
|
12
|
+
live: bool,
|
|
13
|
+
opened: bool,
|
|
14
|
+
) -> list[object]:
|
|
15
|
+
cells: list[object] = []
|
|
16
|
+
for column in columns:
|
|
17
|
+
key = column["key"]
|
|
18
|
+
if key == "status":
|
|
19
|
+
status = Text.assemble(
|
|
20
|
+
(f"{group['passed']}✓", "green"), " ", (f"{group['failed']}✕", "red")
|
|
21
|
+
)
|
|
22
|
+
if group["unfinished"]:
|
|
23
|
+
mark, style = ("◐", "yellow") if live else ("–", "dim")
|
|
24
|
+
status.append(f" {group['unfinished']}{mark}", style=style)
|
|
25
|
+
cells.append(status)
|
|
26
|
+
elif key == "id":
|
|
27
|
+
cells.append(f"{'▾' if opened else '▸'} {group['name']}")
|
|
28
|
+
elif key == "runs":
|
|
29
|
+
cells.append(str(len(group["runs"])))
|
|
30
|
+
elif key == "started":
|
|
31
|
+
cells.append(group["started"] or "")
|
|
32
|
+
elif key in group["shared"]:
|
|
33
|
+
cells.append(format_value(group["shared"][key]))
|
|
34
|
+
else:
|
|
35
|
+
cells.append("")
|
|
36
|
+
return cells
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def invocation_columns(experiment: Mapping[str, Any]) -> list[dict[str, str]]:
|
|
6
|
+
keys = list(experiment["run_columns"])
|
|
7
|
+
if experiment["run_groups"] is not None:
|
|
8
|
+
keys.insert(1, "runs")
|
|
9
|
+
return [{"key": "status", "label": ""}, *({"key": k, "label": k} for k in keys)]
|