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.
Files changed (89) hide show
  1. {labnotes-0.0.28 → labnotes-0.0.30}/PKG-INFO +1 -1
  2. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/context.md +3 -2
  3. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/models.py +1 -0
  4. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/describe_event.py +4 -0
  5. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/run_experiment.py +24 -6
  6. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tui/run_app.py +13 -2
  7. {labnotes-0.0.28 → labnotes-0.0.30}/.gitignore +0 -0
  8. {labnotes-0.0.28 → labnotes-0.0.30}/CHANGELOG.md +0 -0
  9. {labnotes-0.0.28 → labnotes-0.0.30}/MIGRATIONS.md +0 -0
  10. {labnotes-0.0.28 → labnotes-0.0.30}/README.md +0 -0
  11. {labnotes-0.0.28 → labnotes-0.0.30}/pyproject.toml +0 -0
  12. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/__init__.py +0 -0
  13. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/asset_url.py +0 -0
  14. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/cell_progress.py +0 -0
  15. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/cli.py +0 -0
  16. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/context_guide.py +0 -0
  17. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/count_runs.py +0 -0
  18. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/create_app.py +0 -0
  19. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/create_experiment.py +0 -0
  20. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/data_html.py +0 -0
  21. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/default_config_text.py +0 -0
  22. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/default_counts.py +0 -0
  23. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/drain_watch.py +0 -0
  24. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/experiment_notes.py +0 -0
  25. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/experiment_progress.py +0 -0
  26. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/experiment_runs.py +0 -0
  27. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/experiments.py +0 -0
  28. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/format_value.py +0 -0
  29. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/init.py +0 -0
  30. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/list_experiments.py +0 -0
  31. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/list_runs.py +0 -0
  32. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/load_config.py +0 -0
  33. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/load_experiment.py +0 -0
  34. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/notebook_kernel.py +0 -0
  35. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/output_html.py +0 -0
  36. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/parse_run_view.py +0 -0
  37. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/pivot_cells.py +0 -0
  38. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/print_rows.py +0 -0
  39. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/python_blocks.py +0 -0
  40. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/read_config.py +0 -0
  41. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/read_path.py +0 -0
  42. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/render_markdown.py +0 -0
  43. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/__init__.py +0 -0
  44. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/execute_run.py +0 -0
  45. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/lock_path.py +0 -0
  46. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/new_run_id.py +0 -0
  47. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/param_args.py +0 -0
  48. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/param_grid.py +0 -0
  49. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/plan_runs.py +0 -0
  50. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/read_experiment_config.py +0 -0
  51. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/run_folder.py +0 -0
  52. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/run_headless.py +0 -0
  53. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/shell_command.py +0 -0
  54. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/start_experiment.py +0 -0
  55. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/stop_then_kill.py +0 -0
  56. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/wait_for_group.py +0 -0
  57. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/runner/write_run_json.py +0 -0
  58. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/saved_outputs.py +0 -0
  59. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/serve_app.py +0 -0
  60. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/server.py +0 -0
  61. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/sort_href.py +0 -0
  62. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/sql_quote.py +0 -0
  63. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/static/labnotes.css +0 -0
  64. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/static/row-link.js +0 -0
  65. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/static/run-cell.js +0 -0
  66. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/static/view-toggle.js +0 -0
  67. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/stop_idle_kernels.py +0 -0
  68. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/summarize_config.py +0 -0
  69. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/__init__.py +0 -0
  70. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/experiment_rows.py +0 -0
  71. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/file_prefix.py +0 -0
  72. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/iso_time.py +0 -0
  73. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/list_experiment_runs.py +0 -0
  74. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/open_tables.py +0 -0
  75. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/path_glob.py +0 -0
  76. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/path_pattern.py +0 -0
  77. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/read_experiment.py +0 -0
  78. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/read_file.py +0 -0
  79. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/read_run.py +0 -0
  80. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tables/summarize_experiments.py +0 -0
  81. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/templates/base.html +0 -0
  82. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/templates/experiment.html +0 -0
  83. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/templates/index.html +0 -0
  84. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/templates/macros.html +0 -0
  85. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tui/__init__.py +0 -0
  86. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tui/dim.py +0 -0
  87. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tui/format_elapsed.py +0 -0
  88. {labnotes-0.0.28 → labnotes-0.0.30}/src/labnotes/tui/run_tui.py +0 -0
  89. {labnotes-0.0.28 → labnotes-0.0.30}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.28
3
+ Version: 0.0.30
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>
@@ -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. `+` and
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.
@@ -14,6 +14,7 @@ class Controls:
14
14
  run_next: Callable[[str], None]
15
15
  set_concurrency: Callable[[int], None]
16
16
  toggle: Callable[[str], None]
17
+ kill: Callable[[str], None]
17
18
 
18
19
 
19
20
  Listen = Callable[[Controls], None]
@@ -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] = set()
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 signal_runs(signal_number: int) -> None:
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, lambda run: None, lambda count: None, lambda run: 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
- self.set_cell(run, "status", outcome)
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