labnotes 0.2.6__tar.gz → 0.2.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labnotes-0.2.6 → labnotes-0.2.8}/PKG-INFO +6 -2
- {labnotes-0.2.6 → labnotes-0.2.8}/README.md +5 -1
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/cli.py +2 -0
- labnotes-0.2.8/src/labnotes/command_line_group.py +20 -0
- labnotes-0.2.8/src/labnotes/config_migration.py +12 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/context.md +23 -20
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/create_app.py +1 -2
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/create_experiment.py +5 -2
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/experiment_definition.py +4 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/experiments.py +2 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/list_experiments.py +2 -2
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/load_experiment.py +3 -5
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/models.py +0 -3
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/read_config.py +5 -5
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/describe_event.py +2 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/execute_run.py +6 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/first_invocation.py +2 -0
- labnotes-0.2.8/src/labnotes/runner/hold_experiment.py +29 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/prepare_experiment.py +0 -1
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/read_experiment_config.py +6 -10
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/run_experiment.py +12 -50
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/start_experiment.py +4 -1
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/summarize_config.py +1 -3
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/experiment_rows.py +1 -3
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/templates/experiment.html +1 -2
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/experiment_screen.py +0 -1
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/run_app.py +4 -4
- {labnotes-0.2.6 → labnotes-0.2.8}/testing-conventions.toml +7 -7
- {labnotes-0.2.6 → labnotes-0.2.8}/tests/conftest.py +1 -1
- {labnotes-0.2.6 → labnotes-0.2.8}/tests/integration/conftest.py +1 -1
- labnotes-0.2.6/src/labnotes/modes.py +0 -2
- labnotes-0.2.6/src/labnotes/runner/new_param_run_id.py +0 -18
- labnotes-0.2.6/src/labnotes/runner/param_run_name.py +0 -17
- {labnotes-0.2.6 → labnotes-0.2.8}/.gitignore +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/CHANGELOG.md +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/MIGRATIONS.md +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/pyproject.toml +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/__init__.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/__main__.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/copy_experiment.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/create_note.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/data_html.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/format_started.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/format_value.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/get_state.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/grid_cell.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/init.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/is_non_negative_number.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/latest_log.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/load_config.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/load_run.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/log.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/log_path.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/memory_interval.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/note_path.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/output_html.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/parse_param_value.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/parse_params.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/parse_subset.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/pivot_runs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/read_path.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/retries.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/run_output.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/cgroup_memory.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/check_args.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/check_hook_placeholders.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/check_subset.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/claim_run.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/command_flags.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/container_cgroup.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/container_memory.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/continue_run.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/docker_output.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/exit_signal.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/fill_dirs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/fill_experiment_env.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/finalize_lost_runs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/flag_name.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/group_args.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/in_subset.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/invocation_hook.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/invocation_meta.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/override_flags.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/placeholders.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/project_snapshot.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/record_memory.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/run_git.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/run_locked.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/untracked_diff.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/runs_view_command.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/save_note.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/server.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/set_state.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/state_path.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/list_resumable_runs.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/read_memory.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/read_run_file.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/runs_columns.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/check_view_response.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/config.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/format_cell.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/native_rows.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/run_screen.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/runs_view.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/view_column.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/tui/view_process.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/utils/__init__.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/utils/format_bytes.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/utils/process_alive.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/utils/run_usage.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/utils/tree_rss.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/utils/tree_usage.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/view/__init__.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/view/answer.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/view/build.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/view/respond.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/view/serve.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/tests/e2e/conftest.py +0 -0
- {labnotes-0.2.6 → labnotes-0.2.8}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labnotes
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8
|
|
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>
|
|
@@ -79,7 +79,8 @@ labnotes experiments foo runs # foo's runs, oldest first
|
|
|
79
79
|
Each takes `--format table` or `--format json`, which print rows and exit.
|
|
80
80
|
Without `--format`, they open a browser in a terminal: the experiment list,
|
|
81
81
|
Enter to open one, Escape to go back. `foo` and `foo runs` open straight on
|
|
82
|
-
`foo`, which shows its runs
|
|
82
|
+
`foo`, which shows its runs, with runs that older versions nested in one invocation
|
|
83
|
+
folder grouped under it; Enter expands or collapses
|
|
83
84
|
one. Piped, they print a table.
|
|
84
85
|
|
|
85
86
|
`labnotes experiments new [NAME]` creates an experiment folder with a starter
|
|
@@ -96,6 +97,7 @@ experiments folder:
|
|
|
96
97
|
name = "Model and effort"
|
|
97
98
|
command = "uv run scripts/study.py --output {run_dir}"
|
|
98
99
|
repeats = 5
|
|
100
|
+
continues = false
|
|
99
101
|
|
|
100
102
|
[params]
|
|
101
103
|
model = ["opus", "sonnet"]
|
|
@@ -107,6 +109,8 @@ and as `display_name` in `experiments`. The folder name stays the key, so
|
|
|
107
109
|
renaming the experiment means editing `name`.
|
|
108
110
|
Each combination of `[params]` runs `repeats` times, round-robin. Only runs
|
|
109
111
|
that exit 0 count, so running again fills in what failed or never ran.
|
|
112
|
+
`continues` is required: `true` invokes a failed run again in its own folder,
|
|
113
|
+
`false` gives its combination a new one.
|
|
110
114
|
Each run gets a new run folder. `command` runs in a shell from the root, with
|
|
111
115
|
`{run_dir}` replaced by the run folder's absolute path. Pass it to the
|
|
112
116
|
script so it writes its outputs there.
|
|
@@ -53,7 +53,8 @@ labnotes experiments foo runs # foo's runs, oldest first
|
|
|
53
53
|
Each takes `--format table` or `--format json`, which print rows and exit.
|
|
54
54
|
Without `--format`, they open a browser in a terminal: the experiment list,
|
|
55
55
|
Enter to open one, Escape to go back. `foo` and `foo runs` open straight on
|
|
56
|
-
`foo`, which shows its runs
|
|
56
|
+
`foo`, which shows its runs, with runs that older versions nested in one invocation
|
|
57
|
+
folder grouped under it; Enter expands or collapses
|
|
57
58
|
one. Piped, they print a table.
|
|
58
59
|
|
|
59
60
|
`labnotes experiments new [NAME]` creates an experiment folder with a starter
|
|
@@ -70,6 +71,7 @@ experiments folder:
|
|
|
70
71
|
name = "Model and effort"
|
|
71
72
|
command = "uv run scripts/study.py --output {run_dir}"
|
|
72
73
|
repeats = 5
|
|
74
|
+
continues = false
|
|
73
75
|
|
|
74
76
|
[params]
|
|
75
77
|
model = ["opus", "sonnet"]
|
|
@@ -81,6 +83,8 @@ and as `display_name` in `experiments`. The folder name stays the key, so
|
|
|
81
83
|
renaming the experiment means editing `name`.
|
|
82
84
|
Each combination of `[params]` runs `repeats` times, round-robin. Only runs
|
|
83
85
|
that exit 0 count, so running again fills in what failed or never ran.
|
|
86
|
+
`continues` is required: `true` invokes a failed run again in its own folder,
|
|
87
|
+
`false` gives its combination a new one.
|
|
84
88
|
Each run gets a new run folder. `command` runs in a shell from the root, with
|
|
85
89
|
`{run_dir}` replaced by the run folder's absolute path. Pass it to the
|
|
86
90
|
script so it writes its outputs there.
|
|
@@ -2,6 +2,7 @@ from pathlib import Path
|
|
|
2
2
|
|
|
3
3
|
import click
|
|
4
4
|
|
|
5
|
+
from labnotes.command_line_group import CommandLineGroup
|
|
5
6
|
from labnotes.context_guide import context_guide
|
|
6
7
|
from labnotes.experiments import experiments
|
|
7
8
|
from labnotes.init import init
|
|
@@ -11,6 +12,7 @@ from labnotes.server import server
|
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@click.group(
|
|
15
|
+
cls=CommandLineGroup,
|
|
14
16
|
invoke_without_command=True,
|
|
15
17
|
help="Manage experiments in a folder. `labnotes context` prints a guide for"
|
|
16
18
|
" coding agents.",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
COMMAND_LINE = "labnotes.command_line"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CommandLineGroup(click.Group):
|
|
10
|
+
def make_context(
|
|
11
|
+
self,
|
|
12
|
+
info_name: str | None,
|
|
13
|
+
args: list[str],
|
|
14
|
+
parent: click.Context | None = None,
|
|
15
|
+
**extra: Any,
|
|
16
|
+
) -> click.Context:
|
|
17
|
+
command_line = shlex.join(["labnotes", *args])
|
|
18
|
+
ctx = super().make_context(info_name, args, parent, **extra)
|
|
19
|
+
ctx.meta[COMMAND_LINE] = command_line
|
|
20
|
+
return ctx
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
MIGRATION = (
|
|
2
|
+
"https://github.com/thekevinscott/labnotes/blob/main/"
|
|
3
|
+
"docs/migrations.d/2026-11-02-python-remove-modes.md"
|
|
4
|
+
)
|
|
5
|
+
REMOVED_MODE = (
|
|
6
|
+
"mode was removed: every experiment plans what it is missing. "
|
|
7
|
+
f"Delete mode. See {MIGRATION}"
|
|
8
|
+
)
|
|
9
|
+
MISSING_CONTINUES = (
|
|
10
|
+
"continues is required: set continues = true or continues = false. "
|
|
11
|
+
f"See {MIGRATION}"
|
|
12
|
+
)
|
|
@@ -55,6 +55,7 @@ experiments folder:
|
|
|
55
55
|
name = "Model and effort"
|
|
56
56
|
command = "uv run scripts/study.py --output {run_dir}"
|
|
57
57
|
repeats = 5
|
|
58
|
+
continues = false
|
|
58
59
|
|
|
59
60
|
[params]
|
|
60
61
|
model = ["opus", "sonnet"]
|
|
@@ -64,22 +65,26 @@ effort = ["low", "high"]
|
|
|
64
65
|
`name` is optional: a display name for the experiment, shown in the live view
|
|
65
66
|
and as `display_name` in `experiments`. The folder name stays the key, so
|
|
66
67
|
renaming the experiment means editing `name`.
|
|
67
|
-
Each combination of `[params]`
|
|
68
|
-
`run
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
its folder the same
|
|
76
|
-
|
|
68
|
+
Each combination of `[params]` needs `repeats` runs that count (1 if omitted).
|
|
69
|
+
`run` plans only the combinations short of that, round-robin, so running again
|
|
70
|
+
fills in what failed or never ran, and a finished experiment plans nothing
|
|
71
|
+
(`Nothing to run: every combination has its repeats.`). To start over, copy it
|
|
72
|
+
with `experiments new NAME --from OLD`, which drops `mode` and fills in
|
|
73
|
+
`continues`. `mode` is gone; setting it is a config error.
|
|
74
|
+
`continues` is required, in `experiment.toml` or `[experiment]` in
|
|
75
|
+
`.labnotes.toml`. `continues = true` invokes a failed, stopped or lost run again
|
|
76
|
+
in its own folder, with the same params and `{run_dir}`; `run.json` then lists
|
|
77
|
+
each attempt under `"invocations"`. `continues = false` gives the combination a
|
|
78
|
+
new folder. Never delete `run.json`, `stdout-*`, `stderr-*` or `run.lock` from a
|
|
79
|
+
run folder.
|
|
80
|
+
Each new run gets a run folder named for the UTC time it was planned
|
|
81
|
+
(`runs/20261020-000429-brave-fox`), so folder listings sort by start. Older
|
|
82
|
+
versions nested several runs in one invocation folder
|
|
83
|
+
(`runs/20261020-000429-brave-fox/quiet-river`, run id is that path); those still
|
|
84
|
+
list and count. Only one `run` of an experiment goes at a time; a second refuses. `command` runs in a shell from the root, with
|
|
77
85
|
`{run_dir}` replaced by the run folder's absolute path. Pass it to the
|
|
78
|
-
script so it writes its outputs there. `{invocation_dir}` is the
|
|
79
|
-
|
|
80
|
-
run it stays at the first attempt's folder across retries, while `{run_dir}`
|
|
81
|
-
changes per attempt. With several matrix runs, it equals each run's `{run_dir}`.
|
|
82
|
-
Both placeholders are shell-quoted.
|
|
86
|
+
script so it writes its outputs there. `{invocation_dir}` is the same
|
|
87
|
+
folder as `{run_dir}`. Both placeholders are shell-quoted.
|
|
83
88
|
Each param is appended as `--name value`, with the name as written (use
|
|
84
89
|
`source-language` for a `--source-language` flag), booleans as 0 or 1, and lists
|
|
85
90
|
and tables as JSON. labnotes writes `run.json` (params, `command`, created,
|
|
@@ -114,9 +119,7 @@ existing value without dropping other keys. Values must be JSON serializable;
|
|
|
114
119
|
reading before any write returns `{}`. Runs can only read: `set_state` raises
|
|
115
120
|
`RuntimeError` there. Both functions raise outside an invocation and in `meta`.
|
|
116
121
|
Labnotes gives setup/teardown `LABNOTES_STATE` and runs `LABNOTES_RUN_STATE`,
|
|
117
|
-
pointing to
|
|
118
|
-
folder, or, without a shared folder, a temp
|
|
119
|
-
file kept through all runs and teardown and then removed. Queued runs and retries
|
|
122
|
+
pointing to a temp file kept through all runs and teardown and then removed. Queued runs and retries
|
|
120
123
|
read the same file after setup finishes. `meta` receives neither state variable;
|
|
121
124
|
state is never copied into `run.json`.
|
|
122
125
|
Nested labnotes invocations clear inherited `LABNOTES_*` context for their
|
|
@@ -182,7 +185,7 @@ Escape clears it. `x` stops the
|
|
|
182
185
|
running run under the cursor; `x` again kills it. `+` and
|
|
183
186
|
`-` change how many run at once; lowering it lets running runs
|
|
184
187
|
finish. Enter on a run follows its `stdout.log` and `stderr.log` together, as
|
|
185
|
-
they're written, and Escape returns to the table.
|
|
188
|
+
they're written, and Escape returns to the table. A `run` that
|
|
186
189
|
plans a single run opens straight on its output. `q` quits and leaves the runs
|
|
187
190
|
going. Elsewhere, it prints a line per event instead.
|
|
188
191
|
|
|
@@ -218,4 +221,4 @@ going. Elsewhere, it prints a line per event instead.
|
|
|
218
221
|
was retried counts as retried, not failed. If it can't
|
|
219
222
|
start a run, it prints the cause on one line and exits 1.
|
|
220
223
|
|
|
221
|
-
`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.
|
|
224
|
+
`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. 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.
|
|
@@ -63,7 +63,7 @@ def create_app(config: LabnotesConfig) -> Starlette:
|
|
|
63
63
|
await kernel.stop()
|
|
64
64
|
|
|
65
65
|
async def index(request: Request) -> Response:
|
|
66
|
-
experiments = await list_experiments(request.app.state.tables
|
|
66
|
+
experiments = await list_experiments(request.app.state.tables)
|
|
67
67
|
return templates.TemplateResponse(
|
|
68
68
|
request,
|
|
69
69
|
"index.html",
|
|
@@ -75,7 +75,6 @@ def create_app(config: LabnotesConfig) -> Starlette:
|
|
|
75
75
|
loaded = await load_experiment(
|
|
76
76
|
request.app.state.tables,
|
|
77
77
|
config.root,
|
|
78
|
-
config.mode,
|
|
79
78
|
name,
|
|
80
79
|
request.query_params,
|
|
81
80
|
lambda note, content: saved_outputs(kernels.get((name, note)), content),
|
|
@@ -10,6 +10,8 @@ STARTER = """\
|
|
|
10
10
|
# Runs from the project root. {run_dir} becomes the run's folder.
|
|
11
11
|
command = "uv run script.py --output {run_dir}"
|
|
12
12
|
repeats = 1
|
|
13
|
+
# true: a failed run starts again in its own folder instead of a new one.
|
|
14
|
+
continues = false
|
|
13
15
|
|
|
14
16
|
# Each combination runs `repeats` times, passed as --name value.
|
|
15
17
|
[params]
|
|
@@ -41,7 +43,8 @@ def create_experiment(
|
|
|
41
43
|
path = folder(name)
|
|
42
44
|
if path.exists():
|
|
43
45
|
raise ValueError(f"{name} already exists at {path}.")
|
|
44
|
-
|
|
46
|
+
inherited = config.defaults.get("continues") is True
|
|
47
|
+
text = STARTER.replace("continues = false", f"continues = {str(inherited).lower()}")
|
|
45
48
|
if source is not None:
|
|
46
49
|
origin = folder(source) / "experiment.toml"
|
|
47
50
|
if not origin.is_file():
|
|
@@ -53,7 +56,7 @@ def create_experiment(
|
|
|
53
56
|
else:
|
|
54
57
|
path.mkdir(parents=True)
|
|
55
58
|
(path / "experiment.toml").write_text(
|
|
56
|
-
experiment_definition(text, source, command, repeats, params or {}),
|
|
59
|
+
experiment_definition(text, source, command, repeats, params or {}, inherited),
|
|
57
60
|
encoding="utf-8",
|
|
58
61
|
)
|
|
59
62
|
return path
|
|
@@ -7,13 +7,17 @@ def experiment_definition(
|
|
|
7
7
|
command: str | None,
|
|
8
8
|
repeats: int | None,
|
|
9
9
|
params: dict[str, list[object]],
|
|
10
|
+
continues: bool = False,
|
|
10
11
|
) -> str:
|
|
11
12
|
document = tomlkit.parse(text)
|
|
12
13
|
document.pop("from", None)
|
|
14
|
+
document.pop("mode", None)
|
|
13
15
|
if command is not None:
|
|
14
16
|
document["command"] = command
|
|
15
17
|
if repeats is not None:
|
|
16
18
|
document["repeats"] = repeats
|
|
19
|
+
if "continues" not in document:
|
|
20
|
+
document["continues"] = continues
|
|
17
21
|
if params:
|
|
18
22
|
table = document.setdefault("params", tomlkit.table())
|
|
19
23
|
for name, values in params.items():
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
|
|
7
|
+
from labnotes.command_line_group import COMMAND_LINE
|
|
7
8
|
from labnotes.create_experiment import create_experiment
|
|
8
9
|
from labnotes.runner.start_experiment import start_experiment
|
|
9
10
|
from labnotes.utils.format_bytes import format_bytes
|
|
@@ -163,6 +164,7 @@ def experiments(
|
|
|
163
164
|
passthrough or None,
|
|
164
165
|
setup,
|
|
165
166
|
teardown,
|
|
167
|
+
ctx.meta.get(COMMAND_LINE),
|
|
166
168
|
)
|
|
167
169
|
try:
|
|
168
170
|
if sys.stdout.isatty() and sys.stderr.isatty():
|
|
@@ -9,7 +9,7 @@ from labnotes.experiment_runs import experiment_runs
|
|
|
9
9
|
from labnotes.summarize_config import summarize_config
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
async def list_experiments(tables: DirSQL
|
|
12
|
+
async def list_experiments(tables: DirSQL) -> list[dict[str, object]]:
|
|
13
13
|
rows = await tables.query(
|
|
14
14
|
"SELECT name, display_name, config FROM experiments ORDER BY name"
|
|
15
15
|
)
|
|
@@ -17,7 +17,7 @@ async def list_experiments(tables: DirSQL, mode: str) -> list[dict[str, object]]
|
|
|
17
17
|
for row in rows:
|
|
18
18
|
name = str(row["name"])
|
|
19
19
|
config = None if row["config"] is None else json.loads(str(row["config"]))
|
|
20
|
-
summary = summarize_config(config
|
|
20
|
+
summary = summarize_config(config)
|
|
21
21
|
varies = cast(dict[str, list[object]], summary["varies"])
|
|
22
22
|
source = experiment_runs(name, list(varies))
|
|
23
23
|
summary = await check_counts(tables, source, summary)
|
|
@@ -33,7 +33,6 @@ def _no_outputs(note: str, content: str) -> Sequence[str]:
|
|
|
33
33
|
async def load_experiment(
|
|
34
34
|
tables: DirSQL,
|
|
35
35
|
root: Path,
|
|
36
|
-
mode: str,
|
|
37
36
|
name: str,
|
|
38
37
|
params: QueryParams,
|
|
39
38
|
outputs_for: Callable[[str, str], Sequence[str]] = _no_outputs,
|
|
@@ -46,7 +45,7 @@ async def load_experiment(
|
|
|
46
45
|
return None
|
|
47
46
|
[row] = found
|
|
48
47
|
config = None if row["config"] is None else json.loads(str(row["config"]))
|
|
49
|
-
summary = summarize_config(config
|
|
48
|
+
summary = summarize_config(config)
|
|
50
49
|
varies = cast(dict[str, list[object]], summary["varies"])
|
|
51
50
|
source = experiment_runs(name, list(varies))
|
|
52
51
|
summary = await check_counts(tables, source, summary)
|
|
@@ -72,14 +71,13 @@ async def load_experiment(
|
|
|
72
71
|
)
|
|
73
72
|
|
|
74
73
|
runs_live = any(is_running(run) for run in runs)
|
|
75
|
-
nested =
|
|
74
|
+
nested = await nests_runs(tables, source)
|
|
76
75
|
groups = (
|
|
77
76
|
group_runs(runs, list(choices), await count_groups(tables, source))
|
|
78
77
|
if nested
|
|
79
78
|
else None
|
|
80
79
|
)
|
|
81
|
-
|
|
82
|
-
gridded = matrix and params.getlist("view")[-1:] == ["grid"]
|
|
80
|
+
gridded = params.getlist("view")[-1:] == ["grid"]
|
|
83
81
|
axes = {key: varies[key] for key in choices if len(varies[key]) > 1}
|
|
84
82
|
grid = (
|
|
85
83
|
pivot_runs(
|
|
@@ -5,7 +5,6 @@ from typing import Any, Protocol
|
|
|
5
5
|
|
|
6
6
|
from labnotes.default_counts import DEFAULT_COUNTS
|
|
7
7
|
from labnotes.memory_interval import DEFAULT_MEMORY_INTERVAL
|
|
8
|
-
from labnotes.modes import DEFAULT_MODE
|
|
9
8
|
from labnotes.retries import DEFAULT_RETRIES
|
|
10
9
|
|
|
11
10
|
Report = Callable[[dict[str, object]], None]
|
|
@@ -43,7 +42,6 @@ class LabnotesConfig:
|
|
|
43
42
|
experiments: str
|
|
44
43
|
files: str
|
|
45
44
|
runs: str
|
|
46
|
-
mode: str = DEFAULT_MODE
|
|
47
45
|
concurrency: int = 1
|
|
48
46
|
retries: int = DEFAULT_RETRIES
|
|
49
47
|
# Set when the config's folder is itself the one experiment.
|
|
@@ -59,7 +57,6 @@ class ExperimentConfig:
|
|
|
59
57
|
params: dict[str, list[object]]
|
|
60
58
|
name: str | None = None
|
|
61
59
|
counts: str = DEFAULT_COUNTS
|
|
62
|
-
mode: str = DEFAULT_MODE
|
|
63
60
|
concurrency: int = 1
|
|
64
61
|
retries: int = DEFAULT_RETRIES
|
|
65
62
|
meta: str | None = None
|
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from labnotes.models import LabnotesConfig
|
|
5
5
|
from labnotes.is_non_negative_int import is_non_negative_int
|
|
6
6
|
from labnotes.is_positive_int import is_positive_int
|
|
7
|
-
from labnotes.
|
|
7
|
+
from labnotes.config_migration import REMOVED_MODE
|
|
8
8
|
from labnotes.runner.read_experiment_config import SETTINGS
|
|
9
9
|
from labnotes.read_path import DEFAULT_PATHS, read_path
|
|
10
10
|
from labnotes.retries import DEFAULT_RETRIES
|
|
@@ -25,6 +25,8 @@ def read_config(path: Path) -> LabnotesConfig:
|
|
|
25
25
|
defaults = values.get("experiment", {})
|
|
26
26
|
if not isinstance(defaults, dict):
|
|
27
27
|
raise ValueError(f"{path}: experiment must be a table, like [experiment]")
|
|
28
|
+
if "mode" in defaults:
|
|
29
|
+
raise ValueError(f"{path}: [experiment] {REMOVED_MODE}")
|
|
28
30
|
unknown = sorted(defaults.keys() - SETTINGS)
|
|
29
31
|
if unknown:
|
|
30
32
|
raise ValueError(f"{path}: unknown keys in [experiment]: {', '.join(unknown)}")
|
|
@@ -39,9 +41,8 @@ def read_config(path: Path) -> LabnotesConfig:
|
|
|
39
41
|
table: read_path(path, table, values.get(table, {}), single=single)
|
|
40
42
|
for table in DEFAULT_PATHS
|
|
41
43
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
raise ValueError(f"{path}: [experiments] mode must be matrix or adhoc")
|
|
44
|
+
if "mode" in experiments:
|
|
45
|
+
raise ValueError(f"{path}: [experiments] {REMOVED_MODE}")
|
|
45
46
|
concurrency = experiments.get("concurrency", 1)
|
|
46
47
|
if not is_positive_int(concurrency):
|
|
47
48
|
raise ValueError(
|
|
@@ -54,7 +55,6 @@ def read_config(path: Path) -> LabnotesConfig:
|
|
|
54
55
|
path.resolve(),
|
|
55
56
|
root,
|
|
56
57
|
**paths,
|
|
57
|
-
mode=mode,
|
|
58
58
|
concurrency=concurrency,
|
|
59
59
|
retries=retries,
|
|
60
60
|
experiment=root.name if single else None,
|
|
@@ -7,6 +7,8 @@ def describe_event(event: dict[str, object]) -> str:
|
|
|
7
7
|
match event["event"]:
|
|
8
8
|
case "planned":
|
|
9
9
|
runs = len(event["runs"]) # type: ignore[arg-type]
|
|
10
|
+
if not runs:
|
|
11
|
+
return "Nothing to run: every combination has its repeats."
|
|
10
12
|
return f"{runs} runs to go, {event['concurrency']} at a time."
|
|
11
13
|
case "started":
|
|
12
14
|
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
import time
|
|
6
6
|
from collections.abc import Callable, Sequence
|
|
7
7
|
from datetime import UTC, datetime
|
|
8
|
+
from importlib.metadata import version
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
import psutil
|
|
@@ -43,6 +44,7 @@ async def execute_run(
|
|
|
43
44
|
environment: dict[str, str] | None = None,
|
|
44
45
|
git_info: dict[str, str | bool] | None = None,
|
|
45
46
|
continues: bool = False,
|
|
47
|
+
command_line: str | None = None,
|
|
46
48
|
) -> dict[str, object]:
|
|
47
49
|
resolved = shell_command(command, folder, params, args)
|
|
48
50
|
git = git_info if git_info is not None else await asyncio.to_thread(run_git, root)
|
|
@@ -56,6 +58,8 @@ async def execute_run(
|
|
|
56
58
|
"ended_by": None,
|
|
57
59
|
"signal": None,
|
|
58
60
|
"git": git,
|
|
61
|
+
"labnotes_version": version("labnotes"),
|
|
62
|
+
"labnotes_command": command_line,
|
|
59
63
|
}
|
|
60
64
|
if args:
|
|
61
65
|
run["args"] = list(args)
|
|
@@ -76,6 +80,8 @@ async def execute_run(
|
|
|
76
80
|
"error": None,
|
|
77
81
|
"git": None if git is None else git["commit"],
|
|
78
82
|
"args": list(args),
|
|
83
|
+
"labnotes_version": run["labnotes_version"],
|
|
84
|
+
"labnotes_command": command_line,
|
|
79
85
|
**logs,
|
|
80
86
|
}
|
|
81
87
|
record = folder / "run.json"
|
|
@@ -8,6 +8,8 @@ def first_invocation(run: dict[str, object]) -> dict[str, object]:
|
|
|
8
8
|
"error": run.get("error"),
|
|
9
9
|
"git": git.get("commit") if isinstance(git, dict) else None,
|
|
10
10
|
"args": run.get("args", []),
|
|
11
|
+
"labnotes_version": run.get("labnotes_version"),
|
|
12
|
+
"labnotes_command": run.get("labnotes_command"),
|
|
11
13
|
"stdout": "stdout.log",
|
|
12
14
|
"stderr": "stderr.log",
|
|
13
15
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fcntl
|
|
2
|
+
import os
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import contextmanager, suppress
|
|
5
|
+
|
|
6
|
+
from labnotes.models import LabnotesConfig
|
|
7
|
+
from labnotes.runner.runs_parent import runs_parent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@contextmanager
|
|
11
|
+
def hold_experiment(config: LabnotesConfig, name: str) -> Iterator[None]:
|
|
12
|
+
folder = runs_parent(config.root, config.runs, name)
|
|
13
|
+
made = not folder.exists()
|
|
14
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
descriptor = os.open(folder, os.O_RDONLY)
|
|
16
|
+
try:
|
|
17
|
+
try:
|
|
18
|
+
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
19
|
+
except BlockingIOError:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
f"{name} is already running in another labnotes run."
|
|
22
|
+
" Wait for it to finish, or stop it, then run again."
|
|
23
|
+
) from None
|
|
24
|
+
yield
|
|
25
|
+
finally:
|
|
26
|
+
if made:
|
|
27
|
+
with suppress(OSError):
|
|
28
|
+
folder.rmdir()
|
|
29
|
+
os.close(descriptor)
|
|
@@ -10,7 +10,7 @@ from labnotes.is_non_negative_int import is_non_negative_int
|
|
|
10
10
|
from labnotes.is_non_negative_number import is_non_negative_number
|
|
11
11
|
from labnotes.memory_interval import DEFAULT_MEMORY_INTERVAL
|
|
12
12
|
from labnotes.is_positive_int import is_positive_int
|
|
13
|
-
from labnotes.
|
|
13
|
+
from labnotes.config_migration import MISSING_CONTINUES, REMOVED_MODE
|
|
14
14
|
|
|
15
15
|
SETTINGS = {
|
|
16
16
|
"name",
|
|
@@ -18,7 +18,6 @@ SETTINGS = {
|
|
|
18
18
|
"repeats",
|
|
19
19
|
"params",
|
|
20
20
|
"counts",
|
|
21
|
-
"mode",
|
|
22
21
|
"concurrency",
|
|
23
22
|
"retries",
|
|
24
23
|
"meta",
|
|
@@ -37,7 +36,6 @@ VIEWS = {"runs"}
|
|
|
37
36
|
def read_experiment_config(
|
|
38
37
|
path: Path,
|
|
39
38
|
source: str,
|
|
40
|
-
mode: str,
|
|
41
39
|
concurrency: int,
|
|
42
40
|
retries: int,
|
|
43
41
|
defaults: dict[str, object] | None = None,
|
|
@@ -47,6 +45,8 @@ def read_experiment_config(
|
|
|
47
45
|
except tomllib.TOMLDecodeError as error:
|
|
48
46
|
raise ValueError(f"{path}: {error}") from error
|
|
49
47
|
values: dict[str, Any] = compose_experiment(defaults or {}, layer)
|
|
48
|
+
if "mode" in values:
|
|
49
|
+
raise ValueError(f"{path}: {REMOVED_MODE}")
|
|
50
50
|
unknown = sorted(values.keys() - SETTINGS)
|
|
51
51
|
if unknown:
|
|
52
52
|
raise ValueError(f"{path}: unknown keys {', '.join(unknown)}")
|
|
@@ -72,9 +72,6 @@ def read_experiment_config(
|
|
|
72
72
|
counts = values.get("counts", DEFAULT_COUNTS)
|
|
73
73
|
if not isinstance(counts, str) or not counts.strip():
|
|
74
74
|
raise ValueError(f"{path}: counts must be a SQL condition")
|
|
75
|
-
mode = values.get("mode", mode)
|
|
76
|
-
if mode not in MODES:
|
|
77
|
-
raise ValueError(f"{path}: mode must be matrix or adhoc")
|
|
78
75
|
concurrency = values.get("concurrency", concurrency)
|
|
79
76
|
if not is_positive_int(concurrency):
|
|
80
77
|
raise ValueError(f"{path}: concurrency must be a positive integer")
|
|
@@ -102,11 +99,11 @@ def read_experiment_config(
|
|
|
102
99
|
raise ValueError(f"{path}: env.{key} is reserved for labnotes")
|
|
103
100
|
if not isinstance(value, str):
|
|
104
101
|
raise ValueError(f"{path}: env.{key} must be a string")
|
|
105
|
-
|
|
102
|
+
if "continues" not in values:
|
|
103
|
+
raise ValueError(f"{path}: {MISSING_CONTINUES}")
|
|
104
|
+
continues = values["continues"]
|
|
106
105
|
if not isinstance(continues, bool):
|
|
107
106
|
raise ValueError(f"{path}: continues must be true or false")
|
|
108
|
-
if continues and mode != "matrix":
|
|
109
|
-
raise ValueError(f'{path}: continues needs mode = "matrix"')
|
|
110
107
|
views = values.get("views", {})
|
|
111
108
|
if not isinstance(views, dict):
|
|
112
109
|
raise ValueError(f"{path}: views must be a table, like [views]")
|
|
@@ -122,7 +119,6 @@ def read_experiment_config(
|
|
|
122
119
|
params,
|
|
123
120
|
name,
|
|
124
121
|
counts,
|
|
125
|
-
mode,
|
|
126
122
|
concurrency,
|
|
127
123
|
retries,
|
|
128
124
|
meta,
|