labnotes 0.2.3__tar.gz → 0.2.4__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 (210) hide show
  1. {labnotes-0.2.3 → labnotes-0.2.4}/PKG-INFO +1 -1
  2. labnotes-0.2.4/src/labnotes/latest_log.py +6 -0
  3. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/models.py +1 -0
  4. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/run_output.py +6 -1
  5. labnotes-0.2.4/src/labnotes/runner/claim_run.py +15 -0
  6. labnotes-0.2.4/src/labnotes/runner/continue_run.py +21 -0
  7. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/describe_event.py +2 -0
  8. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/execute_run.py +30 -5
  9. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/finalize_lost_runs.py +12 -3
  10. labnotes-0.2.4/src/labnotes/runner/first_invocation.py +13 -0
  11. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/read_experiment_config.py +7 -0
  12. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/run_experiment.py +48 -7
  13. labnotes-0.2.4/src/labnotes/runner/run_locked.py +12 -0
  14. labnotes-0.2.4/src/labnotes/tables/list_resumable_runs.py +16 -0
  15. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/open_tables.py +3 -3
  16. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_log.py +7 -3
  17. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_run.py +6 -2
  18. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/run_app.py +5 -1
  19. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/run_output.py +7 -1
  20. {labnotes-0.2.3 → labnotes-0.2.4}/.gitignore +0 -0
  21. {labnotes-0.2.3 → labnotes-0.2.4}/CHANGELOG.md +0 -0
  22. {labnotes-0.2.3 → labnotes-0.2.4}/MIGRATIONS.md +0 -0
  23. {labnotes-0.2.3 → labnotes-0.2.4}/README.md +0 -0
  24. {labnotes-0.2.3 → labnotes-0.2.4}/pyproject.toml +0 -0
  25. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/__init__.py +0 -0
  26. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/__main__.py +0 -0
  27. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/asset_url.py +0 -0
  28. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/check_counts.py +0 -0
  29. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/cli.py +0 -0
  30. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/compose_experiment.py +0 -0
  31. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/context.md +0 -0
  32. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/context_guide.py +0 -0
  33. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/count_groups.py +0 -0
  34. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/count_runs.py +0 -0
  35. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/create_app.py +0 -0
  36. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/create_experiment.py +0 -0
  37. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/create_note.py +0 -0
  38. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/data_html.py +0 -0
  39. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/default_config_text.py +0 -0
  40. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/default_counts.py +0 -0
  41. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/drain_watch.py +0 -0
  42. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/experiment_notes.py +0 -0
  43. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/experiment_runs.py +0 -0
  44. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/experiments.py +0 -0
  45. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/file_changes.py +0 -0
  46. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/format_duration.py +0 -0
  47. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/format_started.py +0 -0
  48. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/format_value.py +0 -0
  49. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/get_state.py +0 -0
  50. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/grid_cell.py +0 -0
  51. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/group_runs.py +0 -0
  52. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/init.py +0 -0
  53. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/invocation_started.py +0 -0
  54. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/is_non_negative_int.py +0 -0
  55. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/is_non_negative_number.py +0 -0
  56. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/is_positive_int.py +0 -0
  57. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/list_experiments.py +0 -0
  58. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/list_runs.py +0 -0
  59. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/load_config.py +0 -0
  60. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/load_experiment.py +0 -0
  61. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/load_run.py +0 -0
  62. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/log.py +0 -0
  63. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/log_path.py +0 -0
  64. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/memory_interval.py +0 -0
  65. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/modes.py +0 -0
  66. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/nests_runs.py +0 -0
  67. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/note_path.py +0 -0
  68. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/notebook_kernel.py +0 -0
  69. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/output_html.py +0 -0
  70. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/parse_run_view.py +0 -0
  71. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/parse_subset.py +0 -0
  72. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/pivot_runs.py +0 -0
  73. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/print_rows.py +0 -0
  74. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/python_blocks.py +0 -0
  75. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/read_config.py +0 -0
  76. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/read_path.py +0 -0
  77. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/render_markdown.py +0 -0
  78. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/retries.py +0 -0
  79. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/__init__.py +0 -0
  80. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/cgroup_memory.py +0 -0
  81. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/check_args.py +0 -0
  82. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/check_hook_placeholders.py +0 -0
  83. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/check_subset.py +0 -0
  84. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/command_flags.py +0 -0
  85. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/container_cgroup.py +0 -0
  86. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/container_memory.py +0 -0
  87. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/docker_output.py +0 -0
  88. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/exit_signal.py +0 -0
  89. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/fill_dirs.py +0 -0
  90. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/fill_experiment_env.py +0 -0
  91. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/flag_name.py +0 -0
  92. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/group_args.py +0 -0
  93. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/in_subset.py +0 -0
  94. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/invocation_hook.py +0 -0
  95. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/invocation_meta.py +0 -0
  96. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/new_param_run_id.py +0 -0
  97. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/new_run_id.py +0 -0
  98. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  99. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/override_flags.py +0 -0
  100. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/param_args.py +0 -0
  101. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/param_grid.py +0 -0
  102. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/param_run_name.py +0 -0
  103. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/placeholders.py +0 -0
  104. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/plan_runs.py +0 -0
  105. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/prepare_experiment.py +0 -0
  106. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/project_snapshot.py +0 -0
  107. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/record_memory.py +0 -0
  108. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/run_folder.py +0 -0
  109. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/run_git.py +0 -0
  110. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/run_headless.py +0 -0
  111. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/runs_parent.py +0 -0
  112. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/shell_command.py +0 -0
  113. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/signal_tree.py +0 -0
  114. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/start_experiment.py +0 -0
  115. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/stop_then_kill.py +0 -0
  116. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/wait_for_tree.py +0 -0
  117. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runner/write_run_json.py +0 -0
  118. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/runs_view_command.py +0 -0
  119. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/save_conflict_copy.py +0 -0
  120. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/save_note.py +0 -0
  121. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/saved_outputs.py +0 -0
  122. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/serve_app.py +0 -0
  123. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/server.py +0 -0
  124. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/set_state.py +0 -0
  125. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/shared_params.py +0 -0
  126. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/sort_href.py +0 -0
  127. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/sql_quote.py +0 -0
  128. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/state_path.py +0 -0
  129. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/elapsed.js +0 -0
  130. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/experiment.js +0 -0
  131. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/labnotes.css +0 -0
  132. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/local-time.js +0 -0
  133. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/new-note.js +0 -0
  134. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/note-edit.js +0 -0
  135. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/note-editor.js +0 -0
  136. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/row-link.js +0 -0
  137. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/run-cell.js +0 -0
  138. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/run-output.js +0 -0
  139. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/static/view-toggle.js +0 -0
  140. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/summarize_config.py +0 -0
  141. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/__init__.py +0 -0
  142. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/experiment_rows.py +0 -0
  143. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/experiment_run_ids.py +0 -0
  144. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/experiment_toml.py +0 -0
  145. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/experiments_ddl.py +0 -0
  146. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/file_prefix.py +0 -0
  147. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/file_text.py +0 -0
  148. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/iso_time.py +0 -0
  149. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/list_experiment_runs.py +0 -0
  150. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/note_text.py +0 -0
  151. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/path_glob.py +0 -0
  152. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/path_pattern.py +0 -0
  153. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_blocks.py +0 -0
  154. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_experiment.py +0 -0
  155. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_file.py +0 -0
  156. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_memory.py +0 -0
  157. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/read_run_file.py +0 -0
  158. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/runs_columns.py +0 -0
  159. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tables/summarize_experiments.py +0 -0
  160. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/base.html +0 -0
  161. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/editor_imports.html +0 -0
  162. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/experiment.html +0 -0
  163. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/index.html +0 -0
  164. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/note.html +0 -0
  165. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/templates/run.html +0 -0
  166. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/__init__.py +0 -0
  167. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/browse_app.py +0 -0
  168. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/browse_tui.py +0 -0
  169. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/check_view_response.py +0 -0
  170. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/config.py +0 -0
  171. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/dim.py +0 -0
  172. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/experiment_screen.py +0 -0
  173. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/experiments_screen.py +0 -0
  174. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/fill_table.py +0 -0
  175. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/format_cell.py +0 -0
  176. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/format_elapsed.py +0 -0
  177. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/group_cells.py +0 -0
  178. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/invocation_columns.py +0 -0
  179. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/invocation_rows.py +0 -0
  180. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/leave_prompt.py +0 -0
  181. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/log_block.py +0 -0
  182. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/log_blocks.py +0 -0
  183. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/native_rows.py +0 -0
  184. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/reload_on_change.py +0 -0
  185. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/run_cells.py +0 -0
  186. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/run_screen.py +0 -0
  187. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/run_tui.py +0 -0
  188. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/runs_table.py +0 -0
  189. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/runs_view.py +0 -0
  190. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/stopped_title.py +0 -0
  191. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/view_column.py +0 -0
  192. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/tui/view_process.py +0 -0
  193. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/__init__.py +0 -0
  194. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/format_bytes.py +0 -0
  195. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/process_alive.py +0 -0
  196. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/run_usage.py +0 -0
  197. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/tree_rss.py +0 -0
  198. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/utils/tree_usage.py +0 -0
  199. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/view/__init__.py +0 -0
  200. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/view/answer.py +0 -0
  201. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/view/build.py +0 -0
  202. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/view/respond.py +0 -0
  203. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/view/serve.py +0 -0
  204. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/wait_for_note.py +0 -0
  205. {labnotes-0.2.3 → labnotes-0.2.4}/src/labnotes/watch_rows.py +0 -0
  206. {labnotes-0.2.3 → labnotes-0.2.4}/testing-conventions.toml +0 -0
  207. {labnotes-0.2.3 → labnotes-0.2.4}/tests/conftest.py +0 -0
  208. {labnotes-0.2.3 → labnotes-0.2.4}/tests/e2e/conftest.py +0 -0
  209. {labnotes-0.2.3 → labnotes-0.2.4}/tests/integration/conftest.py +0 -0
  210. {labnotes-0.2.3 → labnotes-0.2.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.2.3
3
+ Version: 0.2.4
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>
@@ -0,0 +1,6 @@
1
+ from pathlib import Path
2
+
3
+
4
+ def latest_log(folder: Path, stream: str) -> Path:
5
+ # A run that continues writes one log per invocation; follow the newest.
6
+ return max(folder.glob(f"{stream}-*.txt"), default=folder / f"{stream}.log")
@@ -67,3 +67,4 @@ class ExperimentConfig:
67
67
  setup: str | None = None
68
68
  teardown: str | None = None
69
69
  env: dict[str, str] = field(default_factory=dict)
70
+ continues: bool = False
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
 
6
6
  from dirsql import DirSQL
7
7
 
8
+ from labnotes.latest_log import latest_log
8
9
  from labnotes.load_run import load_run
9
10
  from labnotes.tables.read_log import STREAMS
10
11
 
@@ -19,6 +20,7 @@ async def run_output(
19
20
  return
20
21
  folder = root / str(run["folder"])
21
22
  offsets = dict.fromkeys(STREAMS, 0)
23
+ paths: dict[str, Path] = {}
22
24
  decoders = {
23
25
  stream: codecs.getincrementaldecoder("utf-8")("replace") for stream in STREAMS
24
26
  }
@@ -28,9 +30,12 @@ async def run_output(
28
30
  def read(streams: Iterable[str]) -> Iterator[str]:
29
31
  nonlocal last, mid_line
30
32
  for stream in streams:
31
- path = folder / f"{stream}.log"
33
+ path = latest_log(folder, stream)
32
34
  if not path.exists():
33
35
  continue
36
+ if paths.setdefault(stream, path) != path:
37
+ paths[stream] = path
38
+ offsets[stream] = 0
34
39
  with path.open("rb") as log:
35
40
  log.seek(offsets[stream])
36
41
  data = log.read()
@@ -0,0 +1,15 @@
1
+ import fcntl
2
+ import os
3
+ from pathlib import Path
4
+
5
+ LOCK_FILE = "run.lock"
6
+
7
+
8
+ def claim_run(folder: Path) -> int | None:
9
+ lock = os.open(folder / LOCK_FILE, os.O_RDWR | os.O_CREAT, 0o644)
10
+ try:
11
+ fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
12
+ except BlockingIOError:
13
+ os.close(lock)
14
+ return None
15
+ return lock
@@ -0,0 +1,21 @@
1
+ from labnotes.runner.first_invocation import first_invocation
2
+
3
+ # Per-invocation keys that a later invocation may leave unset.
4
+ UNSET = {"args", "meta", "pid", "pid_created", "retry_of"}
5
+
6
+
7
+ def continue_run(
8
+ previous: dict[str, object], run: dict[str, object], invocation: dict[str, object]
9
+ ) -> dict[str, object]:
10
+ invocations = previous.get("invocations")
11
+ if not isinstance(invocations, list):
12
+ invocations = [] if not previous else [first_invocation(previous)]
13
+ kept = {key: value for key, value in previous.items() if key not in UNSET}
14
+ return (
15
+ kept
16
+ | run
17
+ | {
18
+ "created": previous.get("created", run["created"]),
19
+ "invocations": [*invocations, invocation],
20
+ }
21
+ )
@@ -18,6 +18,8 @@ def describe_event(event: dict[str, object]) -> str:
18
18
  return f"Stopping {event['run']}."
19
19
  case "killing run":
20
20
  return f"Killing {event['run']}."
21
+ case "retried" if event["retry"] == event["run"]:
22
+ return f"{event['run']} retried in its folder."
21
23
  case "retried":
22
24
  return f"{event['run']} retried as {event['retry']}."
23
25
  case "retrying":
@@ -1,13 +1,16 @@
1
1
  import asyncio
2
2
  import contextlib
3
+ import json
3
4
  import os
4
5
  import time
5
6
  from collections.abc import Callable, Sequence
7
+ from datetime import UTC, datetime
6
8
  from pathlib import Path
7
9
 
8
10
  import psutil
9
11
 
10
12
  from labnotes.log_path import LOG_ENV, RUN_ENV
13
+ from labnotes.runner.continue_run import continue_run
11
14
  from labnotes.runner.exit_signal import exit_signal
12
15
  from labnotes.runner.record_memory import record_memory
13
16
  from labnotes.runner.run_git import run_git
@@ -39,8 +42,10 @@ async def execute_run(
39
42
  memory_interval: float,
40
43
  environment: dict[str, str] | None = None,
41
44
  git_info: dict[str, str | bool] | None = None,
45
+ continues: bool = False,
42
46
  ) -> dict[str, object]:
43
47
  resolved = shell_command(command, folder, params, args)
48
+ git = git_info if git_info is not None else await asyncio.to_thread(run_git, root)
44
49
  run: dict[str, object] = {
45
50
  "params": params,
46
51
  "command": resolved,
@@ -50,9 +55,7 @@ async def execute_run(
50
55
  "error": None,
51
56
  "ended_by": None,
52
57
  "signal": None,
53
- "git": git_info
54
- if git_info is not None
55
- else await asyncio.to_thread(run_git, root),
58
+ "git": git,
56
59
  }
57
60
  if args:
58
61
  run["args"] = list(args)
@@ -60,14 +63,32 @@ async def execute_run(
60
63
  run["retry_of"] = retry_of
61
64
  if meta is not None:
62
65
  run["meta"] = meta
66
+ logs = {stream: f"{stream}.log" for stream in ("stdout", "stderr")}
67
+ invocation: dict[str, object] | None = None
68
+ if continues:
69
+ stamp = datetime.now(UTC).strftime("%Y%m%d-%H%M%S-%f")
70
+ logs = {stream: f"{stream}-{stamp}.txt" for stream in logs}
71
+ invocation = {
72
+ "started": run["created"],
73
+ "ended": None,
74
+ "exit_code": None,
75
+ "ended_by": None,
76
+ "error": None,
77
+ "git": None if git is None else git["commit"],
78
+ "args": list(args),
79
+ **logs,
80
+ }
81
+ record = folder / "run.json"
82
+ previous = json.loads(record.read_text()) if record.exists() else {}
83
+ run = continue_run(previous, run, invocation)
63
84
  write_run_json(folder, run)
64
85
  run_environment = os.environ if environment is None else environment
65
86
  botbox_labels = f"labnotes.run={folder.resolve()}"
66
87
  if existing_labels := run_environment.get("BOTBOX_LABELS"):
67
88
  botbox_labels = f"{existing_labels}\n{botbox_labels}"
68
89
  with (
69
- (folder / "stdout.log").open("wb") as stdout,
70
- (folder / "stderr.log").open("wb") as stderr,
90
+ (folder / logs["stdout"]).open("wb") as stdout,
91
+ (folder / logs["stderr"]).open("wb") as stderr,
71
92
  ):
72
93
  try:
73
94
  process = await asyncio.create_subprocess_shell(
@@ -138,5 +159,9 @@ async def execute_run(
138
159
  await asyncio.wait({sampler})
139
160
  running.discard(process)
140
161
  run["ended"] = iso_time(time.time())
162
+ if invocation is not None:
163
+ invocation |= {
164
+ key: run[key] for key in ("ended", "exit_code", "ended_by", "error")
165
+ }
141
166
  write_run_json(folder, run)
142
167
  return run
@@ -3,6 +3,8 @@ import time
3
3
 
4
4
  from labnotes.list_runs import UNFINISHED
5
5
  from labnotes.models import LabnotesConfig
6
+ from labnotes.runner.claim_run import LOCK_FILE
7
+ from labnotes.runner.run_locked import run_locked
6
8
  from labnotes.runner.write_run_json import write_run_json
7
9
  from labnotes.tables.iso_time import iso_time
8
10
  from labnotes.tables.open_tables import open_tables
@@ -15,10 +17,17 @@ async def finalize_lost_runs(config: LabnotesConfig) -> None:
15
17
  )
16
18
  for row in rows:
17
19
  run = json.loads(str(row["run"]))
20
+ folder = config.root / str(row["folder"])
18
21
  pid, created = run.get("pid"), run.get("pid_created")
19
- if isinstance(pid, int) and process_alive(
22
+ if (folder / LOCK_FILE).exists():
23
+ if run_locked(folder):
24
+ continue
25
+ elif isinstance(pid, int) and process_alive(
20
26
  pid, created if isinstance(created, int | float) else None
21
27
  ):
22
28
  continue
23
- run |= {"ended": iso_time(time.time()), "ended_by": "lost"}
24
- write_run_json(config.root / str(row["folder"]), run)
29
+ lost = {"ended": iso_time(time.time()), "ended_by": "lost"}
30
+ run |= lost
31
+ if invocations := run.get("invocations"):
32
+ invocations[-1] |= lost
33
+ write_run_json(folder, run)
@@ -0,0 +1,13 @@
1
+ def first_invocation(run: dict[str, object]) -> dict[str, object]:
2
+ git = run.get("git")
3
+ return {
4
+ "started": run.get("created"),
5
+ "ended": run.get("ended"),
6
+ "exit_code": run.get("exit_code"),
7
+ "ended_by": run.get("ended_by"),
8
+ "error": run.get("error"),
9
+ "git": git.get("commit") if isinstance(git, dict) else None,
10
+ "args": run.get("args", []),
11
+ "stdout": "stdout.log",
12
+ "stderr": "stderr.log",
13
+ }
@@ -27,6 +27,7 @@ SETTINGS = {
27
27
  "setup",
28
28
  "teardown",
29
29
  "env",
30
+ "continues",
30
31
  }
31
32
  HOOKS = ("setup", "teardown")
32
33
  VIEWS = {"runs"}
@@ -97,6 +98,11 @@ def read_experiment_config(
97
98
  raise ValueError(f"{path}: env.{key} is reserved for labnotes")
98
99
  if not isinstance(value, str):
99
100
  raise ValueError(f"{path}: env.{key} must be a string")
101
+ continues = values.get("continues", False)
102
+ if not isinstance(continues, bool):
103
+ raise ValueError(f"{path}: continues must be true or false")
104
+ if continues and mode != "matrix":
105
+ raise ValueError(f'{path}: continues needs mode = "matrix"')
100
106
  views = values.get("views", {})
101
107
  if not isinstance(views, dict):
102
108
  raise ValueError(f"{path}: views must be a table, like [views]")
@@ -120,4 +126,5 @@ def read_experiment_config(
120
126
  values.get("setup"),
121
127
  values.get("teardown"),
122
128
  env,
129
+ continues,
123
130
  )
@@ -17,6 +17,7 @@ from labnotes.models import (
17
17
  )
18
18
  from labnotes.retries import RETRY_EXIT_CODE, RETRY_SECONDS
19
19
  from labnotes.runner.check_hook_placeholders import check_hook_placeholders
20
+ from labnotes.runner.claim_run import claim_run
20
21
  from labnotes.runner.execute_run import execute_run
21
22
  from labnotes.runner.fill_experiment_env import fill_experiment_env
22
23
  from labnotes.runner.fill_dirs import fill_dirs
@@ -33,6 +34,7 @@ from labnotes.runner.signal_tree import signal_tree
33
34
  from labnotes.state_path import STATE_ENV
34
35
  from labnotes.tables.experiment_run_ids import experiment_run_ids
35
36
  from labnotes.tables.list_experiment_runs import list_experiment_runs
37
+ from labnotes.tables.list_resumable_runs import list_resumable_runs
36
38
  from labnotes.tables.open_tables import open_tables
37
39
 
38
40
  Entry = tuple[str, dict[str, object]]
@@ -82,9 +84,37 @@ async def run_experiment(
82
84
  params, experiment.params, lambda run: taken(f"{group}/{run}")
83
85
  )
84
86
 
87
+ locks: dict[str, int] = {}
88
+
89
+ def claim(run: str) -> bool:
90
+ if run not in locks:
91
+ lock = claim_run(run_folder(config.root, config.runs, name, run))
92
+ if lock is None:
93
+ return False
94
+ locks[run] = lock
95
+ return True
96
+
97
+ def release(run: str) -> None:
98
+ if run in locks:
99
+ os.close(locks.pop(run))
100
+
101
+ resumable: list[Entry] = []
102
+ if experiment.continues:
103
+ resumable = [
104
+ (str(row["id"]), json.loads(str(row["params"])))
105
+ for row in await list_resumable_runs(tables, name, experiment.counts)
106
+ ]
85
107
  plan: list[Entry] = []
86
108
  for params in cells:
87
- run = new_id(params)
109
+ resumed = next(
110
+ (
111
+ entry
112
+ for entry in resumable
113
+ if entry[1] == params and entry[0] not in planned and claim(entry[0])
114
+ ),
115
+ None,
116
+ )
117
+ run = new_id(params) if resumed is None else resumed[0]
88
118
  planned.add(run)
89
119
  plan.append((run, params))
90
120
  run_dirs = [
@@ -129,7 +159,7 @@ async def run_experiment(
129
159
  running: dict[str, set[asyncio.subprocess.Process]] = {}
130
160
  trees: dict[str, set[psutil.Process]] = {}
131
161
  stopped: set[str] = set()
132
- results: list[dict[str, object]] = []
162
+ results: dict[str, dict[str, object]] = {}
133
163
  stopping = asyncio.Event()
134
164
 
135
165
  def signal_run(run: str, signal_number: int) -> None:
@@ -175,9 +205,11 @@ async def run_experiment(
175
205
  waiting: dict[str, asyncio.TimerHandle] = {}
176
206
 
177
207
  def requeue(run: str, attempt: int) -> None:
178
- again = new_id(ended[run])
179
- planned.add(again)
180
- parents[again] = run
208
+ again = run
209
+ if not experiment.continues:
210
+ again = new_id(ended[run])
211
+ planned.add(again)
212
+ parents[again] = run
181
213
  attempts[again] = attempt
182
214
  plan.append((again, ended[run]))
183
215
  report({"event": "retried", "run": run, "retry": again, "params": ended[run]})
@@ -207,6 +239,7 @@ async def run_experiment(
207
239
 
208
240
  def cancel_retry(run: str) -> None:
209
241
  waiting.pop(run).cancel()
242
+ release(run)
210
243
  report({"event": "retry cancelled", "run": run})
211
244
 
212
245
  limit = concurrency
@@ -245,6 +278,8 @@ async def run_experiment(
245
278
  async def start(run: str, params: dict[str, object]) -> None:
246
279
  folder = run_folder(config.root, config.runs, name, run)
247
280
  folder.mkdir(parents=True, exist_ok=True)
281
+ if experiment.continues:
282
+ claim(run)
248
283
  if snapshot is not None and snapshot.sha is not None:
249
284
  (folder / "git-sha.txt").write_text(snapshot.sha + "\n")
250
285
  (folder / "git-diff.patch").write_bytes(snapshot.diff or b"")
@@ -287,15 +322,18 @@ async def run_experiment(
287
322
  if snapshot is not None and snapshot.sha is not None
288
323
  else None
289
324
  ),
325
+ continues=experiment.continues,
290
326
  )
291
327
  del running[run], trees[run]
292
328
  report({"event": "ended", "run": run, **result})
293
329
  ended[run] = params
294
- results.append(result)
330
+ results[run] = result
295
331
  if result["exit_code"] == RETRY_EXIT_CODE and not (
296
332
  stopping.is_set() or run in stopped
297
333
  ):
298
334
  retry_later(run)
335
+ if run not in waiting:
336
+ release(run)
299
337
 
300
338
  async def teardown() -> None:
301
339
  if experiment.teardown is not None:
@@ -336,5 +374,8 @@ async def run_experiment(
336
374
  except Exception as failure: # noqa: BLE001
337
375
  report({"event": "teardown failed", "error": str(failure)})
338
376
  raise
377
+ finally:
378
+ for run in list(locks):
379
+ release(run)
339
380
  await teardown()
340
- return results
381
+ return list(results.values())
@@ -0,0 +1,12 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from labnotes.runner.claim_run import claim_run
5
+
6
+
7
+ def run_locked(folder: Path) -> bool:
8
+ lock = claim_run(folder)
9
+ if lock is None:
10
+ return True
11
+ os.close(lock)
12
+ return False
@@ -0,0 +1,16 @@
1
+ from dirsql import DirSQL
2
+
3
+ from labnotes.list_runs import UNFINISHED
4
+ from labnotes.sql_quote import quote_string
5
+
6
+
7
+ async def list_resumable_runs(
8
+ tables: DirSQL, name: str, counts: str
9
+ ) -> list[dict[str, object]]:
10
+ # Ended badly: a nonzero or missing exit code, not merely uncounted.
11
+ return await tables.query(
12
+ f"SELECT id, params FROM runs WHERE experiment = {quote_string(name)}"
13
+ f" AND NOT coalesce(({counts}), false) AND NOT {UNFINISHED}"
14
+ " AND (exit_code IS NULL OR exit_code <> 0)"
15
+ " AND NOT coalesce(run ->> 'excluded', false) ORDER BY created, id"
16
+ )
@@ -31,8 +31,8 @@ CREATE TABLE runs (
31
31
  # Sizes only: the server reads log text from disk, the row change is the signal.
32
32
  LOGS = """
33
33
  CREATE TABLE logs (
34
- experiment TEXT, run TEXT, stream TEXT, size INTEGER,
35
- PRIMARY KEY (experiment, run, stream)
34
+ experiment TEXT, run TEXT, stream TEXT, file TEXT, size INTEGER,
35
+ PRIMARY KEY (experiment, run, file)
36
36
  )
37
37
  """
38
38
  BLOCKS = """
@@ -90,7 +90,7 @@ def open_tables(config: LabnotesConfig, run_files: bool = False) -> DirSQL:
90
90
  Table(
91
91
  name="logs",
92
92
  ddl=LOGS,
93
- glob=path_glob(f"{config.runs}/std*.log"),
93
+ glob=path_glob(f"{config.runs}/std*"),
94
94
  on_file=partial(
95
95
  read_log, root, config.runs, experiment=config.experiment
96
96
  ),
@@ -1,22 +1,26 @@
1
+ import re
1
2
  from pathlib import Path
2
3
 
3
4
  from labnotes.tables.path_pattern import path_pattern
4
5
 
5
6
  STREAMS = ("stdout", "stderr")
7
+ LOG_NAME = re.compile(r"(?P<stream>[^/.-]+)(?:\.log|-[^/]+\.txt)")
6
8
 
7
9
 
8
10
  def read_log(
9
11
  root: Path, template: str, path: str, experiment: str | None = None
10
12
  ) -> list[dict[str, object]]:
11
13
  relative = Path(path).relative_to(root).as_posix()
12
- found = path_pattern(f"{template}/{{stream}}.log").fullmatch(relative)
13
- if found is None or found["stream"] not in STREAMS:
14
+ found = path_pattern(f"{template}/{{log}}").fullmatch(relative)
15
+ name = None if found is None else LOG_NAME.fullmatch(found["log"])
16
+ if found is None or name is None or name["stream"] not in STREAMS:
14
17
  return []
15
18
  return [
16
19
  {
17
20
  "experiment": experiment or found["experiment"],
18
21
  "run": found["run"],
19
- "stream": found["stream"],
22
+ "stream": name["stream"],
23
+ "file": found["log"],
20
24
  "size": Path(path).stat().st_size,
21
25
  }
22
26
  ]
@@ -13,6 +13,10 @@ def read_run(
13
13
  return []
14
14
  run = json.loads(Path(path).read_text(encoding="utf-8"))
15
15
  folder = relative.removesuffix("/run.json")
16
+ logs = {"stdout": "stdout.log", "stderr": "stderr.log"}
17
+ match run.get("invocations"):
18
+ case [*_, {"stdout": str() as stdout, "stderr": str() as stderr}]:
19
+ logs = {"stdout": stdout, "stderr": stderr}
16
20
  return [
17
21
  {
18
22
  "experiment": experiment or found["experiment"],
@@ -22,8 +26,8 @@ def read_run(
22
26
  "ended": run.get("ended"),
23
27
  "exit_code": run.get("exit_code"),
24
28
  "error": run.get("error"),
25
- "stdout": f"{folder}/stdout.log",
26
- "stderr": f"{folder}/stderr.log",
29
+ "stdout": f"{folder}/{logs['stdout']}",
30
+ "stderr": f"{folder}/{logs['stderr']}",
27
31
  "params": json.dumps(run.get("params", {})),
28
32
  "meta": None if (meta := run.get("meta")) is None else json.dumps(meta),
29
33
  "command": run.get("command"),
@@ -164,8 +164,12 @@ class RunApp(App[None]):
164
164
  if self.waiting.pop(run, None) is not None:
165
165
  outcome = f"{FAILED} exited {RETRY_EXIT_CODE}, retried"
166
166
  self.set_cell(run, "status", Text(outcome, style="red"))
167
- self.retried.add(run)
168
167
  again = str(event["retry"])
168
+ if again == run:
169
+ self.failed.discard(run)
170
+ self.ended.discard(run)
171
+ else:
172
+ self.retried.add(run)
169
173
  args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
170
174
  self.cells[again] = self.waiting_cells(again, args)
171
175
  self.pending.add(again)
@@ -6,6 +6,8 @@ from rich.text import Text
6
6
  from textual.geometry import Size
7
7
  from textual.widgets import RichLog
8
8
 
9
+ from labnotes.latest_log import latest_log
10
+
9
11
  STREAMS = ("stdout", "stderr")
10
12
 
11
13
 
@@ -16,6 +18,7 @@ class RunOutput(RichLog):
16
18
 
17
19
  def reset(self) -> None:
18
20
  self.offsets = dict.fromkeys(STREAMS, 0)
21
+ self.paths: dict[str, Path] = {}
19
22
  self.decoders = {
20
23
  stream: codecs.getincrementaldecoder("utf-8")(errors="replace")
21
24
  for stream in STREAMS
@@ -34,9 +37,12 @@ class RunOutput(RichLog):
34
37
 
35
38
  def follow(self, folder: Path) -> None:
36
39
  for stream in STREAMS:
37
- path = folder / f"{stream}.log"
40
+ path = latest_log(folder, stream)
38
41
  if not path.exists():
39
42
  continue
43
+ if self.paths.setdefault(stream, path) != path:
44
+ self.paths[stream] = path
45
+ self.offsets[stream] = 0
40
46
  with path.open("rb") as log:
41
47
  log.seek(self.offsets[stream])
42
48
  data = log.read()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes