labnotes 0.0.30__tar.gz → 0.0.32__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 (90) hide show
  1. {labnotes-0.0.30 → labnotes-0.0.32}/PKG-INFO +1 -1
  2. labnotes-0.0.32/src/labnotes/check_counts.py +17 -0
  3. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/context.md +2 -1
  4. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/list_experiments.py +2 -0
  5. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/load_experiment.py +2 -0
  6. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/models.py +2 -0
  7. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/describe_event.py +4 -0
  8. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/run_experiment.py +25 -4
  9. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/run_headless.py +1 -0
  10. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/static/labnotes.css +1 -0
  11. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/templates/experiment.html +1 -1
  12. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/templates/index.html +3 -1
  13. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tui/run_app.py +41 -10
  14. {labnotes-0.0.30 → labnotes-0.0.32}/.gitignore +0 -0
  15. {labnotes-0.0.30 → labnotes-0.0.32}/CHANGELOG.md +0 -0
  16. {labnotes-0.0.30 → labnotes-0.0.32}/MIGRATIONS.md +0 -0
  17. {labnotes-0.0.30 → labnotes-0.0.32}/README.md +0 -0
  18. {labnotes-0.0.30 → labnotes-0.0.32}/pyproject.toml +0 -0
  19. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/__init__.py +0 -0
  20. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/asset_url.py +0 -0
  21. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/cell_progress.py +0 -0
  22. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/cli.py +0 -0
  23. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/context_guide.py +0 -0
  24. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/count_runs.py +0 -0
  25. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/create_app.py +0 -0
  26. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/create_experiment.py +0 -0
  27. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/data_html.py +0 -0
  28. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/default_config_text.py +0 -0
  29. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/default_counts.py +0 -0
  30. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/drain_watch.py +0 -0
  31. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/experiment_notes.py +0 -0
  32. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/experiment_progress.py +0 -0
  33. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/experiment_runs.py +0 -0
  34. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/experiments.py +0 -0
  35. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/format_value.py +0 -0
  36. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/init.py +0 -0
  37. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/list_runs.py +0 -0
  38. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/load_config.py +0 -0
  39. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/notebook_kernel.py +0 -0
  40. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/output_html.py +0 -0
  41. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/parse_run_view.py +0 -0
  42. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/pivot_cells.py +0 -0
  43. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/print_rows.py +0 -0
  44. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/python_blocks.py +0 -0
  45. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/read_config.py +0 -0
  46. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/read_path.py +0 -0
  47. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/render_markdown.py +0 -0
  48. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/__init__.py +0 -0
  49. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/execute_run.py +0 -0
  50. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/lock_path.py +0 -0
  51. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/new_run_id.py +0 -0
  52. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/param_args.py +0 -0
  53. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/param_grid.py +0 -0
  54. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/plan_runs.py +0 -0
  55. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/read_experiment_config.py +0 -0
  56. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/run_folder.py +0 -0
  57. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/shell_command.py +0 -0
  58. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/start_experiment.py +0 -0
  59. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/stop_then_kill.py +0 -0
  60. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/wait_for_group.py +0 -0
  61. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/runner/write_run_json.py +0 -0
  62. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/saved_outputs.py +0 -0
  63. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/serve_app.py +0 -0
  64. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/server.py +0 -0
  65. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/sort_href.py +0 -0
  66. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/sql_quote.py +0 -0
  67. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/static/row-link.js +0 -0
  68. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/static/run-cell.js +0 -0
  69. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/static/view-toggle.js +0 -0
  70. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/stop_idle_kernels.py +0 -0
  71. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/summarize_config.py +0 -0
  72. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/__init__.py +0 -0
  73. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/experiment_rows.py +0 -0
  74. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/file_prefix.py +0 -0
  75. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/iso_time.py +0 -0
  76. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/list_experiment_runs.py +0 -0
  77. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/open_tables.py +0 -0
  78. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/path_glob.py +0 -0
  79. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/path_pattern.py +0 -0
  80. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/read_experiment.py +0 -0
  81. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/read_file.py +0 -0
  82. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/read_run.py +0 -0
  83. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tables/summarize_experiments.py +0 -0
  84. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/templates/base.html +0 -0
  85. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/templates/macros.html +0 -0
  86. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tui/__init__.py +0 -0
  87. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tui/dim.py +0 -0
  88. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tui/format_elapsed.py +0 -0
  89. {labnotes-0.0.30 → labnotes-0.0.32}/src/labnotes/tui/run_tui.py +0 -0
  90. {labnotes-0.0.30 → labnotes-0.0.32}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.30
