labnotes 0.2.8__tar.gz → 0.2.9__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 (229) hide show
  1. {labnotes-0.2.8 → labnotes-0.2.9}/PKG-INFO +1 -1
  2. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/load_experiment.py +8 -2
  3. labnotes-0.2.9/src/labnotes/param_names.py +9 -0
  4. labnotes-0.2.9/src/labnotes/run_columns.py +7 -0
  5. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/read_experiment_config.py +10 -4
  6. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/browse_app.py +2 -16
  7. labnotes-0.2.9/src/labnotes/tui/experiment_screen.py +143 -0
  8. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/experiments_screen.py +2 -10
  9. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/format_cell.py +4 -2
  10. labnotes-0.2.9/src/labnotes/tui/guard_view.py +35 -0
  11. labnotes-0.2.9/src/labnotes/tui/load_view.py +27 -0
  12. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/native_rows.py +3 -3
  13. labnotes-0.2.9/src/labnotes/tui/run_cells.py +12 -0
  14. labnotes-0.2.8/src/labnotes/tui/run_screen.py → labnotes-0.2.9/src/labnotes/tui/run_panel.py +5 -8
  15. labnotes-0.2.9/src/labnotes/tui/runs_browser.py +132 -0
  16. labnotes-0.2.9/src/labnotes/tui/table_columns.py +8 -0
  17. labnotes-0.2.9/src/labnotes/tui/table_rows.py +12 -0
  18. labnotes-0.2.9/src/labnotes/tui/view_pane.py +54 -0
  19. labnotes-0.2.9/src/labnotes/view/__init__.py +22 -0
  20. labnotes-0.2.9/src/labnotes/view/changed.py +19 -0
  21. labnotes-0.2.9/src/labnotes/view/experiment.py +20 -0
  22. labnotes-0.2.9/src/labnotes/view_specs.py +10 -0
  23. {labnotes-0.2.8 → labnotes-0.2.9}/testing-conventions.toml +3 -15
  24. labnotes-0.2.8/src/labnotes/tui/experiment_screen.py +0 -154
  25. labnotes-0.2.8/src/labnotes/tui/group_cells.py +0 -36
  26. labnotes-0.2.8/src/labnotes/tui/invocation_columns.py +0 -9
  27. labnotes-0.2.8/src/labnotes/tui/invocation_rows.py +0 -32
  28. labnotes-0.2.8/src/labnotes/tui/run_cells.py +0 -22
  29. labnotes-0.2.8/src/labnotes/view/__init__.py +0 -3
  30. {labnotes-0.2.8 → labnotes-0.2.9}/.gitignore +0 -0
  31. {labnotes-0.2.8 → labnotes-0.2.9}/CHANGELOG.md +0 -0
  32. {labnotes-0.2.8 → labnotes-0.2.9}/MIGRATIONS.md +0 -0
  33. {labnotes-0.2.8 → labnotes-0.2.9}/README.md +0 -0
  34. {labnotes-0.2.8 → labnotes-0.2.9}/pyproject.toml +0 -0
  35. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/__init__.py +0 -0
  36. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/__main__.py +0 -0
  37. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/asset_url.py +0 -0
  38. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/check_counts.py +0 -0
  39. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/cli.py +0 -0
  40. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/command_line_group.py +0 -0
  41. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/compose_experiment.py +0 -0
  42. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/config_migration.py +0 -0
  43. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/context.md +0 -0
  44. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/context_guide.py +0 -0
  45. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/copy_experiment.py +0 -0
  46. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/count_groups.py +0 -0
  47. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/count_runs.py +0 -0
  48. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/create_app.py +0 -0
  49. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/create_experiment.py +0 -0
  50. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/create_note.py +0 -0
  51. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/data_html.py +0 -0
  52. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/default_config_text.py +0 -0
  53. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/default_counts.py +0 -0
  54. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/drain_watch.py +0 -0
  55. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/experiment_definition.py +0 -0
  56. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/experiment_notes.py +0 -0
  57. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/experiment_runs.py +0 -0
  58. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/experiments.py +0 -0
  59. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/file_changes.py +0 -0
  60. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/format_duration.py +0 -0
  61. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/format_started.py +0 -0
  62. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/format_value.py +0 -0
  63. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/get_state.py +0 -0
  64. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/grid_cell.py +0 -0
  65. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/group_runs.py +0 -0
  66. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/init.py +0 -0
  67. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/invocation_started.py +0 -0
  68. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/is_non_negative_int.py +0 -0
  69. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/is_non_negative_number.py +0 -0
  70. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/is_positive_int.py +0 -0
  71. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/latest_log.py +0 -0
  72. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/list_experiments.py +0 -0
  73. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/list_runs.py +0 -0
  74. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/load_config.py +0 -0
  75. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/load_run.py +0 -0
  76. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/log.py +0 -0
  77. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/log_path.py +0 -0
  78. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/memory_interval.py +0 -0
  79. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/models.py +0 -0
  80. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/nests_runs.py +0 -0
  81. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/note_path.py +0 -0
  82. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/notebook_kernel.py +0 -0
  83. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/output_html.py +0 -0
  84. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/parse_param_value.py +0 -0
  85. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/parse_params.py +0 -0
  86. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/parse_run_view.py +0 -0
  87. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/parse_subset.py +0 -0
  88. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/pivot_runs.py +0 -0
  89. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/print_rows.py +0 -0
  90. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/python_blocks.py +0 -0
  91. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/read_config.py +0 -0
  92. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/read_path.py +0 -0
  93. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/render_markdown.py +0 -0
  94. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/retries.py +0 -0
  95. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/run_output.py +0 -0
  96. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/__init__.py +0 -0
  97. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/cgroup_memory.py +0 -0
  98. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/check_args.py +0 -0
  99. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/check_hook_placeholders.py +0 -0
  100. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/check_subset.py +0 -0
  101. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/claim_run.py +0 -0
  102. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/command_flags.py +0 -0
  103. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/container_cgroup.py +0 -0
  104. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/container_memory.py +0 -0
  105. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/continue_run.py +0 -0
  106. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/describe_event.py +0 -0
  107. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/docker_output.py +0 -0
  108. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/execute_run.py +0 -0
  109. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/exit_signal.py +0 -0
  110. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/fill_dirs.py +0 -0
  111. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/fill_experiment_env.py +0 -0
  112. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/finalize_lost_runs.py +0 -0
  113. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/first_invocation.py +0 -0
  114. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/flag_name.py +0 -0
  115. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/group_args.py +0 -0
  116. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/hold_experiment.py +0 -0
  117. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/in_subset.py +0 -0
  118. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/invocation_hook.py +0 -0
  119. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/invocation_meta.py +0 -0
  120. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/new_run_id.py +0 -0
  121. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  122. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/override_flags.py +0 -0
  123. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/param_args.py +0 -0
  124. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/param_grid.py +0 -0
  125. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/placeholders.py +0 -0
  126. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/plan_runs.py +0 -0
  127. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/prepare_experiment.py +0 -0
  128. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/project_snapshot.py +0 -0
  129. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/record_memory.py +0 -0
  130. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/run_experiment.py +0 -0
  131. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/run_folder.py +0 -0
  132. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/run_git.py +0 -0
  133. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/run_headless.py +0 -0
  134. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/run_locked.py +0 -0
  135. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/runs_parent.py +0 -0
  136. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/shell_command.py +0 -0
  137. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/signal_tree.py +0 -0
  138. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/start_experiment.py +0 -0
  139. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/stop_then_kill.py +0 -0
  140. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/untracked_diff.py +0 -0
  141. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/wait_for_tree.py +0 -0
  142. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runner/write_run_json.py +0 -0
  143. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/runs_view_command.py +0 -0
  144. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/save_conflict_copy.py +0 -0
  145. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/save_note.py +0 -0
  146. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/saved_outputs.py +0 -0
  147. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/serve_app.py +0 -0
  148. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/server.py +0 -0
  149. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/set_state.py +0 -0
  150. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/shared_params.py +0 -0
  151. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/sort_href.py +0 -0
  152. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/sql_quote.py +0 -0
  153. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/state_path.py +0 -0
  154. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/elapsed.js +0 -0
  155. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/experiment.js +0 -0
  156. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/labnotes.css +0 -0
  157. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/local-time.js +0 -0
  158. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/new-note.js +0 -0
  159. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/note-edit.js +0 -0
  160. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/note-editor.js +0 -0
  161. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/row-link.js +0 -0
  162. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/run-cell.js +0 -0
  163. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/run-output.js +0 -0
  164. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/static/view-toggle.js +0 -0
  165. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/summarize_config.py +0 -0
  166. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/__init__.py +0 -0
  167. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/experiment_rows.py +0 -0
  168. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/experiment_run_ids.py +0 -0
  169. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/experiment_toml.py +0 -0
  170. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/experiments_ddl.py +0 -0
  171. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/file_prefix.py +0 -0
  172. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/file_text.py +0 -0
  173. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/iso_time.py +0 -0
  174. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/list_experiment_runs.py +0 -0
  175. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/list_resumable_runs.py +0 -0
  176. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/note_text.py +0 -0
  177. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/open_tables.py +0 -0
  178. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/path_glob.py +0 -0
  179. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/path_pattern.py +0 -0
  180. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_blocks.py +0 -0
  181. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_experiment.py +0 -0
  182. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_file.py +0 -0
  183. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_log.py +0 -0
  184. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_memory.py +0 -0
  185. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_run.py +0 -0
  186. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/read_run_file.py +0 -0
  187. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/runs_columns.py +0 -0
  188. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tables/summarize_experiments.py +0 -0
  189. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/base.html +0 -0
  190. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/editor_imports.html +0 -0
  191. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/experiment.html +0 -0
  192. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/index.html +0 -0
  193. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/note.html +0 -0
  194. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/templates/run.html +0 -0
  195. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/__init__.py +0 -0
  196. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/browse_tui.py +0 -0
  197. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/check_view_response.py +0 -0
  198. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/config.py +0 -0
  199. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/dim.py +0 -0
  200. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/fill_table.py +0 -0
  201. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/format_elapsed.py +0 -0
  202. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/leave_prompt.py +0 -0
  203. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/log_block.py +0 -0
  204. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/log_blocks.py +0 -0
  205. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/reload_on_change.py +0 -0
  206. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/run_app.py +0 -0
  207. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/run_output.py +0 -0
  208. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/run_tui.py +0 -0
  209. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/runs_table.py +0 -0
  210. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/runs_view.py +0 -0
  211. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/stopped_title.py +0 -0
  212. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/view_column.py +0 -0
  213. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/tui/view_process.py +0 -0
  214. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/__init__.py +0 -0
  215. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/format_bytes.py +0 -0
  216. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/process_alive.py +0 -0
  217. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/run_usage.py +0 -0
  218. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/tree_rss.py +0 -0
  219. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/utils/tree_usage.py +0 -0
  220. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/view/answer.py +0 -0
  221. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/view/build.py +0 -0
  222. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/view/respond.py +0 -0
  223. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/view/serve.py +0 -0
  224. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/wait_for_note.py +0 -0
  225. {labnotes-0.2.8 → labnotes-0.2.9}/src/labnotes/watch_rows.py +0 -0
  226. {labnotes-0.2.8 → labnotes-0.2.9}/tests/conftest.py +0 -0
  227. {labnotes-0.2.8 → labnotes-0.2.9}/tests/e2e/conftest.py +0 -0
  228. {labnotes-0.2.8 → labnotes-0.2.9}/tests/integration/conftest.py +0 -0
  229. {labnotes-0.2.8 → labnotes-0.2.9}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.2.8
