labnotes 0.0.136__tar.gz → 0.0.138__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 (222) hide show
  1. {labnotes-0.0.136 → labnotes-0.0.138}/PKG-INFO +7 -8
  2. {labnotes-0.0.136 → labnotes-0.0.138}/README.md +6 -7
  3. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/cli.py +0 -4
  4. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/context.md +13 -23
  5. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/experiments.py +2 -2
  6. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/load_experiment.py +14 -2
  7. labnotes-0.0.138/src/labnotes/runner/start_experiment.py +37 -0
  8. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/leave_prompt.py +2 -3
  9. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/run_app.py +2 -5
  10. {labnotes-0.0.136 → labnotes-0.0.138}/testing-conventions.toml +0 -30
  11. {labnotes-0.0.136 → labnotes-0.0.138}/tests/integration/conftest.py +0 -19
  12. {labnotes-0.0.136 → labnotes-0.0.138}/uv.lock +6 -6
  13. labnotes-0.0.136/src/labnotes/daemon/control_run.py +0 -16
  14. labnotes-0.0.136/src/labnotes/daemon/daemon_app.py +0 -179
  15. labnotes-0.0.136/src/labnotes/daemon/daemon_client.py +0 -12
  16. labnotes-0.0.136/src/labnotes/daemon/daemon_running.py +0 -14
  17. labnotes-0.0.136/src/labnotes/daemon/daemon_server.py +0 -12
  18. labnotes-0.0.136/src/labnotes/daemon/describe_status.py +0 -21
  19. labnotes-0.0.136/src/labnotes/daemon/ensure_daemon.py +0 -26
  20. labnotes-0.0.136/src/labnotes/daemon/exit_when_idle.py +0 -15
  21. labnotes-0.0.136/src/labnotes/daemon/last_stderr.py +0 -15
  22. labnotes-0.0.136/src/labnotes/daemon/remote_start.py +0 -94
  23. labnotes-0.0.136/src/labnotes/daemon/run_daemon.py +0 -29
  24. labnotes-0.0.136/src/labnotes/daemon/run_logs.py +0 -54
  25. labnotes-0.0.136/src/labnotes/daemon/serve_daemon.py +0 -16
  26. labnotes-0.0.136/src/labnotes/daemon/socket_path.py +0 -13
  27. labnotes-0.0.136/src/labnotes/daemon/stop_runs.py +0 -12
  28. labnotes-0.0.136/src/labnotes/daemon/watch_daemon.py +0 -77
  29. labnotes-0.0.136/src/labnotes/daemon/watch_status.py +0 -19
  30. labnotes-0.0.136/src/labnotes/daemon_command.py +0 -14
  31. labnotes-0.0.136/src/labnotes/run_command.py +0 -32
  32. labnotes-0.0.136/src/labnotes/run_control.py +0 -45
  33. labnotes-0.0.136/src/labnotes/run_status.py +0 -29
  34. labnotes-0.0.136/src/labnotes/runner/hold_folder.py +0 -15
  35. labnotes-0.0.136/src/labnotes/utils/__init__.py +0 -0
  36. {labnotes-0.0.136 → labnotes-0.0.138}/.gitignore +0 -0
  37. {labnotes-0.0.136 → labnotes-0.0.138}/CHANGELOG.md +0 -0
  38. {labnotes-0.0.136 → labnotes-0.0.138}/MIGRATIONS.md +0 -0
  39. {labnotes-0.0.136 → labnotes-0.0.138}/pyproject.toml +0 -0
  40. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/__init__.py +0 -0
  41. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/__main__.py +0 -0
  42. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/asset_url.py +0 -0
  43. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/check_counts.py +0 -0
  44. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/compose_experiment.py +0 -0
  45. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/context_guide.py +0 -0
  46. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/count_groups.py +0 -0
  47. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/count_runs.py +0 -0
  48. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/create_app.py +0 -0
  49. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/create_experiment.py +0 -0
  50. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/create_note.py +0 -0
  51. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/data_html.py +0 -0
  52. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/default_config_text.py +0 -0
  53. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/default_counts.py +0 -0
  54. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/drain_watch.py +0 -0
  55. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/experiment_notes.py +0 -0
  56. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/experiment_runs.py +0 -0
  57. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/file_changes.py +0 -0
  58. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/format_duration.py +0 -0
  59. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/format_started.py +0 -0
  60. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/format_value.py +0 -0
  61. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/get_state.py +0 -0
  62. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/grid_cell.py +0 -0
  63. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/group_runs.py +0 -0
  64. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/init.py +0 -0
  65. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/invocation_started.py +0 -0
  66. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/is_non_negative_int.py +0 -0
  67. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/is_non_negative_number.py +0 -0
  68. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/is_positive_int.py +0 -0
  69. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/list_experiments.py +0 -0
  70. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/list_runs.py +0 -0
  71. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/load_config.py +0 -0
  72. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/load_run.py +0 -0
  73. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/log.py +0 -0
  74. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/log_path.py +0 -0
  75. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/memory_interval.py +0 -0
  76. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/models.py +0 -0
  77. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/modes.py +0 -0
  78. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/nests_runs.py +0 -0
  79. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/note_path.py +0 -0
  80. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/notebook_kernel.py +0 -0
  81. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/output_html.py +0 -0
  82. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/parse_run_view.py +0 -0
  83. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/parse_subset.py +0 -0
  84. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/pivot_runs.py +0 -0
  85. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/print_rows.py +0 -0
  86. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/python_blocks.py +0 -0
  87. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/read_config.py +0 -0
  88. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/read_path.py +0 -0
  89. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/render_markdown.py +0 -0
  90. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/retries.py +0 -0
  91. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/run_output.py +0 -0
  92. {labnotes-0.0.136/src/labnotes/daemon → labnotes-0.0.138/src/labnotes/runner}/__init__.py +0 -0
  93. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/cgroup_memory.py +0 -0
  94. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/check_args.py +0 -0
  95. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/check_subset.py +0 -0
  96. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/container_cgroup.py +0 -0
  97. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/container_memory.py +0 -0
  98. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/describe_event.py +0 -0
  99. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/docker_output.py +0 -0
  100. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/execute_run.py +0 -0
  101. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/exit_signal.py +0 -0
  102. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/fill_dirs.py +0 -0
  103. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/fill_experiment_env.py +0 -0
  104. {labnotes-0.0.136/src/labnotes/daemon → labnotes-0.0.138/src/labnotes/runner}/finalize_lost_runs.py +0 -0
  105. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/flag_name.py +0 -0
  106. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/hook_dirs.py +0 -0
  107. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/in_subset.py +0 -0
  108. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/invocation_hook.py +0 -0
  109. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/invocation_meta.py +0 -0
  110. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/new_param_run_id.py +0 -0
  111. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/new_run_id.py +0 -0
  112. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  113. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/param_args.py +0 -0
  114. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/param_grid.py +0 -0
  115. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/param_run_name.py +0 -0
  116. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/plan_runs.py +0 -0
  117. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/prepare_experiment.py +0 -0
  118. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/read_experiment_config.py +0 -0
  119. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/record_memory.py +0 -0
  120. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/run_experiment.py +0 -0
  121. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/run_folder.py +0 -0
  122. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/run_git.py +0 -0
  123. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/run_headless.py +0 -0
  124. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/runs_parent.py +0 -0
  125. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/shell_command.py +0 -0
  126. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/signal_tree.py +0 -0
  127. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/stop_then_kill.py +0 -0
  128. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/wait_for_tree.py +0 -0
  129. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runner/write_run_json.py +0 -0
  130. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/runs_view_command.py +0 -0
  131. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/save_conflict_copy.py +0 -0
  132. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/save_note.py +0 -0
  133. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/saved_outputs.py +0 -0
  134. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/serve_app.py +0 -0
  135. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/server.py +0 -0
  136. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/set_state.py +0 -0
  137. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/shared_params.py +0 -0
  138. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/sort_href.py +0 -0
  139. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/sql_quote.py +0 -0
  140. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/state_path.py +0 -0
  141. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/elapsed.js +0 -0
  142. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/experiment.js +0 -0
  143. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/labnotes.css +0 -0
  144. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/local-time.js +0 -0
  145. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/new-note.js +0 -0
  146. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/note-edit.js +0 -0
  147. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/note-editor.js +0 -0
  148. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/row-link.js +0 -0
  149. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/run-cell.js +0 -0
  150. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/run-output.js +0 -0
  151. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/static/view-toggle.js +0 -0
  152. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/summarize_config.py +0 -0
  153. {labnotes-0.0.136/src/labnotes/runner → labnotes-0.0.138/src/labnotes/tables}/__init__.py +0 -0
  154. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/experiment_rows.py +0 -0
  155. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/experiment_run_ids.py +0 -0
  156. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/experiment_toml.py +0 -0
  157. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/experiments_ddl.py +0 -0
  158. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/file_prefix.py +0 -0
  159. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/file_text.py +0 -0
  160. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/iso_time.py +0 -0
  161. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/list_experiment_runs.py +0 -0
  162. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/note_text.py +0 -0
  163. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/open_tables.py +0 -0
  164. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/path_glob.py +0 -0
  165. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/path_pattern.py +0 -0
  166. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_blocks.py +0 -0
  167. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_experiment.py +0 -0
  168. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_file.py +0 -0
  169. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_log.py +0 -0
  170. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_memory.py +0 -0
  171. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_run.py +0 -0
  172. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/read_run_file.py +0 -0
  173. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/runs_columns.py +0 -0
  174. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tables/summarize_experiments.py +0 -0
  175. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/base.html +0 -0
  176. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/editor_imports.html +0 -0
  177. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/experiment.html +0 -0
  178. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/index.html +0 -0
  179. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/note.html +0 -0
  180. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/templates/run.html +0 -0
  181. {labnotes-0.0.136/src/labnotes/tables → labnotes-0.0.138/src/labnotes/tui}/__init__.py +0 -0
  182. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/browse_app.py +0 -0
  183. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/browse_tui.py +0 -0
  184. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/check_view_response.py +0 -0
  185. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/config.py +0 -0
  186. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/dim.py +0 -0
  187. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/experiment_screen.py +0 -0
  188. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/experiments_screen.py +0 -0
  189. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/fill_table.py +0 -0
  190. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/format_cell.py +0 -0
  191. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/format_elapsed.py +0 -0
  192. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/group_cells.py +0 -0
  193. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/invocation_columns.py +0 -0
  194. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/invocation_rows.py +0 -0
  195. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/log_block.py +0 -0
  196. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/log_blocks.py +0 -0
  197. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/native_rows.py +0 -0
  198. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/reload_on_change.py +0 -0
  199. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/run_cells.py +0 -0
  200. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/run_output.py +0 -0
  201. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/run_screen.py +0 -0
  202. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/run_tui.py +0 -0
  203. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/runs_table.py +0 -0
  204. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/runs_view.py +0 -0
  205. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/stopped_title.py +0 -0
  206. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/view_column.py +0 -0
  207. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/tui/view_process.py +0 -0
  208. {labnotes-0.0.136/src/labnotes/tui → labnotes-0.0.138/src/labnotes/utils}/__init__.py +0 -0
  209. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/utils/format_bytes.py +0 -0
  210. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/utils/process_alive.py +0 -0
  211. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/utils/run_usage.py +0 -0
  212. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/utils/tree_rss.py +0 -0
  213. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/utils/tree_usage.py +0 -0
  214. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/view/__init__.py +0 -0
  215. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/view/answer.py +0 -0
  216. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/view/build.py +0 -0
  217. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/view/respond.py +0 -0
  218. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/view/serve.py +0 -0
  219. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/wait_for_note.py +0 -0
  220. {labnotes-0.0.136 → labnotes-0.0.138}/src/labnotes/watch_rows.py +0 -0
  221. {labnotes-0.0.136 → labnotes-0.0.138}/tests/conftest.py +0 -0
  222. {labnotes-0.0.136 → labnotes-0.0.138}/tests/e2e/conftest.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.136
3
+ Version: 0.0.138
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,27 +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
- - Ctrl-C stops the running scripts and records their end. A second Ctrl-C
211
- kills them. SIGTERM stops them the same way, then kills any still going 5
212
- seconds later. Once the runs are done, either one closes the live view.
213
- - `labnotes run stop` stops every run the daemon has going: SIGTERM, then
214
- SIGKILL 5 seconds later.
215
- - `labnotes run status` prints what the daemon has going, a line per event so
216
- far plus the latest line each running run logged and what each running run
217
- uses (`usage` events with `--json`). `-f` keeps following, and
218
- waits for a daemon when none is up. `--json` prints one JSON object per line.
219
- Following doesn't keep the daemon up.
220
- - `labnotes run control NAME ACTION [ARG]` sends one live-view control to a
221
- running experiment: `kill` a run, `set_concurrency` to a number,
222
- `interrupt` or `finish`.
223
- - A second `run` of the same experiment fails while the first is going.
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.
224
214
  - It exits 0 even if runs fail; the summary line counts failures. A run that
225
215
  was retried counts as retried, not failed. If it can't
226
- 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.
227
217
 
228
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,
@@ -0,0 +1,37 @@
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.run_experiment import run_experiment
7
+
8
+
9
+ def start_experiment(
10
+ config: LabnotesConfig,
11
+ name: str,
12
+ experiment_config: Path | None,
13
+ concurrency: int | None,
14
+ timeout: int | None,
15
+ only: dict[str, str] | None,
16
+ args: list[str] | None,
17
+ setup: str | None,
18
+ teardown: str | None,
19
+ ) -> Start:
20
+ async def start(report: Report, listen: Listen) -> list[dict[str, object]]:
21
+ experiment, limit = await prepare_experiment(
22
+ config, name, experiment_config, concurrency, only, args, setup, teardown
23
+ )
24
+ await finalize_lost_runs(config)
25
+ return await run_experiment(
26
+ config,
27
+ name,
28
+ experiment,
29
+ limit,
30
+ report,
31
+ listen,
32
+ timeout=timeout,
33
+ only=only,
34
+ args=args,
35
+ )
36
+
37
+ 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"])
@@ -1,179 +0,0 @@
1
- import asyncio
2
- import dataclasses
3
- import itertools
4
- import json
5
- from collections.abc import AsyncIterator
6
- from pathlib import Path
7
-
8
- from starlette.applications import Starlette
9
- from starlette.requests import Request
10
- from starlette.responses import JSONResponse, Response, StreamingResponse
11
- from starlette.routing import Route
12
-
13
- from labnotes.models import Controls, LabnotesConfig
14
- from labnotes.runner.prepare_experiment import prepare_experiment
15
- from labnotes.runner.run_experiment import run_experiment
16
- from labnotes.runner.stop_then_kill import GRACE_SECONDS
17
-
18
- Event = dict[str, object]
19
- ACTIONS = frozenset(field.name for field in dataclasses.fields(Controls))
20
-
21
-
22
- def daemon_app(config: LabnotesConfig) -> Starlette:
23
- sessions: dict[str, Controls | None] = {}
24
- owners: dict[str, int] = {}
25
- clients: dict[int, asyncio.Queue[Event]] = {}
26
- watchers: set[int] = set()
27
- history: dict[str, list[Event]] = {}
28
- ids = itertools.count(1)
29
- tasks: set[asyncio.Task[str | None]] = set()
30
-
31
- def broadcast(event: Event) -> None:
32
- for queue in clients.values():
33
- queue.put_nowait(event)
34
-
35
- def refuse(status: int, error: str) -> Response:
36
- return JSONResponse({"error": error}, status_code=status)
37
-
38
- def release(client: int) -> None:
39
- for name, owner in list(owners.items()):
40
- controls = sessions.get(name)
41
- if owner == client and controls is not None:
42
- del owners[name]
43
- controls.finish()
44
-
45
- def record(event: Event) -> None:
46
- history.setdefault(str(event["experiment"]), []).append(event)
47
- broadcast(event)
48
-
49
- async def events(request: Request) -> Response:
50
- client = next(ids)
51
- watching = request.query_params.get("watch") == "1"
52
-
53
- async def stream() -> AsyncIterator[str]:
54
- clients[client] = asyncio.Queue()
55
- if watching:
56
- watchers.add(client)
57
- for past in history.values():
58
- for old in past:
59
- clients[client].put_nowait(old)
60
- clients[client].put_nowait({"event": "replayed"})
61
- try:
62
- event: Event = {"event": "connected", "client": client}
63
- while True:
64
- yield f"data: {json.dumps(event, default=str)}\n\n"
65
- event = await clients[client].get()
66
- finally:
67
- del clients[client]
68
- watchers.discard(client)
69
- release(client)
70
-
71
- return StreamingResponse(stream(), media_type="text/event-stream")
72
-
73
- async def start(name: str, body: dict[str, object]) -> Response:
74
- path = body.get("experiment_config")
75
- try:
76
- experiment, concurrency = await prepare_experiment(
77
- config,
78
- name,
79
- None if path is None else Path(str(path)),
80
- body.get("concurrency"), # type: ignore[arg-type]
81
- body.get("only"), # type: ignore[arg-type]
82
- body.get("args"), # type: ignore[arg-type]
83
- body.get("setup"), # type: ignore[arg-type]
84
- body.get("teardown"), # type: ignore[arg-type]
85
- )
86
- except ValueError as error:
87
- return refuse(400, str(error))
88
- ready = asyncio.Event()
89
- owner = body.get("client")
90
- if isinstance(owner, int):
91
- owners[name] = owner
92
-
93
- def listen(controls: Controls) -> None:
94
- sessions[name] = controls
95
- ready.set()
96
- if name in owners and owners[name] not in clients:
97
- release(owners[name])
98
-
99
- async def session() -> str | None:
100
- error = None
101
- try:
102
- await run_experiment(
103
- config,
104
- name,
105
- experiment,
106
- concurrency,
107
- lambda event: record({**event, "experiment": name}),
108
- listen,
109
- body.get("timeout"), # type: ignore[arg-type]
110
- only=body.get("only"), # type: ignore[arg-type]
111
- args=body.get("args"), # type: ignore[arg-type]
112
- environment=body.get("environment"), # type: ignore[arg-type]
113
- )
114
- # Clients hear why the session ended; the daemon keeps serving.
115
- except Exception as failure: # noqa: BLE001
116
- error = str(failure)
117
- del sessions[name]
118
- owners.pop(name, None)
119
- history.pop(name, None)
120
- broadcast({"event": "done", "experiment": name, "error": error})
121
- return error
122
-
123
- task = asyncio.create_task(session())
124
- tasks.add(task)
125
- task.add_done_callback(tasks.discard)
126
- waiting = asyncio.create_task(ready.wait())
127
- await asyncio.wait({task, waiting}, return_when=asyncio.FIRST_COMPLETED)
128
- waiting.cancel()
129
- if not ready.is_set():
130
- return refuse(500, str(task.result()))
131
- return JSONResponse({}, status_code=202)
132
-
133
- async def run(request: Request) -> Response:
134
- name = request.path_params["name"]
135
- body = await request.json()
136
- if name in sessions:
137
- return refuse(409, f"{name} is already running.")
138
- sessions[name] = None
139
- response = await start(name, body)
140
- if response.status_code != 202:
141
- sessions.pop(name, None)
142
- return response
143
-
144
- async def control(request: Request) -> Response:
145
- name, action = request.path_params["name"], request.path_params["action"]
146
- body = await request.json()
147
- controls = sessions.get(name)
148
- if action not in ACTIONS or controls is None:
149
- return refuse(404, f"No {action} for {name}.")
150
- getattr(controls, action)(*body["args"])
151
- return JSONResponse({})
152
-
153
- def stop_all() -> None:
154
- stopping = [controls for controls in sessions.values() if controls]
155
-
156
- def interrupt() -> None:
157
- for controls in stopping:
158
- controls.interrupt()
159
-
160
- interrupt()
161
- asyncio.get_running_loop().call_later(GRACE_SECONDS, interrupt)
162
-
163
- async def stop(request: Request) -> Response:
164
- stop_all()
165
- return JSONResponse({})
166
-
167
- app = Starlette(
168
- routes=[
169
- Route("/events", events),
170
- Route("/stop", stop, methods=["POST"]),
171
- Route("/experiments/{name}/run", run, methods=["POST"]),
172
- Route("/experiments/{name}/{action}", control, methods=["POST"]),
173
- ]
174
- )
175
- app.state.idle = (
176
- lambda: all(client in watchers for client in clients) and not sessions
177
- )
178
- app.state.stop = stop_all
179
- return app
@@ -1,12 +0,0 @@
1
- from pathlib import Path
2
-
3
- import httpx2
4
-
5
- from labnotes.daemon.socket_path import socket_path
6
-
7
-
8
- def daemon_client(root: Path) -> httpx2.AsyncClient:
9
- transport = httpx2.AsyncHTTPTransport(uds=str(socket_path(root)))
10
- return httpx2.AsyncClient(
11
- transport=transport, base_url="http://labnotes", timeout=None
12
- )
@@ -1,14 +0,0 @@
1
- import asyncio
2
- from pathlib import Path
3
-
4
- from labnotes.daemon.socket_path import socket_path
5
-
6
-
7
- async def daemon_running(root: Path) -> bool:
8
- try:
9
- _, writer = await asyncio.open_unix_connection(socket_path(root))
10
- except (FileNotFoundError, ConnectionRefusedError):
11
- return False
12
- writer.close()
13
- await writer.wait_closed()
14
- return True
@@ -1,12 +0,0 @@
1
- from collections.abc import Iterator
2
- from contextlib import contextmanager
3
-
4
- import uvicorn
5
-
6
-
7
- class DaemonServer(uvicorn.Server):
8
- # A signal stops the runs through the daemon's own handlers; uvicorn's
9
- # would shut the server down under them.
10
- @contextmanager
11
- def capture_signals(self) -> Iterator[None]:
12
- yield