labnotes 0.0.35__tar.gz → 0.0.37__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.35 → labnotes-0.0.37}/PKG-INFO +1 -1
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/context.md +4 -1
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/run_experiment.py +9 -2
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tui/run_app.py +64 -6
- {labnotes-0.0.35 → labnotes-0.0.37}/.gitignore +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/CHANGELOG.md +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/MIGRATIONS.md +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/README.md +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/pyproject.toml +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/experiment_progress.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/init.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/models.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/new_invocation_id.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/wait_for_group.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/server.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/stop_idle_kernels.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/templates/macros.html +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/testing-conventions.toml +0 -0
- {labnotes-0.0.35 → labnotes-0.0.37}/uv.lock +0 -0
|
@@ -86,6 +86,7 @@ A script run by labnotes should:
|
|
|
86
86
|
- take its output folder as an argument, passed in `command` as `{run_dir}`, and write everything it produces there;
|
|
87
87
|
- exit non-zero when it fails, so the run counts as failed and runs again next time;
|
|
88
88
|
- stop any background process it starts before it exits; the run ends when `command` exits.
|
|
89
|
+
- flush its output as it goes (`print(..., flush=True)`, or `PYTHONUNBUFFERED=1`), so the live view shows it as it happens.
|
|
89
90
|
|
|
90
91
|
Runs can take a long time. Start `run` in the background, then check on it with `labnotes experiments foo runs`; a run with no `ended` is still going.
|
|
91
92
|
|
|
@@ -98,7 +99,9 @@ the end of the queue. `/` filters rows by params text; Enter keeps the filter,
|
|
|
98
99
|
Escape clears it. `D` deselects every pending run the filter shows. `k` stops the
|
|
99
100
|
running run under the cursor; `k` again kills it. `+` and
|
|
100
101
|
`-` change how many run at once; lowering it lets running runs
|
|
101
|
-
finish
|
|
102
|
+
finish. Enter on a run follows its `stdout.log`; `e` switches to
|
|
103
|
+
`stderr.log` and back, and Escape returns to the table. An `adhoc` experiment that
|
|
104
|
+
plans a single run opens straight on its output. `q` quits. Elsewhere, or with `--no-tui`, it prints a
|
|
102
105
|
line per event instead.
|
|
103
106
|
|
|
104
107
|
- `--concurrency N` runs N at a time to start with (default 1).
|
|
@@ -53,7 +53,14 @@ async def run_experiment(
|
|
|
53
53
|
planned.add(run)
|
|
54
54
|
plan.append((run, params))
|
|
55
55
|
runs = [{"run": run, "params": params} for run, params in plan]
|
|
56
|
-
report(
|
|
56
|
+
report(
|
|
57
|
+
{
|
|
58
|
+
"event": "planned",
|
|
59
|
+
"runs": runs,
|
|
60
|
+
"concurrency": concurrency,
|
|
61
|
+
"mode": experiment.mode,
|
|
62
|
+
}
|
|
63
|
+
)
|
|
57
64
|
running: dict[str, set[asyncio.subprocess.Process]] = {}
|
|
58
65
|
stopped: set[str] = set()
|
|
59
66
|
results: list[dict[str, object]] = []
|
|
@@ -166,7 +173,7 @@ async def run_experiment(
|
|
|
166
173
|
async def start(run: str, params: dict[str, object]) -> None:
|
|
167
174
|
folder = run_folder(config.root, config.runs, name, run)
|
|
168
175
|
folder.mkdir(parents=True)
|
|
169
|
-
report({"event": "started", "run": run, "params": params})
|
|
176
|
+
report({"event": "started", "run": run, "params": params, "folder": folder})
|
|
170
177
|
running[run] = set()
|
|
171
178
|
result = await execute_run(
|
|
172
179
|
config.root,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import codecs
|
|
1
2
|
import signal
|
|
2
3
|
import time
|
|
4
|
+
from pathlib import Path
|
|
3
5
|
|
|
4
6
|
from rich.text import Text
|
|
5
7
|
from textual.app import App, ComposeResult
|
|
6
8
|
from textual.binding import Binding
|
|
7
|
-
from textual.widgets import DataTable, Footer, Header, Input, Static
|
|
9
|
+
from textual.widgets import DataTable, Footer, Header, Input, Log, Static
|
|
8
10
|
|
|
9
11
|
from labnotes.models import Controls, Start
|
|
10
12
|
from labnotes.runner.describe_event import describe_event
|
|
@@ -14,6 +16,7 @@ from labnotes.tui.dim import dim
|
|
|
14
16
|
from labnotes.tui.format_elapsed import format_elapsed
|
|
15
17
|
|
|
16
18
|
COLUMNS = ("run", "params", "status", "time")
|
|
19
|
+
OTHER_STREAM = {"stdout": "stderr", "stderr": "stdout"}
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
class RunApp(App[None]):
|
|
@@ -26,7 +29,8 @@ class RunApp(App[None]):
|
|
|
26
29
|
Binding("h", "hide_failed", "Hide failed"),
|
|
27
30
|
Binding("r", "retry", "Retry"),
|
|
28
31
|
Binding("slash", "filter", "Filter", key_display="/"),
|
|
29
|
-
Binding("
|
|
32
|
+
Binding("e", "switch_output", "stdout/stderr"),
|
|
33
|
+
Binding("escape", "leave", "Back"),
|
|
30
34
|
Binding("plus,equals_sign", "concurrency(1)", "More at once", key_display="+"),
|
|
31
35
|
Binding("minus", "concurrency(-1)", "Fewer at once", key_display="-"),
|
|
32
36
|
Binding("q", "quit", "Quit"),
|
|
@@ -56,10 +60,16 @@ class RunApp(App[None]):
|
|
|
56
60
|
self.filter = ""
|
|
57
61
|
self.failed: set[str] = set()
|
|
58
62
|
self.hiding = False
|
|
63
|
+
self.folders: dict[str, Path] = {}
|
|
64
|
+
self.viewing: str | None = None
|
|
65
|
+
self.stream = "stdout"
|
|
66
|
+
self.offset: int
|
|
67
|
+
self.decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
59
68
|
|
|
60
69
|
def compose(self) -> ComposeResult:
|
|
61
70
|
yield Header()
|
|
62
71
|
yield DataTable(id="runs", cursor_type="row")
|
|
72
|
+
yield Log(id="output")
|
|
63
73
|
yield Input(id="filter", placeholder="Filter params")
|
|
64
74
|
yield Static(id="status")
|
|
65
75
|
yield Footer()
|
|
@@ -69,9 +79,11 @@ class RunApp(App[None]):
|
|
|
69
79
|
for column in COLUMNS:
|
|
70
80
|
table.add_column(column, key=column)
|
|
71
81
|
self.query_one(Input).display = False
|
|
82
|
+
self.query_one(Log).display = False
|
|
72
83
|
|
|
73
84
|
def on_ready(self) -> None:
|
|
74
85
|
self.set_interval(1, self.tick)
|
|
86
|
+
self.set_interval(0.25, self.follow)
|
|
75
87
|
self.run_worker(self.drive())
|
|
76
88
|
|
|
77
89
|
async def drive(self) -> None:
|
|
@@ -95,10 +107,14 @@ class RunApp(App[None]):
|
|
|
95
107
|
self.redraw(table.cursor_row)
|
|
96
108
|
self.show_concurrency(event)
|
|
97
109
|
self.show(describe_event(event))
|
|
110
|
+
match event["mode"], event["runs"]:
|
|
111
|
+
case "adhoc", [only]:
|
|
112
|
+
self.view(only["run"], "stdout") # type: ignore[index]
|
|
98
113
|
case "started":
|
|
99
114
|
self.idle = False
|
|
100
115
|
self.pending.discard(run)
|
|
101
116
|
self.started[run] = time.monotonic()
|
|
117
|
+
self.folders[run] = Path(str(event["folder"]))
|
|
102
118
|
if run == self.next_run:
|
|
103
119
|
self.show("")
|
|
104
120
|
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
@@ -139,9 +155,7 @@ class RunApp(App[None]):
|
|
|
139
155
|
self.show(describe_event(event))
|
|
140
156
|
case "idle":
|
|
141
157
|
self.idle = True
|
|
142
|
-
|
|
143
|
-
passed = len(self.ended) - failed
|
|
144
|
-
self.show(f"{passed} passed, {failed} failed. q to quit.")
|
|
158
|
+
self.show_summary()
|
|
145
159
|
case "concurrency":
|
|
146
160
|
self.show_concurrency(event)
|
|
147
161
|
self.show(describe_event(event))
|
|
@@ -152,6 +166,11 @@ class RunApp(App[None]):
|
|
|
152
166
|
case _:
|
|
153
167
|
self.show(describe_event(event))
|
|
154
168
|
|
|
169
|
+
def show_summary(self) -> None:
|
|
170
|
+
failed = len(self.failed)
|
|
171
|
+
passed = len(self.ended) - failed
|
|
172
|
+
self.show(f"{passed} passed, {failed} failed. q to quit.")
|
|
173
|
+
|
|
155
174
|
def reorder(self, event: dict[str, object], row: int) -> None:
|
|
156
175
|
waiting: list[str] = [*event["pending"], *event["held"]] # type: ignore[misc]
|
|
157
176
|
for run in waiting:
|
|
@@ -239,7 +258,46 @@ class RunApp(App[None]):
|
|
|
239
258
|
field.display = True
|
|
240
259
|
field.focus()
|
|
241
260
|
|
|
242
|
-
def
|
|
261
|
+
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
262
|
+
self.view(str(event.row_key.value), "stdout")
|
|
263
|
+
|
|
264
|
+
def view(self, run: str, stream: str) -> None:
|
|
265
|
+
self.viewing = run
|
|
266
|
+
self.stream = stream
|
|
267
|
+
self.offset = 0
|
|
268
|
+
self.decoder.reset()
|
|
269
|
+
self.query_one(Log).clear()
|
|
270
|
+
self.query_one(DataTable).display = False
|
|
271
|
+
self.query_one(Log).display = True
|
|
272
|
+
self.show(f"{run} {stream}. e switches, Escape goes back.")
|
|
273
|
+
self.follow()
|
|
274
|
+
|
|
275
|
+
def follow(self) -> None:
|
|
276
|
+
if self.viewing not in self.folders:
|
|
277
|
+
return
|
|
278
|
+
path = self.folders[self.viewing] / f"{self.stream}.log"
|
|
279
|
+
if not path.exists():
|
|
280
|
+
return
|
|
281
|
+
with path.open("rb") as log:
|
|
282
|
+
log.seek(self.offset)
|
|
283
|
+
data = log.read()
|
|
284
|
+
self.offset += len(data)
|
|
285
|
+
self.query_one(Log).write(self.decoder.decode(data))
|
|
286
|
+
|
|
287
|
+
def action_switch_output(self) -> None:
|
|
288
|
+
if self.viewing is not None:
|
|
289
|
+
self.view(self.viewing, OTHER_STREAM[self.stream])
|
|
290
|
+
|
|
291
|
+
def action_leave(self) -> None:
|
|
292
|
+
if self.viewing is not None:
|
|
293
|
+
self.viewing = None
|
|
294
|
+
self.query_one(Log).display = False
|
|
295
|
+
self.query_one(DataTable).display = True
|
|
296
|
+
if self.idle:
|
|
297
|
+
self.show_summary()
|
|
298
|
+
else:
|
|
299
|
+
self.show("")
|
|
300
|
+
return
|
|
243
301
|
field = self.query_one(Input)
|
|
244
302
|
field.value = ""
|
|
245
303
|
field.display = False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|