labnotes 0.2.7__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 (232) hide show
  1. {labnotes-0.2.7 → labnotes-0.2.9}/PKG-INFO +6 -2
  2. {labnotes-0.2.7 → labnotes-0.2.9}/README.md +5 -1
  3. labnotes-0.2.9/src/labnotes/config_migration.py +12 -0
  4. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/context.md +23 -20
  5. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/create_app.py +1 -2
  6. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/create_experiment.py +5 -2
  7. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/experiment_definition.py +4 -0
  8. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/list_experiments.py +2 -2
  9. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/load_experiment.py +11 -7
  10. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/models.py +0 -3
  11. labnotes-0.2.9/src/labnotes/param_names.py +9 -0
  12. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/read_config.py +5 -5
  13. labnotes-0.2.9/src/labnotes/run_columns.py +7 -0
  14. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/describe_event.py +2 -0
  15. labnotes-0.2.9/src/labnotes/runner/hold_experiment.py +29 -0
  16. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/prepare_experiment.py +0 -1
  17. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/read_experiment_config.py +16 -14
  18. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/run_experiment.py +10 -50
  19. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/start_experiment.py +2 -1
  20. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/summarize_config.py +1 -3
  21. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/experiment_rows.py +1 -3
  22. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/templates/experiment.html +1 -2
  23. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/browse_app.py +2 -16
  24. labnotes-0.2.9/src/labnotes/tui/experiment_screen.py +143 -0
  25. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/experiments_screen.py +2 -10
  26. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/format_cell.py +4 -2
  27. labnotes-0.2.9/src/labnotes/tui/guard_view.py +35 -0
  28. labnotes-0.2.9/src/labnotes/tui/load_view.py +27 -0
  29. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/native_rows.py +3 -3
  30. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/run_app.py +4 -4
  31. labnotes-0.2.9/src/labnotes/tui/run_cells.py +12 -0
  32. labnotes-0.2.7/src/labnotes/tui/run_screen.py → labnotes-0.2.9/src/labnotes/tui/run_panel.py +5 -8
  33. labnotes-0.2.9/src/labnotes/tui/runs_browser.py +132 -0
  34. labnotes-0.2.9/src/labnotes/tui/table_columns.py +8 -0
  35. labnotes-0.2.9/src/labnotes/tui/table_rows.py +12 -0
  36. labnotes-0.2.9/src/labnotes/tui/view_pane.py +54 -0
  37. labnotes-0.2.9/src/labnotes/view/__init__.py +22 -0
  38. labnotes-0.2.9/src/labnotes/view/changed.py +19 -0
  39. labnotes-0.2.9/src/labnotes/view/experiment.py +20 -0
  40. labnotes-0.2.9/src/labnotes/view_specs.py +10 -0
  41. {labnotes-0.2.7 → labnotes-0.2.9}/testing-conventions.toml +10 -22
  42. {labnotes-0.2.7 → labnotes-0.2.9}/tests/conftest.py +1 -1
  43. {labnotes-0.2.7 → labnotes-0.2.9}/tests/integration/conftest.py +1 -1
  44. labnotes-0.2.7/src/labnotes/modes.py +0 -2
  45. labnotes-0.2.7/src/labnotes/runner/new_param_run_id.py +0 -18
  46. labnotes-0.2.7/src/labnotes/runner/param_run_name.py +0 -17
  47. labnotes-0.2.7/src/labnotes/tui/experiment_screen.py +0 -155
  48. labnotes-0.2.7/src/labnotes/tui/group_cells.py +0 -36
  49. labnotes-0.2.7/src/labnotes/tui/invocation_columns.py +0 -9
  50. labnotes-0.2.7/src/labnotes/tui/invocation_rows.py +0 -32
  51. labnotes-0.2.7/src/labnotes/tui/run_cells.py +0 -22
  52. labnotes-0.2.7/src/labnotes/view/__init__.py +0 -3
  53. {labnotes-0.2.7 → labnotes-0.2.9}/.gitignore +0 -0
  54. {labnotes-0.2.7 → labnotes-0.2.9}/CHANGELOG.md +0 -0
  55. {labnotes-0.2.7 → labnotes-0.2.9}/MIGRATIONS.md +0 -0
  56. {labnotes-0.2.7 → labnotes-0.2.9}/pyproject.toml +0 -0
  57. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/__init__.py +0 -0
  58. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/__main__.py +0 -0
  59. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/asset_url.py +0 -0
  60. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/check_counts.py +0 -0
  61. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/cli.py +0 -0
  62. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/command_line_group.py +0 -0
  63. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/compose_experiment.py +0 -0
  64. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/context_guide.py +0 -0
  65. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/copy_experiment.py +0 -0
  66. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/count_groups.py +0 -0
  67. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/count_runs.py +0 -0
  68. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/create_note.py +0 -0
  69. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/data_html.py +0 -0
  70. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/default_config_text.py +0 -0
  71. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/default_counts.py +0 -0
  72. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/drain_watch.py +0 -0
  73. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/experiment_notes.py +0 -0
  74. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/experiment_runs.py +0 -0
  75. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/experiments.py +0 -0
  76. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/file_changes.py +0 -0
  77. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/format_duration.py +0 -0
  78. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/format_started.py +0 -0
  79. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/format_value.py +0 -0
  80. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/get_state.py +0 -0
  81. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/grid_cell.py +0 -0
  82. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/group_runs.py +0 -0
  83. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/init.py +0 -0
  84. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/invocation_started.py +0 -0
  85. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/is_non_negative_int.py +0 -0
  86. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/is_non_negative_number.py +0 -0
  87. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/is_positive_int.py +0 -0
  88. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/latest_log.py +0 -0
  89. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/list_runs.py +0 -0
  90. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/load_config.py +0 -0
  91. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/load_run.py +0 -0
  92. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/log.py +0 -0
  93. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/log_path.py +0 -0
  94. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/memory_interval.py +0 -0
  95. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/nests_runs.py +0 -0
  96. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/note_path.py +0 -0
  97. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/notebook_kernel.py +0 -0
  98. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/output_html.py +0 -0
  99. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/parse_param_value.py +0 -0
  100. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/parse_params.py +0 -0
  101. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/parse_run_view.py +0 -0
  102. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/parse_subset.py +0 -0
  103. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/pivot_runs.py +0 -0
  104. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/print_rows.py +0 -0
  105. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/python_blocks.py +0 -0
  106. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/read_path.py +0 -0
  107. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/render_markdown.py +0 -0
  108. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/retries.py +0 -0
  109. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/run_output.py +0 -0
  110. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/__init__.py +0 -0
  111. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/cgroup_memory.py +0 -0
  112. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/check_args.py +0 -0
  113. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/check_hook_placeholders.py +0 -0
  114. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/check_subset.py +0 -0
  115. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/claim_run.py +0 -0
  116. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/command_flags.py +0 -0
  117. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/container_cgroup.py +0 -0
  118. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/container_memory.py +0 -0
  119. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/continue_run.py +0 -0
  120. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/docker_output.py +0 -0
  121. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/execute_run.py +0 -0
  122. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/exit_signal.py +0 -0
  123. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/fill_dirs.py +0 -0
  124. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/fill_experiment_env.py +0 -0
  125. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/finalize_lost_runs.py +0 -0
  126. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/first_invocation.py +0 -0
  127. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/flag_name.py +0 -0
  128. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/group_args.py +0 -0
  129. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/in_subset.py +0 -0
  130. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/invocation_hook.py +0 -0
  131. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/invocation_meta.py +0 -0
  132. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/new_run_id.py +0 -0
  133. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  134. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/override_flags.py +0 -0
  135. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/param_args.py +0 -0
  136. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/param_grid.py +0 -0
  137. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/placeholders.py +0 -0
  138. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/plan_runs.py +0 -0
  139. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/project_snapshot.py +0 -0
  140. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/record_memory.py +0 -0
  141. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/run_folder.py +0 -0
  142. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/run_git.py +0 -0
  143. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/run_headless.py +0 -0
  144. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/run_locked.py +0 -0
  145. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/runs_parent.py +0 -0
  146. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/shell_command.py +0 -0
  147. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/signal_tree.py +0 -0
  148. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/stop_then_kill.py +0 -0
  149. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/untracked_diff.py +0 -0
  150. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/wait_for_tree.py +0 -0
  151. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runner/write_run_json.py +0 -0
  152. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/runs_view_command.py +0 -0
  153. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/save_conflict_copy.py +0 -0
  154. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/save_note.py +0 -0
  155. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/saved_outputs.py +0 -0
  156. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/serve_app.py +0 -0
  157. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/server.py +0 -0
  158. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/set_state.py +0 -0
  159. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/shared_params.py +0 -0
  160. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/sort_href.py +0 -0
  161. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/sql_quote.py +0 -0
  162. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/state_path.py +0 -0
  163. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/static/elapsed.js +0 -0
  164. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/static/experiment.js +0 -0
  165. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/static/labnotes.css +0 -0
  166. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/static/local-time.js +0 -0
  167. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/static/new-note.js +0 -0
  168. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/static/note-edit.js +0 -0
  169. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/static/note-editor.js +0 -0
  170. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/static/row-link.js +0 -0
  171. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/static/run-cell.js +0 -0
  172. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/static/run-output.js +0 -0
  173. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/static/view-toggle.js +0 -0
  174. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/__init__.py +0 -0
  175. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/experiment_run_ids.py +0 -0
  176. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/experiment_toml.py +0 -0
  177. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/experiments_ddl.py +0 -0
  178. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/file_prefix.py +0 -0
  179. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/file_text.py +0 -0
  180. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/iso_time.py +0 -0
  181. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/list_experiment_runs.py +0 -0
  182. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/list_resumable_runs.py +0 -0
  183. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/note_text.py +0 -0
  184. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/open_tables.py +0 -0
  185. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/path_glob.py +0 -0
  186. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/path_pattern.py +0 -0
  187. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/read_blocks.py +0 -0
  188. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/read_experiment.py +0 -0
  189. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/read_file.py +0 -0
  190. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/read_log.py +0 -0
  191. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/read_memory.py +0 -0
  192. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/read_run.py +0 -0
  193. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/read_run_file.py +0 -0
  194. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/runs_columns.py +0 -0
  195. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tables/summarize_experiments.py +0 -0
  196. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/templates/base.html +0 -0
  197. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/templates/editor_imports.html +0 -0
  198. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/templates/index.html +0 -0
  199. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/templates/note.html +0 -0
  200. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/templates/run.html +0 -0
  201. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/__init__.py +0 -0
  202. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/browse_tui.py +0 -0
  203. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/check_view_response.py +0 -0
  204. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/config.py +0 -0
  205. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/dim.py +0 -0
  206. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/fill_table.py +0 -0
  207. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/format_elapsed.py +0 -0
  208. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/leave_prompt.py +0 -0
  209. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/log_block.py +0 -0
  210. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/log_blocks.py +0 -0
  211. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/reload_on_change.py +0 -0
  212. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/run_output.py +0 -0
  213. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/run_tui.py +0 -0
  214. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/runs_table.py +0 -0
  215. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/runs_view.py +0 -0
  216. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/stopped_title.py +0 -0
  217. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/view_column.py +0 -0
  218. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/tui/view_process.py +0 -0
  219. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/utils/__init__.py +0 -0
  220. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/utils/format_bytes.py +0 -0
  221. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/utils/process_alive.py +0 -0
  222. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/utils/run_usage.py +0 -0
  223. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/utils/tree_rss.py +0 -0
  224. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/utils/tree_usage.py +0 -0
  225. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/view/answer.py +0 -0
  226. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/view/build.py +0 -0
  227. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/view/respond.py +0 -0
  228. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/view/serve.py +0 -0
  229. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/wait_for_note.py +0 -0
  230. {labnotes-0.2.7 → labnotes-0.2.9}/src/labnotes/watch_rows.py +0 -0
  231. {labnotes-0.2.7 → labnotes-0.2.9}/tests/e2e/conftest.py +0 -0
  232. {labnotes-0.2.7 → 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.7
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>
@@ -79,7 +79,8 @@ labnotes experiments foo runs # foo's runs, oldest first
79
79
  Each takes `--format table` or `--format json`, which print rows and exit.
80
80
  Without `--format`, they open a browser in a terminal: the experiment list,
81
81
  Enter to open one, Escape to go back. `foo` and `foo runs` open straight on
82
- `foo`, which shows its runs grouped by invocation; Enter expands or collapses
82
+ `foo`, which shows its runs, with runs that older versions nested in one invocation
83
+ folder grouped under it; Enter expands or collapses
83
84
  one. Piped, they print a table.
84
85
 
85
86
  `labnotes experiments new [NAME]` creates an experiment folder with a starter
@@ -96,6 +97,7 @@ experiments folder:
96
97
  name = "Model and effort"
97
98
  command = "uv run scripts/study.py --output {run_dir}"
98
99
  repeats = 5
100
+ continues = false
99
101
 
100
102
  [params]
101
103
  model = ["opus", "sonnet"]
@@ -107,6 +109,8 @@ and as `display_name` in `experiments`. The folder name stays the key, so
107
109
  renaming the experiment means editing `name`.
108
110
  Each combination of `[params]` runs `repeats` times, round-robin. Only runs
109
111
  that exit 0 count, so running again fills in what failed or never ran.
112
+ `continues` is required: `true` invokes a failed run again in its own folder,
113
+ `false` gives its combination a new one.
110
114
  Each run gets a new run folder. `command` runs in a shell from the root, with
111
115
  `{run_dir}` replaced by the run folder's absolute path. Pass it to the
112
116
  script so it writes its outputs there.
@@ -53,7 +53,8 @@ labnotes experiments foo runs # foo's runs, oldest first
53
53
  Each takes `--format table` or `--format json`, which print rows and exit.
54
54
  Without `--format`, they open a browser in a terminal: the experiment list,
55
55
  Enter to open one, Escape to go back. `foo` and `foo runs` open straight on
56
- `foo`, which shows its runs grouped by invocation; Enter expands or collapses
56
+ `foo`, which shows its runs, with runs that older versions nested in one invocation
57
+ folder grouped under it; Enter expands or collapses
57
58
  one. Piped, they print a table.
58
59
 
59
60
  `labnotes experiments new [NAME]` creates an experiment folder with a starter
@@ -70,6 +71,7 @@ experiments folder:
70
71
  name = "Model and effort"
71
72
  command = "uv run scripts/study.py --output {run_dir}"
72
73
  repeats = 5
74
+ continues = false
73
75
 
74
76
  [params]
75
77
  model = ["opus", "sonnet"]
@@ -81,6 +83,8 @@ and as `display_name` in `experiments`. The folder name stays the key, so
81
83
  renaming the experiment means editing `name`.
82
84
  Each combination of `[params]` runs `repeats` times, round-robin. Only runs
83
85
  that exit 0 count, so running again fills in what failed or never ran.
86
+ `continues` is required: `true` invokes a failed run again in its own folder,
87
+ `false` gives its combination a new one.
84
88
  Each run gets a new run folder. `command` runs in a shell from the root, with
85
89
  `{run_dir}` replaced by the run folder's absolute path. Pass it to the
86
90
  script so it writes its outputs there.
@@ -0,0 +1,12 @@
1
+ MIGRATION = (
2
+ "https://github.com/thekevinscott/labnotes/blob/main/"
3
+ "docs/migrations.d/2026-11-02-python-remove-modes.md"
4
+ )
5
+ REMOVED_MODE = (
6
+ "mode was removed: every experiment plans what it is missing. "
7
+ f"Delete mode. See {MIGRATION}"
8
+ )
9
+ MISSING_CONTINUES = (
10
+ "continues is required: set continues = true or continues = false. "
11
+ f"See {MIGRATION}"
12
+ )
@@ -55,6 +55,7 @@ experiments folder:
55
55
  name = "Model and effort"
56
56
  command = "uv run scripts/study.py --output {run_dir}"
57
57
  repeats = 5
58
+ continues = false
58
59
 
59
60
  [params]
60
61
  model = ["opus", "sonnet"]
@@ -64,22 +65,26 @@ effort = ["low", "high"]
64
65
  `name` is optional: a display name for the experiment, shown in the live view
65
66
  and as `display_name` in `experiments`. The folder name stays the key, so
66
67
  renaming the experiment means editing `name`.
67
- Each combination of `[params]` runs `repeats` times (1 if omitted), round-robin, on every
68
- `run`, whatever is already on disk. That is `mode = "adhoc"`, the default.
69
- `mode = "matrix"` counts what is there: only runs that count fill a combination,
70
- so running again fills in what failed or never ran. `mode` under
71
- `[experiments]` in `.labnotes.toml` sets it for every experiment that doesn't
72
- set its own.
73
- Each run gets a new run folder. In `matrix` mode it is named for the UTC time it was planned
74
- (`runs/20261020-000429-brave-fox`). In `adhoc` mode a `run` command that plans one run names
75
- its folder the same way. One that plans more gets one folder named that way, and its runs nest
76
- inside it (`runs/20261020-000429-brave-fox/quiet-river`); the run id is that path under `runs/`. Either way, folder listings sort by start. `command` runs in a shell from the root, with
68
+ Each combination of `[params]` needs `repeats` runs that count (1 if omitted).
69
+ `run` plans only the combinations short of that, round-robin, so running again
70
+ fills in what failed or never ran, and a finished experiment plans nothing
71
+ (`Nothing to run: every combination has its repeats.`). To start over, copy it
72
+ with `experiments new NAME --from OLD`, which drops `mode` and fills in
73
+ `continues`. `mode` is gone; setting it is a config error.
74
+ `continues` is required, in `experiment.toml` or `[experiment]` in
75
+ `.labnotes.toml`. `continues = true` invokes a failed, stopped or lost run again
76
+ in its own folder, with the same params and `{run_dir}`; `run.json` then lists
77
+ each attempt under `"invocations"`. `continues = false` gives the combination a
78
+ new folder. Never delete `run.json`, `stdout-*`, `stderr-*` or `run.lock` from a
79
+ run folder.
80
+ Each new run gets a run folder named for the UTC time it was planned
81
+ (`runs/20261020-000429-brave-fox`), so folder listings sort by start. Older
82
+ versions nested several runs in one invocation folder
83
+ (`runs/20261020-000429-brave-fox/quiet-river`, run id is that path); those still
84
+ list and count. Only one `run` of an experiment goes at a time; a second refuses. `command` runs in a shell from the root, with
77
85
  `{run_dir}` replaced by the run folder's absolute path. Pass it to the
78
- script so it writes its outputs there. `{invocation_dir}` is the shared adhoc
79
- group folder's absolute path when several runs are planned. With one planned
80
- run it stays at the first attempt's folder across retries, while `{run_dir}`
81
- changes per attempt. With several matrix runs, it equals each run's `{run_dir}`.
82
- Both placeholders are shell-quoted.
86
+ script so it writes its outputs there. `{invocation_dir}` is the same
87
+ folder as `{run_dir}`. Both placeholders are shell-quoted.
83
88
  Each param is appended as `--name value`, with the name as written (use
84
89
  `source-language` for a `--source-language` flag), booleans as 0 or 1, and lists
85
90
  and tables as JSON. labnotes writes `run.json` (params, `command`, created,
@@ -114,9 +119,7 @@ existing value without dropping other keys. Values must be JSON serializable;
114
119
  reading before any write returns `{}`. Runs can only read: `set_state` raises
115
120
  `RuntimeError` there. Both functions raise outside an invocation and in `meta`.
116
121
  Labnotes gives setup/teardown `LABNOTES_STATE` and runs `LABNOTES_RUN_STATE`,
117
- pointing to `state.json` in the multi-run adhoc group folder or the lone run's
118
- folder, or, without a shared folder, a temp
119
- file kept through all runs and teardown and then removed. Queued runs and retries
122
+ pointing to a temp file kept through all runs and teardown and then removed. Queued runs and retries
120
123
  read the same file after setup finishes. `meta` receives neither state variable;
121
124
  state is never copied into `run.json`.
122
125
  Nested labnotes invocations clear inherited `LABNOTES_*` context for their
@@ -182,7 +185,7 @@ Escape clears it. `x` stops the
182
185
  running run under the cursor; `x` again kills it. `+` and
183
186
  `-` change how many run at once; lowering it lets running runs
184
187
  finish. Enter on a run follows its `stdout.log` and `stderr.log` together, as
185
- they're written, and Escape returns to the table. An `adhoc` experiment that
188
+ they're written, and Escape returns to the table. A `run` that
186
189
  plans a single run opens straight on its output. `q` quits and leaves the runs
187
190
  going. Elsewhere, it prints a line per event instead.
188
191
 
@@ -218,4 +221,4 @@ going. Elsewhere, it prints a line per event instead.
218
221
  was retried counts as retried, not failed. If it can't
219
222
  start a run, it prints the cause on one line and exits 1.
220
223
 
221
- `uvx labnotes server` serves the experiments, their markdown files and their runs as a local site, read from the same `.labnotes.toml`. An experiment's page shows its `experiment.toml` on one line, then a Runs tab with a table of its runs and a Notebooks tab with one tab per markdown file. For a matrix experiment, the Runs tab also has a grid (`?view=grid`): one cell per combination of the params that vary, with how many runs count toward `repeats`. Clicking a run opens its `stdout.log` and `stderr.log` together, followed as they're written until the run ends.
224
+ `uvx labnotes server` serves the experiments, their markdown files and their runs as a local site, read from the same `.labnotes.toml`. An experiment's page shows its `experiment.toml` on one line, then a Runs tab with a table of its runs and a Notebooks tab with one tab per markdown file. The Runs tab also has a grid (`?view=grid`): one cell per combination of the params that vary, with how many runs count toward `repeats`. Clicking a run opens its `stdout.log` and `stderr.log` together, followed as they're written until the run ends.
@@ -63,7 +63,7 @@ def create_app(config: LabnotesConfig) -> Starlette:
63
63
  await kernel.stop()
64
64
 
65
65
  async def index(request: Request) -> Response:
66
- experiments = await list_experiments(request.app.state.tables, config.mode)
66
+ experiments = await list_experiments(request.app.state.tables)
67
67
  return templates.TemplateResponse(
68
68
  request,
69
69
  "index.html",
@@ -75,7 +75,6 @@ def create_app(config: LabnotesConfig) -> Starlette:
75
75
  loaded = await load_experiment(
76
76
  request.app.state.tables,
77
77
  config.root,
78
- config.mode,
79
78
  name,
80
79
  request.query_params,
81
80
  lambda note, content: saved_outputs(kernels.get((name, note)), content),
@@ -10,6 +10,8 @@ STARTER = """\
10
10
  # Runs from the project root. {run_dir} becomes the run's folder.
11
11
  command = "uv run script.py --output {run_dir}"
12
12
  repeats = 1
13
+ # true: a failed run starts again in its own folder instead of a new one.
14
+ continues = false
13
15
 
14
16
  # Each combination runs `repeats` times, passed as --name value.
15
17
  [params]
@@ -41,7 +43,8 @@ def create_experiment(
41
43
  path = folder(name)
42
44
  if path.exists():
43
45
  raise ValueError(f"{name} already exists at {path}.")
44
- text = STARTER
46
+ inherited = config.defaults.get("continues") is True
47
+ text = STARTER.replace("continues = false", f"continues = {str(inherited).lower()}")
45
48
  if source is not None:
46
49
  origin = folder(source) / "experiment.toml"
47
50
  if not origin.is_file():
@@ -53,7 +56,7 @@ def create_experiment(
53
56
  else:
54
57
  path.mkdir(parents=True)
55
58
  (path / "experiment.toml").write_text(
56
- experiment_definition(text, source, command, repeats, params or {}),
59
+ experiment_definition(text, source, command, repeats, params or {}, inherited),
57
60
  encoding="utf-8",
58
61
  )
59
62
  return path
@@ -7,13 +7,17 @@ def experiment_definition(
7
7
  command: str | None,
8
8
  repeats: int | None,
9
9
  params: dict[str, list[object]],
10
+ continues: bool = False,
10
11
  ) -> str:
11
12
  document = tomlkit.parse(text)
12
13
  document.pop("from", None)
14
+ document.pop("mode", None)
13
15
  if command is not None:
14
16
  document["command"] = command
15
17
  if repeats is not None:
16
18
  document["repeats"] = repeats
19
+ if "continues" not in document:
20
+ document["continues"] = continues
17
21
  if params:
18
22
  table = document.setdefault("params", tomlkit.table())
19
23
  for name, values in params.items():
@@ -9,7 +9,7 @@ from labnotes.experiment_runs import experiment_runs
9
9
  from labnotes.summarize_config import summarize_config
10
10
 
11
11
 
12
- async def list_experiments(tables: DirSQL, mode: str) -> list[dict[str, object]]:
12
+ async def list_experiments(tables: DirSQL) -> list[dict[str, object]]:
13
13
  rows = await tables.query(
14
14
  "SELECT name, display_name, config FROM experiments ORDER BY name"
15
15
  )
@@ -17,7 +17,7 @@ async def list_experiments(tables: DirSQL, mode: str) -> list[dict[str, object]]
17
17
  for row in rows:
18
18
  name = str(row["name"])
19
19
  config = None if row["config"] is None else json.loads(str(row["config"]))
20
- summary = summarize_config(config, mode)
20
+ summary = summarize_config(config)
21
21
  varies = cast(dict[str, list[object]], summary["varies"])
22
22
  source = experiment_runs(name, list(varies))
23
23
  summary = await check_counts(tables, source, summary)
@@ -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
 
@@ -33,7 +35,6 @@ def _no_outputs(note: str, content: str) -> Sequence[str]:
33
35
  async def load_experiment(
34
36
  tables: DirSQL,
35
37
  root: Path,
36
- mode: str,
37
38
  name: str,
38
39
  params: QueryParams,
39
40
  outputs_for: Callable[[str, str], Sequence[str]] = _no_outputs,
@@ -46,11 +47,11 @@ async def load_experiment(
46
47
  return None
47
48
  [row] = found
48
49
  config = None if row["config"] is None else json.loads(str(row["config"]))
49
- summary = summarize_config(config, mode)
50
+ summary = summarize_config(config)
50
51
  varies = cast(dict[str, list[object]], summary["varies"])
51
52
  source = experiment_runs(name, list(varies))
52
53
  summary = await check_counts(tables, source, summary)
53
- shown = ["id", *(key for key in varies if key != "id"), "started", "duration"]
54
+ shown = run_columns(config)
54
55
  choices = {
55
56
  key: {format_value(value): json.dumps(value) for value in values}
56
57
  for key, values in varies.items()
@@ -71,15 +72,16 @@ async def load_experiment(
71
72
  pid, created if isinstance(created, int | float) else None
72
73
  )
73
74
 
74
- runs_live = any(is_running(run) for run in runs)
75
- nested = summary["mode"] == "adhoc" and await nests_runs(tables, source)
75
+ for run in runs:
76
+ run["labnotes_running"] = is_running(run)
77
+ runs_live = any(run["labnotes_running"] for run in runs)
78
+ nested = await nests_runs(tables, source)
76
79
  groups = (
77
80
  group_runs(runs, list(choices), await count_groups(tables, source))
78
81
  if nested
79
82
  else None
80
83
  )
81
- matrix = summary["mode"] == "matrix"
82
- gridded = matrix and params.getlist("view")[-1:] == ["grid"]
84
+ gridded = params.getlist("view")[-1:] == ["grid"]
83
85
  axes = {key: varies[key] for key in choices if len(varies[key]) > 1}
84
86
  grid = (
85
87
  pivot_runs(
@@ -94,6 +96,7 @@ async def load_experiment(
94
96
  return {
95
97
  "name": row["name"],
96
98
  "display_name": row["display_name"],
99
+ "config": config,
97
100
  "config_source": row["source"],
98
101
  "counts_set": "counts" in (config or {}),
99
102
  **summary,
@@ -117,6 +120,7 @@ async def load_experiment(
117
120
  "run_view": view,
118
121
  "run_choices": choices,
119
122
  "runs_view": runs_view_command(config),
123
+ "views": view_specs(config),
120
124
  "sort_hrefs": {
121
125
  column: sort_href(view, column) for column in ["status", *shown]
122
126
  },
@@ -5,7 +5,6 @@ from typing import Any, Protocol
5
5
 
6
6
  from labnotes.default_counts import DEFAULT_COUNTS
7
7
  from labnotes.memory_interval import DEFAULT_MEMORY_INTERVAL
8
- from labnotes.modes import DEFAULT_MODE
9
8
  from labnotes.retries import DEFAULT_RETRIES
10
9
 
11
10
  Report = Callable[[dict[str, object]], None]
@@ -43,7 +42,6 @@ class LabnotesConfig:
43
42
  experiments: str
44
43
  files: str
45
44
  runs: str
46
- mode: str = DEFAULT_MODE
47
45
  concurrency: int = 1
48
46
  retries: int = DEFAULT_RETRIES
49
47
  # Set when the config's folder is itself the one experiment.
@@ -59,7 +57,6 @@ class ExperimentConfig:
59
57
  params: dict[str, list[object]]
60
58
  name: str | None = None
61
59
  counts: str = DEFAULT_COUNTS
62
- mode: str = DEFAULT_MODE
63
60
  concurrency: int = 1
64
61
  retries: int = DEFAULT_RETRIES
65
62
  meta: str | None = None
@@ -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"]
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
  from labnotes.models import LabnotesConfig
5
5
  from labnotes.is_non_negative_int import is_non_negative_int
6
6
  from labnotes.is_positive_int import is_positive_int
7
- from labnotes.modes import DEFAULT_MODE, MODES
7
+ from labnotes.config_migration import REMOVED_MODE
8
8
  from labnotes.runner.read_experiment_config import SETTINGS
9
9
  from labnotes.read_path import DEFAULT_PATHS, read_path
10
10
  from labnotes.retries import DEFAULT_RETRIES
@@ -25,6 +25,8 @@ def read_config(path: Path) -> LabnotesConfig:
25
25
  defaults = values.get("experiment", {})
26
26
  if not isinstance(defaults, dict):
27
27
  raise ValueError(f"{path}: experiment must be a table, like [experiment]")
28
+ if "mode" in defaults:
29
+ raise ValueError(f"{path}: [experiment] {REMOVED_MODE}")
28
30
  unknown = sorted(defaults.keys() - SETTINGS)
29
31
  if unknown:
30
32
  raise ValueError(f"{path}: unknown keys in [experiment]: {', '.join(unknown)}")
@@ -39,9 +41,8 @@ def read_config(path: Path) -> LabnotesConfig:
39
41
  table: read_path(path, table, values.get(table, {}), single=single)
40
42
  for table in DEFAULT_PATHS
41
43
  }
42
- mode = experiments.get("mode", DEFAULT_MODE)
43
- if mode not in MODES:
44
- raise ValueError(f"{path}: [experiments] mode must be matrix or adhoc")
44
+ if "mode" in experiments:
45
+ raise ValueError(f"{path}: [experiments] {REMOVED_MODE}")
45
46
  concurrency = experiments.get("concurrency", 1)
46
47
  if not is_positive_int(concurrency):
47
48
  raise ValueError(
@@ -54,7 +55,6 @@ def read_config(path: Path) -> LabnotesConfig:
54
55
  path.resolve(),
55
56
  root,
56
57
  **paths,
57
- mode=mode,
58
58
  concurrency=concurrency,
59
59
  retries=retries,
60
60
  experiment=root.name if single else None,
@@ -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"]
@@ -7,6 +7,8 @@ def describe_event(event: dict[str, object]) -> str:
7
7
  match event["event"]:
8
8
  case "planned":
9
9
  runs = len(event["runs"]) # type: ignore[arg-type]
10
+ if not runs:
11
+ return "Nothing to run: every combination has its repeats."
10
12
  return f"{runs} runs to go, {event['concurrency']} at a time."
11
13
  case "started":
12
14
  args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
@@ -0,0 +1,29 @@
1
+ import fcntl
2
+ import os
3
+ from collections.abc import Iterator
4
+ from contextlib import contextmanager, suppress
5
+
6
+ from labnotes.models import LabnotesConfig
7
+ from labnotes.runner.runs_parent import runs_parent
8
+
9
+
10
+ @contextmanager
11
+ def hold_experiment(config: LabnotesConfig, name: str) -> Iterator[None]:
12
+ folder = runs_parent(config.root, config.runs, name)
13
+ made = not folder.exists()
14
+ folder.mkdir(parents=True, exist_ok=True)
15
+ descriptor = os.open(folder, os.O_RDONLY)
16
+ try:
17
+ try:
18
+ fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
19
+ except BlockingIOError:
20
+ raise ValueError(
21
+ f"{name} is already running in another labnotes run."
22
+ " Wait for it to finish, or stop it, then run again."
23
+ ) from None
24
+ yield
25
+ finally:
26
+ if made:
27
+ with suppress(OSError):
28
+ folder.rmdir()
29
+ os.close(descriptor)
@@ -31,7 +31,6 @@ async def prepare_experiment(
31
31
  experiment = read_experiment_config(
32
32
  config.path if source is None else experiment_config,
33
33
  source or "",
34
- config.mode,
35
34
  config.concurrency,
36
35
  config.retries,
37
36
  config.defaults,
@@ -10,7 +10,7 @@ from labnotes.is_non_negative_int import is_non_negative_int
10
10
  from labnotes.is_non_negative_number import is_non_negative_number
11
11
  from labnotes.memory_interval import DEFAULT_MEMORY_INTERVAL
12
12
  from labnotes.is_positive_int import is_positive_int
13
- from labnotes.modes import MODES
13
+ from labnotes.config_migration import MISSING_CONTINUES, REMOVED_MODE
14
14
 
15
15
  SETTINGS = {
16
16
  "name",
@@ -18,7 +18,6 @@ SETTINGS = {
18
18
  "repeats",
19
19
  "params",
20
20
  "counts",
21
- "mode",
22
21
  "concurrency",
23
22
  "retries",
24
23
  "meta",
@@ -31,13 +30,12 @@ SETTINGS = {
31
30
  "from",
32
31
  }
33
32
  HOOKS = ("setup", "teardown")
34
- VIEWS = {"runs"}
33
+ MAX_VIEWS = 8
35
34
 
36
35
 
37
36
  def read_experiment_config(
38
37
  path: Path,
39
38
  source: str,
40
- mode: str,
41
39
  concurrency: int,
42
40
  retries: int,
43
41
  defaults: dict[str, object] | None = None,
@@ -47,6 +45,8 @@ def read_experiment_config(
47
45
  except tomllib.TOMLDecodeError as error:
48
46
  raise ValueError(f"{path}: {error}") from error
49
47
  values: dict[str, Any] = compose_experiment(defaults or {}, layer)
48
+ if "mode" in values:
49
+ raise ValueError(f"{path}: {REMOVED_MODE}")
50
50
  unknown = sorted(values.keys() - SETTINGS)
51
51
  if unknown:
52
52
  raise ValueError(f"{path}: unknown keys {', '.join(unknown)}")
@@ -72,9 +72,6 @@ def read_experiment_config(
72
72
  counts = values.get("counts", DEFAULT_COUNTS)
73
73
  if not isinstance(counts, str) or not counts.strip():
74
74
  raise ValueError(f"{path}: counts must be a SQL condition")
75
- mode = values.get("mode", mode)
76
- if mode not in MODES:
77
- raise ValueError(f"{path}: mode must be matrix or adhoc")
78
75
  concurrency = values.get("concurrency", concurrency)
79
76
  if not is_positive_int(concurrency):
80
77
  raise ValueError(f"{path}: concurrency must be a positive integer")
@@ -102,27 +99,32 @@ def read_experiment_config(
102
99
  raise ValueError(f"{path}: env.{key} is reserved for labnotes")
103
100
  if not isinstance(value, str):
104
101
  raise ValueError(f"{path}: env.{key} must be a string")
105
- continues = values.get("continues", False)
102
+ if "continues" not in values:
103
+ raise ValueError(f"{path}: {MISSING_CONTINUES}")
104
+ continues = values["continues"]
106
105
  if not isinstance(continues, bool):
107
106
  raise ValueError(f"{path}: continues must be true or false")
108
- if continues and mode != "matrix":
109
- raise ValueError(f'{path}: continues needs mode = "matrix"')
110
107
  views = values.get("views", {})
111
108
  if not isinstance(views, dict):
112
109
  raise ValueError(f"{path}: views must be a table, like [views]")
113
- unknown = sorted(views.keys() - VIEWS)
114
- if unknown:
115
- raise ValueError(f"{path}: unknown keys in views: {', '.join(unknown)}")
116
110
  runs = views.get("runs")
117
111
  if runs is not None and (not isinstance(runs, str) or not runs.strip()):
118
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
+ )
119
122
  return ExperimentConfig(
120
123
  command,
121
124
  repeats,
122
125
  params,
123
126
  name,
124
127
  counts,
125
- mode,
126
128
  concurrency,
127
129
  retries,
128
130
  meta,