3
+ Version: 0.0.32
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>
@@ -0,0 +1,17 @@
1
+ from dirsql import DirSQL
2
+
3
+
4
+ async def check_counts(
5
+ tables: DirSQL, source: str, summary: dict[str, object]
6
+ ) -> dict[str, object]:
7
+ rule = str(summary["completed_run"])
8
+ try:
9
+ await tables.query(f"SELECT ({rule}) FROM {source} LIMIT 0")
10
+ except RuntimeError as error:
11
+ # A broken rule counts nothing, so the pages still render and say why.
12
+ return {
13
+ **summary,
14
+ "completed_run": "0",
15
+ "counts_error": f"{rule} doesn't work: {error}",
16
+ }
17
+ return summary
@@ -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. Failed runs show their status in red. Runs not started yet show grayed as `pending`, and as
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,
@@ -3,6 +3,7 @@ from typing import cast
3
3
 
4
4
  from dirsql import DirSQL
5
5
 
6
+ from labnotes.check_counts import check_counts
6
7
  from labnotes.experiment_progress import experiment_progress
7
8
  from labnotes.experiment_runs import experiment_runs
8
9
  from labnotes.summarize_config import summarize_config
@@ -19,6 +20,7 @@ async def list_experiments(tables: DirSQL) -> list[dict[str, object]]:
19
20
  summary = summarize_config(config)
20
21
  varies = cast(dict[str, list[object]], summary["varies"])
21
22
  source = experiment_runs(name, list(varies))
23
+ summary = await check_counts(tables, source, summary)
22
24
  progress = await experiment_progress(tables, source, summary)
23
25
  listed.append(
24
26
  {"name": name, "display_name": row["display_name"], **summary, **progress}
@@ -6,6 +6,7 @@ from typing import cast
6
6
  from dirsql import DirSQL
7
7
 
8
8
  from labnotes.experiment_notes import experiment_notes
9
+ from labnotes.check_counts import check_counts
9
10
  from labnotes.experiment_progress import experiment_progress
10
11
  from labnotes.experiment_runs import experiment_runs
11
12
  from labnotes.list_runs import list_runs
@@ -39,6 +40,7 @@ async def load_experiment(
39
40
  summary = summarize_config(config)
40
41
  varies = cast(dict[str, list[object]], summary["varies"])
41
42
  source = experiment_runs(name, list(varies))
43
+ summary = await check_counts(tables, source, summary)
42
44
  progress = await experiment_progress(tables, source, summary)
43
45
  shown = ["id", *(key for key in varies if key != "id")]
44
46
  view = parse_run_view(params, shown)
@@ -15,6 +15,8 @@ class Controls:
15
15
  set_concurrency: Callable[[int], None]
16
16
  toggle: Callable[[str], None]
17
17
  kill: Callable[[str], None]
18
+ retry: Callable[[str], None]
19
+ finish: Callable[[], None]
18
20
 
19
21
 
20
22
  Listen = Callable[[Controls], None]
@@ -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
- idle = asyncio.Event()
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.set()
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 idle.wait()
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))
@@ -341,6 +341,7 @@ h2 .clear { font-family: var(--sans); font-size: 0.82rem; margin-left: 0.8rem; c
341
341
  .status { font-size: 0.8rem; font-weight: 700; }
342
342
  .status.good { color: var(--good); }
343
343
  .status.bad { color: var(--bad); }
344
+ .counts-error { color: var(--bad); }
344
345
 
345
346
  .record {
346
347
  display: grid;
@@ -66,7 +66,7 @@
66
66
  {% for key, value in e.other.items() %}
67
67
  <dt>{{ key }}</dt><dd><code>{{ value | value }}</code></dd>
68
68
  {% endfor %}
69
- <dt>complete when</dt><dd><code>{{ e.completed_run }}</code></dd>
69
+ <dt>complete when</dt>{% if e.counts_error %}<dd class="counts-error">{{ e.counts_error }}</dd>{% else %}<dd><code>{{ e.completed_run }}</code></dd>{% endif %}
70
70
  </dl>
71
71
  </section>
72
72
 
@@ -37,7 +37,9 @@
37
37
  </td>
38
38
  <td class="num" data-label="Runs">{{ experiment.runs }}</td>
39
39
  <td class="progress-col">
40
- {% if experiment.filled is not none and experiment.target %}
40
+ {% if experiment.counts_error %}
41
+ <span class="counts-error" title="{{ experiment.counts_error }}">counts rule doesn't work</span>
42
+ {% elif experiment.filled is not none and experiment.target %}
41
43
  <div class="progress-line">
42
44
  {{ meter(experiment.filled, experiment.target) }}
43
45
  <span class="num">{{ experiment.filled }}<span class="of">/{{ experiment.target }}</span></span>
@@ -23,6 +23,8 @@ class RunApp(App[None]):
23
23
  Binding("d", "toggle_run", "Deselect"),
24
24
  Binding("D", "deselect_shown", "Deselect shown"),
25
25
  Binding("k", "kill_run", "Stop run"),
26
+ Binding("h", "hide_failed", "Hide failed"),
27
+ Binding("r", "retry", "Retry"),
26
28
  Binding("slash", "filter", "Filter", key_display="/"),
27
29
  Binding("escape", "clear_filter", "Clear filter"),
28
30
  Binding("plus,equals_sign", "concurrency(1)", "More at once", key_display="+"),
@@ -41,13 +43,18 @@ class RunApp(App[None]):
41
43
  lambda count: None,
42
44
  lambda run: None,
43
45
  lambda run: None,
46
+ lambda run: None,
47
+ lambda: None,
44
48
  )
45
49
  self.concurrency = 1
46
- self.done = False
50
+ self.idle = False
51
+ self.ended: set[str] = set()
47
52
  self.started: dict[str, float] = {}
48
53
  self.pending: set[str] = set()
49
54
  self.cells: dict[str, list[object]] = {}
50
55
  self.filter = ""
56
+ self.failed: set[str] = set()
57
+ self.hiding = False
51
58
 
52
59
  def compose(self) -> ComposeResult:
53
60
  yield Header()
@@ -68,10 +75,7 @@ class RunApp(App[None]):
68
75
 
69
76
  async def drive(self) -> None:
70
77
  self.results = await self.start(self.show_event, self.listen)
71
- self.done = True
72
- failed = sum(result["exit_code"] != 0 for result in self.results)
73
- passed = len(self.results) - failed
74
- self.show(f"{passed} passed, {failed} failed. q to quit.")
78
+ self.exit()
75
79
 
76
80
  def listen(self, controls: Controls) -> None:
77
81
  self.controls = controls
@@ -91,6 +95,7 @@ class RunApp(App[None]):
91
95
  self.show_concurrency(event)
92
96
  self.show(describe_event(event))
93
97
  case "started":
98
+ self.idle = False
94
99
  self.pending.discard(run)
95
100
  self.started[run] = time.monotonic()
96
101
  args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
@@ -100,9 +105,13 @@ class RunApp(App[None]):
100
105
  case "ended":
101
106
  outcome = event["error"] or f"exited {event['exit_code']}"
102
107
  style = "red" if event["error"] or event["exit_code"] else ""
108
+ if style:
109
+ self.failed.add(run)
110
+ self.ended.add(run)
103
111
  self.set_cell(run, "status", Text(str(outcome), style=style))
104
112
  self.tick()
105
113
  del self.started[run]
114
+ self.redraw(table.cursor_row)
106
115
  case "queued":
107
116
  self.reorder(event, table.cursor_row)
108
117
  table.move_cursor(row=table.get_row_index(run))
@@ -117,6 +126,18 @@ class RunApp(App[None]):
117
126
  self.set_cell(run, "status", dim("pending"))
118
127
  self.reorder(event, table.cursor_row)
119
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.")
120
141
  case "concurrency":
121
142
  self.show_concurrency(event)
122
143
  self.show(describe_event(event))
@@ -137,7 +158,8 @@ class RunApp(App[None]):
137
158
  table = self.query_one(DataTable)
138
159
  table.clear()
139
160
  for run, cells in self.cells.items():
140
- if self.filter in str(cells[1]):
161
+ hidden = self.hiding and run in self.failed
162
+ if self.filter in str(cells[1]) and not hidden:
141
163
  table.add_row(*cells, key=run)
142
164
  table.move_cursor(row=row)
143
165
 
@@ -159,14 +181,14 @@ class RunApp(App[None]):
159
181
  self.query_one("#status", Static).update(text)
160
182
 
161
183
  def action_interrupt(self) -> None:
162
- if self.done:
163
- self.exit()
184
+ if self.idle:
185
+ self.controls.finish()
164
186
  else:
165
187
  self.controls.interrupt()
166
188
 
167
189
  async def action_quit(self) -> None:
168
- if self.done:
169
- self.exit()
190
+ if self.idle:
191
+ self.controls.finish()
170
192
  else:
171
193
  self.show("Runs are still going. Ctrl-C stops them.")
172
194
 
@@ -185,6 +207,15 @@ class RunApp(App[None]):
185
207
  if (run := self.cursor_run()) is not None:
186
208
  self.controls.toggle(run)
187
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
+
188
219
  def action_kill_run(self) -> None:
189
220
  if (run := self.cursor_run()) is not None:
190
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