labnotes 0.0.117__tar.gz → 0.0.119__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 (205) hide show
  1. {labnotes-0.0.117 → labnotes-0.0.119}/PKG-INFO +3 -1
  2. {labnotes-0.0.117 → labnotes-0.0.119}/README.md +2 -0
  3. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/context.md +9 -1
  4. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/daemon_app.py +2 -0
  5. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/remote_start.py +2 -0
  6. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/experiments.py +13 -2
  7. labnotes-0.0.119/src/labnotes/grid_cell.py +21 -0
  8. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/load_experiment.py +15 -0
  9. labnotes-0.0.119/src/labnotes/parse_subset.py +10 -0
  10. labnotes-0.0.119/src/labnotes/pivot_runs.py +43 -0
  11. labnotes-0.0.119/src/labnotes/runner/check_subset.py +13 -0
  12. labnotes-0.0.119/src/labnotes/runner/in_subset.py +5 -0
  13. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/prepare_experiment.py +4 -0
  14. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/run_experiment.py +6 -1
  15. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/static/labnotes.css +22 -0
  16. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/templates/experiment.html +40 -0
  17. {labnotes-0.0.117 → labnotes-0.0.119}/testing-conventions.toml +5 -5
  18. {labnotes-0.0.117 → labnotes-0.0.119}/.gitignore +0 -0
  19. {labnotes-0.0.117 → labnotes-0.0.119}/CHANGELOG.md +0 -0
  20. {labnotes-0.0.117 → labnotes-0.0.119}/MIGRATIONS.md +0 -0
  21. {labnotes-0.0.117 → labnotes-0.0.119}/pyproject.toml +0 -0
  22. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/__init__.py +0 -0
  23. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/__main__.py +0 -0
  24. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/asset_url.py +0 -0
  25. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/check_counts.py +0 -0
  26. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/cli.py +0 -0
  27. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/compose_experiment.py +0 -0
  28. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/context_guide.py +0 -0
  29. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/count_groups.py +0 -0
  30. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/count_runs.py +0 -0
  31. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/create_app.py +0 -0
  32. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/create_experiment.py +0 -0
  33. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/create_note.py +0 -0
  34. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/__init__.py +0 -0
  35. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/control_run.py +0 -0
  36. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/daemon_client.py +0 -0
  37. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/daemon_running.py +0 -0
  38. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/daemon_server.py +0 -0
  39. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/describe_status.py +0 -0
  40. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/ensure_daemon.py +0 -0
  41. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/exit_when_idle.py +0 -0
  42. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/finalize_lost_runs.py +0 -0
  43. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/last_stderr.py +0 -0
  44. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/run_daemon.py +0 -0
  45. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/run_logs.py +0 -0
  46. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/serve_daemon.py +0 -0
  47. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/socket_path.py +0 -0
  48. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/stop_runs.py +0 -0
  49. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/watch_daemon.py +0 -0
  50. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon/watch_status.py +0 -0
  51. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/daemon_command.py +0 -0
  52. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/data_html.py +0 -0
  53. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/default_config_text.py +0 -0
  54. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/default_counts.py +0 -0
  55. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/drain_watch.py +0 -0
  56. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/experiment_notes.py +0 -0
  57. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/experiment_runs.py +0 -0
  58. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/file_changes.py +0 -0
  59. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/format_duration.py +0 -0
  60. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/format_started.py +0 -0
  61. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/format_value.py +0 -0
  62. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/group_runs.py +0 -0
  63. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/init.py +0 -0
  64. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/invocation_started.py +0 -0
  65. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/is_non_negative_int.py +0 -0
  66. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/is_non_negative_number.py +0 -0
  67. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/is_positive_int.py +0 -0
  68. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/list_experiments.py +0 -0
  69. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/list_runs.py +0 -0
  70. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/load_config.py +0 -0
  71. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/load_run.py +0 -0
  72. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/log.py +0 -0
  73. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/log_path.py +0 -0
  74. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/memory_interval.py +0 -0
  75. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/models.py +0 -0
  76. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/modes.py +0 -0
  77. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/nests_runs.py +0 -0
  78. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/note_path.py +0 -0
  79. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/notebook_kernel.py +0 -0
  80. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/output_html.py +0 -0
  81. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/parse_run_view.py +0 -0
  82. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/print_rows.py +0 -0
  83. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/python_blocks.py +0 -0
  84. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/read_config.py +0 -0
  85. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/read_path.py +0 -0
  86. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/render_markdown.py +0 -0
  87. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/retries.py +0 -0
  88. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/run_command.py +0 -0
  89. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/run_control.py +0 -0
  90. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/run_output.py +0 -0
  91. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/run_status.py +0 -0
  92. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/__init__.py +0 -0
  93. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/describe_event.py +0 -0
  94. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/execute_run.py +0 -0
  95. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/exit_signal.py +0 -0
  96. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/hold_folder.py +0 -0
  97. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/invocation_meta.py +0 -0
  98. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/new_param_run_id.py +0 -0
  99. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/new_run_id.py +0 -0
  100. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  101. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/param_args.py +0 -0
  102. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/param_grid.py +0 -0
  103. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/param_run_name.py +0 -0
  104. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/plan_runs.py +0 -0
  105. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/read_experiment_config.py +0 -0
  106. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/record_memory.py +0 -0
  107. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/run_folder.py +0 -0
  108. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/run_headless.py +0 -0
  109. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/runs_parent.py +0 -0
  110. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/shell_command.py +0 -0
  111. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/signal_tree.py +0 -0
  112. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/stop_then_kill.py +0 -0
  113. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/wait_for_tree.py +0 -0
  114. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runner/write_run_json.py +0 -0
  115. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/runs_view_command.py +0 -0
  116. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/save_conflict_copy.py +0 -0
  117. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/save_note.py +0 -0
  118. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/saved_outputs.py +0 -0
  119. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/serve_app.py +0 -0
  120. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/server.py +0 -0
  121. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/shared_params.py +0 -0
  122. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/sort_href.py +0 -0
  123. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/sql_quote.py +0 -0
  124. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/static/elapsed.js +0 -0
  125. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/static/experiment.js +0 -0
  126. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/static/local-time.js +0 -0
  127. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/static/new-note.js +0 -0
  128. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/static/note-edit.js +0 -0
  129. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/static/note-editor.js +0 -0
  130. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/static/row-link.js +0 -0
  131. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/static/run-cell.js +0 -0
  132. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/static/run-output.js +0 -0
  133. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/static/view-toggle.js +0 -0
  134. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/summarize_config.py +0 -0
  135. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/__init__.py +0 -0
  136. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/experiment_rows.py +0 -0
  137. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/experiment_run_ids.py +0 -0
  138. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/experiment_toml.py +0 -0
  139. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/experiments_ddl.py +0 -0
  140. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/file_prefix.py +0 -0
  141. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/file_text.py +0 -0
  142. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/iso_time.py +0 -0
  143. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/list_experiment_runs.py +0 -0
  144. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/note_text.py +0 -0
  145. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/open_tables.py +0 -0
  146. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/path_glob.py +0 -0
  147. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/path_pattern.py +0 -0
  148. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/read_blocks.py +0 -0
  149. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/read_experiment.py +0 -0
  150. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/read_file.py +0 -0
  151. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/read_log.py +0 -0
  152. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/read_memory.py +0 -0
  153. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/read_run.py +0 -0
  154. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/read_run_file.py +0 -0
  155. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/runs_columns.py +0 -0
  156. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tables/summarize_experiments.py +0 -0
  157. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/templates/base.html +0 -0
  158. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/templates/editor_imports.html +0 -0
  159. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/templates/index.html +0 -0
  160. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/templates/note.html +0 -0
  161. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/templates/run.html +0 -0
  162. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/__init__.py +0 -0
  163. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/browse_app.py +0 -0
  164. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/browse_tui.py +0 -0
  165. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/check_view_response.py +0 -0
  166. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/config.py +0 -0
  167. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/dim.py +0 -0
  168. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/experiment_screen.py +0 -0
  169. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/experiments_screen.py +0 -0
  170. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/fill_table.py +0 -0
  171. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/format_cell.py +0 -0
  172. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/format_elapsed.py +0 -0
  173. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/group_cells.py +0 -0
  174. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/invocation_columns.py +0 -0
  175. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/invocation_rows.py +0 -0
  176. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/leave_prompt.py +0 -0
  177. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/log_block.py +0 -0
  178. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/log_blocks.py +0 -0
  179. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/native_rows.py +0 -0
  180. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/reload_on_change.py +0 -0
  181. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/run_app.py +0 -0
  182. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/run_cells.py +0 -0
  183. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/run_output.py +0 -0
  184. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/run_screen.py +0 -0
  185. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/run_tui.py +0 -0
  186. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/runs_table.py +0 -0
  187. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/runs_view.py +0 -0
  188. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/stopped_title.py +0 -0
  189. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/view_column.py +0 -0
  190. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/tui/view_process.py +0 -0
  191. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/utils/__init__.py +0 -0
  192. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/utils/format_bytes.py +0 -0
  193. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/utils/process_alive.py +0 -0
  194. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/utils/run_usage.py +0 -0
  195. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/utils/tree_rss.py +0 -0
  196. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/utils/tree_usage.py +0 -0
  197. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/view/__init__.py +0 -0
  198. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/view/answer.py +0 -0
  199. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/view/build.py +0 -0
  200. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/view/respond.py +0 -0
  201. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/view/serve.py +0 -0
  202. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/wait_for_note.py +0 -0
  203. {labnotes-0.0.117 → labnotes-0.0.119}/src/labnotes/watch_rows.py +0 -0
  204. {labnotes-0.0.117 → labnotes-0.0.119}/tests/integration/conftest.py +0 -0
  205. {labnotes-0.0.117 → labnotes-0.0.119}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.117