3
+ Version: 0.2.9
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>
@@ -19,9 +19,11 @@ from labnotes.parse_run_view import parse_run_view
19
19
  from labnotes.pivot_runs import pivot_runs
20
20
  from labnotes.python_blocks import python_blocks
21
21
  from labnotes.render_markdown import render_markdown
22
+ from labnotes.run_columns import run_columns
22
23
  from labnotes.runs_view_command import runs_view_command
23
24
  from labnotes.sort_href import sort_href
24
25
  from labnotes.sql_quote import quote_string
26
+ from labnotes.view_specs import view_specs
25
27
  from labnotes.summarize_config import summarize_config
26
28
  from labnotes.utils.process_alive import process_alive
27
29
 
@@ -49,7 +51,7 @@ async def load_experiment(
49
51
  varies = cast(dict[str, list[object]], summary["varies"])
50
52
  source = experiment_runs(name, list(varies))
51
53
  summary = await check_counts(tables, source, summary)
52
- shown = ["id", *(key for key in varies if key != "id"), "started", "duration"]
54
+ shown = run_columns(config)
53
55
  choices = {
54
56
  key: {format_value(value): json.dumps(value) for value in values}
55
57
  for key, values in varies.items()
@@ -70,7 +72,9 @@ async def load_experiment(
70
72
  pid, created if isinstance(created, int | float) else None
71
73
  )
72
74
 
73
- runs_live = any(is_running(run) for run in runs)
75
+ for run in runs:
76
+ run["labnotes_running"] = is_running(run)
77
+ runs_live = any(run["labnotes_running"] for run in runs)
74
78
  nested = await nests_runs(tables, source)
75
79
  groups = (
76
80
  group_runs(runs, list(choices), await count_groups(tables, source))
@@ -92,6 +96,7 @@ async def load_experiment(
92
96
  return {
93
97
  "name": row["name"],
94
98
  "display_name": row["display_name"],
99
+ "config": config,
95
100
  "config_source": row["source"],
96
101
  "counts_set": "counts" in (config or {}),
97
102
  **summary,
@@ -115,6 +120,7 @@ async def load_experiment(
115
120
  "run_view": view,
116
121
  "run_choices": choices,
117
122
  "runs_view": runs_view_command(config),
123
+ "views": view_specs(config),
118
124
  "sort_hrefs": {
119
125
  column: sort_href(view, column) for column in ["status", *shown]
120
126
  },
@@ -0,0 +1,9 @@
1
+ from collections.abc import Mapping
2
+ from typing import cast
3
+
4
+ from labnotes.summarize_config import summarize_config
5
+
6
+
7
+ def param_names(config: Mapping[str, object] | None) -> list[str]:
8
+ summary = summarize_config(None if config is None else dict(config))
9
+ return [key for key in cast(dict[str, object], summary["varies"]) if key != "id"]
@@ -0,0 +1,7 @@
1
+ from collections.abc import Mapping
2
+
3
+ from labnotes.param_names import param_names
4
+
5
+
6
+ def run_columns(config: Mapping[str, object] | None) -> list[str]:
7
+ return ["id", *param_names(config), "started", "duration"]
@@ -30,7 +30,7 @@ SETTINGS = {
30
30
  "from",
31
31
  }
32
32
  HOOKS = ("setup", "teardown")
33
- VIEWS = {"runs"}
33
+ MAX_VIEWS = 8
34
34
 
35
35
 
36
36
  def read_experiment_config(
@@ -107,12 +107,18 @@ def read_experiment_config(
107
107
  views = values.get("views", {})
108
108
  if not isinstance(views, dict):
109
109
  raise ValueError(f"{path}: views must be a table, like [views]")
110
- unknown = sorted(views.keys() - VIEWS)
111
- if unknown:
112
- raise ValueError(f"{path}: unknown keys in views: {', '.join(unknown)}")
113
110
  runs = views.get("runs")
114
111
  if runs is not None and (not isinstance(runs, str) or not runs.strip()):
115
112
  raise ValueError(f"{path}: views.runs must be a command")
113
+ shown = {name: spec for name, spec in views.items() if name != "runs"}
114
+ if len(shown) > MAX_VIEWS:
115
+ raise ValueError(f"{path}: views holds at most {MAX_VIEWS} views besides runs")
116
+ for name, spec in shown.items():
117
+ source, _, cls = spec.rpartition(":") if isinstance(spec, str) else ("", "", "")
118
+ if not source.strip() or not cls.strip():
119
+ raise ValueError(
120
+ f'{path}: views.{name} must be "path/to/file.py:ClassName"'
121
+ )
116
122
  return ExperimentConfig(
117
123
  command,
118
124
  repeats,
@@ -5,8 +5,6 @@ from labnotes.models import LabnotesConfig
5
5
  from labnotes.tables.open_tables import open_tables
6
6
  from labnotes.tui.experiment_screen import ExperimentScreen
7
7
  from labnotes.tui.experiments_screen import ExperimentsScreen
8
- from labnotes.tui.runs_view import Request
9
- from labnotes.tui.view_process import ViewProcess
10
8
 
11
9
 
12
10
  class BrowseApp(App[BaseException | None]):
@@ -17,23 +15,11 @@ class BrowseApp(App[BaseException | None]):
17
15
  self.config = config
18
16
  self.experiment_name = name
19
17
  self.title = config.root.name
20
- self.views: dict[str, ViewProcess] = {}
21
18
 
22
19
  def on_mount(self) -> None:
23
20
  tables = open_tables(self.config, run_files=True)
24
- self.push_screen(ExperimentsScreen(tables, self.config, self.view_request))
21
+ self.push_screen(ExperimentsScreen(tables, self.config))
25
22
  if self.experiment_name is not None:
26
23
  self.push_screen(
27
- ExperimentScreen(
28
- tables, self.config, self.experiment_name, self.view_request
29
- )
24
+ ExperimentScreen(tables, self.config, self.experiment_name)
30
25
  )
31
-
32
- def view_request(self, command: str) -> Request:
33
- if command not in self.views:
34
- self.views[command] = ViewProcess(command, self.config.root)
35
- return self.views[command].request
36
-
37
- async def on_unmount(self) -> None:
38
- for process in self.views.values():
39
- await process.close()
@@ -0,0 +1,143 @@
1
+ from collections.abc import Mapping
2
+ from types import MappingProxyType
3
+ from typing import Any
4
+
5
+ from dirsql import DirSQL
6
+ from starlette.datastructures import QueryParams
7
+ from textual.app import ComposeResult
8
+ from textual.binding import Binding
9
+ from textual.screen import Screen
10
+ from textual.widget import Widget
11
+ from textual.widgets import ContentSwitcher, Footer, Header
12
+ from textual.worker import Worker, WorkerState
13
+
14
+ from labnotes.load_experiment import load_experiment
15
+ from labnotes.models import LabnotesConfig
16
+ from labnotes.tui.native_rows import native_rows
17
+ from labnotes.tui.reload_on_change import reload_on_change
18
+ from labnotes.tui.stopped_title import stopped_title
19
+ from labnotes.tui.view_pane import ViewPane
20
+ from labnotes.view.experiment import Experiment
21
+ from labnotes.watch_rows import watch_rows
22
+
23
+ WATCHED = {"runs", "experiment_configs"}
24
+ BUILT_IN = "labnotes.tui.runs_browser:RunsBrowser"
25
+ MAX_VIEWS = 8
26
+
27
+
28
+ class ExperimentScreen(Screen[None]):
29
+ BINDINGS = [Binding("escape", "back", "Back")]
30
+
31
+ def __init__(
32
+ self,
33
+ tables: DirSQL,
34
+ config: LabnotesConfig,
35
+ name: str,
36
+ ) -> None:
37
+ super().__init__()
38
+ self.tables = tables
39
+ self.config = config
40
+ self.experiment_name = name
41
+ self.found = False
42
+ self.shown: Experiment | None = None
43
+ self.left: dict[str, Widget] = {}
44
+
45
+ def compose(self) -> ComposeResult:
46
+ yield Header()
47
+ with ContentSwitcher(initial="view-1"):
48
+ yield ViewPane("runs", BUILT_IN, self.config.root, id="view-1")
49
+ yield Footer()
50
+
51
+ def on_mount(self) -> None:
52
+ self.sub_title = self.experiment_name
53
+ self.run_worker(
54
+ reload_on_change(
55
+ watch_rows(self.tables, WATCHED, self.experiment_name),
56
+ self.load,
57
+ self.stopped,
58
+ ),
59
+ exit_on_error=False,
60
+ )
61
+ self.run_worker(self.watch_files(), exit_on_error=False)
62
+
63
+ def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
64
+ if event.state == WorkerState.ERROR:
65
+ self.app.exit(event.worker.error)
66
+
67
+ def stopped(self, error: BaseException | None) -> None:
68
+ self.sub_title = stopped_title(self.sub_title, error)
69
+
70
+ async def load(self) -> None:
71
+ loaded: dict[str, Any] | None = await load_experiment(
72
+ self.tables,
73
+ self.config.root,
74
+ self.experiment_name,
75
+ QueryParams(),
76
+ )
77
+ if loaded is None and not self.found:
78
+ raise ValueError(f"No experiment named {self.experiment_name}.")
79
+ if loaded is None:
80
+ self.sub_title = f"{self.experiment_name} was removed."
81
+ self.shown = None
82
+ self.deliver(None)
83
+ return
84
+ if not self.found:
85
+ await self.add_views(loaded["views"])
86
+ self.found = True
87
+ self.sub_title = str(loaded["display_name"])
88
+ self.shown = Experiment(
89
+ self.config.root,
90
+ self.experiment_name,
91
+ native_rows(loaded),
92
+ MappingProxyType(loaded["config"] or {}),
93
+ )
94
+ self.deliver(self.shown)
95
+
96
+ async def add_views(self, specs: Mapping[str, str]) -> None:
97
+ names = list(specs)[:MAX_VIEWS]
98
+ if not names:
99
+ return
100
+ panes = [
101
+ ViewPane(name, specs[name], self.config.root, id=f"view-{number}")
102
+ for number, name in enumerate(names, 2)
103
+ ]
104
+ for pane in panes:
105
+ pane.display = False
106
+ await self.query_one(ContentSwitcher).mount_all(panes)
107
+ # Screen bindings are fixed per class; each screen's view names differ.
108
+ for number, name in enumerate(["runs", *names], 1):
109
+ self._bindings.bind(str(number), f"show_view({number})", name)
110
+ self._bindings.bind("0", "show_view(1)")
111
+ self.refresh_bindings()
112
+
113
+ def deliver(
114
+ self, experiment: Experiment | None, paths: frozenset[str] = frozenset()
115
+ ) -> None:
116
+ for pane in self.query(ViewPane):
117
+ pane.deliver(experiment, paths)
118
+
119
+ async def watch_files(self) -> None:
120
+ async for row in watch_rows(self.tables, {"run_files"}, self.experiment_name):
121
+ if self.shown is not None:
122
+ self.deliver(self.shown, frozenset({str(row["path"])}))
123
+
124
+ def action_show_view(self, number: int) -> None:
125
+ switcher = self.query_one(ContentSwitcher)
126
+ if switcher.current is not None and self.focused is not None:
127
+ self.left[switcher.current] = self.focused
128
+ switcher.current = f"view-{number}"
129
+ self.call_after_refresh(self.refocus, switcher.current)
130
+
131
+ def refocus(self, current: str) -> None:
132
+ widget = self.left.get(current)
133
+ if widget is not None and widget.focusable:
134
+ widget.focus()
135
+ return
136
+ self.set_focus(None)
137
+ self.focus_next()
138
+
139
+ def action_back(self) -> None:
140
+ if self.query_one(ContentSwitcher).current == "view-1":
141
+ self.app.pop_screen()
142
+ else:
143
+ self.action_show_view(1)
@@ -1,5 +1,3 @@
1
- from collections.abc import Callable
2
-
3
1
  from dirsql import DirSQL
4
2
  from textual.app import ComposeResult
5
3
  from textual.screen import Screen
@@ -12,7 +10,6 @@ from labnotes.tui.experiment_screen import ExperimentScreen
12
10
  from labnotes.tui.fill_table import fill_table
13
11
  from labnotes.tui.reload_on_change import reload_on_change
14
12
  from labnotes.tui.runs_table import RunsTable
15
- from labnotes.tui.runs_view import Request
16
13
  from labnotes.tui.stopped_title import stopped_title
17
14
  from labnotes.watch_rows import watch_rows
18
15
 
@@ -20,13 +17,10 @@ WATCHED = {"files", "runs", "experiment_configs"}
20
17
 
21
18
 
22
19
  class ExperimentsScreen(Screen[None]):
23
- def __init__(
24
- self, tables: DirSQL, config: LabnotesConfig, views: Callable[[str], Request]
25
- ) -> None:
20
+ def __init__(self, tables: DirSQL, config: LabnotesConfig) -> None:
26
21
  super().__init__()
27
22
  self.tables = tables
28
23
  self.config = config
29
- self.views = views
30
24
 
31
25
  def compose(self) -> ComposeResult:
32
26
  yield Header()
@@ -63,7 +57,5 @@ class ExperimentsScreen(Screen[None]):
63
57
 
64
58
  def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
65
59
  self.app.push_screen(
66
- ExperimentScreen(
67
- self.tables, self.config, str(event.row_key.value), self.views
68
- )
60
+ ExperimentScreen(self.tables, self.config, str(event.row_key.value))
69
61
  )
@@ -7,13 +7,15 @@ from labnotes.tui.dim import dim
7
7
  from labnotes.tui.format_elapsed import format_elapsed
8
8
 
9
9
 
10
- def format_cell(key: str, value: object, live: bool) -> object:
10
+ def format_cell(key: str, value: object) -> object:
11
11
  if key == "status" and value == "passed":
12
12
  return Text("✓", style="green")
13
13
  if key == "status" and value == "failed":
14
14
  return Text("✕", style="red")
15
+ if key == "status" and value == "running":
16
+ return Text("◐", style="yellow")
15
17
  if key == "status" and value == "unfinished":
16
- return Text("◐", style="yellow") if live else dim("–")
18
+ return dim("–")
17
19
  if key == "duration" and isinstance(value, int) and not isinstance(value, bool):
18
20
  return format_elapsed(value)
19
21
  if key == "started" and isinstance(value, str):
@@ -0,0 +1,35 @@
1
+ from collections.abc import Callable, Iterable
2
+ from typing import cast
3
+
4
+ from textual.message import Message
5
+ from textual.widget import Widget
6
+
7
+
8
+ def guard_view(view: type[Widget], failed: Callable[[Exception], None]) -> type[Widget]:
9
+ """Subclass a view so errors in its compose and handlers reach `failed`.
10
+
11
+ Textual ends the app on any error a widget raises. A view is someone else's
12
+ code, so its errors stay in its pane instead.
13
+ """
14
+
15
+ def compose(self: Widget) -> Iterable[Widget]:
16
+ try:
17
+ yield from view.compose(self)
18
+ except Exception as error:
19
+ failed(error)
20
+
21
+ # _dispatch_message is private to Textual, but every handler runs under it.
22
+ async def _dispatch_message(self: Widget, message: Message) -> None:
23
+ try:
24
+ await view._dispatch_message(self, message)
25
+ except Exception as error:
26
+ failed(error)
27
+
28
+ namespace = {
29
+ "__module__": view.__module__,
30
+ "__qualname__": view.__qualname__,
31
+ "compose": compose,
32
+ "_dispatch_message": _dispatch_message,
33
+ }
34
+ # type() builds a subclass of view, so it is a Widget subclass.
35
+ return cast(type[Widget], type(view.__name__, (view,), namespace))
@@ -0,0 +1,27 @@
1
+ import importlib
2
+ import sys
3
+ from importlib.machinery import SourceFileLoader
4
+ from pathlib import Path
5
+ from types import ModuleType
6
+
7
+ from textual.widget import Widget
8
+
9
+
10
+ def load_view(spec: str, root: Path) -> type[Widget]:
11
+ """Import the widget class `spec` names: `file.py:Class` from `root`, or `module:Class`."""
12
+ source, _, name = spec.rpartition(":")
13
+ if source.endswith(".py"):
14
+ path = root / source
15
+ module = ModuleType(f"labnotes_view_{path.stem}")
16
+ module.__file__ = str(path)
17
+ # Dataclasses and pickling look a class's module up by name.
18
+ sys.modules[module.__name__] = module
19
+ SourceFileLoader(module.__name__, str(path)).exec_module(module)
20
+ else:
21
+ module = importlib.import_module(source)
22
+ view = getattr(module, name, None)
23
+ if view is None:
24
+ raise ImportError(f"{source} has no {name}")
25
+ if not isinstance(view, type) or not issubclass(view, Widget):
26
+ raise TypeError(f"{spec} is not a Textual widget")
27
+ return view
@@ -11,7 +11,7 @@ def native_row(run: Mapping[str, Any]) -> dict[str, Any]:
11
11
  if isinstance(row.get(key), str):
12
12
  row[key] = json.loads(row[key])
13
13
  if run["labnotes_unfinished"]:
14
- row["status"] = "unfinished"
14
+ row["status"] = "running" if run["labnotes_running"] else "unfinished"
15
15
  elif run["labnotes_completed"]:
16
16
  row["status"] = "passed"
17
17
  else:
@@ -20,5 +20,5 @@ def native_row(run: Mapping[str, Any]) -> dict[str, Any]:
20
20
  return row
21
21
 
22
22
 
23
- def native_rows(experiment: Mapping[str, Any]) -> dict[str, dict[str, Any]]:
24
- return {str(run["folder"]): native_row(run) for run in experiment["runs_list"]}
23
+ def native_rows(experiment: Mapping[str, Any]) -> tuple[dict[str, Any], ...]:
24
+ return tuple(native_row(run) for run in experiment["runs_list"])
@@ -0,0 +1,12 @@
1
+ from collections.abc import Mapping, Sequence
2
+
3
+ from labnotes.tui.format_cell import format_cell
4
+
5
+
6
+ def run_cells(
7
+ row: Mapping[str, object], columns: Sequence[Mapping[str, str]]
8
+ ) -> list[object]:
9
+ return [
10
+ format_cell(column["key"], row[column["key"]]) if column["key"] in row else ""
11
+ for column in columns
12
+ ]
@@ -1,16 +1,15 @@
1
1
  from pathlib import Path
2
2
 
3
3
  from textual.app import ComposeResult
4
- from textual.binding import Binding
5
- from textual.screen import Screen
6
- from textual.widgets import Footer, Header
4
+ from textual.containers import Vertical
5
+ from textual.widgets import Static
7
6
 
8
7
  from labnotes.tui.log_blocks import LogBlocks
9
8
  from labnotes.tui.run_output import RunOutput
10
9
 
11
10
 
12
- class RunScreen(Screen[None]):
13
- BINDINGS = [Binding("escape", "app.pop_screen", "Back")]
11
+ class RunPanel(Vertical):
12
+ DEFAULT_CSS = "RunPanel #run-id { padding: 0 1; text-style: bold; }"
14
13
 
15
14
  def __init__(self, run_id: str, folder: Path) -> None:
16
15
  super().__init__()
@@ -18,13 +17,11 @@ class RunScreen(Screen[None]):
18
17
  self.folder = folder
19
18
 
20
19
  def compose(self) -> ComposeResult:
21
- yield Header()
20
+ yield Static(self.run_id, id="run-id", markup=False)
22
21
  yield LogBlocks(id="blocks")
23
22
  yield RunOutput(id="output")
24
- yield Footer()
25
23
 
26
24
  def on_mount(self) -> None:
27
- self.sub_title = self.run_id
28
25
  self.follow()
29
26
  self.set_interval(0.25, self.follow)
30
27
 
@@ -0,0 +1,132 @@
1
+ from collections.abc import Mapping
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.binding import Binding
7
+ from textual.widget import Widget
8
+ from textual.widgets import DataTable, Static
9
+
10
+ from labnotes.runs_view_command import runs_view_command
11
+ from labnotes.tui.fill_table import fill_table
12
+ from labnotes.tui.run_output import RunOutput
13
+ from labnotes.tui.run_panel import RunPanel
14
+ from labnotes.tui.runs_table import RunsTable
15
+ from labnotes.tui.runs_view import RunsView
16
+ from labnotes.tui.table_columns import table_columns
17
+ from labnotes.tui.table_rows import table_rows
18
+ from labnotes.tui.view_process import ViewProcess
19
+ from labnotes.view.changed import Changed
20
+ from labnotes.view.experiment import Experiment
21
+
22
+
23
+ class RunsBrowser(Widget):
24
+ """Show an experiment's runs, then a run's output: the built-in view."""
25
+
26
+ DEFAULT_CSS = """
27
+ RunsBrowser RunsTable { height: 1fr; }
28
+ RunsBrowser #view-status { color: $text-warning; }
29
+ """
30
+ BINDINGS = [
31
+ Binding("enter", "open_row", "Open", priority=True),
32
+ Binding("escape", "close_run", "Back"),
33
+ ]
34
+
35
+ def __init__(self) -> None:
36
+ super().__init__()
37
+ self.process: ViewProcess | None = None
38
+ self.view: RunsView | None = None
39
+ self.experiment = Experiment(Path(), "", ())
40
+ self.viewing: str | None = None
41
+
42
+ def compose(self) -> ComposeResult:
43
+ yield RunsTable(id="runs", cursor_type="row")
44
+ yield Static(id="view-status")
45
+
46
+ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool:
47
+ if action == "open_row":
48
+ return self.viewing is None
49
+ if action == "close_run":
50
+ return self.viewing is not None
51
+ return True
52
+
53
+ def on_labnotes_changed(self, message: Changed) -> None:
54
+ if message.paths:
55
+ if self.view is not None:
56
+ for path in sorted(message.paths):
57
+ self.view.touch(path)
58
+ self.ask(self.view)
59
+ return
60
+ experiment = message.experiment
61
+ if experiment is None:
62
+ fill_table(self.query_one(DataTable), [], [])
63
+ return
64
+ self.experiment = experiment
65
+ command = runs_view_command(dict(experiment.config))
66
+ if command is not None and self.view is None:
67
+ self.process = ViewProcess(command, experiment.root)
68
+ self.view = RunsView(self.process.request)
69
+ if self.view is not None:
70
+ rows = {str(row["folder"]): dict(row) for row in experiment.runs}
71
+ self.view.update(table_columns(experiment.config), rows)
72
+ self.ask(self.view)
73
+ self.redraw()
74
+
75
+ async def on_unmount(self) -> None:
76
+ if self.process is not None:
77
+ await self.process.close()
78
+
79
+ def ask(self, view: RunsView) -> None:
80
+ request = view.next_request()
81
+ if request is not None:
82
+ self.run_worker(self.answer(view, request), exit_on_error=False)
83
+
84
+ async def answer(self, view: RunsView, request: dict[str, Any]) -> None:
85
+ if await view.exchange(request):
86
+ self.redraw()
87
+ else:
88
+ self.show_status()
89
+ self.ask(view)
90
+
91
+ def redraw(self) -> None:
92
+ rows: Mapping[str, Mapping[str, Any]]
93
+ if self.view is None:
94
+ columns = table_columns(self.experiment.config)
95
+ rows = {str(row["folder"]): row for row in self.experiment.runs}
96
+ else:
97
+ columns, rows = self.view.table()
98
+ fill_table(
99
+ self.query_one(DataTable),
100
+ [column["label"] for column in columns],
101
+ table_rows(self.experiment.runs, columns, rows),
102
+ )
103
+ self.show_status()
104
+
105
+ def show_status(self) -> None:
106
+ text = "" if self.view is None else self.view.status()
107
+ status = self.query_one("#view-status", Static)
108
+ status.update(text)
109
+ status.display = bool(text) and self.viewing is None
110
+
111
+ async def action_open_row(self) -> None:
112
+ table = self.query_one(DataTable)
113
+ if not table.row_count:
114
+ return
115
+ folder = str(
116
+ table.coordinate_to_cell_key(table.cursor_coordinate).row_key.value
117
+ )
118
+ ids = {str(row["folder"]): str(row["id"]) for row in self.experiment.runs}
119
+ panel = RunPanel(ids[folder], self.experiment.root / folder)
120
+ self.viewing = folder
121
+ table.display = False
122
+ self.show_status()
123
+ await self.mount(panel)
124
+ panel.query_one(RunOutput).focus()
125
+
126
+ def action_close_run(self) -> None:
127
+ self.viewing = None
128
+ self.query_one(RunPanel).remove()
129
+ table = self.query_one(DataTable)
130
+ table.display = True
131
+ table.focus()
132
+ self.show_status()
@@ -0,0 +1,8 @@
1
+ from collections.abc import Mapping
2
+
3
+ from labnotes.run_columns import run_columns
4
+
5
+
6
+ def table_columns(config: Mapping[str, object]) -> list[dict[str, str]]:
7
+ keys = run_columns(config)
8
+ return [{"key": "status", "label": ""}, *({"key": k, "label": k} for k in keys)]
@@ -0,0 +1,12 @@
1
+ from collections.abc import Mapping, Sequence
2
+
3
+ from labnotes.tui.run_cells import run_cells
4
+
5
+
6
+ def table_rows(
7
+ runs: Sequence[Mapping[str, object]],
8
+ columns: Sequence[Mapping[str, str]],
9
+ rows: Mapping[str, Mapping[str, object]],
10
+ ) -> list[tuple[str, list[object]]]:
11
+ folders = [str(run["folder"]) for run in runs]
12
+ return [(folder, run_cells(rows[folder], columns)) for folder in folders]