labnotes 0.0.26__tar.gz → 0.0.28__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.26 → labnotes-0.0.28}/PKG-INFO +1 -1
  2. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/context.md +3 -1
  3. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tui/run_app.py +61 -19
  4. {labnotes-0.0.26 → labnotes-0.0.28}/.gitignore +0 -0
  5. {labnotes-0.0.26 → labnotes-0.0.28}/CHANGELOG.md +0 -0
  6. {labnotes-0.0.26 → labnotes-0.0.28}/MIGRATIONS.md +0 -0
  7. {labnotes-0.0.26 → labnotes-0.0.28}/README.md +0 -0
  8. {labnotes-0.0.26 → labnotes-0.0.28}/pyproject.toml +0 -0
  9. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/__init__.py +0 -0
  10. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/asset_url.py +0 -0
  11. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/cell_progress.py +0 -0
  12. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/cli.py +0 -0
  13. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/context_guide.py +0 -0
  14. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/count_runs.py +0 -0
  15. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/create_app.py +0 -0
  16. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/create_experiment.py +0 -0
  17. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/data_html.py +0 -0
  18. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/default_config_text.py +0 -0
  19. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/default_counts.py +0 -0
  20. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/drain_watch.py +0 -0
  21. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/experiment_notes.py +0 -0
  22. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/experiment_progress.py +0 -0
  23. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/experiment_runs.py +0 -0
  24. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/experiments.py +0 -0
  25. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/format_value.py +0 -0
  26. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/init.py +0 -0
  27. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/list_experiments.py +0 -0
  28. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/list_runs.py +0 -0
  29. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/load_config.py +0 -0
  30. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/load_experiment.py +0 -0
  31. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/models.py +0 -0
  32. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/notebook_kernel.py +0 -0
  33. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/output_html.py +0 -0
  34. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/parse_run_view.py +0 -0
  35. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/pivot_cells.py +0 -0
  36. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/print_rows.py +0 -0
  37. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/python_blocks.py +0 -0
  38. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/read_config.py +0 -0
  39. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/read_path.py +0 -0
  40. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/render_markdown.py +0 -0
  41. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/__init__.py +0 -0
  42. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/describe_event.py +0 -0
  43. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/execute_run.py +0 -0
  44. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/lock_path.py +0 -0
  45. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/new_run_id.py +0 -0
  46. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/param_args.py +0 -0
  47. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/param_grid.py +0 -0
  48. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/plan_runs.py +0 -0
  49. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/read_experiment_config.py +0 -0
  50. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/run_experiment.py +0 -0
  51. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/run_folder.py +0 -0
  52. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/run_headless.py +0 -0
  53. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/shell_command.py +0 -0
  54. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/start_experiment.py +0 -0
  55. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/stop_then_kill.py +0 -0
  56. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/wait_for_group.py +0 -0
  57. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/runner/write_run_json.py +0 -0
  58. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/saved_outputs.py +0 -0
  59. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/serve_app.py +0 -0
  60. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/server.py +0 -0
  61. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/sort_href.py +0 -0
  62. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/sql_quote.py +0 -0
  63. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/static/labnotes.css +0 -0
  64. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/static/row-link.js +0 -0
  65. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/static/run-cell.js +0 -0
  66. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/static/view-toggle.js +0 -0
  67. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/stop_idle_kernels.py +0 -0
  68. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/summarize_config.py +0 -0
  69. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/__init__.py +0 -0
  70. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/experiment_rows.py +0 -0
  71. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/file_prefix.py +0 -0
  72. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/iso_time.py +0 -0
  73. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/list_experiment_runs.py +0 -0
  74. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/open_tables.py +0 -0
  75. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/path_glob.py +0 -0
  76. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/path_pattern.py +0 -0
  77. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/read_experiment.py +0 -0
  78. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/read_file.py +0 -0
  79. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/read_run.py +0 -0
  80. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tables/summarize_experiments.py +0 -0
  81. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/templates/base.html +0 -0
  82. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/templates/experiment.html +0 -0
  83. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/templates/index.html +0 -0
  84. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/templates/macros.html +0 -0
  85. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tui/__init__.py +0 -0
  86. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tui/dim.py +0 -0
  87. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tui/format_elapsed.py +0 -0
  88. {labnotes-0.0.26 → labnotes-0.0.28}/src/labnotes/tui/run_tui.py +0 -0
  89. {labnotes-0.0.26 → labnotes-0.0.28}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.26
3
+ Version: 0.0.28
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>
@@ -89,7 +89,9 @@ In a terminal, `run` shows a live table of runs (status and time) and stays
89
89
  open when they finish. 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
- the end of the queue. `+` and `-` change how many run at once; lowering it lets running runs
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
94
+ `-` change how many run at once; lowering it lets running runs
93
95
  finish; `q` quits. Elsewhere, or with `--no-tui`, it prints a
94
96
  line per event instead.
95
97
 
@@ -3,7 +3,7 @@ import time
3
3
 
4
4
  from textual.app import App, ComposeResult
5
5
  from textual.binding import Binding
6
- from textual.widgets import DataTable, Footer, Header, Static
6
+ from textual.widgets import DataTable, Footer, Header, Input, Static
7
7
 
8
8
  from labnotes.models import Controls, Start
9
9
  from labnotes.runner.describe_event import describe_event
@@ -20,6 +20,9 @@ class RunApp(App[None]):
20
20
  Binding("ctrl+c", "interrupt", "Stop runs"),
21
21
  Binding("n", "run_next", "Run next"),
22
22
  Binding("d", "toggle_run", "Deselect"),
23
+ Binding("D", "deselect_shown", "Deselect shown"),
24
+ Binding("slash", "filter", "Filter", key_display="/"),
25
+ Binding("escape", "clear_filter", "Clear filter"),
23
26
  Binding("plus,equals_sign", "concurrency(1)", "More at once", key_display="+"),
24
27
  Binding("minus", "concurrency(-1)", "Fewer at once", key_display="-"),
25
28
  Binding("q", "quit", "Quit"),
@@ -37,10 +40,13 @@ class RunApp(App[None]):
37
40
  self.done = False
38
41
  self.started: dict[str, float] = {}
39
42
  self.pending: set[str] = set()
43
+ self.cells: dict[str, list[object]] = {}
44
+ self.filter = ""
40
45
 
41
46
  def compose(self) -> ComposeResult:
42
47
  yield Header()
43
48
  yield DataTable(id="runs", cursor_type="row")
49
+ yield Input(id="filter", placeholder="Filter params")
44
50
  yield Static(id="status")
45
51
  yield Footer()
46
52
 
@@ -48,6 +54,7 @@ class RunApp(App[None]):
48
54
  table = self.query_one(DataTable)
49
55
  for column in COLUMNS:
50
56
  table.add_column(column, key=column)
57
+ self.query_one(Input).display = False
51
58
 
52
59
  def on_ready(self) -> None:
53
60
  self.set_interval(1, self.tick)
@@ -72,8 +79,9 @@ class RunApp(App[None]):
72
79
  for planned in event["runs"]: # type: ignore[attr-defined]
73
80
  args = " ".join(param_args(planned["params"]))
74
81
  cells = (planned["run"], args, "pending", "")
75
- table.add_row(*map(dim, cells), key=planned["run"])
82
+ self.cells[planned["run"]] = [*map(dim, cells)]
76
83
  self.pending.add(planned["run"])
84
+ self.redraw(table.cursor_row)
77
85
  self.show_concurrency(event)
78
86
  self.show(describe_event(event))
79
87
  case "started":
@@ -82,54 +90,63 @@ class RunApp(App[None]):
82
90
  args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
83
91
  cells = (run, args, "running", format_elapsed(0))
84
92
  for column, cell in zip(COLUMNS, cells):
85
- table.update_cell(run, column, cell, update_width=True)
93
+ self.set_cell(run, column, cell)
86
94
  case "ended":
87
95
  outcome = event["error"] or f"exited {event['exit_code']}"
88
- table.update_cell(run, "status", outcome, update_width=True)
96
+ self.set_cell(run, "status", outcome)
89
97
  self.tick()
90
98
  del self.started[run]
91
99
  case "queued":
92
- self.reorder(event)
100
+ self.reorder(event, table.cursor_row)
93
101
  table.move_cursor(row=table.get_row_index(run))
