labnotes 0.0.29__tar.gz → 0.0.31__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.29 → labnotes-0.0.31}/PKG-INFO +1 -1
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/context.md +2 -1
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/models.py +2 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/describe_event.py +4 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/run_experiment.py +25 -4
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/run_headless.py +1 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tui/run_app.py +44 -11
- {labnotes-0.0.29 → labnotes-0.0.31}/.gitignore +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/CHANGELOG.md +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/MIGRATIONS.md +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/README.md +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/pyproject.toml +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/experiment_progress.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/init.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/lock_path.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/wait_for_group.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/server.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/stop_idle_kernels.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/templates/macros.html +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.29 → labnotes-0.0.31}/uv.lock +0 -0
|
@@ -86,7 +86,8 @@ A script run by labnotes should:
|
|
|
86
86
|
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.
|
|
87
87
|
|
|
88
88
|
In a terminal, `run` shows a live table of runs (status and time) and stays
|
|
89
|
-
open when they finish.
|
|
89
|
+
open when they finish. Failed runs show their status in red; `h` hides or shows them, and `r` on one
|
|
90
|
+
runs it again under a new id. Runs not started yet show grayed as `pending`, and as
|
|
90
91
|
`skipped` once stopped. Move the cursor to a pending run and press `n` to run it
|
|
91
92
|
next. `d` deselects a pending run so it won't run; `d` again puts it back at
|
|
92
93
|
the end of the queue. `/` filters rows by params text; Enter keeps the filter,
|
|
@@ -22,6 +22,10 @@ def describe_event(event: dict[str, object]) -> str:
|
|
|
22
22
|
return f"Stopping {event['run']}."
|
|
23
23
|
case "killing run":
|
|
24
24
|
return f"Killing {event['run']}."
|
|
25
|
+
case "retried":
|
|
26
|
+
return f"{event['run']} retried as {event['retry']}."
|
|
27
|
+
case "idle":
|
|
28
|
+
return "No runs left."
|
|
25
29
|
case "concurrency":
|
|
26
30
|
return f"{event['concurrency']} at a time."
|
|
27
31
|
case "stopping":
|
|
@@ -113,10 +113,22 @@ async def run_experiment(
|
|
|
113
113
|
arrange("reselected", run)
|
|
114
114
|
fill()
|
|
115
115
|
|
|
116
|
+
ended: dict[str, dict[str, object]] = {}
|
|
117
|
+
|
|
118
|
+
def retry(run: str) -> None:
|
|
119
|
+
if stopping.is_set() or run not in ended:
|
|
120
|
+
return
|
|
121
|
+
again = new_run_id(taken)
|
|
122
|
+
planned.add(again)
|
|
123
|
+
plan.append((again, ended[run]))
|
|
124
|
+
report({"event": "retried", "run": run, "retry": again, "params": ended[run]})
|
|
125
|
+
fill()
|
|
126
|
+
|
|
116
127
|
limit = concurrency
|
|
117
128
|
live: set[asyncio.Task[None]] = set()
|
|
118
129
|
started: list[asyncio.Task[None]] = []
|
|
119
|
-
|
|
130
|
+
finishing = False
|
|
131
|
+
done = asyncio.Event()
|
|
120
132
|
|
|
121
133
|
def fill() -> None:
|
|
122
134
|
while plan and len(live) < limit:
|
|
@@ -125,7 +137,15 @@ async def run_experiment(
|
|
|
125
137
|
live.add(task)
|
|
126
138
|
started.append(task)
|
|
127
139
|
if not live:
|
|
128
|
-
idle
|
|
140
|
+
report({"event": "idle"})
|
|
141
|
+
if finishing:
|
|
142
|
+
done.set()
|
|
143
|
+
|
|
144
|
+
def finish() -> None:
|
|
145
|
+
nonlocal finishing
|
|
146
|
+
finishing = True
|
|
147
|
+
if not (live or plan):
|
|
148
|
+
done.set()
|
|
129
149
|
|
|
130
150
|
def end(task: asyncio.Task[None]) -> None:
|
|
131
151
|
live.remove(task)
|
|
@@ -147,10 +167,11 @@ async def run_experiment(
|
|
|
147
167
|
)
|
|
148
168
|
del running[run]
|
|
149
169
|
report({"event": "ended", "run": run, **result})
|
|
170
|
+
ended[run] = params
|
|
150
171
|
results.append(result)
|
|
151
172
|
|
|
152
|
-
listen(Controls(interrupt, run_next, set_concurrency, toggle, kill))
|
|
173
|
+
listen(Controls(interrupt, run_next, set_concurrency, toggle, kill, retry, finish))
|
|
153
174
|
fill()
|
|
154
|
-
await
|
|
175
|
+
await done.wait()
|
|
155
176
|
await asyncio.gather(*started)
|
|
156
177
|
return results
|
|
@@ -13,5 +13,6 @@ def run_headless(start: Start) -> list[dict[str, object]]:
|
|
|
13
13
|
loop = asyncio.get_running_loop()
|
|
14
14
|
loop.add_signal_handler(signal.SIGINT, controls.interrupt)
|
|
15
15
|
stop_then_kill(controls.interrupt, signal.SIGTERM, signal.SIGHUP)
|
|
16
|
+
controls.finish()
|
|
16
17
|
|
|
17
18
|
return asyncio.run(start(lambda event: click.echo(describe_event(event)), listen))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import signal
|
|
2
2
|
import time
|
|
3
3
|
|
|
4
|
+
from rich.text import Text
|
|
4
5
|
from textual.app import App, ComposeResult
|
|
5
6
|
from textual.binding import Binding
|
|
6
7
|
from textual.widgets import DataTable, Footer, Header, Input, Static
|
|
@@ -22,6 +23,8 @@ class RunApp(App[None]):
|
|
|
22
23
|
Binding("d", "toggle_run", "Deselect"),
|
|
23
24
|
Binding("D", "deselect_shown", "Deselect shown"),
|
|
24
25
|
Binding("k", "kill_run", "Stop run"),
|
|
26
|
+
Binding("h", "hide_failed", "Hide failed"),
|
|
27
|
+
Binding("r", "retry", "Retry"),
|
|
25
28
|
Binding("slash", "filter", "Filter", key_display="/"),
|
|
26
29
|
Binding("escape", "clear_filter", "Clear filter"),
|
|
27
30
|
Binding("plus,equals_sign", "concurrency(1)", "More at once", key_display="+"),
|
|
@@ -40,13 +43,18 @@ class RunApp(App[None]):
|
|
|
40
43
|
lambda count: None,
|
|
41
44
|
lambda run: None,
|
|
42
45
|
lambda run: None,
|
|
46
|
+
lambda run: None,
|
|
47
|
+
lambda: None,
|
|
43
48
|
)
|
|
44
49
|
self.concurrency = 1
|
|
45
|
-
self.
|
|
50
|
+
self.idle = False
|
|
51
|
+
self.ended: set[str] = set()
|
|
46
52
|
self.started: dict[str, float] = {}
|
|
47
53
|
self.pending: set[str] = set()
|
|
48
54
|
self.cells: dict[str, list[object]] = {}
|
|
49
55
|
self.filter = ""
|
|
56
|
+
self.failed: set[str] = set()
|
|
57
|
+
self.hiding = False
|
|
50
58
|
|
|
51
59
|
def compose(self) -> ComposeResult:
|
|
52
60
|
yield Header()
|
|
@@ -67,10 +75,7 @@ class RunApp(App[None]):
|
|
|
67
75
|
|
|
68
76
|
async def drive(self) -> None:
|
|
69
77
|
self.results = await self.start(self.show_event, self.listen)
|
|
70
|
-
self.
|
|
71
|
-
failed = sum(result["exit_code"] != 0 for result in self.results)
|
|
72
|
-
passed = len(self.results) - failed
|
|
73
|
-
self.show(f"{passed} passed, {failed} failed. q to quit.")
|
|
78
|
+
self.exit()
|
|
74
79
|
|
|
75
80
|
def listen(self, controls: Controls) -> None:
|
|
76
81
|
self.controls = controls
|
|
@@ -90,6 +95,7 @@ class RunApp(App[None]):
|
|
|
90
95
|
self.show_concurrency(event)
|
|
91
96
|
self.show(describe_event(event))
|
|
92
97
|
case "started":
|
|
98
|
+
self.idle = False
|
|
93
99
|
self.pending.discard(run)
|
|
94
100
|
self.started[run] = time.monotonic()
|
|
95
101
|
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
@@ -98,9 +104,14 @@ class RunApp(App[None]):
|
|
|
98
104
|
self.set_cell(run, column, cell)
|
|
99
105
|
case "ended":
|
|
100
106
|
outcome = event["error"] or f"exited {event['exit_code']}"
|
|
101
|
-
|
|
107
|
+
style = "red" if event["error"] or event["exit_code"] else ""
|
|
108
|
+
if style:
|
|
109
|
+
self.failed.add(run)
|
|
110
|
+
self.ended.add(run)
|
|
111
|
+
self.set_cell(run, "status", Text(str(outcome), style=style))
|
|
102
112
|
self.tick()
|
|
103
113
|
del self.started[run]
|
|
114
|
+
self.redraw(table.cursor_row)
|
|
104
115
|
case "queued":
|
|
105
116
|
self.reorder(event, table.cursor_row)
|
|
106
117
|
table.move_cursor(row=table.get_row_index(run))
|
|
@@ -115,6 +126,18 @@ class RunApp(App[None]):
|
|
|
115
126
|
self.set_cell(run, "status", dim("pending"))
|
|
116
127
|
self.reorder(event, table.cursor_row)
|
|
117
128
|
self.show(describe_event(event))
|
|
129
|
+
case "retried":
|
|
130
|
+
again = str(event["retry"])
|
|
131
|
+
args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
|
|
132
|
+
self.cells[again] = [*map(dim, (again, args, "pending", ""))]
|
|
133
|
+
self.pending.add(again)
|
|
134
|
+
self.redraw(table.cursor_row)
|
|
135
|
+
self.show(describe_event(event))
|
|
136
|
+
case "idle":
|
|
137
|
+
self.idle = True
|
|
138
|
+
failed = len(self.failed)
|
|
139
|
+
passed = len(self.ended) - failed
|
|
140
|
+
self.show(f"{passed} passed, {failed} failed. q to quit.")
|
|
118
141
|
case "concurrency":
|
|
119
142
|
self.show_concurrency(event)
|
|
120
143
|
self.show(describe_event(event))
|
|
@@ -135,7 +158,8 @@ class RunApp(App[None]):
|
|
|
135
158
|
table = self.query_one(DataTable)
|
|
136
159
|
table.clear()
|
|
137
160
|
for run, cells in self.cells.items():
|
|
138
|
-
|
|
161
|
+
hidden = self.hiding and run in self.failed
|
|
162
|
+
if self.filter in str(cells[1]) and not hidden:
|
|
139
163
|
table.add_row(*cells, key=run)
|
|
140
164
|
table.move_cursor(row=row)
|
|
141
165
|
|
|
@@ -157,14 +181,14 @@ class RunApp(App[None]):
|
|
|
157
181
|
self.query_one("#status", Static).update(text)
|
|
158
182
|
|
|
159
183
|
def action_interrupt(self) -> None:
|
|
160
|
-
if self.
|
|
161
|
-
self.
|
|
184
|
+
if self.idle:
|
|
185
|
+
self.controls.finish()
|
|
162
186
|
else:
|
|
163
187
|
self.controls.interrupt()
|
|
164
188
|
|
|
165
189
|
async def action_quit(self) -> None:
|
|
166
|
-
if self.
|
|
167
|
-
self.
|
|
190
|
+
if self.idle:
|
|
191
|
+
self.controls.finish()
|
|
168
192
|
else:
|
|
169
193
|
self.show("Runs are still going. Ctrl-C stops them.")
|
|
170
194
|
|
|
@@ -183,6 +207,15 @@ class RunApp(App[None]):
|
|
|
183
207
|
if (run := self.cursor_run()) is not None:
|
|
184
208
|
self.controls.toggle(run)
|
|
185
209
|
|
|
210
|
+
def action_hide_failed(self) -> None:
|
|
211
|
+
self.hiding = not self.hiding
|
|
212
|
+
self.redraw(self.query_one(DataTable).cursor_row)
|
|
213
|
+
self.show("Failed runs hidden." if self.hiding else "Failed runs shown.")
|
|
214
|
+
|
|
215
|
+
def action_retry(self) -> None:
|
|
216
|
+
if (run := self.cursor_run()) is not None and run in self.failed:
|
|
217
|
+
self.controls.retry(run)
|
|
218
|
+
|
|
186
219
|
def action_kill_run(self) -> None:
|
|
187
220
|
if (run := self.cursor_run()) is not None:
|
|
188
221
|
self.controls.kill(run)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|