labnotes 0.0.88__tar.gz → 0.0.90__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.88 → labnotes-0.0.90}/PKG-INFO +1 -1
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/experiment_screen.py +7 -2
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/run_app.py +9 -39
- labnotes-0.0.90/src/labnotes/tui/run_output.py +41 -0
- labnotes-0.0.90/src/labnotes/tui/run_screen.py +30 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/testing-conventions.toml +7 -13
- {labnotes-0.0.88 → labnotes-0.0.90}/.gitignore +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/CHANGELOG.md +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/MIGRATIONS.md +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/README.md +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/pyproject.toml +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/context.md +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/daemon_app.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/remote_start.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/init.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/models.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/server.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.88 → labnotes-0.0.90}/uv.lock +0 -0
|
@@ -14,6 +14,7 @@ from labnotes.tui.fill_table import fill_table
|
|
|
14
14
|
from labnotes.tui.invocation_columns import invocation_columns
|
|
15
15
|
from labnotes.tui.invocation_rows import invocation_rows
|
|
16
16
|
from labnotes.tui.reload_on_change import reload_on_change
|
|
17
|
+
from labnotes.tui.run_screen import RunScreen
|
|
17
18
|
from labnotes.tui.runs_table import RunsTable
|
|
18
19
|
from labnotes.watch_rows import watch_rows
|
|
19
20
|
|
|
@@ -22,7 +23,7 @@ WATCHED = {"runs", "experiment_configs"}
|
|
|
22
23
|
|
|
23
24
|
class ExperimentScreen(Screen[None]):
|
|
24
25
|
BINDINGS = [
|
|
25
|
-
Binding("enter", "
|
|
26
|
+
Binding("enter", "open_row", "Open", priority=True),
|
|
26
27
|
Binding("escape", "app.pop_screen", "Back"),
|
|
27
28
|
]
|
|
28
29
|
|
|
@@ -77,7 +78,7 @@ class ExperimentScreen(Screen[None]):
|
|
|
77
78
|
invocation_rows(self.experiment, self.opened),
|
|
78
79
|
)
|
|
79
80
|
|
|
80
|
-
def
|
|
81
|
+
def action_open_row(self) -> None:
|
|
81
82
|
table = self.query_one(DataTable)
|
|
82
83
|
if not table.row_count:
|
|
83
84
|
return
|
|
@@ -90,3 +91,7 @@ class ExperimentScreen(Screen[None]):
|
|
|
90
91
|
if key in groups:
|
|
91
92
|
self.opened ^= {key}
|
|
92
93
|
self.redraw()
|
|
94
|
+
return
|
|
95
|
+
runs = {str(run["id"]): run for run in self.experiment["runs_list"]}
|
|
96
|
+
folder = self.config.root / str(runs[key]["folder"])
|
|
97
|
+
self.app.push_screen(RunScreen(key, folder))
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import codecs
|
|
3
2
|
import signal
|
|
4
3
|
import time
|
|
5
4
|
from pathlib import Path
|
|
@@ -7,7 +6,7 @@ from pathlib import Path
|
|
|
7
6
|
from rich.text import Text
|
|
8
7
|
from textual.app import App, ComposeResult
|
|
9
8
|
from textual.binding import Binding
|
|
10
|
-
from textual.widgets import DataTable, Footer, Header, Input,
|
|
9
|
+
from textual.widgets import DataTable, Footer, Header, Input, Static
|
|
11
10
|
|
|
12
11
|
from labnotes.models import Controls, Start
|
|
13
12
|
from labnotes.runner.describe_event import describe_event
|
|
@@ -15,10 +14,10 @@ from labnotes.runner.stop_then_kill import stop_then_kill
|
|
|
15
14
|
from labnotes.runner.param_args import param_args
|
|
16
15
|
from labnotes.tui.dim import dim
|
|
17
16
|
from labnotes.tui.format_elapsed import format_elapsed
|
|
17
|
+
from labnotes.tui.run_output import RunOutput
|
|
18
18
|
from labnotes.tui.runs_table import RunsTable
|
|
19
19
|
|
|
20
20
|
COLUMNS = ("run", "params", "status", "time")
|
|
21
|
-
STREAMS = ("stdout", "stderr")
|
|
22
21
|
PENDING, PASSED, FAILED = "○", "✓", "✗"
|
|
23
22
|
SPINNER = "|/-\\"
|
|
24
23
|
SKIPPED, DESELECTED = "–", "⊘"
|
|
@@ -80,18 +79,11 @@ class RunApp(App[None]):
|
|
|
80
79
|
self.viewing: str | None = None
|
|
81
80
|
self.grabbed: str | None = None
|
|
82
81
|
self.grabbed_from: list[str] = []
|
|
83
|
-
self.offsets: dict[str, int]
|
|
84
|
-
self.decoders = {
|
|
85
|
-
stream: codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
86
|
-
for stream in STREAMS
|
|
87
|
-
}
|
|
88
|
-
self.last_stream: str | None
|
|
89
|
-
self.mid_line: bool
|
|
90
82
|
|
|
91
83
|
def compose(self) -> ComposeResult:
|
|
92
84
|
yield Header()
|
|
93
85
|
yield RunsTable(id="runs", cursor_type="row")
|
|
94
|
-
yield
|
|
86
|
+
yield RunOutput(id="output")
|
|
95
87
|
yield Input(id="filter", placeholder="Filter params")
|
|
96
88
|
yield Static(id="status")
|
|
97
89
|
yield Footer()
|
|
@@ -101,7 +93,7 @@ class RunApp(App[None]):
|
|
|
101
93
|
for column in COLUMNS:
|
|
102
94
|
table.add_column(column, key=column)
|
|
103
95
|
self.query_one(Input).display = False
|
|
104
|
-
self.query_one(
|
|
96
|
+
self.query_one(RunOutput).display = False
|
|
105
97
|
self.query_one(Header).mount(Static(id="concurrency"))
|
|
106
98
|
table.set_interval(0.1, self.spin)
|
|
107
99
|
|
|
@@ -373,44 +365,22 @@ class RunApp(App[None]):
|
|
|
373
365
|
|
|
374
366
|
def view(self, run: str) -> None:
|
|
375
367
|
self.viewing = run
|
|
376
|
-
self.
|
|
377
|
-
for decoder in self.decoders.values():
|
|
378
|
-
decoder.reset()
|
|
379
|
-
self.last_stream = None
|
|
380
|
-
self.mid_line = False
|
|
381
|
-
self.query_one(Log).clear()
|
|
368
|
+
self.query_one(RunOutput).reset()
|
|
382
369
|
self.query_one(DataTable).display = False
|
|
383
|
-
self.query_one(
|
|
370
|
+
self.query_one(RunOutput).display = True
|
|
384
371
|
self.sub_title = run
|
|
385
372
|
self.show("")
|
|
386
373
|
self.refresh_bindings()
|
|
387
374
|
self.follow()
|
|
388
375
|
|
|
389
376
|
def follow(self) -> None:
|
|
390
|
-
if self.viewing
|
|
391
|
-
|
|
392
|
-
for stream in STREAMS:
|
|
393
|
-
path = self.folders[self.viewing] / f"{stream}.log"
|
|
394
|
-
if not path.exists():
|
|
395
|
-
continue
|
|
396
|
-
with path.open("rb") as log:
|
|
397
|
-
log.seek(self.offsets[stream])
|
|
398
|
-
data = log.read()
|
|
399
|
-
self.offsets[stream] += len(data)
|
|
400
|
-
text = self.decoders[stream].decode(data)
|
|
401
|
-
if not text:
|
|
402
|
-
continue
|
|
403
|
-
# Don't glue one stream's unfinished line onto the other's output.
|
|
404
|
-
if self.mid_line and stream != self.last_stream:
|
|
405
|
-
text = "\n" + text
|
|
406
|
-
self.query_one(Log).write(text)
|
|
407
|
-
self.last_stream = stream
|
|
408
|
-
self.mid_line = not text.endswith("\n")
|
|
377
|
+
if self.viewing in self.folders:
|
|
378
|
+
self.query_one(RunOutput).follow(self.folders[self.viewing])
|
|
409
379
|
|
|
410
380
|
def action_leave(self) -> None:
|
|
411
381
|
if self.viewing is not None:
|
|
412
382
|
self.viewing = None
|
|
413
|
-
self.query_one(
|
|
383
|
+
self.query_one(RunOutput).display = False
|
|
414
384
|
self.query_one(DataTable).display = True
|
|
415
385
|
self.sub_title = ""
|
|
416
386
|
self.refresh_bindings()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import codecs
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from textual.widgets import Log
|
|
5
|
+
|
|
6
|
+
STREAMS = ("stdout", "stderr")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RunOutput(Log):
|
|
10
|
+
def __init__(self, id: str | None = None) -> None:
|
|
11
|
+
super().__init__(id=id)
|
|
12
|
+
self.reset()
|
|
13
|
+
|
|
14
|
+
def reset(self) -> None:
|
|
15
|
+
self.offsets = dict.fromkeys(STREAMS, 0)
|
|
16
|
+
self.decoders = {
|
|
17
|
+
stream: codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
18
|
+
for stream in STREAMS
|
|
19
|
+
}
|
|
20
|
+
self.last_stream: str | None = None
|
|
21
|
+
self.mid_line = False
|
|
22
|
+
self.clear()
|
|
23
|
+
|
|
24
|
+
def follow(self, folder: Path) -> None:
|
|
25
|
+
for stream in STREAMS:
|
|
26
|
+
path = folder / f"{stream}.log"
|
|
27
|
+
if not path.exists():
|
|
28
|
+
continue
|
|
29
|
+
with path.open("rb") as log:
|
|
30
|
+
log.seek(self.offsets[stream])
|
|
31
|
+
data = log.read()
|
|
32
|
+
self.offsets[stream] += len(data)
|
|
33
|
+
text = self.decoders[stream].decode(data)
|
|
34
|
+
if not text:
|
|
35
|
+
continue
|
|
36
|
+
# Don't glue one stream's unfinished line onto the other's output.
|
|
37
|
+
if self.mid_line and stream != self.last_stream:
|
|
38
|
+
text = "\n" + text
|
|
39
|
+
self.write(text)
|
|
40
|
+
self.last_stream = stream
|
|
41
|
+
self.mid_line = not text.endswith("\n")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
|
+
from textual.binding import Binding
|
|
5
|
+
from textual.screen import Screen
|
|
6
|
+
from textual.widgets import Footer, Header
|
|
7
|
+
|
|
8
|
+
from labnotes.tui.run_output import RunOutput
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RunScreen(Screen[None]):
|
|
12
|
+
BINDINGS = [Binding("escape", "app.pop_screen", "Back")]
|
|
13
|
+
|
|
14
|
+
def __init__(self, run_id: str, folder: Path) -> None:
|
|
15
|
+
super().__init__()
|
|
16
|
+
self.run_id = run_id
|
|
17
|
+
self.folder = folder
|
|
18
|
+
|
|
19
|
+
def compose(self) -> ComposeResult:
|
|
20
|
+
yield Header()
|
|
21
|
+
yield RunOutput(id="output")
|
|
22
|
+
yield Footer()
|
|
23
|
+
|
|
24
|
+
def on_mount(self) -> None:
|
|
25
|
+
self.sub_title = self.run_id
|
|
26
|
+
self.follow()
|
|
27
|
+
self.set_interval(0.25, self.follow)
|
|
28
|
+
|
|
29
|
+
def follow(self) -> None:
|
|
30
|
+
self.query_one(RunOutput).follow(self.folder)
|
|
@@ -5,21 +5,15 @@ lines = [12]
|
|
|
5
5
|
reason = "LOCK_EX and LOCK_NB are disjoint bits, so | equals + and ^"
|
|
6
6
|
|
|
7
7
|
[[python.exempt]]
|
|
8
|
-
path = "src/labnotes/tui/
|
|
8
|
+
path = "src/labnotes/tui/run_output.py"
|
|
9
9
|
rules = ["mutation"]
|
|
10
|
-
lines = [
|
|
10
|
+
lines = [37]
|
|
11
11
|
reason = "last_stream is always the same str object taken from STREAMS, so is not equals !="
|
|
12
12
|
|
|
13
|
-
[[python.exempt]]
|
|
14
|
-
path = "src/labnotes/runner/run_experiment.py"
|
|
15
|
-
rules = ["mutation"]
|
|
16
|
-
lines = [47]
|
|
17
|
-
reason = "mode is validated to matrix or adhoc; \"matrix\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
|
|
18
|
-
|
|
19
13
|
[[python.exempt]]
|
|
20
14
|
path = "src/labnotes/load_experiment.py"
|
|
21
15
|
rules = ["mutation"]
|
|
22
|
-
lines = [
|
|
16
|
+
lines = [61]
|
|
23
17
|
reason = "summarize_config returns mode matrix or adhoc; \"matrix\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
|
|
24
18
|
|
|
25
19
|
[[python.exempt]]
|
|
@@ -43,7 +37,7 @@ reason = "the daemon answers a run with 202, 400, 409 or 500, so every status bu
|
|
|
43
37
|
[[python.exempt]]
|
|
44
38
|
path = "src/labnotes/run_control.py"
|
|
45
39
|
rules = ["mutation"]
|
|
46
|
-
lines = [
|
|
40
|
+
lines = [39]
|
|
47
41
|
reason = "argument counts are small ints, which CPython caches, so is not equals !="
|
|
48
42
|
|
|
49
43
|
[[python.exempt]]
|
|
@@ -61,11 +55,11 @@ reason = "run columns come from string literals in load_experiment, which CPytho
|
|
|
61
55
|
[[python.exempt]]
|
|
62
56
|
path = "src/labnotes/tui/experiment_screen.py"
|
|
63
57
|
rules = ["mutation"]
|
|
64
|
-
lines = [
|
|
58
|
+
lines = [53]
|
|
65
59
|
reason = "WorkerState members are singletons, so is equals =="
|
|
66
60
|
|
|
67
61
|
[[python.exempt]]
|
|
68
62
|
path = "src/labnotes/tui/experiments_screen.py"
|
|
69
63
|
rules = ["mutation"]
|
|
70
|
-
lines = [
|
|
71
|
-
reason = "WorkerState members are singletons, so is equals
|
|
64
|
+
lines = [36]
|
|
65
|
+
reason = "WorkerState members are singletons, so is 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
|