labnotes 0.0.28__tar.gz → 0.0.30__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.28 → labnotes-0.0.30}/PKG-INFO +1 -1
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/context.md +3 -2
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/models.py +1 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/describe_event.py +4 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/run_experiment.py +24 -6
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tui/run_app.py +13 -2
- {labnotes-0.0.28 → labnotes-0.0.30}/.gitignore +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/CHANGELOG.md +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/MIGRATIONS.md +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/README.md +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/pyproject.toml +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/experiment_progress.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/init.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/lock_path.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/wait_for_group.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/server.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/stop_idle_kernels.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/templates/macros.html +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.28 → labnotes-0.0.30}/uv.lock +0 -0
|
@@ -86,11 +86,12 @@ 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. Runs not started yet show grayed as `pending`, and as
|
|
89
|
+
open when they finish. Failed runs show their status in red. Runs not started yet show grayed as `pending`, and as
|
|
90
90
|
`skipped` once stopped. Move the cursor to a pending run and press `n` to run it
|
|
91
91
|
next. `d` deselects a pending run so it won't run; `d` again puts it back at
|
|
92
92
|
the end of the queue. `/` filters rows by params text; Enter keeps the filter,
|
|
93
|
-
Escape clears it. `D` deselects every pending run the filter shows.
|
|
93
|
+
Escape clears it. `D` deselects every pending run the filter shows. `k` stops the
|
|
94
|
+
running run under the cursor; `k` again kills it. `+` and
|
|
94
95
|
`-` change how many run at once; lowering it lets running runs
|
|
95
96
|
finish; `q` quits. Elsewhere, or with `--no-tui`, it prints a
|
|
96
97
|
line per event instead.
|
|
@@ -18,6 +18,10 @@ def describe_event(event: dict[str, object]) -> str:
|
|
|
18
18
|
return f"{event['run']} deselected."
|
|
19
19
|
case "reselected":
|
|
20
20
|
return f"{event['run']} back in the queue."
|
|
21
|
+
case "stopping run":
|
|
22
|
+
return f"Stopping {event['run']}."
|
|
23
|
+
case "killing run":
|
|
24
|
+
return f"Killing {event['run']}."
|
|
21
25
|
case "concurrency":
|
|
22
26
|
return f"{event['concurrency']} at a time."
|
|
23
27
|
case "stopping":
|
|
@@ -48,25 +48,41 @@ async def run_experiment(
|
|
|
48
48
|
plan.append((run, params))
|
|
49
49
|
runs = [{"run": run, "params": params} for run, params in plan]
|
|
50
50
|
report({"event": "planned", "runs": runs, "concurrency": concurrency})
|
|
51
|
-
running: set[asyncio.subprocess.Process] =
|
|
51
|
+
running: dict[str, set[asyncio.subprocess.Process]] = {}
|
|
52
|
+
stopped: set[str] = set()
|
|
52
53
|
results: list[dict[str, object]] = []
|
|
53
54
|
stopping = asyncio.Event()
|
|
54
55
|
|
|
55
|
-
def
|
|
56
|
-
for process in running:
|
|
56
|
+
def signal_run(run: str, signal_number: int) -> None:
|
|
57
|
+
for process in running[run]:
|
|
57
58
|
with contextlib.suppress(ProcessLookupError):
|
|
58
59
|
os.killpg(process.pid, signal_number)
|
|
59
60
|
|
|
61
|
+
def signal_runs(signal_number: int) -> None:
|
|
62
|
+
for run in running:
|
|
63
|
+
signal_run(run, signal_number)
|
|
64
|
+
|
|
60
65
|
def interrupt() -> None:
|
|
61
66
|
plan.clear()
|
|
62
67
|
if not stopping.is_set():
|
|
63
68
|
stopping.set()
|
|
64
69
|
report({"event": "stopping"})
|
|
65
70
|
signal_runs(signal.SIGTERM)
|
|
66
|
-
elif running:
|
|
71
|
+
elif any(running.values()):
|
|
67
72
|
report({"event": "killing"})
|
|
68
73
|
signal_runs(signal.SIGKILL)
|
|
69
74
|
|
|
75
|
+
def kill(run: str) -> None:
|
|
76
|
+
if not running.get(run):
|
|
77
|
+
return
|
|
78
|
+
if run in stopped:
|
|
79
|
+
report({"event": "killing run", "run": run})
|
|
80
|
+
signal_run(run, signal.SIGKILL)
|
|
81
|
+
else:
|
|
82
|
+
stopped.add(run)
|
|
83
|
+
report({"event": "stopping run", "run": run})
|
|
84
|
+
signal_run(run, signal.SIGTERM)
|
|
85
|
+
|
|
70
86
|
held: list[Entry] = []
|
|
71
87
|
|
|
72
88
|
def find(entries: list[Entry], run: str) -> Entry | None:
|
|
@@ -125,13 +141,15 @@ async def run_experiment(
|
|
|
125
141
|
folder = run_folder(config.root, config.runs, name, run)
|
|
126
142
|
folder.mkdir(parents=True)
|
|
127
143
|
report({"event": "started", "run": run, "params": params})
|
|
144
|
+
running[run] = set()
|
|
128
145
|
result = await execute_run(
|
|
129
|
-
config.root, folder, experiment.command, params, running, stopping
|
|
146
|
+
config.root, folder, experiment.command, params, running[run], stopping
|
|
130
147
|
)
|
|
148
|
+
del running[run]
|
|
131
149
|
report({"event": "ended", "run": run, **result})
|
|
132
150
|
results.append(result)
|
|
133
151
|
|
|
134
|
-
listen(Controls(interrupt, run_next, set_concurrency, toggle))
|
|
152
|
+
listen(Controls(interrupt, run_next, set_concurrency, toggle, kill))
|
|
135
153
|
fill()
|
|
136
154
|
await idle.wait()
|
|
137
155
|
await asyncio.gather(*started)
|
|
@@ -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
|
|
@@ -21,6 +22,7 @@ class RunApp(App[None]):
|
|
|
21
22
|
Binding("n", "run_next", "Run next"),
|
|
22
23
|
Binding("d", "toggle_run", "Deselect"),
|
|
23
24
|
Binding("D", "deselect_shown", "Deselect shown"),
|
|
25
|
+
Binding("k", "kill_run", "Stop run"),
|
|
24
26
|
Binding("slash", "filter", "Filter", key_display="/"),
|
|
25
27
|
Binding("escape", "clear_filter", "Clear filter"),
|
|
26
28
|
Binding("plus,equals_sign", "concurrency(1)", "More at once", key_display="+"),
|
|
@@ -34,7 +36,11 @@ class RunApp(App[None]):
|
|
|
34
36
|
self.title = title
|
|
35
37
|
self.results: list[dict[str, object]] = []
|
|
36
38
|
self.controls = Controls(
|
|
37
|
-
lambda: None,
|
|
39
|
+
lambda: None,
|
|
40
|
+
lambda run: None,
|
|
41
|
+
lambda count: None,
|
|
42
|
+
lambda run: None,
|
|
43
|
+
lambda run: None,
|
|
38
44
|
)
|
|
39
45
|
self.concurrency = 1
|
|
40
46
|
self.done = False
|
|
@@ -93,7 +99,8 @@ class RunApp(App[None]):
|
|
|
93
99
|
self.set_cell(run, column, cell)
|
|
94
100
|
case "ended":
|
|
95
101
|
outcome = event["error"] or f"exited {event['exit_code']}"
|
|
96
|
-
|
|
102
|
+
style = "red" if event["error"] or event["exit_code"] else ""
|
|
103
|
+
self.set_cell(run, "status", Text(str(outcome), style=style))
|
|
97
104
|
self.tick()
|
|
98
105
|
del self.started[run]
|
|
99
106
|
case "queued":
|
|
@@ -178,6 +185,10 @@ class RunApp(App[None]):
|
|
|
178
185
|
if (run := self.cursor_run()) is not None:
|
|
179
186
|
self.controls.toggle(run)
|
|
180
187
|
|
|
188
|
+
def action_kill_run(self) -> None:
|
|
189
|
+
if (run := self.cursor_run()) is not None:
|
|
190
|
+
self.controls.kill(run)
|
|
191
|
+
|
|
181
192
|
def action_deselect_shown(self) -> None:
|
|
182
193
|
shown = [str(key.value) for key in self.query_one(DataTable).rows]
|
|
183
194
|
for run in shown:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|