labnotes 0.2.3__tar.gz → 0.2.4__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.3 → labnotes-0.2.4}/PKG-INFO +1 -1
- labnotes-0.2.4/src/labnotes/latest_log.py +6 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/models.py +1 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/run_output.py +6 -1
- labnotes-0.2.4/src/labnotes/runner/claim_run.py +15 -0
- labnotes-0.2.4/src/labnotes/runner/continue_run.py +21 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/describe_event.py +2 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/execute_run.py +30 -5
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/finalize_lost_runs.py +12 -3
- labnotes-0.2.4/src/labnotes/runner/first_invocation.py +13 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/read_experiment_config.py +7 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/run_experiment.py +48 -7
- labnotes-0.2.4/src/labnotes/runner/run_locked.py +12 -0
- labnotes-0.2.4/src/labnotes/tables/list_resumable_runs.py +16 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/open_tables.py +3 -3
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_log.py +7 -3
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_run.py +6 -2
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/run_app.py +5 -1
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/run_output.py +7 -1
- {labnotes-0.2.3 → labnotes-0.2.4}/.gitignore +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/CHANGELOG.md +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/MIGRATIONS.md +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/README.md +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/pyproject.toml +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/__init__.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/__main__.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/cli.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/context.md +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/create_app.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/create_note.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/data_html.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/experiments.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/format_started.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/format_value.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/get_state.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/grid_cell.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/init.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/is_non_negative_number.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/load_config.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/load_run.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/log.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/log_path.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/memory_interval.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/modes.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/note_path.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/output_html.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/parse_subset.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/pivot_runs.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/read_config.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/read_path.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/retries.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/cgroup_memory.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/check_args.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/check_hook_placeholders.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/check_subset.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/command_flags.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/container_cgroup.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/container_memory.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/docker_output.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/exit_signal.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/fill_dirs.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/fill_experiment_env.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/flag_name.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/group_args.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/in_subset.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/invocation_hook.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/invocation_meta.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/override_flags.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/placeholders.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/project_snapshot.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/record_memory.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/run_git.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runs_view_command.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/save_note.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/server.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/set_state.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/state_path.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_memory.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_run_file.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/runs_columns.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/check_view_response.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/config.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/format_cell.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/native_rows.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/run_screen.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/runs_view.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/view_column.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/view_process.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/__init__.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/format_bytes.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/process_alive.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/run_usage.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/tree_rss.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/tree_usage.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/view/__init__.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/view/answer.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/view/build.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/view/respond.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/view/serve.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/testing-conventions.toml +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/tests/conftest.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/tests/e2e/conftest.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/tests/integration/conftest.py +0 -0
- {labnotes-0.2.3 → labnotes-0.2.4}/uv.lock +0 -0
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
from dirsql import DirSQL
|
|
7
7
|
|
|
8
|
+
from labnotes.latest_log import latest_log
|
|
8
9
|
from labnotes.load_run import load_run
|
|
9
10
|
from labnotes.tables.read_log import STREAMS
|
|
10
11
|
|
|
@@ -19,6 +20,7 @@ async def run_output(
|
|
|
19
20
|
return
|
|
20
21
|
folder = root / str(run["folder"])
|
|
21
22
|
offsets = dict.fromkeys(STREAMS, 0)
|
|
23
|
+
paths: dict[str, Path] = {}
|
|
22
24
|
decoders = {
|
|
23
25
|
stream: codecs.getincrementaldecoder("utf-8")("replace") for stream in STREAMS
|
|
24
26
|
}
|
|
@@ -28,9 +30,12 @@ async def run_output(
|
|
|
28
30
|
def read(streams: Iterable[str]) -> Iterator[str]:
|
|
29
31
|
nonlocal last, mid_line
|
|
30
32
|
for stream in streams:
|
|
31
|
-
path = folder
|
|
33
|
+
path = latest_log(folder, stream)
|
|
32
34
|
if not path.exists():
|
|
33
35
|
continue
|
|
36
|
+
if paths.setdefault(stream, path) != path:
|
|
37
|
+
paths[stream] = path
|
|
38
|
+
offsets[stream] = 0
|
|
34
39
|
with path.open("rb") as log:
|
|
35
40
|
log.seek(offsets[stream])
|
|
36
41
|
data = log.read()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fcntl
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
LOCK_FILE = "run.lock"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def claim_run(folder: Path) -> int | None:
|
|
9
|
+
lock = os.open(folder / LOCK_FILE, os.O_RDWR | os.O_CREAT, 0o644)
|
|
10
|
+
try:
|
|
11
|
+
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
12
|
+
except BlockingIOError:
|
|
13
|
+
os.close(lock)
|
|
14
|
+
return None
|
|
15
|
+
return lock
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from labnotes.runner.first_invocation import first_invocation
|
|
2
|
+
|
|
3
|
+
# Per-invocation keys that a later invocation may leave unset.
|
|
4
|
+
UNSET = {"args", "meta", "pid", "pid_created", "retry_of"}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def continue_run(
|
|
8
|
+
previous: dict[str, object], run: dict[str, object], invocation: dict[str, object]
|
|
9
|
+
) -> dict[str, object]:
|
|
10
|
+
invocations = previous.get("invocations")
|
|
11
|
+
if not isinstance(invocations, list):
|
|
12
|
+
invocations = [] if not previous else [first_invocation(previous)]
|
|
13
|
+
kept = {key: value for key, value in previous.items() if key not in UNSET}
|
|
14
|
+
return (
|
|
15
|
+
kept
|
|
16
|
+
| run
|
|
17
|
+
| {
|
|
18
|
+
"created": previous.get("created", run["created"]),
|
|
19
|
+
"invocations": [*invocations, invocation],
|
|
20
|
+
}
|
|
21
|
+
)
|
|
@@ -18,6 +18,8 @@ def describe_event(event: dict[str, object]) -> str:
|
|
|
18
18
|
return f"Stopping {event['run']}."
|
|
19
19
|
case "killing run":
|
|
20
20
|
return f"Killing {event['run']}."
|
|
21
|
+
case "retried" if event["retry"] == event["run"]:
|
|
22
|
+
return f"{event['run']} retried in its folder."
|
|
21
23
|
case "retried":
|
|
22
24
|
return f"{event['run']} retried as {event['retry']}."
|
|
23
25
|
case "retrying":
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import contextlib
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
import time
|
|
5
6
|
from collections.abc import Callable, Sequence
|
|
7
|
+
from datetime import UTC, datetime
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
|
|
8
10
|
import psutil
|
|
9
11
|
|
|
10
12
|
from labnotes.log_path import LOG_ENV, RUN_ENV
|
|
13
|
+
from labnotes.runner.continue_run import continue_run
|
|
11
14
|
from labnotes.runner.exit_signal import exit_signal
|
|
12
15
|
from labnotes.runner.record_memory import record_memory
|
|
13
16
|
from labnotes.runner.run_git import run_git
|
|
@@ -39,8 +42,10 @@ async def execute_run(
|
|
|
39
42
|
memory_interval: float,
|
|
40
43
|
environment: dict[str, str] | None = None,
|
|
41
44
|
git_info: dict[str, str | bool] | None = None,
|
|
45
|
+
continues: bool = False,
|
|
42
46
|
) -> dict[str, object]:
|
|
43
47
|
resolved = shell_command(command, folder, params, args)
|
|
48
|
+
git = git_info if git_info is not None else await asyncio.to_thread(run_git, root)
|
|
44
49
|
run: dict[str, object] = {
|
|
45
50
|
"params": params,
|
|
46
51
|
"command": resolved,
|
|
@@ -50,9 +55,7 @@ async def execute_run(
|
|
|
50
55
|
"error": None,
|
|
51
56
|
"ended_by": None,
|
|
52
57
|
"signal": None,
|
|
53
|
-
"git":
|
|
54
|
-
if git_info is not None
|
|
55
|
-
else await asyncio.to_thread(run_git, root),
|
|
58
|
+
"git": git,
|
|
56
59
|
}
|
|
57
60
|
if args:
|
|
58
61
|
run["args"] = list(args)
|
|
@@ -60,14 +63,32 @@ async def execute_run(
|
|
|
60
63
|
run["retry_of"] = retry_of
|
|
61
64
|
if meta is not None:
|
|
62
65
|
run["meta"] = meta
|
|
66
|
+
logs = {stream: f"{stream}.log" for stream in ("stdout", "stderr")}
|
|
67
|
+
invocation: dict[str, object] | None = None
|
|
68
|
+
if continues:
|
|
69
|
+
stamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S-%f")
|
|
70
|
+
logs = {stream: f"{stream}-{stamp}.txt" for stream in logs}
|
|
71
|
+
invocation = {
|
|
72
|
+
"started": run["created"],
|
|
73
|
+
"ended": None,
|
|
74
|
+
"exit_code": None,
|
|
75
|
+
"ended_by": None,
|
|
76
|
+
"error": None,
|
|
77
|
+
"git": None if git is None else git["commit"],
|
|
78
|
+
"args": list(args),
|
|
79
|
+
**logs,
|
|
80
|
+
}
|
|
81
|
+
record = folder / "run.json"
|
|
82
|
+
previous = json.loads(record.read_text()) if record.exists() else {}
|
|
83
|
+
run = continue_run(previous, run, invocation)
|
|
63
84
|
write_run_json(folder, run)
|
|
64
85
|
run_environment = os.environ if environment is None else environment
|
|
65
86
|
botbox_labels = f"labnotes.run={folder.resolve()}"
|
|
66
87
|
if existing_labels := run_environment.get("BOTBOX_LABELS"):
|
|
67
88
|
botbox_labels = f"{existing_labels}\n{botbox_labels}"
|
|
68
89
|
with (
|
|
69
|
-
(folder / "stdout
|
|
70
|
-
(folder / "stderr
|
|
90
|
+
(folder / logs["stdout"]).open("wb") as stdout,
|
|
91
|
+
(folder / logs["stderr"]).open("wb") as stderr,
|
|
71
92
|
):
|
|
72
93
|
try:
|
|
73
94
|
process = await asyncio.create_subprocess_shell(
|
|
@@ -138,5 +159,9 @@ async def execute_run(
|
|
|
138
159
|
await asyncio.wait({sampler})
|
|
139
160
|
running.discard(process)
|
|
140
161
|
run["ended"] = iso_time(time.time())
|
|
162
|
+
if invocation is not None:
|
|
163
|
+
invocation |= {
|
|
164
|
+
key: run[key] for key in ("ended", "exit_code", "ended_by", "error")
|
|
165
|
+
}
|
|
141
166
|
write_run_json(folder, run)
|
|
142
167
|
return run
|
|
@@ -3,6 +3,8 @@ import time
|
|
|
3
3
|
|
|
4
4
|
from labnotes.list_runs import UNFINISHED
|
|
5
5
|
from labnotes.models import LabnotesConfig
|
|
6
|
+
from labnotes.runner.claim_run import LOCK_FILE
|
|
7
|
+
from labnotes.runner.run_locked import run_locked
|
|
6
8
|
from labnotes.runner.write_run_json import write_run_json
|
|
7
9
|
from labnotes.tables.iso_time import iso_time
|
|
8
10
|
from labnotes.tables.open_tables import open_tables
|
|
@@ -15,10 +17,17 @@ async def finalize_lost_runs(config: LabnotesConfig) -> None:
|
|
|
15
17
|
)
|
|
16
18
|
for row in rows:
|
|
17
19
|
run = json.loads(str(row["run"]))
|
|
20
|
+
folder = config.root / str(row["folder"])
|
|
18
21
|
pid, created = run.get("pid"), run.get("pid_created")
|
|
19
|
-
if
|
|
22
|
+
if (folder / LOCK_FILE).exists():
|
|
23
|
+
if run_locked(folder):
|
|
24
|
+
continue
|
|
25
|
+
elif isinstance(pid, int) and process_alive(
|
|
20
26
|
pid, created if isinstance(created, int | float) else None
|
|
21
27
|
):
|
|
22
28
|
continue
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
lost = {"ended": iso_time(time.time()), "ended_by": "lost"}
|
|
30
|
+
run |= lost
|
|
31
|
+
if invocations := run.get("invocations"):
|
|
32
|
+
invocations[-1] |= lost
|
|
33
|
+
write_run_json(folder, run)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
def first_invocation(run: dict[str, object]) -> dict[str, object]:
|
|
2
|
+
git = run.get("git")
|
|
3
|
+
return {
|
|
4
|
+
"started": run.get("created"),
|
|
5
|
+
"ended": run.get("ended"),
|
|
6
|
+
"exit_code": run.get("exit_code"),
|
|
7
|
+
"ended_by": run.get("ended_by"),
|
|
8
|
+
"error": run.get("error"),
|
|
9
|
+
"git": git.get("commit") if isinstance(git, dict) else None,
|
|
10
|
+
"args": run.get("args", []),
|
|
11
|
+
"stdout": "stdout.log",
|
|
12
|
+
"stderr": "stderr.log",
|
|
13
|
+
}
|
|
@@ -27,6 +27,7 @@ SETTINGS = {
|
|
|
27
27
|
"setup",
|
|
28
28
|
"teardown",
|
|
29
29
|
"env",
|
|
30
|
+
"continues",
|
|
30
31
|
}
|
|
31
32
|
HOOKS = ("setup", "teardown")
|
|
32
33
|
VIEWS = {"runs"}
|
|
@@ -97,6 +98,11 @@ def read_experiment_config(
|
|
|
97
98
|
raise ValueError(f"{path}: env.{key} is reserved for labnotes")
|
|
98
99
|
if not isinstance(value, str):
|
|
99
100
|
raise ValueError(f"{path}: env.{key} must be a string")
|
|
101
|
+
continues = values.get("continues", False)
|
|
102
|
+
if not isinstance(continues, bool):
|
|
103
|
+
raise ValueError(f"{path}: continues must be true or false")
|
|
104
|
+
if continues and mode != "matrix":
|
|
105
|
+
raise ValueError(f'{path}: continues needs mode = "matrix"')
|
|
100
106
|
views = values.get("views", {})
|
|
101
107
|
if not isinstance(views, dict):
|
|
102
108
|
raise ValueError(f"{path}: views must be a table, like [views]")
|
|
@@ -120,4 +126,5 @@ def read_experiment_config(
|
|
|
120
126
|
values.get("setup"),
|
|
121
127
|
values.get("teardown"),
|
|
122
128
|
env,
|
|
129
|
+
continues,
|
|
123
130
|
)
|
|
@@ -17,6 +17,7 @@ from labnotes.models import (
|
|
|
17
17
|
)
|
|
18
18
|
from labnotes.retries import RETRY_EXIT_CODE, RETRY_SECONDS
|
|
19
19
|
from labnotes.runner.check_hook_placeholders import check_hook_placeholders
|
|
20
|
+
from labnotes.runner.claim_run import claim_run
|
|
20
21
|
from labnotes.runner.execute_run import execute_run
|
|
21
22
|
from labnotes.runner.fill_experiment_env import fill_experiment_env
|
|
22
23
|
from labnotes.runner.fill_dirs import fill_dirs
|
|
@@ -33,6 +34,7 @@ from labnotes.runner.signal_tree import signal_tree
|
|
|
33
34
|
from labnotes.state_path import STATE_ENV
|
|
34
35
|
from labnotes.tables.experiment_run_ids import experiment_run_ids
|
|
35
36
|
from labnotes.tables.list_experiment_runs import list_experiment_runs
|
|
37
|
+
from labnotes.tables.list_resumable_runs import list_resumable_runs
|
|
36
38
|
from labnotes.tables.open_tables import open_tables
|
|
37
39
|
|
|
38
40
|
Entry = tuple[str, dict[str, object]]
|
|
@@ -82,9 +84,37 @@ async def run_experiment(
|
|
|
82
84
|
params, experiment.params, lambda run: taken(f"{group}/{run}")
|
|
83
85
|
)
|
|
84
86
|
|
|
87
|
+
locks: dict[str, int] = {}
|
|
88
|
+
|
|
89
|
+
def claim(run: str) -> bool:
|
|
90
|
+
if run not in locks:
|
|
91
|
+
lock = claim_run(run_folder(config.root, config.runs, name, run))
|
|
92
|
+
if lock is None:
|
|
93
|
+
return False
|
|
94
|
+
locks[run] = lock
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
def release(run: str) -> None:
|
|
98
|
+
if run in locks:
|
|
99
|
+
os.close(locks.pop(run))
|
|
100
|
+
|
|
101
|
+
resumable: list[Entry] = []
|
|
102
|
+
if experiment.continues:
|
|
103
|
+
resumable = [
|
|
104
|
+
(str(row["id"]), json.loads(str(row["params"])))
|
|
105
|
+
for row in await list_resumable_runs(tables, name, experiment.counts)
|
|
106
|
+
]
|
|
85
107
|
plan: list[Entry] = []
|
|
86
108
|
for params in cells:
|
|
87
|
-
|
|
109
|
+
resumed = next(
|
|
110
|
+
(
|
|
111
|
+
entry
|
|
112
|
+
for entry in resumable
|
|
113
|
+
if entry[1] == params and entry[0] not in planned and claim(entry[0])
|
|
114
|
+
),
|
|
115
|
+
None,
|
|
116
|
+
)
|
|
117
|
+
run = new_id(params) if resumed is None else resumed[0]
|
|
88
118
|
planned.add(run)
|
|
89
119
|
plan.append((run, params))
|
|
90
120
|
run_dirs = [
|
|
@@ -129,7 +159,7 @@ async def run_experiment(
|
|
|
129
159
|
running: dict[str, set[asyncio.subprocess.Process]] = {}
|
|
130
160
|
trees: dict[str, set[psutil.Process]] = {}
|
|
131
161
|
stopped: set[str] = set()
|
|
132
|
-
results:
|
|
162
|
+
results: dict[str, dict[str, object]] = {}
|
|
133
163
|
stopping = asyncio.Event()
|
|
134
164
|
|
|
135
165
|
def signal_run(run: str, signal_number: int) -> None:
|
|
@@ -175,9 +205,11 @@ async def run_experiment(
|
|
|
175
205
|
waiting: dict[str, asyncio.TimerHandle] = {}
|
|
176
206
|
|
|
177
207
|
def requeue(run: str, attempt: int) -> None:
|
|
178
|
-
again =
|
|
179
|
-
|
|
180
|
-
|
|
208
|
+
again = run
|
|
209
|
+
if not experiment.continues:
|
|
210
|
+
again = new_id(ended[run])
|
|
211
|
+
planned.add(again)
|
|
212
|
+
parents[again] = run
|
|
181
213
|
attempts[again] = attempt
|
|
182
214
|
plan.append((again, ended[run]))
|
|
183
215
|
report({"event": "retried", "run": run, "retry": again, "params": ended[run]})
|
|
@@ -207,6 +239,7 @@ async def run_experiment(
|
|
|
207
239
|
|
|
208
240
|
def cancel_retry(run: str) -> None:
|
|
209
241
|
waiting.pop(run).cancel()
|
|
242
|
+
release(run)
|
|
210
243
|
report({"event": "retry cancelled", "run": run})
|
|
211
244
|
|
|
212
245
|
limit = concurrency
|
|
@@ -245,6 +278,8 @@ async def run_experiment(
|
|
|
245
278
|
async def start(run: str, params: dict[str, object]) -> None:
|
|
246
279
|
folder = run_folder(config.root, config.runs, name, run)
|
|
247
280
|
folder.mkdir(parents=True, exist_ok=True)
|
|
281
|
+
if experiment.continues:
|
|
282
|
+
claim(run)
|
|
248
283
|
if snapshot is not None and snapshot.sha is not None:
|
|
249
284
|
(folder / "git-sha.txt").write_text(snapshot.sha + "\n")
|
|
250
285
|
(folder / "git-diff.patch").write_bytes(snapshot.diff or b"")
|
|
@@ -287,15 +322,18 @@ async def run_experiment(
|
|
|
287
322
|
if snapshot is not None and snapshot.sha is not None
|
|
288
323
|
else None
|
|
289
324
|
),
|
|
325
|
+
continues=experiment.continues,
|
|
290
326
|
)
|
|
291
327
|
del running[run], trees[run]
|
|
292
328
|
report({"event": "ended", "run": run, **result})
|
|
293
329
|
ended[run] = params
|
|
294
|
-
results
|
|
330
|
+
results[run] = result
|
|
295
331
|
if result["exit_code"] == RETRY_EXIT_CODE and not (
|
|
296
332
|
stopping.is_set() or run in stopped
|
|
297
333
|
):
|
|
298
334
|
retry_later(run)
|
|
335
|
+
if run not in waiting:
|
|
336
|
+
release(run)
|
|
299
337
|
|
|
300
338
|
async def teardown() -> None:
|
|
301
339
|
if experiment.teardown is not None:
|
|
@@ -336,5 +374,8 @@ async def run_experiment(
|
|
|
336
374
|
except Exception as failure: # noqa: BLE001
|
|
337
375
|
report({"event": "teardown failed", "error": str(failure)})
|
|
338
376
|
raise
|
|
377
|
+
finally:
|
|
378
|
+
for run in list(locks):
|
|
379
|
+
release(run)
|
|
339
380
|
await teardown()
|
|
340
|
-
return results
|
|
381
|
+
return list(results.values())
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dirsql import DirSQL
|
|
2
|
+
|
|
3
|
+
from labnotes.list_runs import UNFINISHED
|
|
4
|
+
from labnotes.sql_quote import quote_string
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def list_resumable_runs(
|
|
8
|
+
tables: DirSQL, name: str, counts: str
|
|
9
|
+
) -> list[dict[str, object]]:
|
|
10
|
+
# Ended badly: a nonzero or missing exit code, not merely uncounted.
|
|
11
|
+
return await tables.query(
|
|
12
|
+
f"SELECT id, params FROM runs WHERE experiment = {quote_string(name)}"
|
|
13
|
+
f" AND NOT coalesce(({counts}), false) AND NOT {UNFINISHED}"
|
|
14
|
+
" AND (exit_code IS NULL OR exit_code <> 0)"
|
|
15
|
+
" AND NOT coalesce(run ->> 'excluded', false) ORDER BY created, id"
|
|
16
|
+
)
|
|
@@ -31,8 +31,8 @@ CREATE TABLE runs (
|
|
|
31
31
|
# Sizes only: the server reads log text from disk, the row change is the signal.
|
|
32
32
|
LOGS = """
|
|
33
33
|
CREATE TABLE logs (
|
|
34
|
-
experiment TEXT, run TEXT, stream TEXT, size INTEGER,
|
|
35
|
-
PRIMARY KEY (experiment, run,
|
|
34
|
+
experiment TEXT, run TEXT, stream TEXT, file TEXT, size INTEGER,
|
|
35
|
+
PRIMARY KEY (experiment, run, file)
|
|
36
36
|
)
|
|
37
37
|
"""
|
|
38
38
|
BLOCKS = """
|
|
@@ -90,7 +90,7 @@ def open_tables(config: LabnotesConfig, run_files: bool = False) -> DirSQL:
|
|
|
90
90
|
Table(
|
|
91
91
|
name="logs",
|
|
92
92
|
ddl=LOGS,
|
|
93
|
-
glob=path_glob(f"{config.runs}/std
|
|
93
|
+
glob=path_glob(f"{config.runs}/std*"),
|
|
94
94
|
on_file=partial(
|
|
95
95
|
read_log, root, config.runs, experiment=config.experiment
|
|
96
96
|
),
|
|
@@ -1,22 +1,26 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
from labnotes.tables.path_pattern import path_pattern
|
|
4
5
|
|
|
5
6
|
STREAMS = ("stdout", "stderr")
|
|
7
|
+
LOG_NAME = re.compile(r"(?P<stream>[^/.-]+)(?:\.log|-[^/]+\.txt)")
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
def read_log(
|
|
9
11
|
root: Path, template: str, path: str, experiment: str | None = None
|
|
10
12
|
) -> list[dict[str, object]]:
|
|
11
13
|
relative = Path(path).relative_to(root).as_posix()
|
|
12
|
-
found = path_pattern(f"{template}/{{
|
|
13
|
-
if found is None
|
|
14
|
+
found = path_pattern(f"{template}/{{log}}").fullmatch(relative)
|
|
15
|
+
name = None if found is None else LOG_NAME.fullmatch(found["log"])
|
|
16
|
+
if found is None or name is None or name["stream"] not in STREAMS:
|
|
14
17
|
return []
|
|
15
18
|
return [
|
|
16
19
|
{
|
|
17
20
|
"experiment": experiment or found["experiment"],
|
|
18
21
|
"run": found["run"],
|
|
19
|
-
"stream":
|
|
22
|
+
"stream": name["stream"],
|
|
23
|
+
"file": found["log"],
|
|
20
24
|
"size": Path(path).stat().st_size,
|
|
21
25
|
}
|
|
22
26
|
]
|
|
@@ -13,6 +13,10 @@ def read_run(
|
|
|
13
13
|
return []
|
|
14
14
|
run = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
15
15
|
folder = relative.removesuffix("/run.json")
|
|
16
|
+
logs = {"stdout": "stdout.log", "stderr": "stderr.log"}
|
|
17
|
+
match run.get("invocations"):
|
|
18
|
+
case [*_, {"stdout": str() as stdout, "stderr": str() as stderr}]:
|
|
19
|
+
logs = {"stdout": stdout, "stderr": stderr}
|
|
16
20
|
return [
|
|
17
21
|
{
|
|
18
22
|
"experiment": experiment or found["experiment"],
|
|
@@ -22,8 +26,8 @@ def read_run(
|
|
|
22
26
|
"ended": run.get("ended"),
|
|
23
27
|
"exit_code": run.get("exit_code"),
|
|
24
28
|
"error": run.get("error"),
|
|
25
|
-
"stdout": f"{folder}/stdout
|
|
26
|
-
"stderr": f"{folder}/stderr
|
|
29
|
+
"stdout": f"{folder}/{logs['stdout']}",
|
|
30
|
+
"stderr": f"{folder}/{logs['stderr']}",
|
|
27
31
|
"params": json.dumps(run.get("params", {})),
|
|
28
32
|
"meta": None if (meta := run.get("meta")) is None else json.dumps(meta),
|
|
29
33
|
"command": run.get("command"),
|
|
@@ -164,8 +164,12 @@ class RunApp(App[None]):
|
|
|
164
164
|
if self.waiting.pop(run, None) is not None:
|
|
165
165
|
outcome = f"{FAILED} exited {RETRY_EXIT_CODE}, retried"
|
|
166
166
|
self.set_cell(run, "status", Text(outcome, style="red"))
|
|
167
|
-
self.retried.add(run)
|
|
168
167
|
again = str(event["retry"])
|
|
168
|
+
if again == run:
|
|
169
|
+
self.failed.discard(run)
|
|
170
|
+
self.ended.discard(run)
|
|
171
|
+
else:
|
|
172
|
+
self.retried.add(run)
|
|
169
173
|
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
170
174
|
self.cells[again] = self.waiting_cells(again, args)
|
|
171
175
|
self.pending.add(again)
|
|
@@ -6,6 +6,8 @@ from rich.text import Text
|
|
|
6
6
|
from textual.geometry import Size
|
|
7
7
|
from textual.widgets import RichLog
|
|
8
8
|
|
|
9
|
+
from labnotes.latest_log import latest_log
|
|
10
|
+
|
|
9
11
|
STREAMS = ("stdout", "stderr")
|
|
10
12
|
|
|
11
13
|
|
|
@@ -16,6 +18,7 @@ class RunOutput(RichLog):
|
|
|
16
18
|
|
|
17
19
|
def reset(self) -> None:
|
|
18
20
|
self.offsets = dict.fromkeys(STREAMS, 0)
|
|
21
|
+
self.paths: dict[str, Path] = {}
|
|
19
22
|
self.decoders = {
|
|
20
23
|
stream: codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
21
24
|
for stream in STREAMS
|
|
@@ -34,9 +37,12 @@ class RunOutput(RichLog):
|
|
|
34
37
|
|
|
35
38
|
def follow(self, folder: Path) -> None:
|
|
36
39
|
for stream in STREAMS:
|
|
37
|
-
path = folder
|
|
40
|
+
path = latest_log(folder, stream)
|
|
38
41
|
if not path.exists():
|
|
39
42
|
continue
|
|
43
|
+
if self.paths.setdefault(stream, path) != path:
|
|
44
|
+
self.paths[stream] = path
|
|
45
|
+
self.offsets[stream] = 0
|
|
40
46
|
with path.open("rb") as log:
|
|
41
47
|
log.seek(self.offsets[stream])
|
|
42
48
|
data = log.read()
|
|
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
|