labnotes 0.0.106__tar.gz → 0.0.108__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.106 → labnotes-0.0.108}/PKG-INFO +3 -2
- {labnotes-0.0.106 → labnotes-0.0.108}/README.md +2 -1
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/context.md +5 -3
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/describe_status.py +5 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/watch_daemon.py +21 -2
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/experiments.py +8 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/execute_run.py +5 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/experiment_rows.py +6 -1
- labnotes-0.0.108/src/labnotes/tui/config.py +1 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/run_app.py +58 -20
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/run_tui.py +2 -1
- labnotes-0.0.108/src/labnotes/utils/__init__.py +0 -0
- labnotes-0.0.108/src/labnotes/utils/format_bytes.py +5 -0
- labnotes-0.0.108/src/labnotes/utils/run_usage.py +31 -0
- labnotes-0.0.108/src/labnotes/utils/tree_rss.py +27 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/testing-conventions.toml +6 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/.gitignore +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/CHANGELOG.md +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/MIGRATIONS.md +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/pyproject.toml +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/compose_experiment.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/daemon_app.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/remote_start.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/init.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/is_non_negative_int.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/log.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/log_path.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/models.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/retries.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/exit_signal.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/server.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/experiment_toml.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/experiments_ddl.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/read_blocks.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/log_block.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/log_blocks.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/run_screen.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.106 → labnotes-0.0.108}/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.108
|
|
4
4
|
Summary: Browse experiment designs, notes, and run data from a folder.
|
|
5
5
|
Project-URL: Repository, https://github.com/thekevinscott/labnotes
|
|
6
6
|
Author-email: Kevin Scott <me@thekevinscott.com>
|
|
@@ -122,7 +122,8 @@ line per event instead.
|
|
|
122
122
|
- With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) leaves them
|
|
123
123
|
running. `s` stops the running scripts and records their end, then kills any
|
|
124
124
|
still going 5 seconds later. Escape goes back. SIGTERM stops them like `s`.
|
|
125
|
-
- A second `run` of the same experiment fails
|
|
125
|
+
- A second `run` of the same experiment fails with "already running" while the
|
|
126
|
+
daemon still has the first going.
|
|
126
127
|
- It exits 0 even if runs fail; the summary line counts failures. If it can't
|
|
127
128
|
start or reach the daemon, it prints the cause on one line and exits 1.
|
|
128
129
|
|
|
@@ -98,7 +98,8 @@ line per event instead.
|
|
|
98
98
|
- With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) leaves them
|
|
99
99
|
running. `s` stops the running scripts and records their end, then kills any
|
|
100
100
|
still going 5 seconds later. Escape goes back. SIGTERM stops them like `s`.
|
|
101
|
-
- A second `run` of the same experiment fails
|
|
101
|
+
- A second `run` of the same experiment fails with "already running" while the
|
|
102
|
+
daemon still has the first going.
|
|
102
103
|
- It exits 0 even if runs fail; the summary line counts failures. If it can't
|
|
103
104
|
start or reach the daemon, it prints the cause on one line and exits 1.
|
|
104
105
|
|
|
@@ -112,9 +112,10 @@ shows a waiting retry as `retry in 30s` and the end as `gave up (exit 75)`;
|
|
|
112
112
|
`run status --json` reports `retrying`, `retried`, `gave up` and
|
|
113
113
|
`retry cancelled` events.
|
|
114
114
|
|
|
115
|
-
Runs can take a long time. Start `run` in the background, then check on it with `labnotes experiments foo runs --format json`; a run with no `ended` is still going.
|
|
115
|
+
Runs can take a long time. Start `run` in the background, then check on it with `labnotes experiments foo runs --format json`; a run with no `ended` is still going. Each run lists `size`, the bytes in its run folder, and a running run lists `memory`, the resident bytes of its command and everything it started.
|
|
116
116
|
|
|
117
|
-
In a terminal, `run` shows a live table of runs (status
|
|
117
|
+
In a terminal, `run` shows a live table of runs (status, time, memory and
|
|
118
|
+
folder size) and stays
|
|
118
119
|
open when they finish. Failed runs show their status in red; `f` hides or shows them, and `r` on one
|
|
119
120
|
runs it again under a new id, with `retry_of` set. `j` and `k` move the cursor, `g` and `G` jump to the
|
|
120
121
|
first and last row. Runs not started yet show grayed as `pending`, and as
|
|
@@ -150,7 +151,8 @@ going. Elsewhere, it prints a line per event instead.
|
|
|
150
151
|
- `labnotes run stop` stops every run the daemon has going: SIGTERM, then
|
|
151
152
|
SIGKILL 5 seconds later.
|
|
152
153
|
- `labnotes run status` prints what the daemon has going, a line per event so
|
|
153
|
-
far plus the latest line each running run logged
|
|
154
|
+
far plus the latest line each running run logged and what each running run
|
|
155
|
+
uses (`usage` events with `--json`). `-f` keeps following, and
|
|
154
156
|
waits for a daemon when none is up. `--json` prints one JSON object per line.
|
|
155
157
|
Following doesn't keep the daemon up.
|
|
156
158
|
- `labnotes run control NAME ACTION [ARG]` sends one live-view control to a
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from labnotes.utils.format_bytes import format_bytes
|
|
1
2
|
from labnotes.runner.describe_event import describe_event
|
|
2
3
|
|
|
3
4
|
|
|
@@ -5,6 +6,10 @@ def describe_status(event: dict[str, object]) -> str:
|
|
|
5
6
|
match event:
|
|
6
7
|
case {"event": "output"}:
|
|
7
8
|
said = f"{event['run']}: {event['line']}"
|
|
9
|
+
case {"event": "usage"}:
|
|
10
|
+
memory = format_bytes(event["memory"]) or "?" # type: ignore[arg-type]
|
|
11
|
+
size = format_bytes(event["size"]) or "?" # type: ignore[arg-type]
|
|
12
|
+
said = f"{event['run']} uses {memory} of memory, {size} on disk."
|
|
8
13
|
case {"event": "done", "error": None}:
|
|
9
14
|
said = "Done."
|
|
10
15
|
case {"event": "done"}:
|
|
@@ -6,9 +6,11 @@ import httpx2
|
|
|
6
6
|
|
|
7
7
|
from labnotes.daemon.daemon_client import daemon_client
|
|
8
8
|
from labnotes.daemon.run_logs import RunLogs
|
|
9
|
+
from labnotes.utils.run_usage import run_usage
|
|
9
10
|
|
|
10
11
|
Event = dict[str, object]
|
|
11
12
|
TAIL_SECONDS = 1.0
|
|
13
|
+
USAGE_SECONDS = 30.0
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
async def watch_daemon(root: Path, follow: bool, emit: Callable[[Event], None]) -> None:
|
|
@@ -23,7 +25,21 @@ async def watch_daemon(root: Path, follow: bool, emit: Callable[[Event], None])
|
|
|
23
25
|
await asyncio.sleep(TAIL_SECONDS)
|
|
24
26
|
tail()
|
|
25
27
|
|
|
28
|
+
async def sample() -> None:
|
|
29
|
+
for (experiment, run), folder in list(logs.folders.items()):
|
|
30
|
+
if (experiment, run) not in logs.folders:
|
|
31
|
+
continue
|
|
32
|
+
usage = await run_usage(folder, True)
|
|
33
|
+
if (experiment, run) in logs.folders:
|
|
34
|
+
emit({"event": "usage", "experiment": experiment, "run": run, **usage})
|
|
35
|
+
|
|
36
|
+
async def keep_sampling() -> None:
|
|
37
|
+
while True:
|
|
38
|
+
await asyncio.sleep(USAGE_SECONDS)
|
|
39
|
+
await sample()
|
|
40
|
+
|
|
26
41
|
tailing = asyncio.create_task(keep_tailing()) if follow else None
|
|
42
|
+
sampling = None
|
|
27
43
|
try:
|
|
28
44
|
async with (
|
|
29
45
|
daemon_client(root) as client,
|
|
@@ -36,8 +52,10 @@ async def watch_daemon(root: Path, follow: bool, emit: Callable[[Event], None])
|
|
|
36
52
|
match event:
|
|
37
53
|
case {"event": "replayed"}:
|
|
38
54
|
tail()
|
|
55
|
+
await sample()
|
|
39
56
|
if not follow:
|
|
40
57
|
return
|
|
58
|
+
sampling = asyncio.create_task(keep_sampling())
|
|
41
59
|
continue
|
|
42
60
|
case {"event": "started", "experiment": str(name), "run": str(run)}:
|
|
43
61
|
logs.add(name, run, Path(str(event["folder"])))
|
|
@@ -48,5 +66,6 @@ async def watch_daemon(root: Path, follow: bool, emit: Callable[[Event], None])
|
|
|
48
66
|
except httpx2.TransportError:
|
|
49
67
|
pass
|
|
50
68
|
finally:
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
for task in (tailing, sampling):
|
|
70
|
+
if task is not None:
|
|
71
|
+
task.cancel()
|
|
@@ -6,6 +6,7 @@ import click
|
|
|
6
6
|
|
|
7
7
|
from labnotes.create_experiment import create_experiment
|
|
8
8
|
from labnotes.daemon.remote_start import remote_start
|
|
9
|
+
from labnotes.utils.format_bytes import format_bytes
|
|
9
10
|
from labnotes.load_config import load_config
|
|
10
11
|
from labnotes.print_rows import print_rows
|
|
11
12
|
from labnotes.read_config import CONFIG_FILE
|
|
@@ -14,6 +15,8 @@ from labnotes.tables.experiment_rows import experiment_rows
|
|
|
14
15
|
from labnotes.tui.browse_tui import browse_tui
|
|
15
16
|
from labnotes.tui.run_tui import run_tui
|
|
16
17
|
|
|
18
|
+
BYTES = ("memory", "size")
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
@click.command(
|
|
19
22
|
help="List experiments, show one, list its runs, or run it."
|
|
@@ -103,4 +106,9 @@ def experiments(
|
|
|
103
106
|
raise click.ClickException(str(error)) from error
|
|
104
107
|
if rows is None:
|
|
105
108
|
raise click.ClickException(f"No experiment named {name}.")
|
|
109
|
+
if output_format != "json":
|
|
110
|
+
rows = [
|
|
111
|
+
row | {key: format_bytes(row[key]) for key in BYTES if key in row} # type: ignore[arg-type]
|
|
112
|
+
for row in rows
|
|
113
|
+
]
|
|
106
114
|
print_rows(rows, output_format or "table")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import contextlib
|
|
2
3
|
import os
|
|
3
4
|
import time
|
|
4
5
|
from collections.abc import Callable
|
|
@@ -62,6 +63,10 @@ async def execute_run(
|
|
|
62
63
|
run["error"] = str(error)
|
|
63
64
|
else:
|
|
64
65
|
running.add(process)
|
|
66
|
+
run["pid"] = process.pid
|
|
67
|
+
with contextlib.suppress(psutil.Error):
|
|
68
|
+
run["pid_created"] = psutil.Process(process.pid).create_time()
|
|
69
|
+
write_run_json(folder, run)
|
|
65
70
|
exited = asyncio.ensure_future(process.wait())
|
|
66
71
|
kill_later = None
|
|
67
72
|
try:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
3
|
from labnotes.models import LabnotesConfig
|
|
4
|
+
from labnotes.utils.run_usage import run_usage
|
|
4
5
|
from labnotes.summarize_config import summarize_config
|
|
5
6
|
from labnotes.tables.list_experiment_runs import list_experiment_runs
|
|
6
7
|
from labnotes.tables.open_tables import open_tables
|
|
@@ -21,7 +22,11 @@ async def experiment_rows(
|
|
|
21
22
|
summary = summarize_config(
|
|
22
23
|
json.loads(str(stored)) if stored else None, config.mode
|
|
23
24
|
)
|
|
24
|
-
|
|
25
|
+
runs = await list_experiment_runs(
|
|
25
26
|
tables, name, str(summary["completed_run"])
|
|
26
27
|
)
|
|
28
|
+
for run in runs:
|
|
29
|
+
going = all(run[key] is None for key in ("ended", "exit_code", "error"))
|
|
30
|
+
run |= await run_usage(config.root / str(run["folder"]), going)
|
|
31
|
+
return runs
|
|
27
32
|
return rows
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
RUN_COLUMNS = ("run", "params", "status", "time", "memory", "size")
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import signal
|
|
3
3
|
import time
|
|
4
|
+
from collections.abc import Sequence
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from rich.text import Text
|
|
8
|
+
from textual import work
|
|
7
9
|
from textual.app import App, ComposeResult
|
|
8
10
|
from textual.binding import Binding
|
|
9
11
|
from textual.widgets import DataTable, Footer, Header, Input, Static
|
|
10
12
|
|
|
13
|
+
from labnotes.utils.format_bytes import format_bytes
|
|
11
14
|
from labnotes.models import Controls, Start
|
|
12
15
|
from labnotes.retries import RETRY_EXIT_CODE
|
|
16
|
+
from labnotes.utils.run_usage import run_usage
|
|
13
17
|
from labnotes.runner.describe_event import describe_event
|
|
14
18
|
from labnotes.runner.stop_then_kill import GRACE_SECONDS, stop_then_kill
|
|
15
19
|
from labnotes.runner.param_args import param_args
|
|
@@ -20,7 +24,6 @@ from labnotes.tui.log_blocks import LogBlocks
|
|
|
20
24
|
from labnotes.tui.run_output import RunOutput
|
|
21
25
|
from labnotes.tui.runs_table import RunsTable
|
|
22
26
|
|
|
23
|
-
COLUMNS = ("run", "params", "status", "time")
|
|
24
27
|
PENDING, PASSED, FAILED = "○", "✓", "✗"
|
|
25
28
|
SPINNER = "|/-\\"
|
|
26
29
|
SKIPPED, DESELECTED = "–", "⊘"
|
|
@@ -50,8 +53,9 @@ class RunApp(App[None]):
|
|
|
50
53
|
Binding("minus", "concurrency(-1)", "Fewer at once", key_display="-"),
|
|
51
54
|
]
|
|
52
55
|
|
|
53
|
-
def __init__(self, start: Start, title: str) -> None:
|
|
56
|
+
def __init__(self, start: Start, title: str, columns: Sequence[str]) -> None:
|
|
54
57
|
super().__init__()
|
|
58
|
+
self.columns = tuple(columns)
|
|
55
59
|
self.start = start
|
|
56
60
|
self.title = title
|
|
57
61
|
self.results: list[dict[str, object]] = []
|
|
@@ -73,7 +77,7 @@ class RunApp(App[None]):
|
|
|
73
77
|
self.frame = 0
|
|
74
78
|
self.started: dict[str, float] = {}
|
|
75
79
|
self.pending: set[str] = set()
|
|
76
|
-
self.cells: dict[str,
|
|
80
|
+
self.cells: dict[str, dict[str, object]] = {}
|
|
77
81
|
self.filter = ""
|
|
78
82
|
self.failed: set[str] = set()
|
|
79
83
|
self.retried: set[str] = set()
|
|
@@ -83,6 +87,8 @@ class RunApp(App[None]):
|
|
|
83
87
|
self.viewing: str | None = None
|
|
84
88
|
self.grabbed: str | None = None
|
|
85
89
|
self.grabbed_from: list[str] = []
|
|
90
|
+
self.sampling = False
|
|
91
|
+
self.unmeasured: dict[str, None] = {}
|
|
86
92
|
|
|
87
93
|
def compose(self) -> ComposeResult:
|
|
88
94
|
yield Header()
|
|
@@ -95,7 +101,7 @@ class RunApp(App[None]):
|
|
|
95
101
|
|
|
96
102
|
def on_mount(self) -> None:
|
|
97
103
|
table = self.query_one(DataTable)
|
|
98
|
-
for column in
|
|
104
|
+
for column in self.columns:
|
|
99
105
|
table.add_column(column, key=column)
|
|
100
106
|
self.query_one(Input).display = False
|
|
101
107
|
self.query_one(RunOutput).display = False
|
|
@@ -127,8 +133,9 @@ class RunApp(App[None]):
|
|
|
127
133
|
self.title = str(event["title"])
|
|
128
134
|
for planned in event["runs"]: # type: ignore[attr-defined]
|
|
129
135
|
args = " ".join(param_args(planned["params"]))
|
|
130
|
-
cells
|
|
131
|
-
|
|
136
|
+
self.cells[planned["run"]] = self.waiting_cells(
|
|
137
|
+
planned["run"], args
|
|
138
|
+
)
|
|
132
139
|
self.pending.add(planned["run"])
|
|
133
140
|
self.redraw(table.cursor_row)
|
|
134
141
|
self.show_concurrency(event)
|
|
@@ -145,13 +152,13 @@ class RunApp(App[None]):
|
|
|
145
152
|
if run == self.next_run:
|
|
146
153
|
self.show("")
|
|
147
154
|
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
148
|
-
|
|
149
|
-
run.rpartition("/")[2],
|
|
150
|
-
args,
|
|
151
|
-
SPINNER[self.frame],
|
|
152
|
-
format_elapsed(0),
|
|
153
|
-
|
|
154
|
-
for column, cell in
|
|
155
|
+
shown = {
|
|
156
|
+
"run": run.rpartition("/")[2],
|
|
157
|
+
"params": args,
|
|
158
|
+
"status": SPINNER[self.frame],
|
|
159
|
+
"time": format_elapsed(0),
|
|
160
|
+
}
|
|
161
|
+
for column, cell in shown.items():
|
|
155
162
|
self.set_cell(run, column, cell)
|
|
156
163
|
case "ended":
|
|
157
164
|
outcome = PASSED
|
|
@@ -166,6 +173,8 @@ class RunApp(App[None]):
|
|
|
166
173
|
self.tick()
|
|
167
174
|
del self.started[run]
|
|
168
175
|
self.redraw(table.cursor_row)
|
|
176
|
+
self.unmeasured[run] = None
|
|
177
|
+
self.measure()
|
|
169
178
|
case "queued":
|
|
170
179
|
self.reposition(event, run)
|
|
171
180
|
self.next_run = run
|
|
@@ -201,9 +210,7 @@ class RunApp(App[None]):
|
|
|
201
210
|
self.retried.add(run)
|
|
202
211
|
again = str(event["retry"])
|
|
203
212
|
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
204
|
-
self.cells[again] =
|
|
205
|
-
*map(dim, (again.rpartition("/")[2], args, PENDING, ""))
|
|
206
|
-
]
|
|
213
|
+
self.cells[again] = self.waiting_cells(again, args)
|
|
207
214
|
self.pending.add(again)
|
|
208
215
|
self.redraw(table.cursor_row)
|
|
209
216
|
self.show(describe_event(event))
|
|
@@ -244,14 +251,20 @@ class RunApp(App[None]):
|
|
|
244
251
|
table.clear()
|
|
245
252
|
for run, cells in self.cells.items():
|
|
246
253
|
hidden = self.hiding and run in self.failed
|
|
247
|
-
if self.filter in str(cells
|
|
248
|
-
table.add_row(*cells, key=run)
|
|
254
|
+
if self.filter in str(cells.get("params", "")) and not hidden:
|
|
255
|
+
table.add_row(*(cells.get(c, "") for c in self.columns), key=run)
|
|
249
256
|
table.move_cursor(row=row)
|
|
250
257
|
|
|
258
|
+
def waiting_cells(self, run: str, args: str) -> dict[str, object]:
|
|
259
|
+
shown = {"run": run.rpartition("/")[2], "params": args, "status": PENDING}
|
|
260
|
+
return {
|
|
261
|
+
column: dim(shown.get(column, "")) for column in {*self.columns, *shown}
|
|
262
|
+
}
|
|
263
|
+
|
|
251
264
|
def set_cell(self, run: str, column: str, cell: object) -> None:
|
|
252
|
-
self.cells[run][
|
|
265
|
+
self.cells[run][column] = cell
|
|
253
266
|
table = self.query_one(DataTable)
|
|
254
|
-
if run in table.rows:
|
|
267
|
+
if run in table.rows and column in self.columns:
|
|
255
268
|
table.update_cell(run, column, cell, update_width=True)
|
|
256
269
|
|
|
257
270
|
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool:
|
|
@@ -275,6 +288,31 @@ class RunApp(App[None]):
|
|
|
275
288
|
wait = format_elapsed(max(0, due - now))
|
|
276
289
|
outcome = f"{FAILED} exited {RETRY_EXIT_CODE}, retry in {wait}"
|
|
277
290
|
self.set_cell(run, "status", Text(outcome, style="yellow"))
|
|
291
|
+
if self.started:
|
|
292
|
+
self.measure()
|
|
293
|
+
|
|
294
|
+
def measure(self) -> None:
|
|
295
|
+
if not self.sampling:
|
|
296
|
+
self.sampling = True
|
|
297
|
+
self.sample()
|
|
298
|
+
|
|
299
|
+
@work
|
|
300
|
+
async def sample(self) -> None:
|
|
301
|
+
try:
|
|
302
|
+
for run in list(self.started):
|
|
303
|
+
usage = await run_usage(self.folders[run], True)
|
|
304
|
+
if run in self.started:
|
|
305
|
+
self.show_usage(run, usage)
|
|
306
|
+
while self.unmeasured:
|
|
307
|
+
run = next(iter(self.unmeasured))
|
|
308
|
+
del self.unmeasured[run]
|
|
309
|
+
self.show_usage(run, await run_usage(self.folders[run], False))
|
|
310
|
+
finally:
|
|
311
|
+
self.sampling = False
|
|
312
|
+
|
|
313
|
+
def show_usage(self, run: str, usage: dict[str, int | None]) -> None:
|
|
314
|
+
self.set_cell(run, "memory", format_bytes(usage["memory"]))
|
|
315
|
+
self.set_cell(run, "size", format_bytes(usage["size"]))
|
|
278
316
|
|
|
279
317
|
def spin(self) -> None:
|
|
280
318
|
self.frame = (self.frame + 1) % len(SPINNER)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from labnotes.models import Start
|
|
2
|
+
from labnotes.tui.config import RUN_COLUMNS
|
|
2
3
|
from labnotes.tui.run_app import RunApp
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
def run_tui(start: Start, title: str) -> list[dict[str, object]]:
|
|
6
|
-
app = RunApp(start, title)
|
|
7
|
+
app = RunApp(start, title, RUN_COLUMNS)
|
|
7
8
|
app.run()
|
|
8
9
|
if app.error is not None:
|
|
9
10
|
raise app.error
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from dirsql import DirSQL
|
|
4
|
+
|
|
5
|
+
from labnotes.utils.tree_rss import tree_rss
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def field(name: str) -> str:
|
|
9
|
+
return f"(SELECT content ->> '{name}' FROM './run.json' WHERE json_valid(content))"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
USAGE = (
|
|
13
|
+
"SELECT (SELECT SUM(size) FROM './**') AS size,"
|
|
14
|
+
f" {field('pid')} AS pid, {field('pid_created')} AS created,"
|
|
15
|
+
f" coalesce({field('ended')}, {field('exit_code')}, {field('error')}) AS done"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def run_usage(folder: Path, running: bool) -> dict[str, int | None]:
|
|
20
|
+
# Run folders are often gitignored (DVC, large outputs); size them anyway.
|
|
21
|
+
[row] = await DirSQL(str(folder), tables=[], no_ignore=True).query(USAGE)
|
|
22
|
+
pid, created = row["pid"], row["created"]
|
|
23
|
+
memory = None
|
|
24
|
+
if (
|
|
25
|
+
running
|
|
26
|
+
and row["done"] is None
|
|
27
|
+
and isinstance(pid, int)
|
|
28
|
+
and isinstance(created, int | float)
|
|
29
|
+
):
|
|
30
|
+
memory = tree_rss(pid, created)
|
|
31
|
+
return {"memory": memory, "size": row["size"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
|
|
3
|
+
import psutil
|
|
4
|
+
|
|
5
|
+
# run.json round-trips create_time through JSON, and psutil derives it from
|
|
6
|
+
# boot time, so allow a little slack when matching.
|
|
7
|
+
SAME_START_SECONDS = 0.01
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def tree_rss(pid: int, created: float) -> int | None:
|
|
11
|
+
try:
|
|
12
|
+
root = psutil.Process(pid)
|
|
13
|
+
if abs(root.create_time() - created) > SAME_START_SECONDS:
|
|
14
|
+
return None
|
|
15
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
16
|
+
return None
|
|
17
|
+
try:
|
|
18
|
+
tree = [root, *root.children(recursive=True)]
|
|
19
|
+
except psutil.NoSuchProcess:
|
|
20
|
+
return None
|
|
21
|
+
except psutil.AccessDenied:
|
|
22
|
+
tree = [root]
|
|
23
|
+
sizes = []
|
|
24
|
+
for process in tree:
|
|
25
|
+
with contextlib.suppress(psutil.NoSuchProcess, psutil.AccessDenied):
|
|
26
|
+
sizes.append(process.memory_info().rss)
|
|
27
|
+
return sum(sizes) if sizes else None
|
|
@@ -75,3 +75,9 @@ path = "src/labnotes/runner/exit_signal.py"
|
|
|
75
75
|
rules = ["mutation"]
|
|
76
76
|
lines = [10]
|
|
77
77
|
reason = "exit_code is non-negative here; when bit 7 is set, - 128 equals ^ 128, and when it is clear ^ 128 lands above NSIG like a non-positive difference"
|
|
78
|
+
|
|
79
|
+
[[python.exempt]]
|
|
80
|
+
path = "src/labnotes/experiments.py"
|
|
81
|
+
rules = ["mutation"]
|
|
82
|
+
lines = [109]
|
|
83
|
+
reason = "click hands back the \"json\" choice literal itself, so is not equals !="
|
|
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
|
|
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
|
|
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
|