labnotes 0.0.136__tar.gz → 0.0.138__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.136 → labnotes-0.0.138}/PKG-INFO +7 -8
- {labnotes-0.0.136 → labnotes-0.0.138}/README.md +6 -7
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/cli.py +0 -4
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/context.md +13 -23
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/experiments.py +2 -2
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/load_experiment.py +14 -2
- labnotes-0.0.138/src/labnotes/runner/start_experiment.py +37 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/leave_prompt.py +2 -3
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/run_app.py +2 -5
- {labnotes-0.0.136 → labnotes-0.0.138}/testing-conventions.toml +0 -30
- {labnotes-0.0.136 → labnotes-0.0.138}/tests/integration/conftest.py +0 -19
- {labnotes-0.0.136 → labnotes-0.0.138}/uv.lock +6 -6
- labnotes-0.0.136/src/labnotes/daemon/control_run.py +0 -16
- labnotes-0.0.136/src/labnotes/daemon/daemon_app.py +0 -179
- labnotes-0.0.136/src/labnotes/daemon/daemon_client.py +0 -12
- labnotes-0.0.136/src/labnotes/daemon/daemon_running.py +0 -14
- labnotes-0.0.136/src/labnotes/daemon/daemon_server.py +0 -12
- labnotes-0.0.136/src/labnotes/daemon/describe_status.py +0 -21
- labnotes-0.0.136/src/labnotes/daemon/ensure_daemon.py +0 -26
- labnotes-0.0.136/src/labnotes/daemon/exit_when_idle.py +0 -15
- labnotes-0.0.136/src/labnotes/daemon/last_stderr.py +0 -15
- labnotes-0.0.136/src/labnotes/daemon/remote_start.py +0 -94
- labnotes-0.0.136/src/labnotes/daemon/run_daemon.py +0 -29
- labnotes-0.0.136/src/labnotes/daemon/run_logs.py +0 -54
- labnotes-0.0.136/src/labnotes/daemon/serve_daemon.py +0 -16
- labnotes-0.0.136/src/labnotes/daemon/socket_path.py +0 -13
- labnotes-0.0.136/src/labnotes/daemon/stop_runs.py +0 -12
- labnotes-0.0.136/src/labnotes/daemon/watch_daemon.py +0 -77
- labnotes-0.0.136/src/labnotes/daemon/watch_status.py +0 -19
- labnotes-0.0.136/src/labnotes/daemon_command.py +0 -14
- labnotes-0.0.136/src/labnotes/run_command.py +0 -32
- labnotes-0.0.136/src/labnotes/run_control.py +0 -45
- labnotes-0.0.136/src/labnotes/run_status.py +0 -29
- labnotes-0.0.136/src/labnotes/runner/hold_folder.py +0 -15
- labnotes-0.0.136/src/labnotes/utils/__init__.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/.gitignore +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/CHANGELOG.md +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/MIGRATIONS.md +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/pyproject.toml +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/get_state.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/grid_cell.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/init.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/is_non_negative_number.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/log.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/log_path.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/memory_interval.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/models.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/parse_subset.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/pivot_runs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/retries.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.136/src/labnotes/daemon → labnotes-0.0.138/src/labnotes/runner}/__init__.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/cgroup_memory.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/check_args.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/check_subset.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/container_cgroup.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/container_memory.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/docker_output.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/exit_signal.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/fill_dirs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/fill_experiment_env.py +0 -0
- {labnotes-0.0.136/src/labnotes/daemon → labnotes-0.0.138/src/labnotes/runner}/finalize_lost_runs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/flag_name.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/hook_dirs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/in_subset.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/invocation_hook.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/invocation_meta.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/record_memory.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/run_git.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runs_view_command.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/server.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/set_state.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/state_path.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.136/src/labnotes/runner → labnotes-0.0.138/src/labnotes/tables}/__init__.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_memory.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_run_file.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/runs_columns.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.136/src/labnotes/tables → labnotes-0.0.138/src/labnotes/tui}/__init__.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/check_view_response.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/config.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/format_cell.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/native_rows.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/run_screen.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/runs_view.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/view_column.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/view_process.py +0 -0
- {labnotes-0.0.136/src/labnotes/tui → labnotes-0.0.138/src/labnotes/utils}/__init__.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/utils/format_bytes.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/utils/process_alive.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/utils/run_usage.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/utils/tree_rss.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/utils/tree_usage.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/view/__init__.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/view/answer.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/view/build.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/view/respond.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/view/serve.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/tests/conftest.py +0 -0
- {labnotes-0.0.136 → labnotes-0.0.138}/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.0.138
|
|
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,27 +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
|
-
SIGKILL 5 seconds later.
|
|
215
|
-
- `labnotes run status` prints what the daemon has going, a line per event so
|
|
216
|
-
far plus the latest line each running run logged and what each running run
|
|
217
|
-
uses (`usage` events with `--json`). `-f` keeps following, and
|
|
218
|
-
waits for a daemon when none is up. `--json` prints one JSON object per line.
|
|
219
|
-
Following doesn't keep the daemon up.
|
|
220
|
-
- `labnotes run control NAME ACTION [ARG]` sends one live-view control to a
|
|
221
|
-
running experiment: `kill` a run, `set_concurrency` to a number,
|
|
222
|
-
`interrupt` or `finish`.
|
|
223
|
-
- A second `run` of the same experiment fails while the first is going.
|
|
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.
|
|
224
214
|
- It exits 0 even if runs fail; the summary line counts failures. A run that
|
|
225
215
|
was retried counts as retried, not failed. If it can't
|
|
226
|
-
start
|
|
216
|
+
start a run, it prints the cause on one line and exits 1.
|
|
227
217
|
|
|
228
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,
|
|
@@ -0,0 +1,37 @@
|
|
|
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.run_experiment import run_experiment
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def start_experiment(
|
|
10
|
+
config: LabnotesConfig,
|
|
11
|
+
name: str,
|
|
12
|
+
experiment_config: Path | None,
|
|
13
|
+
concurrency: int | None,
|
|
14
|
+
timeout: int | None,
|
|
15
|
+
only: dict[str, str] | None,
|
|
16
|
+
args: list[str] | None,
|
|
17
|
+
setup: str | None,
|
|
18
|
+
teardown: str | None,
|
|
19
|
+
) -> Start:
|
|
20
|
+
async def start(report: Report, listen: Listen) -> list[dict[str, object]]:
|
|
21
|
+
experiment, limit = await prepare_experiment(
|
|
22
|
+
config, name, experiment_config, concurrency, only, args, setup, teardown
|
|
23
|
+
)
|
|
24
|
+
await finalize_lost_runs(config)
|
|
25
|
+
return await run_experiment(
|
|
26
|
+
config,
|
|
27
|
+
name,
|
|
28
|
+
experiment,
|
|
29
|
+
limit,
|
|
30
|
+
report,
|
|
31
|
+
listen,
|
|
32
|
+
timeout=timeout,
|
|
33
|
+
only=only,
|
|
34
|
+
args=args,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
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"])
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import dataclasses
|
|
3
|
-
import itertools
|
|
4
|
-
import json
|
|
5
|
-
from collections.abc import AsyncIterator
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from starlette.applications import Starlette
|
|
9
|
-
from starlette.requests import Request
|
|
10
|
-
from starlette.responses import JSONResponse, Response, StreamingResponse
|
|
11
|
-
from starlette.routing import Route
|
|
12
|
-
|
|
13
|
-
from labnotes.models import Controls, LabnotesConfig
|
|
14
|
-
from labnotes.runner.prepare_experiment import prepare_experiment
|
|
15
|
-
from labnotes.runner.run_experiment import run_experiment
|
|
16
|
-
from labnotes.runner.stop_then_kill import GRACE_SECONDS
|
|
17
|
-
|
|
18
|
-
Event = dict[str, object]
|
|
19
|
-
ACTIONS = frozenset(field.name for field in dataclasses.fields(Controls))
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def daemon_app(config: LabnotesConfig) -> Starlette:
|
|
23
|
-
sessions: dict[str, Controls | None] = {}
|
|
24
|
-
owners: dict[str, int] = {}
|
|
25
|
-
clients: dict[int, asyncio.Queue[Event]] = {}
|
|
26
|
-
watchers: set[int] = set()
|
|
27
|
-
history: dict[str, list[Event]] = {}
|
|
28
|
-
ids = itertools.count(1)
|
|
29
|
-
tasks: set[asyncio.Task[str | None]] = set()
|
|
30
|
-
|
|
31
|
-
def broadcast(event: Event) -> None:
|
|
32
|
-
for queue in clients.values():
|
|
33
|
-
queue.put_nowait(event)
|
|
34
|
-
|
|
35
|
-
def refuse(status: int, error: str) -> Response:
|
|
36
|
-
return JSONResponse({"error": error}, status_code=status)
|
|
37
|
-
|
|
38
|
-
def release(client: int) -> None:
|
|
39
|
-
for name, owner in list(owners.items()):
|
|
40
|
-
controls = sessions.get(name)
|
|
41
|
-
if owner == client and controls is not None:
|
|
42
|
-
del owners[name]
|
|
43
|
-
controls.finish()
|
|
44
|
-
|
|
45
|
-
def record(event: Event) -> None:
|
|
46
|
-
history.setdefault(str(event["experiment"]), []).append(event)
|
|
47
|
-
broadcast(event)
|
|
48
|
-
|
|
49
|
-
async def events(request: Request) -> Response:
|
|
50
|
-
client = next(ids)
|
|
51
|
-
watching = request.query_params.get("watch") == "1"
|
|
52
|
-
|
|
53
|
-
async def stream() -> AsyncIterator[str]:
|
|
54
|
-
clients[client] = asyncio.Queue()
|
|
55
|
-
if watching:
|
|
56
|
-
watchers.add(client)
|
|
57
|
-
for past in history.values():
|
|
58
|
-
for old in past:
|
|
59
|
-
clients[client].put_nowait(old)
|
|
60
|
-
clients[client].put_nowait({"event": "replayed"})
|
|
61
|
-
try:
|
|
62
|
-
event: Event = {"event": "connected", "client": client}
|
|
63
|
-
while True:
|
|
64
|
-
yield f"data: {json.dumps(event, default=str)}\n\n"
|
|
65
|
-
event = await clients[client].get()
|
|
66
|
-
finally:
|
|
67
|
-
del clients[client]
|
|
68
|
-
watchers.discard(client)
|
|
69
|
-
release(client)
|
|
70
|
-
|
|
71
|
-
return StreamingResponse(stream(), media_type="text/event-stream")
|
|
72
|
-
|
|
73
|
-
async def start(name: str, body: dict[str, object]) -> Response:
|
|
74
|
-
path = body.get("experiment_config")
|
|
75
|
-
try:
|
|
76
|
-
experiment, concurrency = await prepare_experiment(
|
|
77
|
-
config,
|
|
78
|
-
name,
|
|
79
|
-
None if path is None else Path(str(path)),
|
|
80
|
-
body.get("concurrency"), # type: ignore[arg-type]
|
|
81
|
-
body.get("only"), # type: ignore[arg-type]
|
|
82
|
-
body.get("args"), # type: ignore[arg-type]
|
|
83
|
-
body.get("setup"), # type: ignore[arg-type]
|
|
84
|
-
body.get("teardown"), # type: ignore[arg-type]
|
|
85
|
-
)
|
|
86
|
-
except ValueError as error:
|
|
87
|
-
return refuse(400, str(error))
|
|
88
|
-
ready = asyncio.Event()
|
|
89
|
-
owner = body.get("client")
|
|
90
|
-
if isinstance(owner, int):
|
|
91
|
-
owners[name] = owner
|
|
92
|
-
|
|
93
|
-
def listen(controls: Controls) -> None:
|
|
94
|
-
sessions[name] = controls
|
|
95
|
-
ready.set()
|
|
96
|
-
if name in owners and owners[name] not in clients:
|
|
97
|
-
release(owners[name])
|
|
98
|
-
|
|
99
|
-
async def session() -> str | None:
|
|
100
|
-
error = None
|
|
101
|
-
try:
|
|
102
|
-
await run_experiment(
|
|
103
|
-
config,
|
|
104
|
-
name,
|
|
105
|
-
experiment,
|
|
106
|
-
concurrency,
|
|
107
|
-
lambda event: record({**event, "experiment": name}),
|
|
108
|
-
listen,
|
|
109
|
-
body.get("timeout"), # type: ignore[arg-type]
|
|
110
|
-
only=body.get("only"), # type: ignore[arg-type]
|
|
111
|
-
args=body.get("args"), # type: ignore[arg-type]
|
|
112
|
-
environment=body.get("environment"), # type: ignore[arg-type]
|
|
113
|
-
)
|
|
114
|
-
# Clients hear why the session ended; the daemon keeps serving.
|
|
115
|
-
except Exception as failure: # noqa: BLE001
|
|
116
|
-
error = str(failure)
|
|
117
|
-
del sessions[name]
|
|
118
|
-
owners.pop(name, None)
|
|
119
|
-
history.pop(name, None)
|
|
120
|
-
broadcast({"event": "done", "experiment": name, "error": error})
|
|
121
|
-
return error
|
|
122
|
-
|
|
123
|
-
task = asyncio.create_task(session())
|
|
124
|
-
tasks.add(task)
|
|
125
|
-
task.add_done_callback(tasks.discard)
|
|
126
|
-
waiting = asyncio.create_task(ready.wait())
|
|
127
|
-
await asyncio.wait({task, waiting}, return_when=asyncio.FIRST_COMPLETED)
|
|
128
|
-
waiting.cancel()
|
|
129
|
-
if not ready.is_set():
|
|
130
|
-
return refuse(500, str(task.result()))
|
|
131
|
-
return JSONResponse({}, status_code=202)
|
|
132
|
-
|
|
133
|
-
async def run(request: Request) -> Response:
|
|
134
|
-
name = request.path_params["name"]
|
|
135
|
-
body = await request.json()
|
|
136
|
-
if name in sessions:
|
|
137
|
-
return refuse(409, f"{name} is already running.")
|
|
138
|
-
sessions[name] = None
|
|
139
|
-
response = await start(name, body)
|
|
140
|
-
if response.status_code != 202:
|
|
141
|
-
sessions.pop(name, None)
|
|
142
|
-
return response
|
|
143
|
-
|
|
144
|
-
async def control(request: Request) -> Response:
|
|
145
|
-
name, action = request.path_params["name"], request.path_params["action"]
|
|
146
|
-
body = await request.json()
|
|
147
|
-
controls = sessions.get(name)
|
|
148
|
-
if action not in ACTIONS or controls is None:
|
|
149
|
-
return refuse(404, f"No {action} for {name}.")
|
|
150
|
-
getattr(controls, action)(*body["args"])
|
|
151
|
-
return JSONResponse({})
|
|
152
|
-
|
|
153
|
-
def stop_all() -> None:
|
|
154
|
-
stopping = [controls for controls in sessions.values() if controls]
|
|
155
|
-
|
|
156
|
-
def interrupt() -> None:
|
|
157
|
-
for controls in stopping:
|
|
158
|
-
controls.interrupt()
|
|
159
|
-
|
|
160
|
-
interrupt()
|
|
161
|
-
asyncio.get_running_loop().call_later(GRACE_SECONDS, interrupt)
|
|
162
|
-
|
|
163
|
-
async def stop(request: Request) -> Response:
|
|
164
|
-
stop_all()
|
|
165
|
-
return JSONResponse({})
|
|
166
|
-
|
|
167
|
-
app = Starlette(
|
|
168
|
-
routes=[
|
|
169
|
-
Route("/events", events),
|
|
170
|
-
Route("/stop", stop, methods=["POST"]),
|
|
171
|
-
Route("/experiments/{name}/run", run, methods=["POST"]),
|
|
172
|
-
Route("/experiments/{name}/{action}", control, methods=["POST"]),
|
|
173
|
-
]
|
|
174
|
-
)
|
|
175
|
-
app.state.idle = (
|
|
176
|
-
lambda: all(client in watchers for client in clients) and not sessions
|
|
177
|
-
)
|
|
178
|
-
app.state.stop = stop_all
|
|
179
|
-
return app
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
import httpx2
|
|
4
|
-
|
|
5
|
-
from labnotes.daemon.socket_path import socket_path
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def daemon_client(root: Path) -> httpx2.AsyncClient:
|
|
9
|
-
transport = httpx2.AsyncHTTPTransport(uds=str(socket_path(root)))
|
|
10
|
-
return httpx2.AsyncClient(
|
|
11
|
-
transport=transport, base_url="http://labnotes", timeout=None
|
|
12
|
-
)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from labnotes.daemon.socket_path import socket_path
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
async def daemon_running(root: Path) -> bool:
|
|
8
|
-
try:
|
|
9
|
-
_, writer = await asyncio.open_unix_connection(socket_path(root))
|
|
10
|
-
except (FileNotFoundError, ConnectionRefusedError):
|
|
11
|
-
return False
|
|
12
|
-
writer.close()
|
|
13
|
-
await writer.wait_closed()
|
|
14
|
-
return True
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from collections.abc import Iterator
|
|
2
|
-
from contextlib import contextmanager
|
|
3
|
-
|
|
4
|
-
import uvicorn
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class DaemonServer(uvicorn.Server):
|
|
8
|
-
# A signal stops the runs through the daemon's own handlers; uvicorn's
|
|
9
|
-
# would shut the server down under them.
|
|
10
|
-
@contextmanager
|
|
11
|
-
def capture_signals(self) -> Iterator[None]:
|
|
12
|
-
yield
|