labnotes 0.0.138__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 (200) hide show
  1. {labnotes-0.0.138 → labnotes-0.1.0}/PKG-INFO +1 -1
  2. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/execute_run.py +4 -1
  3. labnotes-0.1.0/src/labnotes/runner/project_snapshot.py +118 -0
  4. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/run_experiment.py +15 -4
  5. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/start_experiment.py +14 -11
  6. {labnotes-0.0.138 → labnotes-0.1.0}/.gitignore +0 -0
  7. {labnotes-0.0.138 → labnotes-0.1.0}/CHANGELOG.md +0 -0
  8. {labnotes-0.0.138 → labnotes-0.1.0}/MIGRATIONS.md +0 -0
  9. {labnotes-0.0.138 → labnotes-0.1.0}/README.md +0 -0
  10. {labnotes-0.0.138 → labnotes-0.1.0}/pyproject.toml +0 -0
  11. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/__init__.py +0 -0
  12. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/__main__.py +0 -0
  13. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/asset_url.py +0 -0
  14. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/check_counts.py +0 -0
  15. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/cli.py +0 -0
  16. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/compose_experiment.py +0 -0
  17. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/context.md +0 -0
  18. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/context_guide.py +0 -0
  19. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/count_groups.py +0 -0
  20. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/count_runs.py +0 -0
  21. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/create_app.py +0 -0
  22. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/create_experiment.py +0 -0
  23. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/create_note.py +0 -0
  24. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/data_html.py +0 -0
  25. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/default_config_text.py +0 -0
  26. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/default_counts.py +0 -0
  27. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/drain_watch.py +0 -0
  28. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/experiment_notes.py +0 -0
  29. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/experiment_runs.py +0 -0
  30. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/experiments.py +0 -0
  31. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/file_changes.py +0 -0
  32. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/format_duration.py +0 -0
  33. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/format_started.py +0 -0
  34. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/format_value.py +0 -0
  35. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/get_state.py +0 -0
  36. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/grid_cell.py +0 -0
  37. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/group_runs.py +0 -0
  38. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/init.py +0 -0
  39. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/invocation_started.py +0 -0
  40. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/is_non_negative_int.py +0 -0
  41. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/is_non_negative_number.py +0 -0
  42. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/is_positive_int.py +0 -0
  43. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/list_experiments.py +0 -0
  44. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/list_runs.py +0 -0
  45. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/load_config.py +0 -0
  46. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/load_experiment.py +0 -0
  47. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/load_run.py +0 -0
  48. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/log.py +0 -0
  49. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/log_path.py +0 -0
  50. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/memory_interval.py +0 -0
  51. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/models.py +0 -0
  52. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/modes.py +0 -0
  53. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/nests_runs.py +0 -0
  54. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/note_path.py +0 -0
  55. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/notebook_kernel.py +0 -0
  56. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/output_html.py +0 -0
  57. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/parse_run_view.py +0 -0
  58. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/parse_subset.py +0 -0
  59. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/pivot_runs.py +0 -0
  60. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/print_rows.py +0 -0
  61. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/python_blocks.py +0 -0
  62. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/read_config.py +0 -0
  63. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/read_path.py +0 -0
  64. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/render_markdown.py +0 -0
  65. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/retries.py +0 -0
  66. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/run_output.py +0 -0
  67. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/__init__.py +0 -0
  68. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/cgroup_memory.py +0 -0
  69. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/check_args.py +0 -0
  70. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/check_subset.py +0 -0
  71. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/container_cgroup.py +0 -0
  72. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/container_memory.py +0 -0
  73. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/describe_event.py +0 -0
  74. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/docker_output.py +0 -0
  75. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/exit_signal.py +0 -0
  76. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/fill_dirs.py +0 -0
  77. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/fill_experiment_env.py +0 -0
  78. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/finalize_lost_runs.py +0 -0
  79. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/flag_name.py +0 -0
  80. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/hook_dirs.py +0 -0
  81. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/in_subset.py +0 -0
  82. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/invocation_hook.py +0 -0
  83. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/invocation_meta.py +0 -0
  84. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/new_param_run_id.py +0 -0
  85. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/new_run_id.py +0 -0
  86. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  87. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/param_args.py +0 -0
  88. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/param_grid.py +0 -0
  89. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/param_run_name.py +0 -0
  90. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/plan_runs.py +0 -0
  91. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/prepare_experiment.py +0 -0
  92. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/read_experiment_config.py +0 -0
  93. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/record_memory.py +0 -0
  94. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/run_folder.py +0 -0
  95. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/run_git.py +0 -0
  96. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/run_headless.py +0 -0
  97. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/runs_parent.py +0 -0
  98. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/shell_command.py +0 -0
  99. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/signal_tree.py +0 -0
  100. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/stop_then_kill.py +0 -0
  101. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/wait_for_tree.py +0 -0
  102. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runner/write_run_json.py +0 -0
  103. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/runs_view_command.py +0 -0
  104. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/save_conflict_copy.py +0 -0
  105. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/save_note.py +0 -0
  106. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/saved_outputs.py +0 -0
  107. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/serve_app.py +0 -0
  108. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/server.py +0 -0
  109. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/set_state.py +0 -0
  110. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/shared_params.py +0 -0
  111. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/sort_href.py +0 -0
  112. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/sql_quote.py +0 -0
  113. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/state_path.py +0 -0
  114. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/static/elapsed.js +0 -0
  115. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/static/experiment.js +0 -0
  116. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/static/labnotes.css +0 -0
  117. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/static/local-time.js +0 -0
  118. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/static/new-note.js +0 -0
  119. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/static/note-edit.js +0 -0
  120. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/static/note-editor.js +0 -0
  121. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/static/row-link.js +0 -0
  122. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/static/run-cell.js +0 -0
  123. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/static/run-output.js +0 -0
  124. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/static/view-toggle.js +0 -0
  125. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/summarize_config.py +0 -0
  126. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/__init__.py +0 -0
  127. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/experiment_rows.py +0 -0
  128. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/experiment_run_ids.py +0 -0
  129. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/experiment_toml.py +0 -0
  130. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/experiments_ddl.py +0 -0
  131. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/file_prefix.py +0 -0
  132. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/file_text.py +0 -0
  133. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/iso_time.py +0 -0
  134. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/list_experiment_runs.py +0 -0
  135. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/note_text.py +0 -0
  136. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/open_tables.py +0 -0
  137. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/path_glob.py +0 -0
  138. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/path_pattern.py +0 -0
  139. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/read_blocks.py +0 -0
  140. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/read_experiment.py +0 -0
  141. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/read_file.py +0 -0
  142. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/read_log.py +0 -0
  143. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/read_memory.py +0 -0
  144. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/read_run.py +0 -0
  145. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/read_run_file.py +0 -0
  146. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/runs_columns.py +0 -0
  147. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tables/summarize_experiments.py +0 -0
  148. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/templates/base.html +0 -0
  149. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/templates/editor_imports.html +0 -0
  150. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/templates/experiment.html +0 -0
  151. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/templates/index.html +0 -0
  152. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/templates/note.html +0 -0
  153. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/templates/run.html +0 -0
  154. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/__init__.py +0 -0
  155. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/browse_app.py +0 -0
  156. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/browse_tui.py +0 -0
  157. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/check_view_response.py +0 -0
  158. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/config.py +0 -0
  159. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/dim.py +0 -0
  160. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/experiment_screen.py +0 -0
  161. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/experiments_screen.py +0 -0
  162. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/fill_table.py +0 -0
  163. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/format_cell.py +0 -0
  164. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/format_elapsed.py +0 -0
  165. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/group_cells.py +0 -0
  166. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/invocation_columns.py +0 -0
  167. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/invocation_rows.py +0 -0
  168. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/leave_prompt.py +0 -0
  169. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/log_block.py +0 -0
  170. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/log_blocks.py +0 -0
  171. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/native_rows.py +0 -0
  172. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/reload_on_change.py +0 -0
  173. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/run_app.py +0 -0
  174. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/run_cells.py +0 -0
  175. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/run_output.py +0 -0
  176. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/run_screen.py +0 -0
  177. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/run_tui.py +0 -0
  178. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/runs_table.py +0 -0
  179. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/runs_view.py +0 -0
  180. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/stopped_title.py +0 -0
  181. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/view_column.py +0 -0
  182. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/tui/view_process.py +0 -0
  183. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/utils/__init__.py +0 -0
  184. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/utils/format_bytes.py +0 -0
  185. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/utils/process_alive.py +0 -0
  186. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/utils/run_usage.py +0 -0
  187. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/utils/tree_rss.py +0 -0
  188. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/utils/tree_usage.py +0 -0
  189. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/view/__init__.py +0 -0
  190. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/view/answer.py +0 -0
  191. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/view/build.py +0 -0
  192. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/view/respond.py +0 -0
  193. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/view/serve.py +0 -0
  194. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/wait_for_note.py +0 -0
  195. {labnotes-0.0.138 → labnotes-0.1.0}/src/labnotes/watch_rows.py +0 -0
  196. {labnotes-0.0.138 → labnotes-0.1.0}/testing-conventions.toml +0 -0
  197. {labnotes-0.0.138 → labnotes-0.1.0}/tests/conftest.py +0 -0
  198. {labnotes-0.0.138 → labnotes-0.1.0}/tests/e2e/conftest.py +0 -0
  199. {labnotes-0.0.138 → labnotes-0.1.0}/tests/integration/conftest.py +0 -0
  200. {labnotes-0.0.138 → labnotes-0.1.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.138
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>
@@ -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"],
@@ -3,6 +3,7 @@ from pathlib import Path
3
3
  from labnotes.models import LabnotesConfig, Listen, Report, Start
4
4
  from labnotes.runner.finalize_lost_runs import finalize_lost_runs
5
5
  from labnotes.runner.prepare_experiment import prepare_experiment
6
+ from labnotes.runner.project_snapshot import project_snapshot
6
7
  from labnotes.runner.run_experiment import run_experiment
7
8
 
8
9
 
@@ -22,16 +23,18 @@ def start_experiment(
22
23
  config, name, experiment_config, concurrency, only, args, setup, teardown
23
24
  )
24
25
  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
- )
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
+ )
36
39
 
37
40
  return start
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