labnotes 0.0.102__tar.gz → 0.0.103__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 (169) hide show
  1. {labnotes-0.0.102 → labnotes-0.0.103}/PKG-INFO +1 -1
  2. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/context.md +4 -0
  3. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/daemon_app.py +1 -0
  4. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/remote_start.py +2 -0
  5. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/experiments.py +9 -1
  6. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/load_run.py +1 -1
  7. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/execute_run.py +20 -2
  8. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/run_experiment.py +3 -0
  9. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/templates/run.html +1 -1
  10. {labnotes-0.0.102 → labnotes-0.0.103}/.gitignore +0 -0
  11. {labnotes-0.0.102 → labnotes-0.0.103}/CHANGELOG.md +0 -0
  12. {labnotes-0.0.102 → labnotes-0.0.103}/MIGRATIONS.md +0 -0
  13. {labnotes-0.0.102 → labnotes-0.0.103}/README.md +0 -0
  14. {labnotes-0.0.102 → labnotes-0.0.103}/pyproject.toml +0 -0
  15. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/__init__.py +0 -0
  16. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/__main__.py +0 -0
  17. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/asset_url.py +0 -0
  18. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/check_counts.py +0 -0
  19. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/cli.py +0 -0
  20. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/compose_experiment.py +0 -0
  21. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/context_guide.py +0 -0
  22. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/count_groups.py +0 -0
  23. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/count_runs.py +0 -0
  24. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/create_app.py +0 -0
  25. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/create_experiment.py +0 -0
  26. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/create_note.py +0 -0
  27. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/__init__.py +0 -0
  28. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/control_run.py +0 -0
  29. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/daemon_client.py +0 -0
  30. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/daemon_running.py +0 -0
  31. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/daemon_server.py +0 -0
  32. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/describe_status.py +0 -0
  33. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/ensure_daemon.py +0 -0
  34. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/exit_when_idle.py +0 -0
  35. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/run_daemon.py +0 -0
  36. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/run_logs.py +0 -0
  37. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/serve_daemon.py +0 -0
  38. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/socket_path.py +0 -0
  39. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/stop_runs.py +0 -0
  40. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/watch_daemon.py +0 -0
  41. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon/watch_status.py +0 -0
  42. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/daemon_command.py +0 -0
  43. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/data_html.py +0 -0
  44. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/default_config_text.py +0 -0
  45. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/default_counts.py +0 -0
  46. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/drain_watch.py +0 -0
  47. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/experiment_notes.py +0 -0
  48. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/experiment_runs.py +0 -0
  49. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/file_changes.py +0 -0
  50. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/format_duration.py +0 -0
  51. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/format_started.py +0 -0
  52. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/format_value.py +0 -0
  53. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/group_runs.py +0 -0
  54. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/init.py +0 -0
  55. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/invocation_started.py +0 -0
  56. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/is_positive_int.py +0 -0
  57. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/list_experiments.py +0 -0
  58. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/list_runs.py +0 -0
  59. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/load_config.py +0 -0
  60. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/load_experiment.py +0 -0
  61. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/log.py +0 -0
  62. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/log_path.py +0 -0
  63. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/models.py +0 -0
  64. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/modes.py +0 -0
  65. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/nests_runs.py +0 -0
  66. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/note_path.py +0 -0
  67. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/notebook_kernel.py +0 -0
  68. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/output_html.py +0 -0
  69. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/parse_run_view.py +0 -0
  70. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/print_rows.py +0 -0
  71. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/python_blocks.py +0 -0
  72. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/read_config.py +0 -0
  73. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/read_path.py +0 -0
  74. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/render_markdown.py +0 -0
  75. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/run_command.py +0 -0
  76. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/run_control.py +0 -0
  77. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/run_output.py +0 -0
  78. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/run_status.py +0 -0
  79. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/__init__.py +0 -0
  80. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/describe_event.py +0 -0
  81. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/hold_folder.py +0 -0
  82. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/new_param_run_id.py +0 -0
  83. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/new_run_id.py +0 -0
  84. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  85. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/param_args.py +0 -0
  86. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/param_grid.py +0 -0
  87. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/param_run_name.py +0 -0
  88. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/plan_runs.py +0 -0
  89. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/prepare_experiment.py +0 -0
  90. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/read_experiment_config.py +0 -0
  91. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/run_folder.py +0 -0
  92. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/run_headless.py +0 -0
  93. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/runs_parent.py +0 -0
  94. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/shell_command.py +0 -0
  95. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/signal_tree.py +0 -0
  96. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/stop_then_kill.py +0 -0
  97. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/wait_for_tree.py +0 -0
  98. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/runner/write_run_json.py +0 -0
  99. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/save_conflict_copy.py +0 -0
  100. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/save_note.py +0 -0
  101. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/saved_outputs.py +0 -0
  102. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/serve_app.py +0 -0
  103. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/server.py +0 -0
  104. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/shared_params.py +0 -0
  105. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/sort_href.py +0 -0
  106. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/sql_quote.py +0 -0
  107. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/static/elapsed.js +0 -0
  108. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/static/experiment.js +0 -0
  109. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/static/labnotes.css +0 -0
  110. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/static/local-time.js +0 -0
  111. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/static/new-note.js +0 -0
  112. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/static/note-edit.js +0 -0
  113. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/static/note-editor.js +0 -0
  114. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/static/row-link.js +0 -0
  115. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/static/run-cell.js +0 -0
  116. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/static/run-output.js +0 -0
  117. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/static/view-toggle.js +0 -0
  118. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/summarize_config.py +0 -0
  119. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/__init__.py +0 -0
  120. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/experiment_rows.py +0 -0
  121. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/experiment_run_ids.py +0 -0
  122. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/experiment_toml.py +0 -0
  123. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/experiments_ddl.py +0 -0
  124. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/file_prefix.py +0 -0
  125. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/file_text.py +0 -0
  126. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/iso_time.py +0 -0
  127. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/list_experiment_runs.py +0 -0
  128. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/note_text.py +0 -0
  129. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/open_tables.py +0 -0
  130. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/path_glob.py +0 -0
  131. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/path_pattern.py +0 -0
  132. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/read_blocks.py +0 -0
  133. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/read_experiment.py +0 -0
  134. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/read_file.py +0 -0
  135. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/read_log.py +0 -0
  136. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/read_run.py +0 -0
  137. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tables/summarize_experiments.py +0 -0
  138. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/templates/base.html +0 -0
  139. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/templates/editor_imports.html +0 -0
  140. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/templates/experiment.html +0 -0
  141. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/templates/index.html +0 -0
  142. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/templates/note.html +0 -0
  143. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/__init__.py +0 -0
  144. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/browse_app.py +0 -0
  145. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/browse_tui.py +0 -0
  146. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/dim.py +0 -0
  147. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/experiment_screen.py +0 -0
  148. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/experiments_screen.py +0 -0
  149. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/fill_table.py +0 -0
  150. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/format_elapsed.py +0 -0
  151. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/group_cells.py +0 -0
  152. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/invocation_columns.py +0 -0
  153. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/invocation_rows.py +0 -0
  154. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/leave_prompt.py +0 -0
  155. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/log_block.py +0 -0
  156. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/log_blocks.py +0 -0
  157. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/reload_on_change.py +0 -0
  158. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/run_app.py +0 -0
  159. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/run_cells.py +0 -0
  160. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/run_output.py +0 -0
  161. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/run_screen.py +0 -0
  162. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/run_tui.py +0 -0
  163. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/runs_table.py +0 -0
  164. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/tui/stopped_title.py +0 -0
  165. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/wait_for_note.py +0 -0
  166. {labnotes-0.0.102 → labnotes-0.0.103}/src/labnotes/watch_rows.py +0 -0
  167. {labnotes-0.0.102 → labnotes-0.0.103}/testing-conventions.toml +0 -0
  168. {labnotes-0.0.102 → labnotes-0.0.103}/tests/integration/conftest.py +0 -0
  169. {labnotes-0.0.102 → labnotes-0.0.103}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.102
