labnotes 0.0.125__tar.gz → 0.0.127__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.125 → labnotes-0.0.127}/PKG-INFO +1 -1
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/context.md +9 -1
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/daemon_app.py +2 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/remote_start.py +4 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/experiments.py +16 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/log_path.py +1 -1
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/models.py +2 -0
- labnotes-0.0.127/src/labnotes/runner/cgroup_memory.py +17 -0
- labnotes-0.0.127/src/labnotes/runner/container_cgroup.py +15 -0
- labnotes-0.0.127/src/labnotes/runner/container_memory.py +100 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/describe_event.py +6 -0
- labnotes-0.0.127/src/labnotes/runner/docker_output.py +11 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/execute_run.py +2 -1
- labnotes-0.0.127/src/labnotes/runner/invocation_hook.py +16 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/prepare_experiment.py +7 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/read_experiment_config.py +9 -0
- labnotes-0.0.127/src/labnotes/runner/record_memory.py +28 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/run_experiment.py +21 -3
- labnotes-0.0.125/src/labnotes/runner/record_memory.py +0 -18
- {labnotes-0.0.125 → labnotes-0.0.127}/.gitignore +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/CHANGELOG.md +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/MIGRATIONS.md +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/README.md +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/pyproject.toml +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/finalize_lost_runs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/last_stderr.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/grid_cell.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/init.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/is_non_negative_number.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/log.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/memory_interval.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/parse_subset.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/pivot_runs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/retries.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/check_args.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/check_subset.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/exit_signal.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/flag_name.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/in_subset.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/invocation_meta.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/runs_view_command.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/server.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/read_memory.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/read_run_file.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/runs_columns.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/check_view_response.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/config.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/format_cell.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/native_rows.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/run_app.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/run_screen.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/runs_view.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/view_column.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/tui/view_process.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/utils/__init__.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/utils/format_bytes.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/utils/process_alive.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/utils/run_usage.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/utils/tree_rss.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/utils/tree_usage.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/view/__init__.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/view/answer.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/view/build.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/view/respond.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/view/serve.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/testing-conventions.toml +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.125 → labnotes-0.0.127}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labnotes
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.127
|
|
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>
|
|
@@ -89,6 +89,11 @@ command that prints one JSON object, such as the git commit. labnotes runs it
|
|
|
89
89
|
once from the root when `run` starts and copies it into every run's `run.json`
|
|
90
90
|
as `meta`. If it fails or prints anything else, no run starts. Queued runs keep
|
|
91
91
|
the value from the start, even if the code changed while they waited.
|
|
92
|
+
`setup` and `teardown` (same places; `--setup`/`--teardown` override) are shell
|
|
93
|
+
commands run once per `run` from the root: setup before the first run starts,
|
|
94
|
+
teardown after the last one ends, even if runs failed, were stopped, or setup
|
|
95
|
+
failed (so teardown must cope with a partial setup). A failing setup starts no
|
|
96
|
+
run; a failing teardown fails the invocation.
|
|
92
97
|
A run counts when it exited 0 and its `run.json` doesn't hold
|
|
93
98
|
`"excluded": true`, so excluding a run by hand means adding that key; the next
|
|
94
99
|
`run` replaces it. Other keys you add to `run.json` are kept and show in `run`.
|
|
@@ -125,7 +130,10 @@ Runs can take a long time. Start `run` in the background, then check on it with
|
|
|
125
130
|
|
|
126
131
|
While a run goes, labnotes appends a memory sample to `memory.jsonl` in its run
|
|
127
132
|
folder every 5 seconds: one JSON line, `{"time": <unix seconds>, "rss": <bytes>,
|
|
128
|
-
"processes": <count>}`, for the command
|
|
133
|
+
"processes": <count>}`, for the command's process tree and its labeled Docker
|
|
134
|
+
containers. `LABNOTES_RUN` is the absolute run folder path; scripts label containers
|
|
135
|
+
with `docker run --label "labnotes.run=$LABNOTES_RUN" ...` to count their memory.
|
|
136
|
+
`processes` counts host processes in the run's tree, not containers. It keeps
|
|
129
137
|
every sample, so a run killed for memory still shows the climb.
|
|
130
138
|
`memory_interval` in `experiment.toml` (or `[experiment]` in `.labnotes.toml`)
|
|
131
139
|
sets the seconds between samples; 0 turns it off.
|
|
@@ -80,6 +80,8 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
|
|
|
80
80
|
body.get("concurrency"), # type: ignore[arg-type]
|
|
81
81
|
body.get("only"), # type: ignore[arg-type]
|
|
82
82
|
body.get("args"), # type: ignore[arg-type]
|
|
83
|
+
body.get("setup"), # type: ignore[arg-type]
|
|
84
|
+
body.get("teardown"), # type: ignore[arg-type]
|
|
83
85
|
)
|
|
84
86
|
except ValueError as error:
|
|
85
87
|
return refuse(400, str(error))
|
|
@@ -18,6 +18,8 @@ def remote_start(
|
|
|
18
18
|
timeout: float | None = None,
|
|
19
19
|
only: dict[str, str] | None = None,
|
|
20
20
|
args: list[str] | None = None,
|
|
21
|
+
setup: str | None = None,
|
|
22
|
+
teardown: str | None = None,
|
|
21
23
|
) -> Start:
|
|
22
24
|
async def start(report: Report, listen: Listen) -> list[dict[str, object]]:
|
|
23
25
|
try:
|
|
@@ -50,6 +52,8 @@ def remote_start(
|
|
|
50
52
|
"timeout": timeout,
|
|
51
53
|
"only": only,
|
|
52
54
|
"args": args,
|
|
55
|
+
"setup": setup,
|
|
56
|
+
"teardown": teardown,
|
|
53
57
|
},
|
|
54
58
|
)
|
|
55
59
|
if reply.status_code != 202:
|
|
@@ -66,6 +66,18 @@ class PassthroughCommand(click.Command):
|
|
|
66
66
|
help="With run: stop any run still going after this many seconds."
|
|
67
67
|
" Unset, runs have no time limit.",
|
|
68
68
|
)
|
|
69
|
+
@click.option(
|
|
70
|
+
"--setup",
|
|
71
|
+
metavar="COMMAND",
|
|
72
|
+
help="With run: shell command to run once before any run starts."
|
|
73
|
+
" Overrides experiment.toml and .labnotes.toml.",
|
|
74
|
+
)
|
|
75
|
+
@click.option(
|
|
76
|
+
"--teardown",
|
|
77
|
+
metavar="COMMAND",
|
|
78
|
+
help="With run: shell command to run once after every run ends."
|
|
79
|
+
" Overrides experiment.toml and .labnotes.toml.",
|
|
80
|
+
)
|
|
69
81
|
@click.pass_context
|
|
70
82
|
def experiments(
|
|
71
83
|
ctx: click.Context,
|
|
@@ -75,6 +87,8 @@ def experiments(
|
|
|
75
87
|
concurrency: int | None,
|
|
76
88
|
experiment_config: Path | None,
|
|
77
89
|
timeout: int | None,
|
|
90
|
+
setup: str | None,
|
|
91
|
+
teardown: str | None,
|
|
78
92
|
) -> None:
|
|
79
93
|
passthrough: list[str] | None = ctx.meta.get(PASSTHROUGH)
|
|
80
94
|
config = load_config(ctx)
|
|
@@ -103,6 +117,8 @@ def experiments(
|
|
|
103
117
|
timeout,
|
|
104
118
|
only,
|
|
105
119
|
passthrough or None,
|
|
120
|
+
setup,
|
|
121
|
+
teardown,
|
|
106
122
|
)
|
|
107
123
|
try:
|
|
108
124
|
if sys.stdout.isatty() and sys.stderr.isatty():
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def cgroup_memory(path: Path) -> int:
|
|
5
|
+
current = int((path / "memory.current").read_text())
|
|
6
|
+
try:
|
|
7
|
+
stats = (path / "memory.stat").read_text().splitlines()
|
|
8
|
+
except OSError:
|
|
9
|
+
return current
|
|
10
|
+
for line in stats:
|
|
11
|
+
name, _, value = line.partition(" ")
|
|
12
|
+
if name == "inactive_file":
|
|
13
|
+
try:
|
|
14
|
+
return max(0, current - int(value))
|
|
15
|
+
except ValueError:
|
|
16
|
+
return current
|
|
17
|
+
return current
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
CGROUP_ROOT = Path("/sys/fs/cgroup")
|
|
4
|
+
PROC_ROOT = Path("/proc")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def container_cgroup(pid: int) -> Path | None:
|
|
8
|
+
try:
|
|
9
|
+
lines = (PROC_ROOT / str(pid) / "cgroup").read_text().splitlines()
|
|
10
|
+
except OSError:
|
|
11
|
+
return None
|
|
12
|
+
for line in lines:
|
|
13
|
+
if line.startswith("0::"):
|
|
14
|
+
return CGROUP_ROOT / line.removeprefix("0::").lstrip("/")
|
|
15
|
+
return None
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import shutil
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from labnotes.runner.cgroup_memory import cgroup_memory
|
|
7
|
+
from labnotes.runner.container_cgroup import container_cgroup
|
|
8
|
+
from labnotes.runner.docker_output import docker_output
|
|
9
|
+
|
|
10
|
+
LABEL = "labnotes.run"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContainerMemory:
|
|
14
|
+
"""Track this run's Docker cgroups without starting a CLI process per sample."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, run_id: str) -> None:
|
|
17
|
+
self.run_id = run_id
|
|
18
|
+
self.cgroups: dict[str, Path] = {}
|
|
19
|
+
self.events: asyncio.subprocess.Process | None = None
|
|
20
|
+
self.watcher: asyncio.Task[None] | None = None
|
|
21
|
+
|
|
22
|
+
async def start(self) -> None:
|
|
23
|
+
if shutil.which("docker") is None:
|
|
24
|
+
return
|
|
25
|
+
since = str(int(time.time()))
|
|
26
|
+
ids = await asyncio.to_thread(
|
|
27
|
+
docker_output,
|
|
28
|
+
"ps",
|
|
29
|
+
"--quiet",
|
|
30
|
+
"--no-trunc",
|
|
31
|
+
"--filter",
|
|
32
|
+
f"label={LABEL}={self.run_id}",
|
|
33
|
+
)
|
|
34
|
+
if ids is None:
|
|
35
|
+
return
|
|
36
|
+
for container_id in ids.splitlines():
|
|
37
|
+
await self._add(container_id)
|
|
38
|
+
try:
|
|
39
|
+
self.events = await asyncio.create_subprocess_exec(
|
|
40
|
+
"docker",
|
|
41
|
+
"events",
|
|
42
|
+
"--since",
|
|
43
|
+
since,
|
|
44
|
+
"--filter",
|
|
45
|
+
"type=container",
|
|
46
|
+
"--filter",
|
|
47
|
+
f"label={LABEL}={self.run_id}",
|
|
48
|
+
"--format",
|
|
49
|
+
"{{.Actor.ID}} {{.Action}}",
|
|
50
|
+
stdout=asyncio.subprocess.PIPE,
|
|
51
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
52
|
+
)
|
|
53
|
+
except OSError:
|
|
54
|
+
return
|
|
55
|
+
self.watcher = asyncio.create_task(self._watch())
|
|
56
|
+
|
|
57
|
+
async def _add(self, container_id: str) -> None:
|
|
58
|
+
raw_pid = await asyncio.to_thread(
|
|
59
|
+
docker_output, "inspect", "--format", "{{.State.Pid}}", container_id
|
|
60
|
+
)
|
|
61
|
+
if raw_pid is None:
|
|
62
|
+
return
|
|
63
|
+
try:
|
|
64
|
+
pid = int(raw_pid.strip())
|
|
65
|
+
except ValueError:
|
|
66
|
+
return
|
|
67
|
+
if pid > 0 and (path := container_cgroup(pid)) is not None:
|
|
68
|
+
self.cgroups[container_id] = path
|
|
69
|
+
|
|
70
|
+
async def _watch(self) -> None:
|
|
71
|
+
assert self.events is not None
|
|
72
|
+
assert self.events.stdout is not None
|
|
73
|
+
while line := await self.events.stdout.readline():
|
|
74
|
+
container_id, _, action = line.decode().strip().partition(" ")
|
|
75
|
+
if action == "start":
|
|
76
|
+
await self._add(container_id)
|
|
77
|
+
elif action in {"die", "destroy", "stop"}:
|
|
78
|
+
self.cgroups.pop(container_id, None)
|
|
79
|
+
|
|
80
|
+
def usage(self) -> int:
|
|
81
|
+
total = 0
|
|
82
|
+
for path in self.cgroups.values():
|
|
83
|
+
try:
|
|
84
|
+
total += cgroup_memory(path)
|
|
85
|
+
except (OSError, ValueError):
|
|
86
|
+
pass
|
|
87
|
+
return total
|
|
88
|
+
|
|
89
|
+
async def close(self) -> None:
|
|
90
|
+
if self.watcher is not None:
|
|
91
|
+
self.watcher.cancel()
|
|
92
|
+
await asyncio.gather(self.watcher, return_exceptions=True)
|
|
93
|
+
if self.events is not None:
|
|
94
|
+
if self.events.returncode is None:
|
|
95
|
+
self.events.terminate()
|
|
96
|
+
try:
|
|
97
|
+
await asyncio.wait_for(self.events.wait(), timeout=1)
|
|
98
|
+
except TimeoutError:
|
|
99
|
+
self.events.kill()
|
|
100
|
+
await self.events.wait()
|
|
@@ -33,6 +33,12 @@ def describe_event(event: dict[str, object]) -> str:
|
|
|
33
33
|
return "No runs left."
|
|
34
34
|
case "concurrency":
|
|
35
35
|
return f"{event['concurrency']} at a time."
|
|
36
|
+
case "setting up":
|
|
37
|
+
return "Setting up."
|
|
38
|
+
case "tearing down":
|
|
39
|
+
return "Tearing down."
|
|
40
|
+
case "teardown failed":
|
|
41
|
+
return str(event["error"])
|
|
36
42
|
case "stopping":
|
|
37
43
|
return "Stopping runs. Ctrl-C again to kill them."
|
|
38
44
|
case _:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def docker_output(*args: str) -> str | None:
|
|
5
|
+
try:
|
|
6
|
+
result = subprocess.run(
|
|
7
|
+
["docker", *args], capture_output=True, text=True, timeout=3, check=False
|
|
8
|
+
)
|
|
9
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
10
|
+
return None
|
|
11
|
+
return result.stdout if result.returncode == 0 else None
|
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
import psutil
|
|
9
9
|
|
|
10
|
-
from labnotes.log_path import LOG_ENV
|
|
10
|
+
from labnotes.log_path import LOG_ENV, RUN_ENV
|
|
11
11
|
from labnotes.runner.exit_signal import exit_signal
|
|
12
12
|
from labnotes.runner.record_memory import record_memory
|
|
13
13
|
from labnotes.runner.shell_command import shell_command
|
|
@@ -66,6 +66,7 @@ async def execute_run(
|
|
|
66
66
|
"CLICOLOR_FORCE": "1",
|
|
67
67
|
**os.environ,
|
|
68
68
|
LOG_ENV: str(folder / "log.jsonl"),
|
|
69
|
+
RUN_ENV: str(folder.resolve()),
|
|
69
70
|
},
|
|
70
71
|
stdout=stdout,
|
|
71
72
|
stderr=stderr,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
async def invocation_hook(root: Path, hook: str, command: str) -> None:
|
|
6
|
+
process = await asyncio.create_subprocess_shell(
|
|
7
|
+
command,
|
|
8
|
+
cwd=root,
|
|
9
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
10
|
+
stderr=asyncio.subprocess.PIPE,
|
|
11
|
+
)
|
|
12
|
+
_, stderr = await process.communicate()
|
|
13
|
+
if process.returncode != 0:
|
|
14
|
+
message = f"{hook} command {command} exited {process.returncode}"
|
|
15
|
+
detail = stderr.decode(errors="replace").strip()
|
|
16
|
+
raise ValueError(f"{message}: {detail}" if detail else message)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
from labnotes.models import ExperimentConfig, LabnotesConfig
|
|
@@ -14,6 +15,8 @@ async def prepare_experiment(
|
|
|
14
15
|
concurrency: int | None,
|
|
15
16
|
only: dict[str, str] | None = None,
|
|
16
17
|
args: list[str] | None = None,
|
|
18
|
+
setup: str | None = None,
|
|
19
|
+
teardown: str | None = None,
|
|
17
20
|
) -> tuple[ExperimentConfig, int]:
|
|
18
21
|
if config.experiment not in (None, name):
|
|
19
22
|
raise ValueError(
|
|
@@ -33,6 +36,10 @@ async def prepare_experiment(
|
|
|
33
36
|
config.retries,
|
|
34
37
|
config.defaults,
|
|
35
38
|
)
|
|
39
|
+
if setup is not None:
|
|
40
|
+
experiment = dataclasses.replace(experiment, setup=setup)
|
|
41
|
+
if teardown is not None:
|
|
42
|
+
experiment = dataclasses.replace(experiment, teardown=teardown)
|
|
36
43
|
if only:
|
|
37
44
|
check_subset(experiment.params, only)
|
|
38
45
|
if args:
|
|
@@ -24,7 +24,10 @@ SETTINGS = {
|
|
|
24
24
|
"meta",
|
|
25
25
|
"memory_interval",
|
|
26
26
|
"views",
|
|
27
|
+
"setup",
|
|
28
|
+
"teardown",
|
|
27
29
|
}
|
|
30
|
+
HOOKS = ("setup", "teardown")
|
|
28
31
|
VIEWS = {"runs"}
|
|
29
32
|
|
|
30
33
|
|
|
@@ -81,6 +84,10 @@ def read_experiment_config(
|
|
|
81
84
|
memory_interval = values.get("memory_interval", DEFAULT_MEMORY_INTERVAL)
|
|
82
85
|
if not is_non_negative_number(memory_interval):
|
|
83
86
|
raise ValueError(f"{path}: memory_interval must be 0 or more seconds")
|
|
87
|
+
for hook in HOOKS:
|
|
88
|
+
run = values.get(hook)
|
|
89
|
+
if run is not None and (not isinstance(run, str) or not run.strip()):
|
|
90
|
+
raise ValueError(f"{path}: {hook} must be a string")
|
|
84
91
|
views = values.get("views", {})
|
|
85
92
|
if not isinstance(views, dict):
|
|
86
93
|
raise ValueError(f"{path}: views must be a table, like [views]")
|
|
@@ -101,4 +108,6 @@ def read_experiment_config(
|
|
|
101
108
|
retries,
|
|
102
109
|
meta,
|
|
103
110
|
memory_interval,
|
|
111
|
+
values.get("setup"),
|
|
112
|
+
values.get("teardown"),
|
|
104
113
|
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from labnotes.runner.container_memory import ContainerMemory
|
|
7
|
+
from labnotes.utils.tree_usage import tree_usage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def record_memory(path: Path, pid: int, created: float, interval: float) -> None:
|
|
11
|
+
containers = ContainerMemory(str(path.parent.resolve()))
|
|
12
|
+
try:
|
|
13
|
+
await containers.start()
|
|
14
|
+
with path.open("a", encoding="utf-8") as samples:
|
|
15
|
+
while True:
|
|
16
|
+
usage = tree_usage(pid, created)
|
|
17
|
+
if usage is not None:
|
|
18
|
+
rss, processes = usage
|
|
19
|
+
sample = {
|
|
20
|
+
"time": time.time(),
|
|
21
|
+
"rss": rss + containers.usage(),
|
|
22
|
+
"processes": processes,
|
|
23
|
+
}
|
|
24
|
+
samples.write(json.dumps(sample) + "\n")
|
|
25
|
+
samples.flush()
|
|
26
|
+
await asyncio.sleep(interval)
|
|
27
|
+
finally:
|
|
28
|
+
await containers.close()
|
|
@@ -14,6 +14,7 @@ from labnotes.models import (
|
|
|
14
14
|
from labnotes.retries import RETRY_EXIT_CODE, RETRY_SECONDS
|
|
15
15
|
from labnotes.runner.execute_run import execute_run
|
|
16
16
|
from labnotes.runner.in_subset import in_subset
|
|
17
|
+
from labnotes.runner.invocation_hook import invocation_hook
|
|
17
18
|
from labnotes.runner.invocation_meta import invocation_meta
|
|
18
19
|
from labnotes.runner.new_param_run_id import new_param_run_id
|
|
19
20
|
from labnotes.runner.new_stamped_run_id import new_stamped_run_id
|
|
@@ -233,8 +234,25 @@ async def run_experiment(
|
|
|
233
234
|
):
|
|
234
235
|
retry_later(run)
|
|
235
236
|
|
|
237
|
+
async def teardown() -> None:
|
|
238
|
+
if experiment.teardown is not None:
|
|
239
|
+
report({"event": "tearing down"})
|
|
240
|
+
await invocation_hook(config.root, "teardown", experiment.teardown)
|
|
241
|
+
|
|
236
242
|
listen(Controls(interrupt, set_concurrency, kill, finish))
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
243
|
+
# Like vitest's globalSetup: a failed setup still gets its teardown.
|
|
244
|
+
try:
|
|
245
|
+
if experiment.setup is not None:
|
|
246
|
+
report({"event": "setting up"})
|
|
247
|
+
await invocation_hook(config.root, "setup", experiment.setup)
|
|
248
|
+
fill()
|
|
249
|
+
await done.wait()
|
|
250
|
+
await asyncio.gather(*started)
|
|
251
|
+
except BaseException:
|
|
252
|
+
try:
|
|
253
|
+
await teardown()
|
|
254
|
+
except Exception as failure: # noqa: BLE001
|
|
255
|
+
report({"event": "teardown failed", "error": str(failure)})
|
|
256
|
+
raise
|
|
257
|
+
await teardown()
|
|
240
258
|
return results
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import json
|
|
3
|
-
import time
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from labnotes.utils.tree_usage import tree_usage
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
async def record_memory(path: Path, pid: int, created: float, interval: float) -> None:
|
|
10
|
-
with path.open("a", encoding="utf-8") as samples:
|
|
11
|
-
while True:
|
|
12
|
-
usage = tree_usage(pid, created)
|
|
13
|
-
if usage is not None:
|
|
14
|
-
rss, processes = usage
|
|
15
|
-
sample = {"time": time.time(), "rss": rss, "processes": processes}
|
|
16
|
-
samples.write(json.dumps(sample) + "\n")
|
|
17
|
-
samples.flush()
|
|
18
|
-
await asyncio.sleep(interval)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|