3
+ Version: 0.0.119
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>
@@ -120,6 +120,8 @@ line per event instead.
120
120
 
121
121
  - `--concurrency N` runs N at a time (default 1).
122
122
  - `-e PATH` reads another file instead of `experiment.toml`.
123
+ - `--PARAM=VALUE` runs only the cells where that param has that value, like
124
+ `--model=small`.
123
125
  - With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) leaves them
124
126
  running. `s` stops the running scripts and records their end, then kills any
125
127
  still going 5 seconds later. Escape goes back. SIGTERM stops them like `s`.
@@ -95,6 +95,8 @@ line per event instead.
95
95
 
96
96
  - `--concurrency N` runs N at a time (default 1).
97
97
  - `-e PATH` reads another file instead of `experiment.toml`.
98
+ - `--PARAM=VALUE` runs only the cells where that param has that value, like
99
+ `--model=small`.
98
100
  - With runs going, Ctrl-C asks first. Enter (or a second Ctrl-C) leaves them
99
101
  running. `s` stops the running scripts and records their end, then kills any
100
102
  still going 5 seconds later. Escape goes back. SIGTERM stops them like `s`.
@@ -151,6 +151,14 @@ going. Elsewhere, it prints a line per event instead.
151
151
  in `experiment.toml` sets it, then `concurrency` under `[experiments]` in
