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