3
+ Version: 0.0.103
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>
@@ -117,6 +117,10 @@ going. Elsewhere, it prints a line per event instead.
117
117
  in `experiment.toml` sets it, then `concurrency` under `[experiments]` in
118
118
  `.labnotes.toml`, then 1.
119
119
  - `-e PATH` reads another file instead of `experiment.toml`.
120
+ - `--timeout SECONDS` stops any one run still going after that many seconds:
121
+ SIGTERM, then SIGKILL 5 seconds later. Its
122
+ `run.json` gets `"error": "timed out after ..."` next to its exit code.
123
+ Unset, runs have no time limit.
120
124
  - `run` hands its runs to the project's labnotes daemon, starting it if
121
125
  needed, and shows or waits for them. The daemon listens on a socket in
122
126
  `/tmp/labnotes-<uid>/` and exits once nothing is running and no client has
@@ -102,6 +102,7 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
102
102
  concurrency,
103
103
  lambda event: record({**event, "experiment": name}),
104
104
  listen,
105
+ body.get("timeout"), # type: ignore[arg-type]
105
106
  )
106
107
  # Clients hear why the session ended; the daemon keeps serving.
107
108
  except Exception as failure: # noqa: BLE001
@@ -15,6 +15,7 @@ def remote_start(
15
15
  name: str,
16
16
  experiment_config: Path | None,
17
17
  concurrency: int | None,
18
+ timeout: float | None = None,
18
19
  ) -> Start:
19
20
  async def start(report: Report, listen: Listen) -> list[dict[str, object]]:
20
21
  try:
@@ -44,6 +45,7 @@ def remote_start(
44
45
  if experiment_config is None
45
46
  else str(experiment_config.resolve()),
46
47
  "concurrency": concurrency,
48
+ "timeout": timeout,
47
49
  },
48
50
  )
49
51
  if reply.status_code != 202:
@@ -40,6 +40,13 @@ from labnotes.tui.run_tui import run_tui
40
40
  type=click.Path(exists=True, dir_okay=False, path_type=Path),
41
41
  help="With run: read this file instead of the experiment's experiment.toml.",
42
42
  )
43
+ @click.option(
44
+ "--timeout",
45
+ type=click.IntRange(min=1),
46
+ metavar="SECONDS",
47
+ help="With run: stop any run still going after this many seconds."
48
+ " Unset, runs have no time limit.",
49
+ )
43
50
  @click.pass_context
44
51
  def experiments(
45
52
  ctx: click.Context,
@@ -48,6 +55,7 @@ def experiments(
48
55
  output_format: str | None,
49
56
  concurrency: int | None,
50
57
  experiment_config: Path | None,
58
+ timeout: int | None,
51
59
  ) -> None:
52
60
  config = load_config(ctx)
53
61
  if config is None:
@@ -63,7 +71,7 @@ def experiments(
63
71
  click.echo(f"Created {path / 'experiment.toml'}")
64
72
  return
65
73
  case str(), "run":
66
- start = remote_start(config, name, experiment_config, concurrency)
74
+ start = remote_start(config, name, experiment_config, concurrency, timeout)
67
75
  try:
68
76
  if sys.stdout.isatty() and sys.stderr.isatty():
69
77
  results = run_tui(start, name)
@@ -10,7 +10,7 @@ async def load_run(
10
10
  ) -> dict[str, object] | None:
11
11
  rows = await tables.query(
12
12
  "SELECT runs.experiment, experiments.display_name, runs.id, runs.folder,"
13
- " runs.created, runs.ended, runs.exit_code, runs.params"
13
+ " runs.created, runs.ended, runs.exit_code, runs.error, runs.params"
14
14
  " FROM runs JOIN experiments ON experiments.name = runs.experiment"
15
15
  f" WHERE runs.experiment = {quote_string(experiment)}"
16
16
  f" AND runs.id = {quote_string(run)}"
@@ -1,15 +1,18 @@
1
1
  import asyncio
2
2
  import os
3
3
  import time
4
+ from collections.abc import Callable
4
5
  from pathlib import Path
5
6
 
6
7
  import psutil
7
8
 
8
9
  from labnotes.log_path import LOG_ENV
9
10
  from labnotes.runner.shell_command import shell_command
11
+ from labnotes.runner.stop_then_kill import GRACE_SECONDS
10
12
  from labnotes.runner.wait_for_tree import wait_for_tree
11
13
  from labnotes.runner.write_run_json import write_run_json
12
14
  from labnotes.tables.iso_time import iso_time
15
+ from labnotes.tui.format_elapsed import format_elapsed
13
16
 
14
17
 
15
18
  async def execute_run(
@@ -20,6 +23,8 @@ async def execute_run(
20
23
  running: set[asyncio.subprocess.Process],
21
24
  stopping: asyncio.Event,
22
25
  tree: set[psutil.Process],
26
+ timeout: float | None = None,
27
+ stop: Callable[[], None] = lambda: None,
23
28
  ) -> dict[str, object]:
24
29
  run: dict[str, object] = {
25
30
  "params": params,
@@ -50,14 +55,27 @@ async def execute_run(
50
55
  run["error"] = str(error)
51
56
  else:
52
57
  running.add(process)
58
+ exited = asyncio.ensure_future(process.wait())
59
+ kill_later = None
53
60
  try:
54
- run["exit_code"] = await process.wait()
61
+ if (
62
+ timeout is not None
63
+ and not (await asyncio.wait({exited}, timeout=timeout))[0]
64
+ ):
65
+ run["error"] = f"timed out after {format_elapsed(timeout)}"
66
+ stop()
67
+ kill_later = asyncio.get_running_loop().call_later(
68
+ GRACE_SECONDS, stop
69
+ )
70
+ run["exit_code"] = await exited
55
71
  # The shell can exit on SIGTERM before what it started. Once
56
72
  # stopping, the run stays killable until everything signalled
57
73
  # is gone. A normal exit ends the run even if helpers linger.
58
- if stopping.is_set():
74
+ if stopping.is_set() or kill_later is not None:
59
75
  await wait_for_tree(tree)
60
76
  finally:
77
+ if kill_later is not None:
78
+ kill_later.cancel()
61
79
  running.discard(process)
62
80
  run["ended"] = iso_time(time.time())
63
81
  write_run_json(folder, run)
@@ -32,6 +32,7 @@ async def run_experiment(
32
32
  concurrency: int,
33
33
  report: Report,
34
34
  listen: Listen,
35
+ timeout: float | None = None,
35
36
  ) -> list[dict[str, object]]:
36
37
  tables = open_tables(config)
37
38
  existing = await experiment_run_ids(tables, name)
@@ -203,6 +204,8 @@ async def run_experiment(
203
204
  running[run],
204
205
  stopping,
205
206
  trees[run],
207
+ timeout=timeout,
208
+ stop=lambda: kill(run),
206
209
  )
207
210
  del running[run], trees[run]
208
211
  report({"event": "ended", "run": run, **result})
@@ -5,7 +5,7 @@
5
5
  <section class="intro">
6
6
  <h1 class="mono">{{ run.id }}</h1>
7
7
  <p class="config-summary">
8
- {% if run.ended is none %}<span class="chip run-state">running</span>{% elif run.exit_code is none %}<span class="chip run-state bad">ended</span>{% else %}<span class="chip run-state {{ 'good' if run.exit_code == 0 else 'bad' }}">exit {{ run.exit_code }}</span>{% endif %}
8
+ {% if run.ended is none %}<span class="chip run-state">running</span>{% elif run.error %}<span class="chip run-state bad">{{ run.error }}</span>{% elif run.exit_code is none %}<span class="chip run-state bad">ended</span>{% else %}<span class="chip run-state {{ 'good' if run.exit_code == 0 else 'bad' }}">exit {{ run.exit_code }}</span>{% endif %}
9
9
  {% if run.created %}<span class="setting">started <code>{{ run.created }}</code></span>{% endif %}
10
10
  {% for key, value in run.params.items() %}<span class="setting">{{ key }} <code>{{ value | value }}</code></span>{% endfor %}
11
11
  </p>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes