labnotes 0.0.137__tar.gz → 0.1.0__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 (223) hide show
  1. {labnotes-0.0.137 → labnotes-0.1.0}/PKG-INFO +7 -8
  2. {labnotes-0.0.137 → labnotes-0.1.0}/README.md +6 -7
  3. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/cli.py +0 -4
  4. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/context.md +13 -29
  5. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/experiments.py +2 -2
  6. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/load_experiment.py +14 -2
  7. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/execute_run.py +4 -1
  8. labnotes-0.1.0/src/labnotes/runner/project_snapshot.py +118 -0
  9. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/run_experiment.py +15 -4
  10. labnotes-0.1.0/src/labnotes/runner/start_experiment.py +40 -0
  11. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/leave_prompt.py +2 -3
  12. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/run_app.py +2 -5
  13. {labnotes-0.0.137 → labnotes-0.1.0}/testing-conventions.toml +0 -30
  14. {labnotes-0.0.137 → labnotes-0.1.0}/tests/integration/conftest.py +0 -19
  15. {labnotes-0.0.137 → labnotes-0.1.0}/uv.lock +6 -6
  16. labnotes-0.0.137/src/labnotes/daemon/control_run.py +0 -16
  17. labnotes-0.0.137/src/labnotes/daemon/daemon_app.py +0 -181
  18. labnotes-0.0.137/src/labnotes/daemon/daemon_client.py +0 -12
  19. labnotes-0.0.137/src/labnotes/daemon/daemon_running.py +0 -14
  20. labnotes-0.0.137/src/labnotes/daemon/daemon_server.py +0 -12
  21. labnotes-0.0.137/src/labnotes/daemon/describe_status.py +0 -21
  22. labnotes-0.0.137/src/labnotes/daemon/ensure_daemon.py +0 -26
  23. labnotes-0.0.137/src/labnotes/daemon/exit_when_idle.py +0 -15
  24. labnotes-0.0.137/src/labnotes/daemon/last_stderr.py +0 -15
  25. labnotes-0.0.137/src/labnotes/daemon/remote_start.py +0 -94
  26. labnotes-0.0.137/src/labnotes/daemon/run_daemon.py +0 -29
  27. labnotes-0.0.137/src/labnotes/daemon/run_logs.py +0 -54
  28. labnotes-0.0.137/src/labnotes/daemon/serve_daemon.py +0 -20
  29. labnotes-0.0.137/src/labnotes/daemon/socket_path.py +0 -15
  30. labnotes-0.0.137/src/labnotes/daemon/stop_runs.py +0 -12
  31. labnotes-0.0.137/src/labnotes/daemon/watch_daemon.py +0 -77
  32. labnotes-0.0.137/src/labnotes/daemon/watch_status.py +0 -19
  33. labnotes-0.0.137/src/labnotes/daemon_command.py +0 -14
  34. labnotes-0.0.137/src/labnotes/run_command.py +0 -32
  35. labnotes-0.0.137/src/labnotes/run_control.py +0 -45
  36. labnotes-0.0.137/src/labnotes/run_status.py +0 -29
  37. labnotes-0.0.137/src/labnotes/runner/hold_folder.py +0 -15
  38. labnotes-0.0.137/src/labnotes/utils/__init__.py +0 -0
  39. {labnotes-0.0.137 → labnotes-0.1.0}/.gitignore +0 -0
  40. {labnotes-0.0.137 → labnotes-0.1.0}/CHANGELOG.md +0 -0
  41. {labnotes-0.0.137 → labnotes-0.1.0}/MIGRATIONS.md +0 -0
  42. {labnotes-0.0.137 → labnotes-0.1.0}/pyproject.toml +0 -0
  43. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/__init__.py +0 -0
  44. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/__main__.py +0 -0
  45. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/asset_url.py +0 -0
  46. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/check_counts.py +0 -0
  47. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/compose_experiment.py +0 -0
  48. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/context_guide.py +0 -0
  49. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/count_groups.py +0 -0
  50. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/count_runs.py +0 -0
  51. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/create_app.py +0 -0
  52. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/create_experiment.py +0 -0
  53. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/create_note.py +0 -0
  54. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/data_html.py +0 -0
  55. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/default_config_text.py +0 -0
  56. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/default_counts.py +0 -0
  57. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/drain_watch.py +0 -0
  58. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/experiment_notes.py +0 -0
  59. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/experiment_runs.py +0 -0
  60. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/file_changes.py +0 -0
  61. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/format_duration.py +0 -0
  62. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/format_started.py +0 -0
  63. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/format_value.py +0 -0
  64. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/get_state.py +0 -0
  65. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/grid_cell.py +0 -0
  66. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/group_runs.py +0 -0
  67. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/init.py +0 -0
  68. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/invocation_started.py +0 -0
  69. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/is_non_negative_int.py +0 -0
  70. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/is_non_negative_number.py +0 -0
  71. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/is_positive_int.py +0 -0
  72. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/list_experiments.py +0 -0
  73. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/list_runs.py +0 -0
  74. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/load_config.py +0 -0
  75. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/load_run.py +0 -0
  76. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/log.py +0 -0
  77. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/log_path.py +0 -0
  78. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/memory_interval.py +0 -0
  79. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/models.py +0 -0
  80. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/modes.py +0 -0
  81. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/nests_runs.py +0 -0
  82. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/note_path.py +0 -0
  83. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/notebook_kernel.py +0 -0
  84. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/output_html.py +0 -0
  85. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/parse_run_view.py +0 -0
  86. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/parse_subset.py +0 -0
  87. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/pivot_runs.py +0 -0
  88. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/print_rows.py +0 -0
  89. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/python_blocks.py +0 -0
  90. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/read_config.py +0 -0
  91. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/read_path.py +0 -0
  92. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/render_markdown.py +0 -0
  93. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/retries.py +0 -0
  94. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/run_output.py +0 -0
  95. {labnotes-0.0.137/src/labnotes/daemon → labnotes-0.1.0/src/labnotes/runner}/__init__.py +0 -0
  96. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/cgroup_memory.py +0 -0
  97. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/check_args.py +0 -0
  98. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/check_subset.py +0 -0
  99. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/container_cgroup.py +0 -0
  100. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/container_memory.py +0 -0
  101. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/describe_event.py +0 -0
  102. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/docker_output.py +0 -0
  103. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/exit_signal.py +0 -0
  104. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/fill_dirs.py +0 -0
  105. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/fill_experiment_env.py +0 -0
  106. {labnotes-0.0.137/src/labnotes/daemon → labnotes-0.1.0/src/labnotes/runner}/finalize_lost_runs.py +0 -0
  107. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/flag_name.py +0 -0
  108. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/hook_dirs.py +0 -0
  109. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/in_subset.py +0 -0
  110. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/invocation_hook.py +0 -0
  111. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/invocation_meta.py +0 -0
  112. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/new_param_run_id.py +0 -0
  113. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/new_run_id.py +0 -0
  114. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  115. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/param_args.py +0 -0
  116. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/param_grid.py +0 -0
  117. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/param_run_name.py +0 -0
  118. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/plan_runs.py +0 -0
  119. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/prepare_experiment.py +0 -0
  120. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/read_experiment_config.py +0 -0
  121. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/record_memory.py +0 -0
  122. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/run_folder.py +0 -0
  123. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/run_git.py +0 -0
  124. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/run_headless.py +0 -0
  125. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/runs_parent.py +0 -0
  126. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/shell_command.py +0 -0
  127. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/signal_tree.py +0 -0
  128. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/stop_then_kill.py +0 -0
  129. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/wait_for_tree.py +0 -0
  130. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runner/write_run_json.py +0 -0
  131. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/runs_view_command.py +0 -0
  132. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/save_conflict_copy.py +0 -0
  133. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/save_note.py +0 -0
  134. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/saved_outputs.py +0 -0
  135. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/serve_app.py +0 -0
  136. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/server.py +0 -0
  137. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/set_state.py +0 -0
  138. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/shared_params.py +0 -0
  139. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/sort_href.py +0 -0
  140. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/sql_quote.py +0 -0
  141. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/state_path.py +0 -0
  142. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/elapsed.js +0 -0
  143. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/experiment.js +0 -0
  144. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/labnotes.css +0 -0
  145. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/local-time.js +0 -0
  146. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/new-note.js +0 -0
  147. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/note-edit.js +0 -0
  148. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/note-editor.js +0 -0
  149. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/row-link.js +0 -0
  150. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/run-cell.js +0 -0
  151. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/run-output.js +0 -0
  152. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/static/view-toggle.js +0 -0
  153. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/summarize_config.py +0 -0
  154. {labnotes-0.0.137/src/labnotes/runner → labnotes-0.1.0/src/labnotes/tables}/__init__.py +0 -0
  155. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/experiment_rows.py +0 -0
  156. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/experiment_run_ids.py +0 -0
  157. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/experiment_toml.py +0 -0
  158. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/experiments_ddl.py +0 -0
  159. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/file_prefix.py +0 -0
  160. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/file_text.py +0 -0
  161. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/iso_time.py +0 -0
  162. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/list_experiment_runs.py +0 -0
  163. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/note_text.py +0 -0
  164. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/open_tables.py +0 -0
  165. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/path_glob.py +0 -0
  166. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/path_pattern.py +0 -0
  167. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_blocks.py +0 -0
  168. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_experiment.py +0 -0
  169. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_file.py +0 -0
  170. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_log.py +0 -0
  171. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_memory.py +0 -0
  172. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_run.py +0 -0
  173. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/read_run_file.py +0 -0
  174. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/runs_columns.py +0 -0
  175. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tables/summarize_experiments.py +0 -0
  176. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/base.html +0 -0
  177. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/editor_imports.html +0 -0
  178. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/experiment.html +0 -0
  179. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/index.html +0 -0
  180. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/note.html +0 -0
  181. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/templates/run.html +0 -0
  182. {labnotes-0.0.137/src/labnotes/tables → labnotes-0.1.0/src/labnotes/tui}/__init__.py +0 -0
  183. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/browse_app.py +0 -0
  184. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/browse_tui.py +0 -0
  185. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/check_view_response.py +0 -0
  186. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/config.py +0 -0
  187. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/dim.py +0 -0
  188. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/experiment_screen.py +0 -0
  189. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/experiments_screen.py +0 -0
  190. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/fill_table.py +0 -0
  191. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/format_cell.py +0 -0
  192. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/format_elapsed.py +0 -0
  193. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/group_cells.py +0 -0
  194. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/invocation_columns.py +0 -0
  195. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/invocation_rows.py +0 -0
  196. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/log_block.py +0 -0
  197. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/log_blocks.py +0 -0
  198. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/native_rows.py +0 -0
  199. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/reload_on_change.py +0 -0
  200. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/run_cells.py +0 -0
  201. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/run_output.py +0 -0
  202. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/run_screen.py +0 -0
  203. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/run_tui.py +0 -0
  204. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/runs_table.py +0 -0
  205. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/runs_view.py +0 -0
  206. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/stopped_title.py +0 -0
  207. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/view_column.py +0 -0
  208. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/tui/view_process.py +0 -0
  209. {labnotes-0.0.137/src/labnotes/tui → labnotes-0.1.0/src/labnotes/utils}/__init__.py +0 -0
  210. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/utils/format_bytes.py +0 -0
  211. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/utils/process_alive.py +0 -0
  212. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/utils/run_usage.py +0 -0
  213. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/utils/tree_rss.py +0 -0
  214. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/utils/tree_usage.py +0 -0
  215. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/view/__init__.py +0 -0
  216. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/view/answer.py +0 -0
  217. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/view/build.py +0 -0
  218. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/view/respond.py +0 -0
  219. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/view/serve.py +0 -0
  220. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/wait_for_note.py +0 -0
  221. {labnotes-0.0.137 → labnotes-0.1.0}/src/labnotes/watch_rows.py +0 -0
  222. {labnotes-0.0.137 → labnotes-0.1.0}/tests/conftest.py +0 -0
  223. {labnotes-0.0.137 → labnotes-0.1.0}/tests/e2e/conftest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.137
3
+ Version: 0.1.0
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>
@@ -122,13 +122,12 @@ line per event instead.
122
122
  - `-e PATH` reads another file instead of `experiment.toml`.
123
123
  - `--PARAM=VALUE` runs only the cells where that param has that value, like
124
124
  `--model=small`.
125
- - With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) leaves them
126
- running. `s` stops the running scripts and records their end, then kills any
127
- still going 5 seconds later. Escape goes back. SIGTERM stops them like `s`.
128
- - A second `run` of the same experiment fails with "already running" while the
129
- daemon still has the first going.
130
- - It exits 0 even if runs fail; the summary line counts failures. If it can't
131
- start or reach the daemon, it prints the cause on one line and exits 1.
125
+ - With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) stops the running scripts and records their end,
126
+ then kills any still going 5 seconds later. Escape goes back. SIGTERM and
127
+ SIGHUP stop them the same way.
128
+ - The foreground process schedules the next run when a slot frees. Closing
129
+ its terminal stops the runs. Agents should use a dedicated tmux window.
130
+ - It exits 0 even if runs fail; the summary line counts failures.
132
131
 
133
132
  `labnotes server` serves a local site at http://127.0.0.1:8000 from the same
134
133
  `.labnotes.toml`: a table of experiments, and a page per experiment with its
@@ -97,13 +97,12 @@ line per event instead.
97
97
  - `-e PATH` reads another file instead of `experiment.toml`.
98
98
  - `--PARAM=VALUE` runs only the cells where that param has that value, like
99
99
  `--model=small`.
100
- - With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) leaves them
101
- running. `s` stops the running scripts and records their end, then kills any
102
- still going 5 seconds later. Escape goes back. SIGTERM stops them like `s`.
103
- - A second `run` of the same experiment fails with "already running" while the
104
- daemon still has the first going.
105
- - It exits 0 even if runs fail; the summary line counts failures. If it can't
106
- start or reach the daemon, it prints the cause on one line and exits 1.
100
+ - With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) stops the running scripts and records their end,
101
+ then kills any still going 5 seconds later. Escape goes back. SIGTERM and
102
+ SIGHUP stop them the same way.
103
+ - The foreground process schedules the next run when a slot frees. Closing
104
+ its terminal stops the runs. Agents should use a dedicated tmux window.
105
+ - It exits 0 even if runs fail; the summary line counts failures.
107
106
 
108
107
  `labnotes server` serves a local site at http://127.0.0.1:8000 from the same
109
108
  `.labnotes.toml`: a table of experiments, and a page per experiment with its
@@ -3,12 +3,10 @@ from pathlib import Path
3
3
  import click
4
4
 
5
5
  from labnotes.context_guide import context_guide
6
- from labnotes.daemon_command import daemon_command
7
6
  from labnotes.experiments import experiments
8
7
  from labnotes.init import init
9
8
  from labnotes.load_config import load_config
10
9
  from labnotes.read_config import CONFIG_FILE
11
- from labnotes.run_command import run_command
12
10
  from labnotes.server import server
13
11
 
14
12
 
@@ -39,8 +37,6 @@ def context(ctx: click.Context) -> None:
39
37
  click.echo(context_guide(load_config(ctx), Path.cwd()), nl=False)
40
38
 
41
39
 
42
- main.add_command(daemon_command)
43
40
  main.add_command(experiments)
44
41
  main.add_command(init)
45
- main.add_command(run_command)
46
42
  main.add_command(server)
@@ -85,7 +85,7 @@ run folder. `command` is the shell command the run executed, with the placeholde
85
85
  and the params filled in, written before the run starts, so a killed run has it
86
86
  and a later edit to `experiment.toml` doesn't hide what a run did. `ended_by` is `"timeout"`, `"stop"` (labnotes stopped it),
87
87
  `"signal"` (a signal labnotes didn't send, like the out-of-memory killer),
88
- `"lost"` (its process died unseen; the daemon marks these when it starts, with
88
+ `"lost"` (its process died unseen; the next run marks these when it starts, with
89
89
  `ended` set and `exit_code` null) or null (the script exited itself). `signal` is the signal number read from the
90
90
  exit code (negative, or 128 + N from the shell), else null.
91
91
  `git` is `{"commit": "<full HEAD SHA>", "dirty": false}`, read from the command's
@@ -157,10 +157,10 @@ adding a repeat. When the retries run out, the last run keeps exit 75: it never
157
157
  got to test its params, so treat it as blocked, not as a result. Stopping the
158
158
  runs, or `x` on a run waiting to retry, cancels the retry. A run that was stopped never retries. The live view
159
159
  shows a waiting retry as `retry in 30s` and the end as `gave up (exit 75)`;
160
- `run status --json` reports `retrying`, `retried`, `gave up` and
160
+ The live view reports `retrying`, `retried`, `gave up` and
161
161
  `retry cancelled` events.
162
162
 
163
- Runs can take a long time. Start `run` in the background, then check on it with `labnotes experiments foo runs --format json`; a run with no `ended` is still going. Each run lists `size`, the bytes in its run folder, and a running run lists `memory`, the resident bytes of its command and everything it started.
163
+ Runs can take a long time. Start `run` in a dedicated tmux window, then check on it with `labnotes experiments foo runs --format json`; a run with no `ended` is still going. Each run lists `size`, the bytes in its run folder, and a running run lists `memory`, the resident bytes of its command and everything it started.
164
164
 
165
165
  While a run goes, labnotes appends a memory sample to `memory.jsonl` in its run
166
166
  folder every 5 seconds: one JSON line, `{"time": <unix seconds>, "rss": <bytes>,
@@ -202,33 +202,17 @@ going. Elsewhere, it prints a line per event instead.
202
202
  `run.json` gets `"error": "timed out after ..."` and `"ended_by": "timeout"`
203
203
  next to its exit code.
204
204
  Unset, runs have no time limit.
205
- - `run` hands its runs to the project's labnotes daemon, starting it if
206
- needed, and shows or waits for them. The daemon listens on a socket in
207
- `/tmp/labnotes-<uid>/` and exits once nothing is running and no client has
208
- been connected for 15 seconds. Runs belong to the daemon, so quitting the
209
- live view, closing its terminal, or killing `run` outright leaves them going.
210
- - Each labnotes version starts its own daemon. After an upgrade, new runs go
211
- to the new version's daemon while an older one finishes its runs and exits.
212
- `run status`, `run stop` and `run control` reach only the daemon of the
213
- version you call them with. Each `run` reads `.labnotes.toml` as it is on
214
- disk, even when the daemon is already up.
215
- - Ctrl-C stops the running scripts and records their end. A second Ctrl-C
216
- kills them. SIGTERM stops them the same way, then kills any still going 5
217
- seconds later. Once the runs are done, either one closes the live view.
218
- - `labnotes run stop` stops every run the daemon has going: SIGTERM, then
219
- SIGKILL 5 seconds later.
220
- - `labnotes run status` prints what the daemon has going, a line per event so
221
- far plus the latest line each running run logged and what each running run
222
- uses (`usage` events with `--json`). `-f` keeps following, and
223
- waits for a daemon when none is up. `--json` prints one JSON object per line.
224
- Following doesn't keep the daemon up.
225
- - `labnotes run control NAME ACTION [ARG]` sends one live-view control to a
226
- running experiment: `kill` a run, `set_concurrency` to a number,
227
- `interrupt` or `finish`.
228
- - A second `run` of the same experiment fails while the first is going on
229
- the same daemon.
205
+ - `run` owns its runs in the foreground. In a terminal it shows a live table;
206
+ outside a terminal it prints events until every run ends. A free concurrency
207
+ slot starts the next planned run. Agents should start it in a dedicated tmux
208
+ window so it can keep running between turns.
209
+ - Ctrl-C, SIGTERM, SIGHUP, or quitting the live view stops running scripts:
210
+ SIGTERM first, then SIGKILL after 5 seconds if needed. Runs are recorded
211
+ as stopped before labnotes exits. The next start marks unfinished records
212
+ whose processes are gone as lost.
213
+ - A second invocation starts its own run session and reads the current config.
230
214
  - It exits 0 even if runs fail; the summary line counts failures. A run that
231
215
  was retried counts as retried, not failed. If it can't
232
- start or reach the daemon, it prints the cause on one line and exits 1.
216
+ start a run, it prints the cause on one line and exits 1.
233
217
 
234
218
  `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.
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  import click
6
6
 
7
7
  from labnotes.create_experiment import create_experiment
8
- from labnotes.daemon.remote_start import remote_start
8
+ from labnotes.runner.start_experiment import start_experiment
9
9
  from labnotes.utils.format_bytes import format_bytes
10
10
  from labnotes.load_config import load_config
11
11
  from labnotes.parse_subset import parse_subset
@@ -109,7 +109,7 @@ def experiments(
109
109
  only = parse_subset(ctx.args)
110
110
  except ValueError as error:
111
111
  raise click.UsageError(str(error)) from error
112
- start = remote_start(
112
+ start = start_experiment(
113
113
  config,
114
114
  name,
115
115
  experiment_config,
@@ -8,7 +8,6 @@ from dirsql import DirSQL
8
8
  from labnotes.check_counts import check_counts
9
9
  from labnotes.count_groups import count_groups
10
10
  from labnotes.count_runs import count_runs
11
- from labnotes.daemon.daemon_running import daemon_running
12
11
  from labnotes.experiment_notes import experiment_notes
13
12
  from labnotes.experiment_runs import experiment_runs
14
13
  from labnotes.format_value import format_value
@@ -24,6 +23,7 @@ from labnotes.runs_view_command import runs_view_command
24
23
  from labnotes.sort_href import sort_href
25
24
  from labnotes.sql_quote import quote_string
26
25
  from labnotes.summarize_config import summarize_config
26
+ from labnotes.utils.process_alive import process_alive
27
27
 
28
28
 
29
29
  def _no_outputs(note: str, content: str) -> Sequence[str]:
@@ -60,6 +60,18 @@ async def load_experiment(
60
60
  [totals] = await count_runs(tables, source, str(summary["completed_run"]))
61
61
  notes = await experiment_notes(tables, root, name)
62
62
  runs = await list_runs(tables, source, str(summary["completed_run"]), view)
63
+
64
+ def is_running(row: dict[str, object]) -> bool:
65
+ record = row.get("run")
66
+ if not row.get("labnotes_unfinished") or not isinstance(record, str):
67
+ return False
68
+ fields = json.loads(record)
69
+ pid, created = fields.get("pid"), fields.get("pid_created")
70
+ return isinstance(pid, int) and process_alive(
71
+ pid, created if isinstance(created, int | float) else None
72
+ )
73
+
74
+ runs_live = any(is_running(run) for run in runs)
63
75
  nested = summary["mode"] == "adhoc" and await nests_runs(tables, source)
64
76
  groups = (
65
77
  group_runs(runs, list(choices), await count_groups(tables, source))
@@ -98,7 +110,7 @@ async def load_experiment(
98
110
  for note in notes
99
111
  ],
100
112
  "runs_list": runs,
101
- "runs_live": await daemon_running(root),
113
+ "runs_live": runs_live,
102
114
  "run_groups": groups,
103
115
  "run_grid": grid,
104
116
  "run_columns": shown,
@@ -38,6 +38,7 @@ async def execute_run(
38
38
  state_file: Path,
39
39
  memory_interval: float,
40
40
  environment: dict[str, str] | None = None,
41
+ git_info: dict[str, str | bool] | None = None,
41
42
  ) -> dict[str, object]:
42
43
  resolved = shell_command(command, folder, params, args)
43
44
  run: dict[str, object] = {
@@ -49,7 +50,9 @@ async def execute_run(
49
50
  "error": None,
50
51
  "ended_by": None,
51
52
  "signal": None,
52
- "git": await asyncio.to_thread(run_git, root),
53
+ "git": git_info
54
+ if git_info is not None
55
+ else await asyncio.to_thread(run_git, root),
53
56
  }
54
57
  if args:
55
58
  run["args"] = list(args)
@@ -0,0 +1,118 @@
1
+ import logging
2
+ import os
3
+ import signal
4
+ import shutil
5
+ import subprocess
6
+ from contextlib import contextmanager
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass
9
+ from collections.abc import Iterator
10
+ from pathlib import Path
11
+ from tempfile import TemporaryDirectory
12
+ from types import FrameType
13
+
14
+
15
+ @dataclass
16
+ class ProjectSnapshot:
17
+ root: Path
18
+ sha: str | None = None
19
+ diff: bytes | None = None
20
+
21
+
22
+ def git(root: Path, *args: str) -> bytes:
23
+ return subprocess.check_output(["git", *args], cwd=root, stderr=subprocess.DEVNULL)
24
+
25
+
26
+ @contextmanager
27
+ def project_snapshot(root: Path) -> Iterator[ProjectSnapshot]:
28
+ """Capture the working tree once for all commands in an invocation."""
29
+ try:
30
+ git(root, "rev-parse", "--is-inside-work-tree")
31
+ except (OSError, subprocess.SubprocessError):
32
+ logging.warning("Project is not a git repository; running from the live tree")
33
+ yield ProjectSnapshot(root)
34
+ return
35
+
36
+ try:
37
+ sha = git(root, "rev-parse", "HEAD").decode("ascii").strip()
38
+ diff = git(root, "diff", "HEAD")
39
+ except (OSError, subprocess.SubprocessError):
40
+ logging.warning("Project has no readable Git HEAD; running from the live tree")
41
+ yield ProjectSnapshot(root)
42
+ return
43
+ files = filter(
44
+ None, git(root, "ls-files", "-co", "--exclude-standard", "-z").split(b"\0")
45
+ )
46
+ ignored = filter(
47
+ None,
48
+ git(root, "ls-files", "-oi", "--exclude-standard", "--directory", "-z").split(
49
+ b"\0"
50
+ ),
51
+ )
52
+ with TemporaryDirectory(prefix="labnotes-") as directory:
53
+ snapshot = Path(directory)
54
+ previous: dict[
55
+ signal.Signals,
56
+ Callable[[int, FrameType | None], object] | int | signal.Handlers | None,
57
+ ] = {}
58
+
59
+ def stop(_number: int, _frame: object) -> None:
60
+ raise KeyboardInterrupt
61
+
62
+ try:
63
+ for number in (signal.SIGTERM, signal.SIGHUP):
64
+ try:
65
+ old_handler = signal.signal(number, stop)
66
+ except ValueError:
67
+ break # Signals can only be installed from the main thread.
68
+ previous[number] = old_handler
69
+ subprocess.run(
70
+ [
71
+ "git",
72
+ "clone",
73
+ "--quiet",
74
+ "--shared",
75
+ "--no-checkout",
76
+ str(root),
77
+ str(snapshot),
78
+ ],
79
+ check=True,
80
+ )
81
+ for raw in files:
82
+ relative = Path(raw.decode())
83
+ source = root / relative
84
+ if not source.exists() and not source.is_symlink():
85
+ continue
86
+ target = snapshot / relative
87
+ target.parent.mkdir(parents=True, exist_ok=True)
88
+ if source.is_symlink():
89
+ target.symlink_to(
90
+ os.readlink(source), target_is_directory=source.is_dir()
91
+ )
92
+ elif source.is_dir():
93
+ shutil.copytree(source, target, symlinks=True)
94
+ else:
95
+ shutil.copy2(source, target)
96
+ skipped: set[Path] = set()
97
+ for raw in ignored:
98
+ relative = Path(raw.decode().rstrip("/"))
99
+ if any(parent in relative.parents for parent in skipped):
100
+ continue
101
+ if any((snapshot / parent).is_symlink() for parent in relative.parents):
102
+ continue
103
+ source = root / relative
104
+ if source.is_dir() and (source / "pyvenv.cfg").exists():
105
+ skipped.add(relative)
106
+ continue
107
+ target = snapshot / relative
108
+ if target.is_symlink() or target.exists():
109
+ continue
110
+ target.parent.mkdir(parents=True, exist_ok=True)
111
+ target.symlink_to(source, target_is_directory=source.is_dir())
112
+ subprocess.run(
113
+ ["git", "-C", str(snapshot), "reset", "--quiet", sha], check=True
114
+ )
115
+ finally:
116
+ for number, handler in previous.items():
117
+ signal.signal(number, handler)
118
+ yield ProjectSnapshot(snapshot, sha, diff)
@@ -27,6 +27,7 @@ from labnotes.runner.new_param_run_id import new_param_run_id
27
27
  from labnotes.runner.new_stamped_run_id import new_stamped_run_id
28
28
  from labnotes.runner.param_grid import param_grid
29
29
  from labnotes.runner.plan_runs import plan_runs
30
+ from labnotes.runner.project_snapshot import ProjectSnapshot
30
31
  from labnotes.runner.run_folder import run_folder
31
32
  from labnotes.runner.signal_tree import signal_tree
32
33
  from labnotes.state_path import STATE_ENV
@@ -49,7 +50,9 @@ async def run_experiment(
49
50
  only: dict[str, str] | None = None,
50
51
  args: list[str] | None = None,
51
52
  environment: dict[str, str] | None = None,
53
+ snapshot: ProjectSnapshot | None = None,
52
54
  ) -> list[dict[str, object]]:
55
+ command_root = snapshot.root if snapshot is not None else config.root
53
56
  inherited = dict(os.environ) if environment is None else environment
54
57
  tables = open_tables(config)
55
58
  existing = await experiment_run_ids(tables, name)
@@ -109,7 +112,7 @@ async def run_experiment(
109
112
  meta = None
110
113
  if experiment.meta is not None:
111
114
  meta = await invocation_meta(
112
- config.root, fill_dirs(experiment.meta, dirs), hook_env["meta"]
115
+ command_root, fill_dirs(experiment.meta, dirs), hook_env["meta"]
113
116
  )
114
117
  if dirs:
115
118
  Path(dirs["invocation_dir"]).mkdir(parents=True, exist_ok=True)
@@ -242,12 +245,15 @@ async def run_experiment(
242
245
  async def start(run: str, params: dict[str, object]) -> None:
243
246
  folder = run_folder(config.root, config.runs, name, run)
244
247
  folder.mkdir(parents=True, exist_ok=True)
248
+ if snapshot is not None and snapshot.sha is not None:
249
+ (folder / "git-sha.txt").write_text(snapshot.sha + "\n")
250
+ (folder / "git-diff.patch").write_bytes(snapshot.diff or b"")
245
251
  run_invocation_dir = dirs.get("invocation_dir") or str(folder.resolve())
246
252
  report({"event": "started", "run": run, "params": params, "folder": folder})
247
253
  running[run] = set()
248
254
  trees[run] = set()
249
255
  result = await execute_run(
250
- config.root,
256
+ command_root,
251
257
  folder,
252
258
  fill_dirs(
253
259
  experiment.command,
@@ -276,6 +282,11 @@ async def run_experiment(
276
282
  "invocation_dir": run_invocation_dir,
277
283
  },
278
284
  ),
285
+ git_info=(
286
+ {"commit": snapshot.sha, "dirty": bool(snapshot.diff)}
287
+ if snapshot is not None and snapshot.sha is not None
288
+ else None
289
+ ),
279
290
  )
280
291
  del running[run], trees[run]
281
292
  report({"event": "ended", "run": run, **result})
@@ -290,7 +301,7 @@ async def run_experiment(
290
301
  if experiment.teardown is not None:
291
302
  report({"event": "tearing down"})
292
303
  await invocation_hook(
293
- config.root,
304
+ command_root,
294
305
  "teardown",
295
306
  fill_dirs(experiment.teardown, dirs),
296
307
  hook_env["teardown"],
@@ -311,7 +322,7 @@ async def run_experiment(
311
322
  if experiment.setup is not None:
312
323
  report({"event": "setting up"})
313
324
  await invocation_hook(
314
- config.root,
325
+ command_root,
315
326
  "setup",
316
327
  fill_dirs(experiment.setup, dirs),
317
328
  hook_env["setup"],
@@ -0,0 +1,40 @@
1
+ from pathlib import Path
2
+
3
+ from labnotes.models import LabnotesConfig, Listen, Report, Start
4
+ from labnotes.runner.finalize_lost_runs import finalize_lost_runs
5
+ from labnotes.runner.prepare_experiment import prepare_experiment
6
+ from labnotes.runner.project_snapshot import project_snapshot
7
+ from labnotes.runner.run_experiment import run_experiment
8
+
9
+
10
+ def start_experiment(
11
+ config: LabnotesConfig,
12
+ name: str,
13
+ experiment_config: Path | None,
14
+ concurrency: int | None,
15
+ timeout: int | None,
16
+ only: dict[str, str] | None,
17
+ args: list[str] | None,
18
+ setup: str | None,
19
+ teardown: str | None,
20
+ ) -> Start:
21
+ async def start(report: Report, listen: Listen) -> list[dict[str, object]]:
22
+ experiment, limit = await prepare_experiment(
23
+ config, name, experiment_config, concurrency, only, args, setup, teardown
24
+ )
25
+ await finalize_lost_runs(config)
26
+ with project_snapshot(config.root) as snapshot:
27
+ return await run_experiment(
28
+ config,
29
+ name,
30
+ experiment,
31
+ limit,
32
+ report,
33
+ listen,
34
+ timeout=timeout,
35
+ only=only,
36
+ args=args,
37
+ snapshot=snapshot,
38
+ )
39
+
40
+ return start
@@ -10,8 +10,7 @@ class LeavePrompt(ModalScreen[str | None]):
10
10
  #prompt { width: auto; padding: 1 2; border: round $accent; }
11
11
  """
12
12
  BINDINGS = [
13
- Binding("enter,ctrl+c", "dismiss('leave')", "Leave running"),
14
- Binding("s", "dismiss('stop')", "Stop them"),
13
+ Binding("enter,ctrl+c,s", "dismiss('stop')", "Stop runs"),
15
14
  Binding("escape", "dismiss", "Cancel"),
16
15
  ]
17
16
 
@@ -23,6 +22,6 @@ class LeavePrompt(ModalScreen[str | None]):
23
22
  runs = "run" if self.count == 1 else "runs"
24
23
  yield Static(
25
24
  f"{self.count} {runs} still going. "
26
- "Leave them running (enter) / stop them (s) / cancel (esc).",
25
+ "Stop them (enter or s) / cancel (esc).",
27
26
  id="prompt",
28
27
  )
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  import signal
3
2
  import time
4
3
  from collections.abc import Sequence
@@ -100,7 +99,7 @@ class RunApp(App[None]):
100
99
  def listen(self, controls: Controls) -> None:
101
100
  self.controls = controls
102
101
  stop_then_kill(self.stop_runs, signal.SIGTERM)
103
- asyncio.get_running_loop().add_signal_handler(signal.SIGHUP, self.exit)
102
+ stop_then_kill(self.stop_runs, signal.SIGHUP)
104
103
 
105
104
  def show_event(self, event: dict[str, object]) -> None:
106
105
  table = self.query_one(DataTable)
@@ -286,9 +285,7 @@ class RunApp(App[None]):
286
285
  self.action_interrupt()
287
286
 
288
287
  def answer(self, choice: str | None) -> None:
289
- if choice == "leave":
290
- self.exit()
291
- elif choice == "stop":
288
+ if choice == "stop":
292
289
  self.controls.interrupt()
293
290
  self.set_timer(GRACE_SECONDS, self.controls.interrupt)
294
291
 
@@ -1,9 +1,3 @@
1
- [[python.exempt]]
2
- path = "src/labnotes/runner/hold_folder.py"
3
- rules = ["mutation"]
4
- lines = [12]
5
- reason = "LOCK_EX and LOCK_NB are disjoint bits, so | equals + and ^"
6
-
7
1
  [[python.exempt]]
8
2
  path = "src/labnotes/tui/run_output.py"
9
3
  rules = ["mutation"]
@@ -16,30 +10,6 @@ rules = ["mutation"]
16
10
  lines = [61, 69]
17
11
  reason = "summarize_config returns mode matrix or adhoc; \"matrix\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
18
12
 
19
- [[python.exempt]]
20
- path = "src/labnotes/daemon/exit_when_idle.py"
21
- rules = ["mutation"]
22
- lines = [12]
23
- reason = "quiet counts up by one from zero, so it reaches IDLE_EXIT_SECONDS before passing it: < equals != and, for small ints, is not"
24
-
25
- [[python.exempt]]
26
- path = "src/labnotes/daemon/daemon_app.py"
27
- rules = ["mutation"]
28
- lines = [134]
29
- reason = "start answers 202, 400, 409 or 500, so every status but 202 is above it: != equals >"
30
-
31
- [[python.exempt]]
32
- path = "src/labnotes/daemon/remote_start.py"
33
- rules = ["mutation"]
34
- lines = [51]
35
- reason = "the daemon answers a run with 202, 400, 409 or 500, so every status but 202 is above it: != equals >"
36
-
37
- [[python.exempt]]
38
- path = "src/labnotes/run_control.py"
39
- rules = ["mutation"]
40
- lines = [39]
41
- reason = "argument counts are small ints, which CPython caches, so is not equals !="
42
-
43
13
  [[python.exempt]]
44
14
  path = "src/labnotes/tui/run_cells.py"
45
15
  rules = ["mutation"]
@@ -10,7 +10,6 @@ from pathlib import Path
10
10
  import pytest
11
11
  from playwright.async_api import Page, async_playwright
12
12
 
13
- from labnotes.daemon.socket_path import socket_path
14
13
 
15
14
  CONFIG = (
16
15
  'name = "Alpha study"\ncommand = "go"\nrepeats = 2\nmode = "matrix"\n'
@@ -45,24 +44,6 @@ def free_port() -> int:
45
44
  return int(sock.getsockname()[1])
46
45
 
47
46
 
48
- def daemon(root: Path) -> Iterator[None]:
49
- labnotes = Path(sys.executable).parent / "labnotes"
50
- process = subprocess.Popen(
51
- [labnotes, "-c", root / ".labnotes.toml", "daemon"],
52
- stdout=subprocess.DEVNULL,
53
- stderr=subprocess.DEVNULL,
54
- )
55
- try:
56
- for _ in range(100):
57
- if socket_path(root).exists():
58
- break
59
- time.sleep(0.1)
60
- yield
61
- finally:
62
- process.terminate()
63
- process.wait()
64
-
65
-
66
47
  def serve(root: Path) -> Iterator[str]:
67
48
  port = free_port()
68
49
  labnotes = Path(sys.executable).parent / "labnotes"
@@ -385,20 +385,20 @@ wheels = [
385
385
 
386
386
  [[package]]
387
387
  name = "httpcore2"
388
- version = "2.13.1"
388
+ version = "2.16.0"
389
389
  source = { registry = "https://pypi.org/simple" }
390
390
  dependencies = [
391
391
  { name = "h11" },
392
392
  { name = "truststore" },
393
393
  ]
394
- sdist = { url = "https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size = 68071, upload-time = "2026-09-23T07:47:22.372Z" }
394
+ sdist = { url = "https://files.pythonhosted.org/packages/c0/61/afa9d6b7452f9eca73f73d45324eb0bb52f098fd5e6049bf6b8263be9b4e/httpcore2-2.16.0.tar.gz", hash = "sha256:c72cd0413563b17e1cd8354677dff887466f8ec2551679759e93126534428643", size = 69886, upload-time = "2026-10-21T07:09:45.31Z" }
395
395
  wheels = [
396
- { url = "https://files.pythonhosted.org/packages/09/ba/a4568248771ce81957bfb7cc600264a40fbcda092391ee1c415c50be4bea/httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a", size = 83423, upload-time = "2026-09-23T07:47:19.365Z" },
396
+ { url = "https://files.pythonhosted.org/packages/af/c2/ce417c397afa5181f0e3c6ff01a4d01639414e1adef08730d97571297151/httpcore2-2.16.0-py3-none-any.whl", hash = "sha256:5c4358319f2602d984ad93182c9fee5160f22e9da7babdda870a5e7c4e2dc34f", size = 85070, upload-time = "2026-10-21T07:09:42.802Z" },
397
397
  ]
398
398
 
399
399
  [[package]]
400
400
  name = "httpx2"
401
- version = "2.13.1"
401
+ version = "2.16.0"
402
402
  source = { registry = "https://pypi.org/simple" }
403
403
  dependencies = [
404
404
  { name = "anyio", marker = "sys_platform != 'emscripten'" },
@@ -408,9 +408,9 @@ dependencies = [
408
408
  { name = "truststore", marker = "sys_platform != 'emscripten'" },
409
409
  { name = "typing-extensions", marker = "python_full_version < '3.13'" },
410
410
  ]
411
- sdist = { url = "https://files.pythonhosted.org/packages/d5/44/474bef2a0e9d90f1715d32cb98b0738695ca17ba324095fb2497ed7fbd59/httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa", size = 100405, upload-time = "2026-09-23T07:47:23.052Z" }
411
+ sdist = { url = "https://files.pythonhosted.org/packages/43/73/5ca2cedf5e4e50423d1c258877865a6b62793633b561c58499f8aeb33b7e/httpx2-2.16.0.tar.gz", hash = "sha256:0d54c98128e8f2ff7fbbde0fca8c359e5f032edeaaaac443cdb7f31f685bcd7f", size = 103116, upload-time = "2026-10-21T07:09:46.348Z" }
412
412
  wheels = [
413
- { url = "https://files.pythonhosted.org/packages/d8/9c/6fe8931fd9f381042a9e4c7d5a7b4cbf7016b252bec0c99a49fce42c3326/httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4", size = 95597, upload-time = "2026-09-23T07:47:20.995Z" },
413
+ { url = "https://files.pythonhosted.org/packages/7b/3d/2e4bc4fd1ef9770871669adb348d612a6e4786b303a23f9935b81c48dd60/httpx2-2.16.0-py3-none-any.whl", hash = "sha256:fd2c5d7909e8d86fe92d53020c310ca0e4e431b838c207c5754b9bbf0308d49a", size = 97880, upload-time = "2026-10-21T07:09:44.01Z" },
414
414
  ]
415
415
 
416
416
  [[package]]
@@ -1,16 +0,0 @@
1
- from pathlib import Path
2
-
3
- from labnotes.daemon.daemon_client import daemon_client
4
- from labnotes.daemon.daemon_running import daemon_running
5
-
6
-
7
- async def control_run(
8
- root: Path, name: str, action: str, args: list[object]
9
- ) -> str | None:
10
- if not await daemon_running(root):
11
- return "Nothing is running."
12
- async with daemon_client(root) as client:
13
- reply = await client.post(f"/experiments/{name}/{action}", json={"args": args})
14
- if reply.is_success:
15
- return None
16
- return str(reply.json()["error"])