labnotes 0.0.137__tar.gz → 0.1.0__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.137 → labnotes-0.1.0}/PKG-INFO +7 -8
- {labnotes-0.0.137 → labnotes-0.1.0}/README.md +6 -7
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/cli.py +0 -4
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/context.md +13 -29
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/experiments.py +2 -2
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/load_experiment.py +14 -2
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/execute_run.py +4 -1
- labnotes-0.1.0/src/labnotes/runner/project_snapshot.py +118 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/run_experiment.py +15 -4
- labnotes-0.1.0/src/labnotes/runner/start_experiment.py +40 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/leave_prompt.py +2 -3
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/run_app.py +2 -5
- {labnotes-0.0.137 → labnotes-0.1.0}/testing-conventions.toml +0 -30
- {labnotes-0.0.137 → labnotes-0.1.0}/tests/integration/conftest.py +0 -19
- {labnotes-0.0.137 → labnotes-0.1.0}/uv.lock +6 -6
- labnotes-0.0.137/src/labnotes/daemon/control_run.py +0 -16
- labnotes-0.0.137/src/labnotes/daemon/daemon_app.py +0 -181
- labnotes-0.0.137/src/labnotes/daemon/daemon_client.py +0 -12
- labnotes-0.0.137/src/labnotes/daemon/daemon_running.py +0 -14
- labnotes-0.0.137/src/labnotes/daemon/daemon_server.py +0 -12
- labnotes-0.0.137/src/labnotes/daemon/describe_status.py +0 -21
- labnotes-0.0.137/src/labnotes/daemon/ensure_daemon.py +0 -26
- labnotes-0.0.137/src/labnotes/daemon/exit_when_idle.py +0 -15
- labnotes-0.0.137/src/labnotes/daemon/last_stderr.py +0 -15
- labnotes-0.0.137/src/labnotes/daemon/remote_start.py +0 -94
- labnotes-0.0.137/src/labnotes/daemon/run_daemon.py +0 -29
- labnotes-0.0.137/src/labnotes/daemon/run_logs.py +0 -54
- labnotes-0.0.137/src/labnotes/daemon/serve_daemon.py +0 -20
- labnotes-0.0.137/src/labnotes/daemon/socket_path.py +0 -15
- labnotes-0.0.137/src/labnotes/daemon/stop_runs.py +0 -12
- labnotes-0.0.137/src/labnotes/daemon/watch_daemon.py +0 -77
- labnotes-0.0.137/src/labnotes/daemon/watch_status.py +0 -19
- labnotes-0.0.137/src/labnotes/daemon_command.py +0 -14
- labnotes-0.0.137/src/labnotes/run_command.py +0 -32
- labnotes-0.0.137/src/labnotes/run_control.py +0 -45
- labnotes-0.0.137/src/labnotes/run_status.py +0 -29
- labnotes-0.0.137/src/labnotes/runner/hold_folder.py +0 -15
- labnotes-0.0.137/src/labnotes/utils/__init__.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/.gitignore +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/CHANGELOG.md +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/MIGRATIONS.md +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/pyproject.toml +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/get_state.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/grid_cell.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/init.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/is_non_negative_number.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/log.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/log_path.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/memory_interval.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/models.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/parse_subset.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/pivot_runs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/retries.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.137/src/labnotes/daemon → labnotes-0.1.0/src/labnotes/runner}/__init__.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/cgroup_memory.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/check_args.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/check_subset.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/container_cgroup.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/container_memory.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/docker_output.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/exit_signal.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/fill_dirs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/fill_experiment_env.py +0 -0
- {labnotes-0.0.137/src/labnotes/daemon → labnotes-0.1.0/src/labnotes/runner}/finalize_lost_runs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/flag_name.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/hook_dirs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/in_subset.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/invocation_hook.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/invocation_meta.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/record_memory.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/run_git.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runs_view_command.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/server.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/set_state.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/state_path.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.137/src/labnotes/runner → labnotes-0.1.0/src/labnotes/tables}/__init__.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_memory.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_run_file.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/runs_columns.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.137/src/labnotes/tables → labnotes-0.1.0/src/labnotes/tui}/__init__.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/check_view_response.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/config.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/format_cell.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/native_rows.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/run_screen.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/runs_view.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/view_column.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/view_process.py +0 -0
- {labnotes-0.0.137/src/labnotes/tui → labnotes-0.1.0/src/labnotes/utils}/__init__.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/utils/format_bytes.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/utils/process_alive.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/utils/run_usage.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/utils/tree_rss.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/utils/tree_usage.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/view/__init__.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/view/answer.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/view/build.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/view/respond.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/view/serve.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/tests/conftest.py +0 -0
- {labnotes-0.0.137 → labnotes-0.1.0}/tests/e2e/conftest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labnotes
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
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>
|
|
@@ -122,13 +122,12 @@ line per event instead.
|
|
|
122
122
|
- `-e PATH` reads another file instead of `experiment.toml`.
|
|
123
123
|
- `--PARAM=VALUE` runs only the cells where that param has that value, like
|
|
124
124
|
`--model=small`.
|
|
125
|
-
- With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
-
|
|
129
|
-
|
|
130
|
-
- It exits 0 even if runs fail; the summary line counts failures.
|
|
131
|
-
start or reach the daemon, it prints the cause on one line and exits 1.
|
|
125
|
+
- With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) stops the running scripts and records their end,
|
|
126
|
+
then kills any still going 5 seconds later. Escape goes back. SIGTERM and
|
|
127
|
+
SIGHUP stop them the same way.
|
|
128
|
+
- The foreground process schedules the next run when a slot frees. Closing
|
|
129
|
+
its terminal stops the runs. Agents should use a dedicated tmux window.
|
|
130
|
+
- It exits 0 even if runs fail; the summary line counts failures.
|
|
132
131
|
|
|
133
132
|
`labnotes server` serves a local site at http://127.0.0.1:8000 from the same
|
|
134
133
|
`.labnotes.toml`: a table of experiments, and a page per experiment with its
|
|
@@ -97,13 +97,12 @@ line per event instead.
|
|
|
97
97
|
- `-e PATH` reads another file instead of `experiment.toml`.
|
|
98
98
|
- `--PARAM=VALUE` runs only the cells where that param has that value, like
|
|
99
99
|
`--model=small`.
|
|
100
|
-
- With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
|
|
105
|
-
- It exits 0 even if runs fail; the summary line counts failures.
|
|
106
|
-
start or reach the daemon, it prints the cause on one line and exits 1.
|
|
100
|
+
- With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) stops the running scripts and records their end,
|
|
101
|
+
then kills any still going 5 seconds later. Escape goes back. SIGTERM and
|
|
102
|
+
SIGHUP stop them the same way.
|
|
103
|
+
- The foreground process schedules the next run when a slot frees. Closing
|
|
104
|
+
its terminal stops the runs. Agents should use a dedicated tmux window.
|
|
105
|
+
- It exits 0 even if runs fail; the summary line counts failures.
|
|
107
106
|
|
|
108
107
|
`labnotes server` serves a local site at http://127.0.0.1:8000 from the same
|
|
109
108
|
`.labnotes.toml`: a table of experiments, and a page per experiment with its
|
|
@@ -3,12 +3,10 @@ from pathlib import Path
|
|
|
3
3
|
import click
|
|
4
4
|
|
|
5
5
|
from labnotes.context_guide import context_guide
|
|
6
|
-
from labnotes.daemon_command import daemon_command
|
|
7
6
|
from labnotes.experiments import experiments
|
|
8
7
|
from labnotes.init import init
|
|
9
8
|
from labnotes.load_config import load_config
|
|
10
9
|
from labnotes.read_config import CONFIG_FILE
|
|
11
|
-
from labnotes.run_command import run_command
|
|
12
10
|
from labnotes.server import server
|
|
13
11
|
|
|
14
12
|
|
|
@@ -39,8 +37,6 @@ def context(ctx: click.Context) -> None:
|
|
|
39
37
|
click.echo(context_guide(load_config(ctx), Path.cwd()), nl=False)
|
|
40
38
|
|
|
41
39
|
|
|
42
|
-
main.add_command(daemon_command)
|
|
43
40
|
main.add_command(experiments)
|
|
44
41
|
main.add_command(init)
|
|
45
|
-
main.add_command(run_command)
|
|
46
42
|
main.add_command(server)
|
|
@@ -85,7 +85,7 @@ run folder. `command` is the shell command the run executed, with the placeholde
|
|
|
85
85
|
and the params filled in, written before the run starts, so a killed run has it
|
|
86
86
|
and a later edit to `experiment.toml` doesn't hide what a run did. `ended_by` is `"timeout"`, `"stop"` (labnotes stopped it),
|
|
87
87
|
`"signal"` (a signal labnotes didn't send, like the out-of-memory killer),
|
|
88
|
-
`"lost"` (its process died unseen; the
|
|
88
|
+
`"lost"` (its process died unseen; the next run marks these when it starts, with
|
|
89
89
|
`ended` set and `exit_code` null) or null (the script exited itself). `signal` is the signal number read from the
|
|
90
90
|
exit code (negative, or 128 + N from the shell), else null.
|
|
91
91
|
`git` is `{"commit": "<full HEAD SHA>", "dirty": false}`, read from the command's
|
|
@@ -157,10 +157,10 @@ adding a repeat. When the retries run out, the last run keeps exit 75: it never
|
|
|
157
157
|
got to test its params, so treat it as blocked, not as a result. Stopping the
|
|
158
158
|
runs, or `x` on a run waiting to retry, cancels the retry. A run that was stopped never retries. The live view
|
|
159
159
|
shows a waiting retry as `retry in 30s` and the end as `gave up (exit 75)`;
|
|
160
|
-
|
|
160
|
+
The live view reports `retrying`, `retried`, `gave up` and
|
|
161
161
|
`retry cancelled` events.
|
|
162
162
|
|
|
163
|
-
Runs can take a long time. Start `run` in
|
|
163
|
+
Runs can take a long time. Start `run` in a dedicated tmux window, 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.
|
|
164
164
|
|
|
165
165
|
While a run goes, labnotes appends a memory sample to `memory.jsonl` in its run
|
|
166
166
|
folder every 5 seconds: one JSON line, `{"time": <unix seconds>, "rss": <bytes>,
|
|
@@ -202,33 +202,17 @@ going. Elsewhere, it prints a line per event instead.
|
|
|
202
202
|
`run.json` gets `"error": "timed out after ..."` and `"ended_by": "timeout"`
|
|
203
203
|
next to its exit code.
|
|
204
204
|
Unset, runs have no time limit.
|
|
205
|
-
- `run`
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
disk, even when the daemon is already up.
|
|
215
|
-
- Ctrl-C stops the running scripts and records their end. A second Ctrl-C
|
|
216
|
-
kills them. SIGTERM stops them the same way, then kills any still going 5
|
|
217
|
-
seconds later. Once the runs are done, either one closes the live view.
|
|
218
|
-
- `labnotes run stop` stops every run the daemon has going: SIGTERM, then
|
|
219
|
-
SIGKILL 5 seconds later.
|
|
220
|
-
- `labnotes run status` prints what the daemon has going, a line per event so
|
|
221
|
-
far plus the latest line each running run logged and what each running run
|
|
222
|
-
uses (`usage` events with `--json`). `-f` keeps following, and
|
|
223
|
-
waits for a daemon when none is up. `--json` prints one JSON object per line.
|
|
224
|
-
Following doesn't keep the daemon up.
|
|
225
|
-
- `labnotes run control NAME ACTION [ARG]` sends one live-view control to a
|
|
226
|
-
running experiment: `kill` a run, `set_concurrency` to a number,
|
|
227
|
-
`interrupt` or `finish`.
|
|
228
|
-
- A second `run` of the same experiment fails while the first is going on
|
|
229
|
-
the same daemon.
|
|
205
|
+
- `run` owns its runs in the foreground. In a terminal it shows a live table;
|
|
206
|
+
outside a terminal it prints events until every run ends. A free concurrency
|
|
207
|
+
slot starts the next planned run. Agents should start it in a dedicated tmux
|
|
208
|
+
window so it can keep running between turns.
|
|
209
|
+
- Ctrl-C, SIGTERM, SIGHUP, or quitting the live view stops running scripts:
|
|
210
|
+
SIGTERM first, then SIGKILL after 5 seconds if needed. Runs are recorded
|
|
211
|
+
as stopped before labnotes exits. The next start marks unfinished records
|
|
212
|
+
whose processes are gone as lost.
|
|
213
|
+
- A second invocation starts its own run session and reads the current config.
|
|
230
214
|
- It exits 0 even if runs fail; the summary line counts failures. A run that
|
|
231
215
|
was retried counts as retried, not failed. If it can't
|
|
232
|
-
start
|
|
216
|
+
start a run, it prints the cause on one line and exits 1.
|
|
233
217
|
|
|
234
218
|
`uvx labnotes server` serves the experiments, their markdown files and their runs as a local site, read from the same `.labnotes.toml`. An experiment's page shows its `experiment.toml` on one line, then a Runs tab with a table of its runs and a Notebooks tab with one tab per markdown file. For a matrix experiment, the Runs tab also has a grid (`?view=grid`): one cell per combination of the params that vary, with how many runs count toward `repeats`. Clicking a run opens its `stdout.log` and `stderr.log` together, followed as they're written until the run ends.
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
7
|
from labnotes.create_experiment import create_experiment
|
|
8
|
-
from labnotes.
|
|
8
|
+
from labnotes.runner.start_experiment import start_experiment
|
|
9
9
|
from labnotes.utils.format_bytes import format_bytes
|
|
10
10
|
from labnotes.load_config import load_config
|
|
11
11
|
from labnotes.parse_subset import parse_subset
|
|
@@ -109,7 +109,7 @@ def experiments(
|
|
|
109
109
|
only = parse_subset(ctx.args)
|
|
110
110
|
except ValueError as error:
|
|
111
111
|
raise click.UsageError(str(error)) from error
|
|
112
|
-
start =
|
|
112
|
+
start = start_experiment(
|
|
113
113
|
config,
|
|
114
114
|
name,
|
|
115
115
|
experiment_config,
|
|
@@ -8,7 +8,6 @@ from dirsql import DirSQL
|
|
|
8
8
|
from labnotes.check_counts import check_counts
|
|
9
9
|
from labnotes.count_groups import count_groups
|
|
10
10
|
from labnotes.count_runs import count_runs
|
|
11
|
-
from labnotes.daemon.daemon_running import daemon_running
|
|
12
11
|
from labnotes.experiment_notes import experiment_notes
|
|
13
12
|
from labnotes.experiment_runs import experiment_runs
|
|
14
13
|
from labnotes.format_value import format_value
|
|
@@ -24,6 +23,7 @@ from labnotes.runs_view_command import runs_view_command
|
|
|
24
23
|
from labnotes.sort_href import sort_href
|
|
25
24
|
from labnotes.sql_quote import quote_string
|
|
26
25
|
from labnotes.summarize_config import summarize_config
|
|
26
|
+
from labnotes.utils.process_alive import process_alive
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
def _no_outputs(note: str, content: str) -> Sequence[str]:
|
|
@@ -60,6 +60,18 @@ async def load_experiment(
|
|
|
60
60
|
[totals] = await count_runs(tables, source, str(summary["completed_run"]))
|
|
61
61
|
notes = await experiment_notes(tables, root, name)
|
|
62
62
|
runs = await list_runs(tables, source, str(summary["completed_run"]), view)
|
|
63
|
+
|
|
64
|
+
def is_running(row: dict[str, object]) -> bool:
|
|
65
|
+
record = row.get("run")
|
|
66
|
+
if not row.get("labnotes_unfinished") or not isinstance(record, str):
|
|
67
|
+
return False
|
|
68
|
+
fields = json.loads(record)
|
|
69
|
+
pid, created = fields.get("pid"), fields.get("pid_created")
|
|
70
|
+
return isinstance(pid, int) and process_alive(
|
|
71
|
+
pid, created if isinstance(created, int | float) else None
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
runs_live = any(is_running(run) for run in runs)
|
|
63
75
|
nested = summary["mode"] == "adhoc" and await nests_runs(tables, source)
|
|
64
76
|
groups = (
|
|
65
77
|
group_runs(runs, list(choices), await count_groups(tables, source))
|
|
@@ -98,7 +110,7 @@ async def load_experiment(
|
|
|
98
110
|
for note in notes
|
|
99
111
|
],
|
|
100
112
|
"runs_list": runs,
|
|
101
|
-
"runs_live":
|
|
113
|
+
"runs_live": runs_live,
|
|
102
114
|
"run_groups": groups,
|
|
103
115
|
"run_grid": grid,
|
|
104
116
|
"run_columns": shown,
|
|
@@ -38,6 +38,7 @@ async def execute_run(
|
|
|
38
38
|
state_file: Path,
|
|
39
39
|
memory_interval: float,
|
|
40
40
|
environment: dict[str, str] | None = None,
|
|
41
|
+
git_info: dict[str, str | bool] | None = None,
|
|
41
42
|
) -> dict[str, object]:
|
|
42
43
|
resolved = shell_command(command, folder, params, args)
|
|
43
44
|
run: dict[str, object] = {
|
|
@@ -49,7 +50,9 @@ async def execute_run(
|
|
|
49
50
|
"error": None,
|
|
50
51
|
"ended_by": None,
|
|
51
52
|
"signal": None,
|
|
52
|
-
"git":
|
|
53
|
+
"git": git_info
|
|
54
|
+
if git_info is not None
|
|
55
|
+
else await asyncio.to_thread(run_git, root),
|
|
53
56
|
}
|
|
54
57
|
if args:
|
|
55
58
|
run["args"] = list(args)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import signal
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from tempfile import TemporaryDirectory
|
|
12
|
+
from types import FrameType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ProjectSnapshot:
|
|
17
|
+
root: Path
|
|
18
|
+
sha: str | None = None
|
|
19
|
+
diff: bytes | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def git(root: Path, *args: str) -> bytes:
|
|
23
|
+
return subprocess.check_output(["git", *args], cwd=root, stderr=subprocess.DEVNULL)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@contextmanager
|
|
27
|
+
def project_snapshot(root: Path) -> Iterator[ProjectSnapshot]:
|
|
28
|
+
"""Capture the working tree once for all commands in an invocation."""
|
|
29
|
+
try:
|
|
30
|
+
git(root, "rev-parse", "--is-inside-work-tree")
|
|
31
|
+
except (OSError, subprocess.SubprocessError):
|
|
32
|
+
logging.warning("Project is not a git repository; running from the live tree")
|
|
33
|
+
yield ProjectSnapshot(root)
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
sha = git(root, "rev-parse", "HEAD").decode("ascii").strip()
|
|
38
|
+
diff = git(root, "diff", "HEAD")
|
|
39
|
+
except (OSError, subprocess.SubprocessError):
|
|
40
|
+
logging.warning("Project has no readable Git HEAD; running from the live tree")
|
|
41
|
+
yield ProjectSnapshot(root)
|
|
42
|
+
return
|
|
43
|
+
files = filter(
|
|
44
|
+
None, git(root, "ls-files", "-co", "--exclude-standard", "-z").split(b"\0")
|
|
45
|
+
)
|
|
46
|
+
ignored = filter(
|
|
47
|
+
None,
|
|
48
|
+
git(root, "ls-files", "-oi", "--exclude-standard", "--directory", "-z").split(
|
|
49
|
+
b"\0"
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
with TemporaryDirectory(prefix="labnotes-") as directory:
|
|
53
|
+
snapshot = Path(directory)
|
|
54
|
+
previous: dict[
|
|
55
|
+
signal.Signals,
|
|
56
|
+
Callable[[int, FrameType | None], object] | int | signal.Handlers | None,
|
|
57
|
+
] = {}
|
|
58
|
+
|
|
59
|
+
def stop(_number: int, _frame: object) -> None:
|
|
60
|
+
raise KeyboardInterrupt
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
for number in (signal.SIGTERM, signal.SIGHUP):
|
|
64
|
+
try:
|
|
65
|
+
old_handler = signal.signal(number, stop)
|
|
66
|
+
except ValueError:
|
|
67
|
+
break # Signals can only be installed from the main thread.
|
|
68
|
+
previous[number] = old_handler
|
|
69
|
+
subprocess.run(
|
|
70
|
+
[
|
|
71
|
+
"git",
|
|
72
|
+
"clone",
|
|
73
|
+
"--quiet",
|
|
74
|
+
"--shared",
|
|
75
|
+
"--no-checkout",
|
|
76
|
+
str(root),
|
|
77
|
+
str(snapshot),
|
|
78
|
+
],
|
|
79
|
+
check=True,
|
|
80
|
+
)
|
|
81
|
+
for raw in files:
|
|
82
|
+
relative = Path(raw.decode())
|
|
83
|
+
source = root / relative
|
|
84
|
+
if not source.exists() and not source.is_symlink():
|
|
85
|
+
continue
|
|
86
|
+
target = snapshot / relative
|
|
87
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
if source.is_symlink():
|
|
89
|
+
target.symlink_to(
|
|
90
|
+
os.readlink(source), target_is_directory=source.is_dir()
|
|
91
|
+
)
|
|
92
|
+
elif source.is_dir():
|
|
93
|
+
shutil.copytree(source, target, symlinks=True)
|
|
94
|
+
else:
|
|
95
|
+
shutil.copy2(source, target)
|
|
96
|
+
skipped: set[Path] = set()
|
|
97
|
+
for raw in ignored:
|
|
98
|
+
relative = Path(raw.decode().rstrip("/"))
|
|
99
|
+
if any(parent in relative.parents for parent in skipped):
|
|
100
|
+
continue
|
|
101
|
+
if any((snapshot / parent).is_symlink() for parent in relative.parents):
|
|
102
|
+
continue
|
|
103
|
+
source = root / relative
|
|
104
|
+
if source.is_dir() and (source / "pyvenv.cfg").exists():
|
|
105
|
+
skipped.add(relative)
|
|
106
|
+
continue
|
|
107
|
+
target = snapshot / relative
|
|
108
|
+
if target.is_symlink() or target.exists():
|
|
109
|
+
continue
|
|
110
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
target.symlink_to(source, target_is_directory=source.is_dir())
|
|
112
|
+
subprocess.run(
|
|
113
|
+
["git", "-C", str(snapshot), "reset", "--quiet", sha], check=True
|
|
114
|
+
)
|
|
115
|
+
finally:
|
|
116
|
+
for number, handler in previous.items():
|
|
117
|
+
signal.signal(number, handler)
|
|
118
|
+
yield ProjectSnapshot(snapshot, sha, diff)
|
|
@@ -27,6 +27,7 @@ from labnotes.runner.new_param_run_id import new_param_run_id
|
|
|
27
27
|
from labnotes.runner.new_stamped_run_id import new_stamped_run_id
|
|
28
28
|
from labnotes.runner.param_grid import param_grid
|
|
29
29
|
from labnotes.runner.plan_runs import plan_runs
|
|
30
|
+
from labnotes.runner.project_snapshot import ProjectSnapshot
|
|
30
31
|
from labnotes.runner.run_folder import run_folder
|
|
31
32
|
from labnotes.runner.signal_tree import signal_tree
|
|
32
33
|
from labnotes.state_path import STATE_ENV
|
|
@@ -49,7 +50,9 @@ async def run_experiment(
|
|
|
49
50
|
only: dict[str, str] | None = None,
|
|
50
51
|
args: list[str] | None = None,
|
|
51
52
|
environment: dict[str, str] | None = None,
|
|
53
|
+
snapshot: ProjectSnapshot | None = None,
|
|
52
54
|
) -> list[dict[str, object]]:
|
|
55
|
+
command_root = snapshot.root if snapshot is not None else config.root
|
|
53
56
|
inherited = dict(os.environ) if environment is None else environment
|
|
54
57
|
tables = open_tables(config)
|
|
55
58
|
existing = await experiment_run_ids(tables, name)
|
|
@@ -109,7 +112,7 @@ async def run_experiment(
|
|
|
109
112
|
meta = None
|
|
110
113
|
if experiment.meta is not None:
|
|
111
114
|
meta = await invocation_meta(
|
|
112
|
-
|
|
115
|
+
command_root, fill_dirs(experiment.meta, dirs), hook_env["meta"]
|
|
113
116
|
)
|
|
114
117
|
if dirs:
|
|
115
118
|
Path(dirs["invocation_dir"]).mkdir(parents=True, exist_ok=True)
|
|
@@ -242,12 +245,15 @@ async def run_experiment(
|
|
|
242
245
|
async def start(run: str, params: dict[str, object]) -> None:
|
|
243
246
|
folder = run_folder(config.root, config.runs, name, run)
|
|
244
247
|
folder.mkdir(parents=True, exist_ok=True)
|
|
248
|
+
if snapshot is not None and snapshot.sha is not None:
|
|
249
|
+
(folder / "git-sha.txt").write_text(snapshot.sha + "\n")
|
|
250
|
+
(folder / "git-diff.patch").write_bytes(snapshot.diff or b"")
|
|
245
251
|
run_invocation_dir = dirs.get("invocation_dir") or str(folder.resolve())
|
|
246
252
|
report({"event": "started", "run": run, "params": params, "folder": folder})
|
|
247
253
|
running[run] = set()
|
|
248
254
|
trees[run] = set()
|
|
249
255
|
result = await execute_run(
|
|
250
|
-
|
|
256
|
+
command_root,
|
|
251
257
|
folder,
|
|
252
258
|
fill_dirs(
|
|
253
259
|
experiment.command,
|
|
@@ -276,6 +282,11 @@ async def run_experiment(
|
|
|
276
282
|
"invocation_dir": run_invocation_dir,
|
|
277
283
|
},
|
|
278
284
|
),
|
|
285
|
+
git_info=(
|
|
286
|
+
{"commit": snapshot.sha, "dirty": bool(snapshot.diff)}
|
|
287
|
+
if snapshot is not None and snapshot.sha is not None
|
|
288
|
+
else None
|
|
289
|
+
),
|
|
279
290
|
)
|
|
280
291
|
del running[run], trees[run]
|
|
281
292
|
report({"event": "ended", "run": run, **result})
|
|
@@ -290,7 +301,7 @@ async def run_experiment(
|
|
|
290
301
|
if experiment.teardown is not None:
|
|
291
302
|
report({"event": "tearing down"})
|
|
292
303
|
await invocation_hook(
|
|
293
|
-
|
|
304
|
+
command_root,
|
|
294
305
|
"teardown",
|
|
295
306
|
fill_dirs(experiment.teardown, dirs),
|
|
296
307
|
hook_env["teardown"],
|
|
@@ -311,7 +322,7 @@ async def run_experiment(
|
|
|
311
322
|
if experiment.setup is not None:
|
|
312
323
|
report({"event": "setting up"})
|
|
313
324
|
await invocation_hook(
|
|
314
|
-
|
|
325
|
+
command_root,
|
|
315
326
|
"setup",
|
|
316
327
|
fill_dirs(experiment.setup, dirs),
|
|
317
328
|
hook_env["setup"],
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from labnotes.models import LabnotesConfig, Listen, Report, Start
|
|
4
|
+
from labnotes.runner.finalize_lost_runs import finalize_lost_runs
|
|
5
|
+
from labnotes.runner.prepare_experiment import prepare_experiment
|
|
6
|
+
from labnotes.runner.project_snapshot import project_snapshot
|
|
7
|
+
from labnotes.runner.run_experiment import run_experiment
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def start_experiment(
|
|
11
|
+
config: LabnotesConfig,
|
|
12
|
+
name: str,
|
|
13
|
+
experiment_config: Path | None,
|
|
14
|
+
concurrency: int | None,
|
|
15
|
+
timeout: int | None,
|
|
16
|
+
only: dict[str, str] | None,
|
|
17
|
+
args: list[str] | None,
|
|
18
|
+
setup: str | None,
|
|
19
|
+
teardown: str | None,
|
|
20
|
+
) -> Start:
|
|
21
|
+
async def start(report: Report, listen: Listen) -> list[dict[str, object]]:
|
|
22
|
+
experiment, limit = await prepare_experiment(
|
|
23
|
+
config, name, experiment_config, concurrency, only, args, setup, teardown
|
|
24
|
+
)
|
|
25
|
+
await finalize_lost_runs(config)
|
|
26
|
+
with project_snapshot(config.root) as snapshot:
|
|
27
|
+
return await run_experiment(
|
|
28
|
+
config,
|
|
29
|
+
name,
|
|
30
|
+
experiment,
|
|
31
|
+
limit,
|
|
32
|
+
report,
|
|
33
|
+
listen,
|
|
34
|
+
timeout=timeout,
|
|
35
|
+
only=only,
|
|
36
|
+
args=args,
|
|
37
|
+
snapshot=snapshot,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return start
|
|
@@ -10,8 +10,7 @@ class LeavePrompt(ModalScreen[str | None]):
|
|
|
10
10
|
#prompt { width: auto; padding: 1 2; border: round $accent; }
|
|
11
11
|
"""
|
|
12
12
|
BINDINGS = [
|
|
13
|
-
Binding("enter,ctrl+c", "dismiss('
|
|
14
|
-
Binding("s", "dismiss('stop')", "Stop them"),
|
|
13
|
+
Binding("enter,ctrl+c,s", "dismiss('stop')", "Stop runs"),
|
|
15
14
|
Binding("escape", "dismiss", "Cancel"),
|
|
16
15
|
]
|
|
17
16
|
|
|
@@ -23,6 +22,6 @@ class LeavePrompt(ModalScreen[str | None]):
|
|
|
23
22
|
runs = "run" if self.count == 1 else "runs"
|
|
24
23
|
yield Static(
|
|
25
24
|
f"{self.count} {runs} still going. "
|
|
26
|
-
"
|
|
25
|
+
"Stop them (enter or s) / cancel (esc).",
|
|
27
26
|
id="prompt",
|
|
28
27
|
)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import signal
|
|
3
2
|
import time
|
|
4
3
|
from collections.abc import Sequence
|
|
@@ -100,7 +99,7 @@ class RunApp(App[None]):
|
|
|
100
99
|
def listen(self, controls: Controls) -> None:
|
|
101
100
|
self.controls = controls
|
|
102
101
|
stop_then_kill(self.stop_runs, signal.SIGTERM)
|
|
103
|
-
|
|
102
|
+
stop_then_kill(self.stop_runs, signal.SIGHUP)
|
|
104
103
|
|
|
105
104
|
def show_event(self, event: dict[str, object]) -> None:
|
|
106
105
|
table = self.query_one(DataTable)
|
|
@@ -286,9 +285,7 @@ class RunApp(App[None]):
|
|
|
286
285
|
self.action_interrupt()
|
|
287
286
|
|
|
288
287
|
def answer(self, choice: str | None) -> None:
|
|
289
|
-
if choice == "
|
|
290
|
-
self.exit()
|
|
291
|
-
elif choice == "stop":
|
|
288
|
+
if choice == "stop":
|
|
292
289
|
self.controls.interrupt()
|
|
293
290
|
self.set_timer(GRACE_SECONDS, self.controls.interrupt)
|
|
294
291
|
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
[[python.exempt]]
|
|
2
|
-
path = "src/labnotes/runner/hold_folder.py"
|
|
3
|
-
rules = ["mutation"]
|
|
4
|
-
lines = [12]
|
|
5
|
-
reason = "LOCK_EX and LOCK_NB are disjoint bits, so | equals + and ^"
|
|
6
|
-
|
|
7
1
|
[[python.exempt]]
|
|
8
2
|
path = "src/labnotes/tui/run_output.py"
|
|
9
3
|
rules = ["mutation"]
|
|
@@ -16,30 +10,6 @@ rules = ["mutation"]
|
|
|
16
10
|
lines = [61, 69]
|
|
17
11
|
reason = "summarize_config returns mode matrix or adhoc; \"matrix\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
|
|
18
12
|
|
|
19
|
-
[[python.exempt]]
|
|
20
|
-
path = "src/labnotes/daemon/exit_when_idle.py"
|
|
21
|
-
rules = ["mutation"]
|
|
22
|
-
lines = [12]
|
|
23
|
-
reason = "quiet counts up by one from zero, so it reaches IDLE_EXIT_SECONDS before passing it: < equals != and, for small ints, is not"
|
|
24
|
-
|
|
25
|
-
[[python.exempt]]
|
|
26
|
-
path = "src/labnotes/daemon/daemon_app.py"
|
|
27
|
-
rules = ["mutation"]
|
|
28
|
-
lines = [134]
|
|
29
|
-
reason = "start answers 202, 400, 409 or 500, so every status but 202 is above it: != equals >"
|
|
30
|
-
|
|
31
|
-
[[python.exempt]]
|
|
32
|
-
path = "src/labnotes/daemon/remote_start.py"
|
|
33
|
-
rules = ["mutation"]
|
|
34
|
-
lines = [51]
|
|
35
|
-
reason = "the daemon answers a run with 202, 400, 409 or 500, so every status but 202 is above it: != equals >"
|
|
36
|
-
|
|
37
|
-
[[python.exempt]]
|
|
38
|
-
path = "src/labnotes/run_control.py"
|
|
39
|
-
rules = ["mutation"]
|
|
40
|
-
lines = [39]
|
|
41
|
-
reason = "argument counts are small ints, which CPython caches, so is not equals !="
|
|
42
|
-
|
|
43
13
|
[[python.exempt]]
|
|
44
14
|
path = "src/labnotes/tui/run_cells.py"
|
|
45
15
|
rules = ["mutation"]
|
|
@@ -10,7 +10,6 @@ from pathlib import Path
|
|
|
10
10
|
import pytest
|
|
11
11
|
from playwright.async_api import Page, async_playwright
|
|
12
12
|
|
|
13
|
-
from labnotes.daemon.socket_path import socket_path
|
|
14
13
|
|
|
15
14
|
CONFIG = (
|
|
16
15
|
'name = "Alpha study"\ncommand = "go"\nrepeats = 2\nmode = "matrix"\n'
|
|
@@ -45,24 +44,6 @@ def free_port() -> int:
|
|
|
45
44
|
return int(sock.getsockname()[1])
|
|
46
45
|
|
|
47
46
|
|
|
48
|
-
def daemon(root: Path) -> Iterator[None]:
|
|
49
|
-
labnotes = Path(sys.executable).parent / "labnotes"
|
|
50
|
-
process = subprocess.Popen(
|
|
51
|
-
[labnotes, "-c", root / ".labnotes.toml", "daemon"],
|
|
52
|
-
stdout=subprocess.DEVNULL,
|
|
53
|
-
stderr=subprocess.DEVNULL,
|
|
54
|
-
)
|
|
55
|
-
try:
|
|
56
|
-
for _ in range(100):
|
|
57
|
-
if socket_path(root).exists():
|
|
58
|
-
break
|
|
59
|
-
time.sleep(0.1)
|
|
60
|
-
yield
|
|
61
|
-
finally:
|
|
62
|
-
process.terminate()
|
|
63
|
-
process.wait()
|
|
64
|
-
|
|
65
|
-
|
|
66
47
|
def serve(root: Path) -> Iterator[str]:
|
|
67
48
|
port = free_port()
|
|
68
49
|
labnotes = Path(sys.executable).parent / "labnotes"
|
|
@@ -385,20 +385,20 @@ wheels = [
|
|
|
385
385
|
|
|
386
386
|
[[package]]
|
|
387
387
|
name = "httpcore2"
|
|
388
|
-
version = "2.
|
|
388
|
+
version = "2.16.0"
|
|
389
389
|
source = { registry = "https://pypi.org/simple" }
|
|
390
390
|
dependencies = [
|
|
391
391
|
{ name = "h11" },
|
|
392
392
|
{ name = "truststore" },
|
|
393
393
|
]
|
|
394
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
394
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c0/61/afa9d6b7452f9eca73f73d45324eb0bb52f098fd5e6049bf6b8263be9b4e/httpcore2-2.16.0.tar.gz", hash = "sha256:c72cd0413563b17e1cd8354677dff887466f8ec2551679759e93126534428643", size = 69886, upload-time = "2026-10-21T07:09:45.31Z" }
|
|
395
395
|
wheels = [
|
|
396
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
396
|
+
{ url = "https://files.pythonhosted.org/packages/af/c2/ce417c397afa5181f0e3c6ff01a4d01639414e1adef08730d97571297151/httpcore2-2.16.0-py3-none-any.whl", hash = "sha256:5c4358319f2602d984ad93182c9fee5160f22e9da7babdda870a5e7c4e2dc34f", size = 85070, upload-time = "2026-10-21T07:09:42.802Z" },
|
|
397
397
|
]
|
|
398
398
|
|
|
399
399
|
[[package]]
|
|
400
400
|
name = "httpx2"
|
|
401
|
-
version = "2.
|
|
401
|
+
version = "2.16.0"
|
|
402
402
|
source = { registry = "https://pypi.org/simple" }
|
|
403
403
|
dependencies = [
|
|
404
404
|
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
|
|
@@ -408,9 +408,9 @@ dependencies = [
|
|
|
408
408
|
{ name = "truststore", marker = "sys_platform != 'emscripten'" },
|
|
409
409
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
410
410
|
]
|
|
411
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
411
|
+
sdist = { url = "https://files.pythonhosted.org/packages/43/73/5ca2cedf5e4e50423d1c258877865a6b62793633b561c58499f8aeb33b7e/httpx2-2.16.0.tar.gz", hash = "sha256:0d54c98128e8f2ff7fbbde0fca8c359e5f032edeaaaac443cdb7f31f685bcd7f", size = 103116, upload-time = "2026-10-21T07:09:46.348Z" }
|
|
412
412
|
wheels = [
|
|
413
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
413
|
+
{ url = "https://files.pythonhosted.org/packages/7b/3d/2e4bc4fd1ef9770871669adb348d612a6e4786b303a23f9935b81c48dd60/httpx2-2.16.0-py3-none-any.whl", hash = "sha256:fd2c5d7909e8d86fe92d53020c310ca0e4e431b838c207c5754b9bbf0308d49a", size = 97880, upload-time = "2026-10-21T07:09:44.01Z" },
|
|
414
414
|
]
|
|
415
415
|
|
|
416
416
|
[[package]]
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
from labnotes.daemon.daemon_client import daemon_client
|
|
4
|
-
from labnotes.daemon.daemon_running import daemon_running
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
async def control_run(
|
|
8
|
-
root: Path, name: str, action: str, args: list[object]
|
|
9
|
-
) -> str | None:
|
|
10
|
-
if not await daemon_running(root):
|
|
11
|
-
return "Nothing is running."
|
|
12
|
-
async with daemon_client(root) as client:
|
|
13
|
-
reply = await client.post(f"/experiments/{name}/{action}", json={"args": args})
|
|
14
|
-
if reply.is_success:
|
|
15
|
-
return None
|
|
16
|
-
return str(reply.json()["error"])
|