94
102
  self.show(describe_event(event))
95
103
  case "deselected":
96
104
  self.pending.discard(run)
97
- table.update_cell(run, "status", dim("deselected"))
98
- self.reorder(event)
105
+ self.set_cell(run, "status", dim("deselected"))
106
+ self.reorder(event, table.cursor_row)
99
107
  self.show(describe_event(event))
100
108
  case "reselected":
101
109
  self.pending.add(run)
102
- table.update_cell(run, "status", dim("pending"))
103
- self.reorder(event)
110
+ self.set_cell(run, "status", dim("pending"))
111
+ self.reorder(event, table.cursor_row)
104
112
  self.show(describe_event(event))
105
113
  case "concurrency":
106
114
  self.show_concurrency(event)
107
115
  self.show(describe_event(event))
108
116
  case "stopping":
109
117
  for pending in self.pending:
110
- table.update_cell(pending, "status", dim("skipped"))
118
+ self.set_cell(pending, "status", dim("skipped"))
111
119
  self.show(describe_event(event))
112
120
  case _:
113
121
  self.show(describe_event(event))
114
122
 
115
- def reorder(self, event: dict[str, object]) -> None:
116
- table = self.query_one(DataTable)
123
+ def reorder(self, event: dict[str, object], row: int) -> None:
117
124
  waiting: list[str] = [*event["pending"], *event["held"]] # type: ignore[misc]
118
125
  for run in waiting:
119
- row = table.get_row(run)
120
- table.remove_row(run)
121
- table.add_row(*row, key=run)
126
+ self.cells[run] = self.cells.pop(run)
127
+ self.redraw(row)
128
+
129
+ def redraw(self, row: int) -> None:
130
+ table = self.query_one(DataTable)
131
+ table.clear()
132
+ for run, cells in self.cells.items():
133
+ if self.filter in str(cells[1]):
134
+ table.add_row(*cells, key=run)
135
+ table.move_cursor(row=row)
136
+
137
+ def set_cell(self, run: str, column: str, cell: object) -> None:
138
+ self.cells[run][COLUMNS.index(column)] = cell
139
+ table = self.query_one(DataTable)
140
+ if run in table.rows:
141
+ table.update_cell(run, column, cell, update_width=True)
122
142
 
123
143
  def show_concurrency(self, event: dict[str, object]) -> None:
124
144
  self.concurrency = int(event["concurrency"]) # type: ignore[call-overload]
125
145
  self.sub_title = f"{self.concurrency} at a time"
126
146
 
127
147
  def tick(self) -> None:
128
- table = self.query_one(DataTable)
129
148
  for run, began in self.started.items():
130
- table.update_cell(
131
- run, "time", format_elapsed(time.monotonic() - began), update_width=True
132
- )
149
+ self.set_cell(run, "time", format_elapsed(time.monotonic() - began))
133
150
 
134
151
  def show(self, text: str) -> None:
135
152
  self.query_one("#status", Static).update(text)
@@ -161,5 +178,30 @@ class RunApp(App[None]):
161
178
  if (run := self.cursor_run()) is not None:
162
179
  self.controls.toggle(run)
163
180
 
181
+ def action_deselect_shown(self) -> None:
182
+ shown = [str(key.value) for key in self.query_one(DataTable).rows]
183
+ for run in shown:
184
+ if run in self.pending:
185
+ self.controls.toggle(run)
186
+
164
187
  def action_concurrency(self, change: int) -> None:
165
188
  self.controls.set_concurrency(self.concurrency + change)
189
+
190
+ def action_filter(self) -> None:
191
+ field = self.query_one(Input)
192
+ field.display = True
193
+ field.focus()
194
+
195
+ def action_clear_filter(self) -> None:
196
+ field = self.query_one(Input)
197
+ field.value = ""
198
+ field.display = False
199
+ self.query_one(DataTable).focus()
200
+
201
+ def on_input_changed(self, event: Input.Changed) -> None:
202
+ self.filter = event.value
203
+ self.redraw(self.query_one(DataTable).cursor_row)
204
+
205
+ def on_input_submitted(self, event: Input.Submitted) -> None:
206
+ event.input.display = bool(event.value)
207
+ self.query_one(DataTable).focus()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes