labnotes 0.2.7__tar.gz → 0.2.8__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 (218) hide show
  1. {labnotes-0.2.7 → labnotes-0.2.8}/PKG-INFO +6 -2
  2. {labnotes-0.2.7 → labnotes-0.2.8}/README.md +5 -1
  3. labnotes-0.2.8/src/labnotes/config_migration.py +12 -0
  4. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/context.md +23 -20
  5. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/create_app.py +1 -2
  6. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/create_experiment.py +5 -2
  7. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/experiment_definition.py +4 -0
  8. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/list_experiments.py +2 -2
  9. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/load_experiment.py +3 -5
  10. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/models.py +0 -3
  11. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/read_config.py +5 -5
  12. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/describe_event.py +2 -0
  13. labnotes-0.2.8/src/labnotes/runner/hold_experiment.py +29 -0
  14. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/prepare_experiment.py +0 -1
  15. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/read_experiment_config.py +6 -10
  16. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/run_experiment.py +10 -50
  17. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/start_experiment.py +2 -1
  18. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/summarize_config.py +1 -3
  19. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/experiment_rows.py +1 -3
  20. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/templates/experiment.html +1 -2
  21. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/experiment_screen.py +0 -1
  22. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/run_app.py +4 -4
  23. {labnotes-0.2.7 → labnotes-0.2.8}/testing-conventions.toml +7 -7
  24. {labnotes-0.2.7 → labnotes-0.2.8}/tests/conftest.py +1 -1
  25. {labnotes-0.2.7 → labnotes-0.2.8}/tests/integration/conftest.py +1 -1
  26. labnotes-0.2.7/src/labnotes/modes.py +0 -2
  27. labnotes-0.2.7/src/labnotes/runner/new_param_run_id.py +0 -18
  28. labnotes-0.2.7/src/labnotes/runner/param_run_name.py +0 -17
  29. {labnotes-0.2.7 → labnotes-0.2.8}/.gitignore +0 -0
  30. {labnotes-0.2.7 → labnotes-0.2.8}/CHANGELOG.md +0 -0
  31. {labnotes-0.2.7 → labnotes-0.2.8}/MIGRATIONS.md +0 -0
  32. {labnotes-0.2.7 → labnotes-0.2.8}/pyproject.toml +0 -0
  33. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/__init__.py +0 -0
  34. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/__main__.py +0 -0
  35. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/asset_url.py +0 -0
  36. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/check_counts.py +0 -0
  37. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/cli.py +0 -0
  38. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/command_line_group.py +0 -0
  39. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/compose_experiment.py +0 -0
  40. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/context_guide.py +0 -0
  41. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/copy_experiment.py +0 -0
  42. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/count_groups.py +0 -0
  43. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/count_runs.py +0 -0
  44. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/create_note.py +0 -0
  45. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/data_html.py +0 -0
  46. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/default_config_text.py +0 -0
  47. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/default_counts.py +0 -0
  48. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/drain_watch.py +0 -0
  49. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/experiment_notes.py +0 -0
  50. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/experiment_runs.py +0 -0
  51. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/experiments.py +0 -0
  52. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/file_changes.py +0 -0
  53. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/format_duration.py +0 -0
  54. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/format_started.py +0 -0
  55. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/format_value.py +0 -0
  56. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/get_state.py +0 -0
  57. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/grid_cell.py +0 -0
  58. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/group_runs.py +0 -0
  59. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/init.py +0 -0
  60. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/invocation_started.py +0 -0
  61. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/is_non_negative_int.py +0 -0
  62. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/is_non_negative_number.py +0 -0
  63. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/is_positive_int.py +0 -0
  64. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/latest_log.py +0 -0
  65. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/list_runs.py +0 -0
  66. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/load_config.py +0 -0
  67. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/load_run.py +0 -0
  68. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/log.py +0 -0
  69. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/log_path.py +0 -0
  70. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/memory_interval.py +0 -0
  71. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/nests_runs.py +0 -0
  72. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/note_path.py +0 -0
  73. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/notebook_kernel.py +0 -0
  74. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/output_html.py +0 -0
  75. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/parse_param_value.py +0 -0
  76. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/parse_params.py +0 -0
  77. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/parse_run_view.py +0 -0
  78. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/parse_subset.py +0 -0
  79. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/pivot_runs.py +0 -0
  80. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/print_rows.py +0 -0
  81. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/python_blocks.py +0 -0
  82. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/read_path.py +0 -0
  83. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/render_markdown.py +0 -0
  84. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/retries.py +0 -0
  85. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/run_output.py +0 -0
  86. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/__init__.py +0 -0
  87. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/cgroup_memory.py +0 -0
  88. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/check_args.py +0 -0
  89. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/check_hook_placeholders.py +0 -0
  90. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/check_subset.py +0 -0
  91. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/claim_run.py +0 -0
  92. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/command_flags.py +0 -0
  93. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/container_cgroup.py +0 -0
  94. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/container_memory.py +0 -0
  95. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/continue_run.py +0 -0
  96. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/docker_output.py +0 -0
  97. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/execute_run.py +0 -0
  98. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/exit_signal.py +0 -0
  99. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/fill_dirs.py +0 -0
  100. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/fill_experiment_env.py +0 -0
  101. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/finalize_lost_runs.py +0 -0
  102. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/first_invocation.py +0 -0
  103. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/flag_name.py +0 -0
  104. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/group_args.py +0 -0
  105. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/in_subset.py +0 -0
  106. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/invocation_hook.py +0 -0
  107. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/invocation_meta.py +0 -0
  108. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/new_run_id.py +0 -0
  109. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  110. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/override_flags.py +0 -0
  111. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/param_args.py +0 -0
  112. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/param_grid.py +0 -0
  113. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/placeholders.py +0 -0
  114. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/plan_runs.py +0 -0
  115. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/project_snapshot.py +0 -0
  116. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/record_memory.py +0 -0
  117. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/run_folder.py +0 -0
  118. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/run_git.py +0 -0
  119. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/run_headless.py +0 -0
  120. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/run_locked.py +0 -0
  121. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/runs_parent.py +0 -0
  122. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/shell_command.py +0 -0
  123. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/signal_tree.py +0 -0
  124. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/stop_then_kill.py +0 -0
  125. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/untracked_diff.py +0 -0
  126. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/wait_for_tree.py +0 -0
  127. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runner/write_run_json.py +0 -0
  128. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/runs_view_command.py +0 -0
  129. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/save_conflict_copy.py +0 -0
  130. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/save_note.py +0 -0
  131. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/saved_outputs.py +0 -0
  132. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/serve_app.py +0 -0
  133. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/server.py +0 -0
  134. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/set_state.py +0 -0
  135. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/shared_params.py +0 -0
  136. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/sort_href.py +0 -0
  137. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/sql_quote.py +0 -0
  138. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/state_path.py +0 -0
  139. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/static/elapsed.js +0 -0
  140. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/static/experiment.js +0 -0
  141. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/static/labnotes.css +0 -0
  142. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/static/local-time.js +0 -0
  143. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/static/new-note.js +0 -0
  144. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/static/note-edit.js +0 -0
  145. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/static/note-editor.js +0 -0
  146. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/static/row-link.js +0 -0
  147. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/static/run-cell.js +0 -0
  148. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/static/run-output.js +0 -0
  149. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/static/view-toggle.js +0 -0
  150. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/__init__.py +0 -0
  151. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/experiment_run_ids.py +0 -0
  152. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/experiment_toml.py +0 -0
  153. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/experiments_ddl.py +0 -0
  154. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/file_prefix.py +0 -0
  155. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/file_text.py +0 -0
  156. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/iso_time.py +0 -0
  157. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/list_experiment_runs.py +0 -0
  158. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/list_resumable_runs.py +0 -0
  159. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/note_text.py +0 -0
  160. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/open_tables.py +0 -0
  161. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/path_glob.py +0 -0
  162. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/path_pattern.py +0 -0
  163. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/read_blocks.py +0 -0
  164. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/read_experiment.py +0 -0
  165. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/read_file.py +0 -0
  166. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/read_log.py +0 -0
  167. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/read_memory.py +0 -0
  168. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/read_run.py +0 -0
  169. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/read_run_file.py +0 -0
  170. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/runs_columns.py +0 -0
  171. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tables/summarize_experiments.py +0 -0
  172. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/templates/base.html +0 -0
  173. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/templates/editor_imports.html +0 -0
  174. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/templates/index.html +0 -0
  175. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/templates/note.html +0 -0
  176. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/templates/run.html +0 -0
  177. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/__init__.py +0 -0
  178. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/browse_app.py +0 -0
  179. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/browse_tui.py +0 -0
  180. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/check_view_response.py +0 -0
  181. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/config.py +0 -0
  182. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/dim.py +0 -0
  183. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/experiments_screen.py +0 -0
  184. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/fill_table.py +0 -0
  185. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/format_cell.py +0 -0
  186. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/format_elapsed.py +0 -0
  187. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/group_cells.py +0 -0
  188. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/invocation_columns.py +0 -0
  189. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/invocation_rows.py +0 -0
  190. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/leave_prompt.py +0 -0
  191. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/log_block.py +0 -0
  192. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/log_blocks.py +0 -0
  193. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/native_rows.py +0 -0
  194. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/reload_on_change.py +0 -0
  195. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/run_cells.py +0 -0
  196. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/run_output.py +0 -0
  197. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/run_screen.py +0 -0
  198. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/run_tui.py +0 -0
  199. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/runs_table.py +0 -0
  200. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/runs_view.py +0 -0
  201. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/stopped_title.py +0 -0
  202. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/view_column.py +0 -0
  203. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/tui/view_process.py +0 -0
  204. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/utils/__init__.py +0 -0
  205. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/utils/format_bytes.py +0 -0
  206. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/utils/process_alive.py +0 -0
  207. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/utils/run_usage.py +0 -0
  208. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/utils/tree_rss.py +0 -0
  209. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/utils/tree_usage.py +0 -0
  210. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/view/__init__.py +0 -0
  211. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/view/answer.py +0 -0
  212. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/view/build.py +0 -0
  213. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/view/respond.py +0 -0
  214. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/view/serve.py +0 -0
  215. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/wait_for_note.py +0 -0
  216. {labnotes-0.2.7 → labnotes-0.2.8}/src/labnotes/watch_rows.py +0 -0
  217. {labnotes-0.2.7 → labnotes-0.2.8}/tests/e2e/conftest.py +0 -0
  218. {labnotes-0.2.7 → labnotes-0.2.8}/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.8
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)
@@ -33,7 +33,6 @@ def _no_outputs(note: str, content: str) -> Sequence[str]:
33
33
  async def load_experiment(
34
34
  tables: DirSQL,
35
35
  root: Path,
36
- mode: str,
37
36
  name: str,
38
37
  params: QueryParams,
39
38
  outputs_for: Callable[[str, str], Sequence[str]] = _no_outputs,
@@ -46,7 +45,7 @@ async def load_experiment(
46
45
  return None
47
46
  [row] = found
48
47
  config = None if row["config"] is None else json.loads(str(row["config"]))
49
- summary = summarize_config(config, mode)
48
+ summary = summarize_config(config)
50
49
  varies = cast(dict[str, list[object]], summary["varies"])
51
50
  source = experiment_runs(name, list(varies))
52
51
  summary = await check_counts(tables, source, summary)
@@ -72,14 +71,13 @@ async def load_experiment(
72
71
  )
73
72
 
74
73
  runs_live = any(is_running(run) for run in runs)
75
- nested = summary["mode"] == "adhoc" and await nests_runs(tables, source)
74
+ nested = await nests_runs(tables, source)
76
75
  groups = (
77
76
  group_runs(runs, list(choices), await count_groups(tables, source))
78
77
  if nested
79
78
  else None
80
79
  )
81
- matrix = summary["mode"] == "matrix"
82
- gridded = matrix and params.getlist("view")[-1:] == ["grid"]
80
+ gridded = params.getlist("view")[-1:] == ["grid"]
83
81
  axes = {key: varies[key] for key in choices if len(varies[key]) > 1}
84
82
  grid = (
85
83
  pivot_runs(
@@ -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
@@ -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,
@@ -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",
@@ -37,7 +36,6 @@ VIEWS = {"runs"}
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,11 +99,11 @@ 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]")
@@ -122,7 +119,6 @@ def read_experiment_config(
122
119
  params,
123
120
  name,
124
121
  counts,
125
- mode,
126
122
  concurrency,
127
123
  retries,
128
124
  meta,
@@ -2,7 +2,6 @@ import asyncio
2
2
  import json
3
3
  import os
4
4
  import signal
5
- from contextlib import nullcontext
6
5
  from pathlib import Path
7
6
  from tempfile import TemporaryDirectory
8
7
 
@@ -24,9 +23,9 @@ from labnotes.runner.fill_dirs import fill_dirs
24
23
  from labnotes.runner.in_subset import in_subset
25
24
  from labnotes.runner.invocation_hook import invocation_hook
26
25
  from labnotes.runner.invocation_meta import invocation_meta
27
- from labnotes.runner.new_param_run_id import new_param_run_id
28
26
  from labnotes.runner.new_stamped_run_id import new_stamped_run_id
29
27
  from labnotes.runner.param_grid import param_grid
28
+ from labnotes.runner.placeholders import PLACEHOLDERS
30
29
  from labnotes.runner.plan_runs import plan_runs
31
30
  from labnotes.runner.project_snapshot import ProjectSnapshot
32
31
  from labnotes.runner.run_folder import run_folder
@@ -59,11 +58,8 @@ async def run_experiment(
59
58
  inherited = dict(os.environ) if environment is None else environment
60
59
  tables = open_tables(config)
61
60
  existing = await experiment_run_ids(tables, name)
62
- finished: list[dict[str, object]] = []
63
- match experiment.mode:
64
- case "matrix":
65
- counted = await list_experiment_runs(tables, name, experiment.counts)
66
- finished = [json.loads(str(r["params"])) for r in counted if r["counted"]]
61
+ counted = await list_experiment_runs(tables, name, experiment.counts)
62
+ finished = [json.loads(str(r["params"])) for r in counted if r["counted"]]
67
63
  planned: set[str] = set()
68
64
 
69
65
  def taken(run: str) -> bool:
@@ -73,17 +69,6 @@ async def run_experiment(
73
69
  cell for cell in param_grid(experiment.params) if in_subset(cell, only or {})
74
70
  ]
75
71
  cells = plan_runs(grid, experiment.repeats, finished)
76
- group = None
77
- match experiment.mode:
78
- case "adhoc" if len(cells) > 1:
79
- group = new_stamped_run_id(taken)
80
-
81
- def new_id(params: dict[str, object]) -> str:
82
- if group is None:
83
- return new_stamped_run_id(taken)
84
- return f"{group}/" + new_param_run_id(
85
- params, experiment.params, lambda run: taken(f"{group}/{run}")
86
- )
87
72
 
88
73
  locks: dict[str, int] = {}
89
74
 
@@ -115,18 +100,9 @@ async def run_experiment(
115
100
  ),
116
101
  None,
117
102
  )
118
- run = new_id(params) if resumed is None else resumed[0]
103
+ run = new_stamped_run_id(taken) if resumed is None else resumed[0]
119
104
  planned.add(run)
120
105
  plan.append((run, params))
121
- run_dirs = [
122
- str(run_folder(config.root, config.runs, name, run).resolve())
123
- for run, _ in plan
124
- ]
125
- group_dir = (
126
- str(run_folder(config.root, config.runs, name, group).resolve())
127
- if group is not None
128
- else None
129
- )
130
106
  hooks = {
131
107
  "setup": experiment.setup,
132
108
  "teardown": experiment.teardown,
@@ -141,12 +117,6 @@ async def run_experiment(
141
117
  meta = None
142
118
  if experiment.meta is not None:
143
119
  meta = await invocation_meta(command_root, experiment.meta, hook_env["meta"])
144
- invocation_dir = group_dir
145
- match run_dirs:
146
- case [run_dir]:
147
- invocation_dir = run_dir
148
- if invocation_dir is not None:
149
- Path(invocation_dir).mkdir(parents=True, exist_ok=True)
150
120
  runs = [{"run": run, "params": params} for run, params in plan]
151
121
  report(
152
122
  {
@@ -154,7 +124,6 @@ async def run_experiment(
154
124
  "title": experiment.name or name,
155
125
  "runs": runs,
156
126
  "concurrency": concurrency,
157
- "mode": experiment.mode,
158
127
  }
159
128
  )
160
129
  running: dict[str, set[asyncio.subprocess.Process]] = {}
@@ -208,7 +177,7 @@ async def run_experiment(
208
177
  def requeue(run: str, attempt: int) -> None:
209
178
  again = run
210
179
  if not experiment.continues:
211
- again = new_id(ended[run])
180
+ again = new_stamped_run_id(taken)
212
181
  planned.add(again)
213
182
  parents[again] = run
214
183
  attempts[again] = attempt
@@ -284,8 +253,9 @@ async def run_experiment(
284
253
  if snapshot is not None and snapshot.sha is not None:
285
254
  (folder / "git-sha.txt").write_text(snapshot.sha + "\n")
286
255
  (folder / "git-diff.patch").write_bytes(snapshot.diff or b"")
287
- run_invocation_dir = invocation_dir or str(folder.resolve())
288
256
  report({"event": "started", "run": run, "params": params, "folder": folder})
257
+ # {invocation_dir} stays for configs written before runs were flat.
258
+ dirs = dict.fromkeys(PLACEHOLDERS, str(folder.resolve()))
289
259
  running[run] = set()
290
260
  trees[run] = set()
291
261
  result = await execute_run(
@@ -293,10 +263,7 @@ async def run_experiment(
293
263
  folder,
294
264
  fill_dirs(
295
265
  experiment.command,
296
- {
297
- "run_dir": str(folder.resolve()),
298
- "invocation_dir": run_invocation_dir,
299
- },
266
+ dirs,
300
267
  ),
301
268
  params,
302
269
  running[run],
@@ -313,10 +280,7 @@ async def run_experiment(
313
280
  environment=fill_experiment_env(
314
281
  experiment.env,
315
282
  inherited,
316
- {
317
- "run_dir": str(folder.resolve()),
318
- "invocation_dir": run_invocation_dir,
319
- },
283
+ dirs,
320
284
  ),
321
285
  git_info=(
322
286
  {"commit": snapshot.sha, "dirty": bool(snapshot.diff)}
@@ -348,11 +312,7 @@ async def run_experiment(
348
312
  )
349
313
 
350
314
  listen(Controls(interrupt, set_concurrency, kill, finish))
351
- with (
352
- nullcontext(invocation_dir)
353
- if invocation_dir is not None
354
- else TemporaryDirectory(prefix="labnotes-state-")
355
- ) as state_dir:
315
+ with TemporaryDirectory(prefix="labnotes-state-") as state_dir:
356
316
  state_file = Path(state_dir) / "state.json"
357
317
  for hook in ("setup", "teardown"):
358
318
  if hook in hook_env:
@@ -2,6 +2,7 @@ from pathlib import Path
2
2
 
3
3
  from labnotes.models import LabnotesConfig, Listen, Report, Start
4
4
  from labnotes.runner.finalize_lost_runs import finalize_lost_runs
5
+ from labnotes.runner.hold_experiment import hold_experiment
5
6
  from labnotes.runner.prepare_experiment import prepare_experiment
6
7
  from labnotes.runner.project_snapshot import project_snapshot
7
8
  from labnotes.runner.run_experiment import run_experiment
@@ -24,7 +25,7 @@ def start_experiment(
24
25
  config, name, experiment_config, concurrency, only, args, setup, teardown
25
26
  )
26
27
  await finalize_lost_runs(config)
27
- with project_snapshot(config.root) as snapshot:
28
+ with project_snapshot(config.root) as snapshot, hold_experiment(config, name):
28
29
  return await run_experiment(
29
30
  config,
30
31
  name,
@@ -3,7 +3,7 @@ from math import prod
3
3
  from labnotes.default_counts import DEFAULT_COUNTS
4
4
 
5
5
 
6
- def summarize_config(config: dict[str, object] | None, mode: str) -> dict[str, object]:
6
+ def summarize_config(config: dict[str, object] | None) -> dict[str, object]:
7
7
  config = config or {}
8
8
  params = config.get("params")
9
9
  varies = {
@@ -15,13 +15,11 @@ def summarize_config(config: dict[str, object] | None, mode: str) -> dict[str, o
15
15
  cells = prod(len(values) for values in varies.values())
16
16
  command = config.get("command")
17
17
  counts = config.get("counts")
18
- matrix = config.get("mode", mode) == "matrix"
19
18
  return {
20
19
  "fixed": {},
21
20
  "varies": varies,
22
21
  "cell_count": cells,
23
22
  "runs_per_cell": repeats,
24
23
  "completed_run": counts if isinstance(counts, str) else DEFAULT_COUNTS,
25
- "mode": "matrix" if matrix else "adhoc",
26
24
  "other": {} if command is None else {"command": command},
27
25
  }
@@ -19,9 +19,7 @@ async def experiment_rows(
19
19
  case str(), "runs":
20
20
  [row] = rows
21
21
  stored = row["config"]
22
- summary = summarize_config(
23
- json.loads(str(stored)) if stored else None, config.mode
24
- )
22
+ summary = summarize_config(json.loads(str(stored)) if stored else None)
25
23
  runs = await list_experiment_runs(
26
24
  tables, name, str(summary["completed_run"])
27
25
  )