152
152
  `.labnotes.toml`, then 1.
153
153
  - `-e PATH` reads another file instead of `experiment.toml`.
154
+ - `--PARAM=VALUE` runs only the cells where that param has that value, e.g.
155
+ `labnotes experiments NAME run --model=small`. PARAM must be a key in
156
+ `[params]` and VALUE one of its values, written as it shows in `runs`
157
+ (`true`, `2`); anything else is an error. One value per param; give several
158
+ params to narrow further (`--model=small --lang=en`). It changes only what
159
+ this `run` starts: counts, progress and `repeats` still cover the full
160
+ matrix, so a later plain `run` fills in the rest. Use it instead of editing
161
+ `experiment.toml` to run a slice.
154
162
  - `--timeout SECONDS` stops any one run still going after that many seconds:
155
163
  SIGTERM, then SIGKILL 5 seconds later. Its
156
164
  `run.json` gets `"error": "timed out after ..."` and `"ended_by": "timeout"`
@@ -180,4 +188,4 @@ going. Elsewhere, it prints a line per event instead.
180
188
  was retried counts as retried, not failed. If it can't
181
189
  start or reach the daemon, it prints the cause on one line and exits 1.
182
190
 
183
- `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. Clicking a run opens its `stdout.log` and `stderr.log` together, followed as they're written until the run ends.
191
+ `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.
@@ -78,6 +78,7 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
78
78
  name,
79
79
  None if path is None else Path(str(path)),
80
80
  body.get("concurrency"), # type: ignore[arg-type]
81
+ body.get("only"), # type: ignore[arg-type]
81
82
  )
82
83
  except ValueError as error:
83
84
  return refuse(400, str(error))
@@ -103,6 +104,7 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
103
104
  lambda event: record({**event, "experiment": name}),
104
105
  listen,
105
106
  body.get("timeout"), # type: ignore[arg-type]
107
+ only=body.get("only"), # type: ignore[arg-type]
106
108
  )
107
109
  # Clients hear why the session ended; the daemon keeps serving.
108
110
  except Exception as failure: # noqa: BLE001
@@ -16,6 +16,7 @@ def remote_start(
16
16
  experiment_config: Path | None,
17
17
  concurrency: int | None,
18
18
  timeout: float | None = None,
19
+ only: dict[str, str] | None = None,
19
20
  ) -> Start:
20
21
  async def start(report: Report, listen: Listen) -> list[dict[str, object]]:
21
22
  try:
@@ -46,6 +47,7 @@ def remote_start(
46
47
  else str(experiment_config.resolve()),
47
48
  "concurrency": concurrency,
48
49
  "timeout": timeout,
50
+ "only": only,
49
51
  },
50
52
  )
51
53
  if reply.status_code != 202:
@@ -8,6 +8,7 @@ from labnotes.create_experiment import create_experiment
8
8
  from labnotes.daemon.remote_start import remote_start
9
9
  from labnotes.utils.format_bytes import format_bytes
10
10
  from labnotes.load_config import load_config
11
+ from labnotes.parse_subset import parse_subset
11
12
  from labnotes.print_rows import print_rows
12
13
  from labnotes.read_config import CONFIG_FILE
13
14
  from labnotes.runner.run_headless import run_headless
@@ -20,7 +21,9 @@ BYTES = ("memory", "size")
20
21
 
21
22
  @click.command(
22
23
  help="List experiments, show one, list its runs, or run it."
23
- " `new [NAME]` creates one."
24
+ " `new [NAME]` creates one. With run, --PARAM=VALUE runs only the"
25
+ " matrix cells where that param has that value.",
26
+ context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
24
27
  )
25
28
  @click.argument("name", required=False)
26
29
  @click.argument("action", required=False, metavar="[runs|run]")
@@ -74,7 +77,13 @@ def experiments(
74
77
  click.echo(f"Created {path / 'experiment.toml'}")
75
78
  return
76
79
  case str(), "run":
77
- start = remote_start(config, name, experiment_config, concurrency, timeout)
80
+ try:
81
+ only = parse_subset(ctx.args)
82
+ except ValueError as error:
83
+ raise click.UsageError(str(error)) from error
84
+ start = remote_start(
85
+ config, name, experiment_config, concurrency, timeout, only
86
+ )
78
87
  try:
79
88
  if sys.stdout.isatty() and sys.stderr.isatty():
80
89
  results = run_tui(start, name)
@@ -92,6 +101,8 @@ def experiments(
92
101
  passed = len(results) - failed - retried
93
102
  click.echo(f"{passed} passed, {failed} failed.{again}")
94
103
  return
104
+ if ctx.args:
105
+ raise click.UsageError(f"Unexpected {ctx.args[0]}. --PARAM=VALUE needs run.")
95
106
  if action not in (None, "runs"):
96
107
  raise click.UsageError(f"Unknown action {action}. Use runs or run.")
97
108
  if output_format is None and sys.stdout.isatty() and sys.stderr.isatty():
@@ -0,0 +1,21 @@
1
+ from typing import TypedDict
2
+ from urllib.parse import urlencode
3
+
4
+ from labnotes.format_value import format_value
5
+
6
+
7
+ class Cell(TypedDict):
8
+ values: dict[str, object]
9
+ runs: object
10
+ completed: object
11
+ href: str
12
+
13
+
14
+ def grid_cell(values: dict[str, object], counts: dict[str, object]) -> Cell:
15
+ filters = [(f"f.{key}", format_value(value)) for key, value in values.items()]
16
+ return {
17
+ "values": values,
18
+ "runs": counts.get("runs", 0),
19
+ "completed": counts.get("completed", 0),
20
+ "href": f"?{urlencode(filters)}#runs",
21
+ }
@@ -17,6 +17,7 @@ from labnotes.list_runs import list_runs
17
17
  from labnotes.models import QueryParams
18
18
  from labnotes.nests_runs import nests_runs
19
19
  from labnotes.parse_run_view import parse_run_view
20
+ from labnotes.pivot_runs import pivot_runs
20
21
  from labnotes.python_blocks import python_blocks
21
22
  from labnotes.render_markdown import render_markdown
22
23
  from labnotes.runs_view_command import runs_view_command
@@ -65,6 +66,19 @@ async def load_experiment(
65
66
  if nested
66
67
  else None
67
68
  )
69
+ matrix = summary["mode"] == "matrix"
70
+ gridded = matrix and params.getlist("view")[-1:] == ["grid"]
71
+ axes = {key: varies[key] for key in choices if len(varies[key]) > 1}
72
+ grid = (
73
+ pivot_runs(
74
+ axes,
75
+ await count_runs(
76
+ tables, source, str(summary["completed_run"]), group_by=list(axes)
77
+ ),
78
+ )
79
+ if gridded
80
+ else None
81
+ )
68
82
  return {
69
83
  "name": row["name"],
70
84
  "display_name": row["display_name"],
@@ -86,6 +100,7 @@ async def load_experiment(
86
100
  "runs_list": runs,
87
101
  "runs_live": await daemon_running(root),
88
102
  "run_groups": groups,
103
+ "run_grid": grid,
89
104
  "run_columns": shown,
90
105
  "run_view": view,
91
106
  "run_choices": choices,
@@ -0,0 +1,10 @@
1
+ def parse_subset(args: list[str]) -> dict[str, str]:
2
+ only: dict[str, str] = {}
3
+ for arg in args:
4
+ param, equals, value = arg.removeprefix("--").partition("=")
5
+ if not arg.startswith("--") or not equals or not param:
6
+ raise ValueError(f"Unexpected {arg}. Pick params with --PARAM=VALUE.")
7
+ if param in only:
8
+ raise ValueError(f"--{param} given twice. Pass one value per param.")
9
+ only[param] = value
10
+ return only
@@ -0,0 +1,43 @@
1
+ from itertools import product
2
+ from typing import TypedDict
3
+
4
+ from labnotes.grid_cell import Cell, grid_cell
5
+
6
+
7
+ class Row(TypedDict):
8
+ labels: list[object]
9
+ cells: list[Cell]
10
+
11
+
12
+ class Grid(TypedDict):
13
+ row_keys: list[str]
14
+ column_key: str | None
15
+ columns: list[object]
16
+ rows: list[Row]
17
+
18
+
19
+ def _key(value: object) -> object:
20
+ return int(value) if isinstance(value, bool) else value
21
+
22
+
23
+ def pivot_runs(axes: dict[str, list[object]], counts: list[dict[str, object]]) -> Grid:
24
+ by_cell = {tuple(_key(row[key]) for key in axes): row for row in counts}
25
+ row_keys = [*axes]
26
+ column_key = row_keys.pop() if row_keys else None
27
+ columns: list[object] = [None] if column_key is None else axes[column_key]
28
+ rows: list[Row] = []
29
+ for labels in product(*(axes[key] for key in row_keys)):
30
+ cells = []
31
+ for column in columns:
32
+ values: dict[str, object] = dict(zip(row_keys, labels))
33
+ if column_key is not None:
34
+ values[column_key] = column
35
+ found = by_cell.get(tuple(_key(values[key]) for key in axes), {})
36
+ cells.append(grid_cell(values, found))
37
+ rows.append({"labels": list(labels), "cells": cells})
38
+ return {
39
+ "row_keys": row_keys,
40
+ "column_key": column_key,
41
+ "columns": columns,
42
+ "rows": rows,
43
+ }
@@ -0,0 +1,13 @@
1
+ from labnotes.format_value import format_value
2
+
3
+
4
+ def check_subset(params: dict[str, list[object]], only: dict[str, str]) -> None:
5
+ for param, value in only.items():
6
+ if param not in params:
7
+ declared = ", ".join(params) or "none"
8
+ raise ValueError(f"No param {param}. Params: {declared}.")
9
+ values = [format_value(option) for option in params[param]]
10
+ if value not in values:
11
+ raise ValueError(
12
+ f"No value {value} for {param}. Values: {', '.join(values)}."
13
+ )
@@ -0,0 +1,5 @@
1
+ from labnotes.format_value import format_value
2
+
3
+
4
+ def in_subset(cell: dict[str, object], only: dict[str, str]) -> bool:
5
+ return all(format_value(cell[param]) == value for param, value in only.items())
@@ -1,6 +1,7 @@
1
1
  from pathlib import Path
2
2
 
3
3
  from labnotes.models import ExperimentConfig, LabnotesConfig
4
+ from labnotes.runner.check_subset import check_subset
4
5
  from labnotes.runner.read_experiment_config import read_experiment_config
5
6
  from labnotes.tables.file_text import file_text
6
7
 
@@ -10,6 +11,7 @@ async def prepare_experiment(
10
11
  name: str,
11
12
  experiment_config: Path | None,
12
13
  concurrency: int | None,
14
+ only: dict[str, str] | None = None,
13
15
  ) -> tuple[ExperimentConfig, int]:
14
16
  if config.experiment not in (None, name):
15
17
  raise ValueError(
@@ -29,4 +31,6 @@ async def prepare_experiment(
29
31
  config.retries,
30
32
  config.defaults,
31
33
  )
34
+ if only:
35
+ check_subset(experiment.params, only)
32
36
  return experiment, experiment.concurrency if concurrency is None else concurrency
@@ -13,6 +13,7 @@ from labnotes.models import (
13
13
  )
14
14
  from labnotes.retries import RETRY_EXIT_CODE, RETRY_SECONDS
15
15
  from labnotes.runner.execute_run import execute_run
16
+ from labnotes.runner.in_subset import in_subset
16
17
  from labnotes.runner.invocation_meta import invocation_meta
17
18
  from labnotes.runner.new_param_run_id import new_param_run_id
18
19
  from labnotes.runner.new_stamped_run_id import new_stamped_run_id
@@ -36,6 +37,7 @@ async def run_experiment(
36
37
  listen: Listen,
37
38
  timeout: float | None = None,
38
39
  retry_seconds: float = RETRY_SECONDS,
40
+ only: dict[str, str] | None = None,
39
41
  ) -> list[dict[str, object]]:
40
42
  meta = None
41
43
  if experiment.meta is not None:
@@ -52,7 +54,10 @@ async def run_experiment(
52
54
  def taken(run: str) -> bool:
53
55
  return run in planned or run in existing
54
56
 
55
- cells = plan_runs(param_grid(experiment.params), experiment.repeats, finished)
57
+ grid = [
58
+ cell for cell in param_grid(experiment.params) if in_subset(cell, only or {})
59
+ ]
60
+ cells = plan_runs(grid, experiment.repeats, finished)
56
61
  group = None
57
62
  match experiment.mode:
58
63
  case "adhoc" if len(cells) > 1:
@@ -217,6 +217,22 @@ h3 {
217
217
  .new-note { font: inherit; color: var(--ink-2); background: none; border: 0; padding: 0 0.2rem; cursor: pointer; }
218
218
  .new-note:hover { color: var(--ink); }
219
219
 
220
+ /* Runs grid: counted runs per combination of the params that vary. */
221
+ .runs-grid { border-collapse: separate; border-spacing: 3px; font-size: 0.86rem; }
222
+ .runs-grid th { font-family: var(--mono); font-size: 0.8rem; font-weight: 500; color: var(--ink-2); text-align: left; white-space: nowrap; padding: 0 0.75rem 0.3rem; }
223
+ .runs-grid thead th { vertical-align: bottom; }
224
+ .runs-grid th.label { color: var(--ink-3); }
225
+ .runs-grid th.axis { font-family: var(--sans); font-size: 0.68rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); border-bottom: 1px solid var(--rule); }
226
+ .grid-cell { min-width: 6rem; padding: 0; background: var(--surface); border: 1px solid var(--rule); border-radius: 7px; }
227
+ .grid-cell a { display: block; padding: 0.5rem 0.7rem 0.6rem; color: var(--ink); text-decoration: none; }
228
+ .grid-cell:hover { border-color: var(--rule-strong); }
229
+ .grid-cell .num { display: block; margin-bottom: 0.4rem; }
230
+ .grid-cell .of { color: var(--ink-3); }
231
+ .grid-cell.empty .num { color: var(--ink-3); }
232
+ .grid-cell.done { border-color: color-mix(in srgb, var(--accent) 35%, var(--rule)); }
233
+ .grid-cell .meter { display: block; height: 4px; border-radius: 2px; background: var(--track); overflow: hidden; }
234
+ .grid-cell .meter-fill { display: block; height: 100%; background: var(--accent); }
235
+
220
236
  /* Runs table */
221
237
  .narrowed { color: var(--ink-2); font-size: 0.88rem; margin: 0 0 0.75rem; }
222
238
  .narrowed .clear { margin-left: 0.8rem; color: var(--ink-3); }
@@ -390,6 +406,12 @@ h3 {
390
406
  }
391
407
  .view-toggle button + button { border-left: 1px solid var(--rule-strong); }
392
408
  .view-toggle button[aria-pressed="true"] { background: var(--accent-soft); color: var(--ink); }
409
+ #runs .view-toggle { display: flex; width: fit-content; float: none; margin: 0 0 0.75rem auto; }
410
+ .view-toggle a { display: flex; padding: 0.3rem 0.55rem; background: var(--surface); color: var(--ink-3); }
411
+ .view-toggle a + a { border-left: 1px solid var(--rule-strong); }
412
+ .view-toggle a:hover { color: var(--ink); }
413
+ .view-toggle a[aria-current="page"] { background: var(--accent-soft); color: var(--ink); }
414
+ .view-toggle svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; }
393
415
 
394
416
  /* Index as cards: the same table, laid out as a grid. */
395
417
  .ledger[data-view="cards"] { display: block; background: none; border: 0; border-radius: 0; overflow: visible; }
@@ -51,6 +51,45 @@
51
51
  {% set nested = e.run_groups is not none %}
52
52
  {% set width = e.run_columns | length + (2 if nested else 1) %}
53
53
  {% set narrowed = view.status or view.filters or view.picked %}
54
+ {% set grid = e.run_grid %}
55
+ {% if e.mode == 'matrix' and e.runs %}
56
+ <nav class="view-toggle" aria-label="Runs view">
57
+ <a href="?#runs" title="Table" aria-label="Table"{% if not grid %} aria-current="page"{% endif %}><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 3.5h12M2 8h12M2 12.5h12"/></svg></a>
58
+ <a href="?view=grid#runs" title="Grid" aria-label="Grid"{% if grid %} aria-current="page"{% endif %}><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="2" width="5" height="5" rx="1"/><rect x="9" y="2" width="5" height="5" rx="1"/><rect x="2" y="9" width="5" height="5" rx="1"/><rect x="9" y="9" width="5" height="5" rx="1"/></svg></a>
59
+ </nav>
60
+ {% endif %}
61
+ {% if grid %}
62
+ <div class="scroll">
63
+ <table class="runs-grid">
64
+ <thead>
65
+ {% if grid.column_key is not none %}
66
+ <tr class="axis-row">
67
+ {% if grid.row_keys %}<td colspan="{{ grid.row_keys | length }}"></td>{% endif %}
68
+ <th scope="colgroup" colspan="{{ grid.columns | length }}" class="axis">{{ grid.column_key }}</th>
69
+ </tr>
70
+ <tr>
71
+ {% for key in grid.row_keys %}<th scope="col" class="label">{{ key }}</th>{% endfor %}
72
+ {% for column in grid.columns %}<th scope="col">{{ column | value }}</th>{% endfor %}
73
+ </tr>
74
+ {% endif %}
75
+ </thead>
76
+ <tbody>
77
+ {% for row in grid.rows %}
78
+ <tr>
79
+ {% for label in row.labels %}<th scope="row">{{ label | value }}</th>{% endfor %}
80
+ {% for cell in row.cells %}
81
+ {% set goal = e.runs_per_cell or 1 %}
82
+ <td class="grid-cell{{ ' done' if cell.completed >= goal }}{{ ' empty' if not cell.runs }}" title="{% for key, value in cell['values'].items() %}{{ key }} = {{ value | value }}{{ ', ' if not loop.last }}{% endfor %}{{ ': ' if cell['values'] }}{{ cell.completed }} complete of {{ cell.runs }} recorded">
83
+ <a href="{{ cell.href }}"><span class="num">{{ cell.completed }}<span class="of">/{{ goal }}</span></span>
84
+ <span class="meter"><span class="meter-fill" style="width: {{ [cell.completed / goal * 100, 100] | min }}%"></span></span></a>
85
+ </td>
86
+ {% endfor %}
87
+ </tr>
88
+ {% endfor %}
89
+ </tbody>
90
+ </table>
91
+ </div>
92
+ {% else %}
54
93
  {% if narrowed %}<p class="narrowed"><span class="num">{{ e.runs_list | length }}</span> of <span class="num">{{ e.runs }}</span> <a class="clear" href="{{ '?sort=' ~ (view.sort | urlencode) ~ ('&order=desc' if view.descending else '') if view.sort else '?' }}#runs">Clear filters</a></p>{% endif %}
55
94
  {% if e.runs %}
56
95
  <form id="run-filters" method="get" action="#runs">
@@ -117,6 +156,7 @@
117
156
  {% else %}
118
157
  <p class="empty">No runs recorded yet.</p>
119
158
  {% endif %}
159
+ {% endif %}
120
160
  </section>
121
161
  {% endif %}
122
162
  <script src="{{ asset('experiment.js') }}" defer></script>
@@ -13,7 +13,7 @@ reason = "last_stream is always the same str object taken from STREAMS, so is eq
13
13
  [[python.exempt]]
14
14
  path = "src/labnotes/load_experiment.py"
15
15
  rules = ["mutation"]
16
- lines = [61]
16
+ lines = [61, 69]
17
17
  reason = "summarize_config returns mode matrix or adhoc; \"matrix\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
18
18
 
19
19
  [[python.exempt]]
@@ -25,13 +25,13 @@ reason = "quiet counts up by one from zero, so it reaches IDLE_EXIT_SECONDS befo
25
25
  [[python.exempt]]
26
26
  path = "src/labnotes/daemon/daemon_app.py"
27
27
  rules = ["mutation"]
28
- lines = [132]
28
+ lines = [134]
29
29
  reason = "start answers 202, 400, 409 or 500, so every status but 202 is above it: != equals >"
30
30
 
31
31
  [[python.exempt]]
32
32
  path = "src/labnotes/daemon/remote_start.py"
33
33
  rules = ["mutation"]
34
- lines = [49]
34
+ lines = [51]
35
35
  reason = "the daemon answers a run with 202, 400, 409 or 500, so every status but 202 is above it: != equals >"
36
36
 
37
37
  [[python.exempt]]
@@ -67,7 +67,7 @@ reason = "WorkerState members are singletons, so is equals =="
67
67
  [[python.exempt]]
68
68
  path = "src/labnotes/runner/run_experiment.py"
69
69
  rules = ["mutation"]
70
- lines = [188, 269]
70
+ lines = [193, 274]
71
71
  reason = "a retry is only scheduled below the budget and each one adds one, so tried never passes retries: >= equals ==; both sides are small ints, which CPython caches, so is equals =="
72
72
 
73
73
  [[python.exempt]]
@@ -79,5 +79,5 @@ reason = "exit_code is non-negative here; when bit 7 is set, - 128 equals ^ 128,
79
79
  [[python.exempt]]
80
80
  path = "src/labnotes/experiments.py"
81
81
  rules = ["mutation"]
82
- lines = [109]
82
+ lines = [120]
83
83
  reason = "click hands back the \"json\" choice literal itself, so is not equals !="
File without changes
File without changes
File without changes
File without changes