labnotes 0.0.70__tar.gz → 0.0.71__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 (130) hide show
  1. {labnotes-0.0.70 → labnotes-0.0.71}/PKG-INFO +1 -1
  2. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/context.md +9 -11
  3. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/daemon_app.py +37 -28
  4. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/remote_start.py +2 -1
  5. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/experiments.py +3 -7
  6. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/prepare_experiment.py +2 -9
  7. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/run_experiment.py +1 -0
  8. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tui/run_app.py +5 -2
  9. {labnotes-0.0.70 → labnotes-0.0.71}/testing-conventions.toml +2 -2
  10. labnotes-0.0.70/src/labnotes/runner/start_experiment.py +0 -27
  11. {labnotes-0.0.70 → labnotes-0.0.71}/.gitignore +0 -0
  12. {labnotes-0.0.70 → labnotes-0.0.71}/CHANGELOG.md +0 -0
  13. {labnotes-0.0.70 → labnotes-0.0.71}/MIGRATIONS.md +0 -0
  14. {labnotes-0.0.70 → labnotes-0.0.71}/README.md +0 -0
  15. {labnotes-0.0.70 → labnotes-0.0.71}/pyproject.toml +0 -0
  16. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/__init__.py +0 -0
  17. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/__main__.py +0 -0
  18. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/asset_url.py +0 -0
  19. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/check_counts.py +0 -0
  20. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/cli.py +0 -0
  21. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/context_guide.py +0 -0
  22. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/count_runs.py +0 -0
  23. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/create_app.py +0 -0
  24. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/create_experiment.py +0 -0
  25. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/create_note.py +0 -0
  26. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/__init__.py +0 -0
  27. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/daemon_client.py +0 -0
  28. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/daemon_running.py +0 -0
  29. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/daemon_server.py +0 -0
  30. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/ensure_daemon.py +0 -0
  31. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/exit_when_idle.py +0 -0
  32. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/run_daemon.py +0 -0
  33. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/serve_daemon.py +0 -0
  34. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/socket_path.py +0 -0
  35. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon/stop_runs.py +0 -0
  36. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/daemon_command.py +0 -0
  37. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/data_html.py +0 -0
  38. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/default_config_text.py +0 -0
  39. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/default_counts.py +0 -0
  40. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/drain_watch.py +0 -0
  41. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/experiment_notes.py +0 -0
  42. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/experiment_runs.py +0 -0
  43. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/file_changes.py +0 -0
  44. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/format_duration.py +0 -0
  45. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/format_value.py +0 -0
  46. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/group_runs.py +0 -0
  47. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/init.py +0 -0
  48. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/invocation_started.py +0 -0
  49. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/is_positive_int.py +0 -0
  50. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/list_experiments.py +0 -0
  51. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/list_runs.py +0 -0
  52. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/load_config.py +0 -0
  53. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/load_experiment.py +0 -0
  54. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/models.py +0 -0
  55. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/modes.py +0 -0
  56. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/nests_runs.py +0 -0
  57. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/note_path.py +0 -0
  58. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/notebook_kernel.py +0 -0
  59. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/output_html.py +0 -0
  60. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/parse_run_view.py +0 -0
  61. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/print_rows.py +0 -0
  62. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/python_blocks.py +0 -0
  63. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/read_config.py +0 -0
  64. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/read_path.py +0 -0
  65. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/render_markdown.py +0 -0
  66. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/run_command.py +0 -0
  67. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/__init__.py +0 -0
  68. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/describe_event.py +0 -0
  69. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/execute_run.py +0 -0
  70. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/hold_folder.py +0 -0
  71. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/new_param_run_id.py +0 -0
  72. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/new_run_id.py +0 -0
  73. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  74. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/param_args.py +0 -0
  75. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/param_grid.py +0 -0
  76. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/param_run_name.py +0 -0
  77. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/plan_runs.py +0 -0
  78. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/read_experiment_config.py +0 -0
  79. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/run_folder.py +0 -0
  80. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/run_headless.py +0 -0
  81. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/runs_parent.py +0 -0
  82. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/shell_command.py +0 -0
  83. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/signal_tree.py +0 -0
  84. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/stop_then_kill.py +0 -0
  85. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/wait_for_tree.py +0 -0
  86. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/runner/write_run_json.py +0 -0
  87. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/save_conflict_copy.py +0 -0
  88. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/save_note.py +0 -0
  89. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/saved_outputs.py +0 -0
  90. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/serve_app.py +0 -0
  91. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/server.py +0 -0
  92. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/sort_href.py +0 -0
  93. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/sql_quote.py +0 -0
  94. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/static/experiment.js +0 -0
  95. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/static/labnotes.css +0 -0
  96. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/static/new-note.js +0 -0
  97. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/static/note-edit.js +0 -0
  98. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/static/note-editor.js +0 -0
  99. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/static/row-link.js +0 -0
  100. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/static/run-cell.js +0 -0
  101. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/static/view-toggle.js +0 -0
  102. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/summarize_config.py +0 -0
  103. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/__init__.py +0 -0
  104. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/experiment_rows.py +0 -0
  105. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/experiment_run_ids.py +0 -0
  106. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/file_prefix.py +0 -0
  107. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/file_text.py +0 -0
  108. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/iso_time.py +0 -0
  109. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/list_experiment_runs.py +0 -0
  110. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/note_text.py +0 -0
  111. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/open_tables.py +0 -0
  112. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/path_glob.py +0 -0
  113. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/path_pattern.py +0 -0
  114. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/read_experiment.py +0 -0
  115. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/read_file.py +0 -0
  116. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/read_run.py +0 -0
  117. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tables/summarize_experiments.py +0 -0
  118. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/templates/base.html +0 -0
  119. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/templates/editor_imports.html +0 -0
  120. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/templates/experiment.html +0 -0
  121. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/templates/index.html +0 -0
  122. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/templates/note.html +0 -0
  123. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tui/__init__.py +0 -0
  124. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tui/dim.py +0 -0
  125. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tui/format_elapsed.py +0 -0
  126. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tui/run_tui.py +0 -0
  127. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/tui/runs_table.py +0 -0
  128. {labnotes-0.0.70 → labnotes-0.0.71}/src/labnotes/wait_for_note.py +0 -0
  129. {labnotes-0.0.70 → labnotes-0.0.71}/tests/integration/conftest.py +0 -0
  130. {labnotes-0.0.70 → labnotes-0.0.71}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.70
3
+ Version: 0.0.71
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>
@@ -105,23 +105,21 @@ running run under the cursor; `x` again kills it. `+` and
105
105
  `-` change how many run at once; lowering it lets running runs
106
106
  finish. Enter on a run follows its `stdout.log` and `stderr.log` together, as
107
107
  they're written, and Escape returns to the table. An `adhoc` experiment that
108
- plans a single run opens straight on its output. `q` quits. Elsewhere, it prints a
109
- line per event instead.
108
+ plans a single run opens straight on its output. `q` quits and leaves the runs
109
+ going. Elsewhere, it prints a line per event instead.
110
110
 
111
111
  - `--concurrency N` runs N at a time to start with. Without it, `concurrency`
112
112
  in `experiment.toml` sets it, then `concurrency` under `[experiments]` in
113
113
  `.labnotes.toml`, then 1.
114
114
  - `-e PATH` reads another file instead of `experiment.toml`.
115
+ - `run` hands its runs to the project's labnotes daemon, starting it if
116
+ needed, and shows or waits for them. The daemon listens on `.labnotes.sock`
117
+ in the project folder and exits once nothing is running and no client has
118
+ been connected for 15 seconds. Runs belong to the daemon, so quitting the
119
+ live view, closing its terminal, or killing `run` outright leaves them going.
115
120
  - Ctrl-C stops the running scripts and records their end. A second Ctrl-C
116
- kills them. Closing the terminal or sending SIGTERM stops them the same way,
117
- then kills any still going 5 seconds later. Once the runs are done, either
118
- one closes the live view.
119
- - Outside a terminal, `run` hands its runs to the project's labnotes daemon,
120
- starting it if needed, and waits for them. The daemon listens on
121
- `.labnotes.sock` in the project folder and exits once nothing is running and
122
- no client has been connected for 15 seconds. Runs belong to the daemon, so
123
- killing the waiting `run` outright leaves them going. Ctrl-C or SIGTERM to it
124
- still stops them.
121
+ kills them. SIGTERM stops them the same way, then kills any still going 5
122
+ seconds later. Once the runs are done, either one closes the live view.
125
123
  - `labnotes run stop` stops every run the daemon has going: SIGTERM, then
126
124
  SIGKILL 5 seconds later.
127
125
  - A second `run` of the same experiment fails while the first is going.
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
- import contextlib
3
2
  import dataclasses
3
+ import itertools
4
4
  import json
5
5
  from collections.abc import AsyncIterator
6
6
  from pathlib import Path
@@ -11,7 +11,6 @@ from starlette.responses import JSONResponse, Response, StreamingResponse
11
11
  from starlette.routing import Route
12
12
 
13
13
  from labnotes.models import Controls, LabnotesConfig
14
- from labnotes.runner.hold_folder import hold_folder
15
14
  from labnotes.runner.prepare_experiment import prepare_experiment
16
15
  from labnotes.runner.run_experiment import run_experiment
17
16
  from labnotes.runner.stop_then_kill import GRACE_SECONDS
@@ -22,35 +21,45 @@ ACTIONS = frozenset(field.name for field in dataclasses.fields(Controls))
22
21
 
23
22
  def daemon_app(config: LabnotesConfig) -> Starlette:
24
23
  sessions: dict[str, Controls | None] = {}
25
- clients: set[asyncio.Queue[Event]] = set()
24
+ owners: dict[str, int] = {}
25
+ clients: dict[int, asyncio.Queue[Event]] = {}
26
+ ids = itertools.count(1)
26
27
  tasks: set[asyncio.Task[str | None]] = set()
27
28
 
28
29
  def broadcast(event: Event) -> None:
29
- for queue in clients:
30
+ for queue in clients.values():
30
31
  queue.put_nowait(event)
31
32
 
32
33
  def refuse(status: int, error: str) -> Response:
33
34
  return JSONResponse({"error": error}, status_code=status)
34
35
 
36
+ def release(client: int) -> None:
37
+ for name, owner in list(owners.items()):
38
+ controls = sessions.get(name)
39
+ if owner == client and controls is not None:
40
+ del owners[name]
41
+ controls.finish()
42
+
35
43
  async def events(request: Request) -> Response:
36
- queue: asyncio.Queue[Event] = asyncio.Queue()
44
+ client = next(ids)
37
45
 
38
46
  async def stream() -> AsyncIterator[str]:
39
- clients.add(queue)
47
+ clients[client] = asyncio.Queue()
40
48
  try:
41
- event: Event = {"event": "connected"}
49
+ event: Event = {"event": "connected", "client": client}
42
50
  while True:
43
51
  yield f"data: {json.dumps(event, default=str)}\n\n"
44
- event = await queue.get()
52
+ event = await clients[client].get()
45
53
  finally:
46
- clients.discard(queue)
54
+ del clients[client]
55
+ release(client)
47
56
 
48
57
  return StreamingResponse(stream(), media_type="text/event-stream")
49
58
 
50
59
  async def start(name: str, body: dict[str, object]) -> Response:
51
60
  path = body.get("experiment_config")
52
61
  try:
53
- experiment, concurrency, runs = await prepare_experiment(
62
+ experiment, concurrency = await prepare_experiment(
54
63
  config,
55
64
  name,
56
65
  None if path is None else Path(str(path)),
@@ -58,33 +67,33 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
58
67
  )
59
68
  except ValueError as error:
60
69
  return refuse(400, str(error))
61
- lock = contextlib.ExitStack()
62
- try:
63
- lock.enter_context(hold_folder(runs))
64
- except BlockingIOError:
65
- return refuse(409, f"{name} is already running ({runs} is held).")
66
70
  ready = asyncio.Event()
71
+ owner = body.get("client")
72
+ if isinstance(owner, int):
73
+ owners[name] = owner
67
74
 
68
75
  def listen(controls: Controls) -> None:
69
76
  sessions[name] = controls
70
77
  ready.set()
78
+ if name in owners and owners[name] not in clients:
79
+ release(owners[name])
71
80
 
72
81
  async def session() -> str | None:
73
82
  error = None
74
- with lock:
75
- try:
76
- await run_experiment(
77
- config,
78
- name,
79
- experiment,
80
- concurrency,
81
- lambda event: broadcast({**event, "experiment": name}),
82
- listen,
83
- )
84
- # Clients hear why the session ended; the daemon keeps serving.
85
- except Exception as failure: # noqa: BLE001
86
- error = str(failure)
83
+ try:
84
+ await run_experiment(
85
+ config,
86
+ name,
87
+ experiment,
88
+ concurrency,
89
+ lambda event: broadcast({**event, "experiment": name}),
90
+ listen,
91
+ )
92
+ # Clients hear why the session ended; the daemon keeps serving.
93
+ except Exception as failure: # noqa: BLE001
94
+ error = str(failure)
87
95
  del sessions[name]
96
+ owners.pop(name, None)
88
97
  broadcast({"event": "done", "experiment": name, "error": error})
89
98
  return error
90
99
 
@@ -25,10 +25,11 @@ def remote_start(
25
25
  client.stream("GET", "/events") as response,
26
26
  ):
27
27
  events = aiter(httpx2.EventSource(response))
28
- await anext(events)
28
+ connected: dict[str, object] = (await anext(events)).json() # type: ignore[assignment]
29
29
  reply = await client.post(
30
30
  f"/experiments/{name}/run",
31
31
  json={
32
+ "client": connected["client"],
32
33
  "experiment_config": None
33
34
  if experiment_config is None
34
35
  else str(experiment_config.resolve()),
@@ -10,7 +10,6 @@ from labnotes.load_config import load_config
10
10
  from labnotes.print_rows import print_rows
11
11
  from labnotes.read_config import CONFIG_FILE
12
12
  from labnotes.runner.run_headless import run_headless
13
- from labnotes.runner.start_experiment import start_experiment
14
13
  from labnotes.tables.experiment_rows import experiment_rows
15
14
  from labnotes.tui.run_tui import run_tui
16
15
 
@@ -63,15 +62,12 @@ def experiments(
63
62
  click.echo(f"Created {path / 'experiment.toml'}")
64
63
  return
65
64
  case str(), "run":
65
+ start = remote_start(config, name, experiment_config, concurrency)
66
66
  try:
67
67
  if sys.stdout.isatty() and sys.stderr.isatty():
68
- results = start_experiment(
69
- config, name, experiment_config, concurrency, run_tui
70
- )
68
+ results = run_tui(start, name)
71
69
  else:
72
- results = run_headless(
73
- remote_start(config, name, experiment_config, concurrency)
74
- )
70
+ results = run_headless(start)
75
71
  except ValueError as error:
76
72
  raise click.ClickException(str(error)) from error
77
73
  failed = sum(result["exit_code"] != 0 for result in results)
@@ -2,7 +2,6 @@ from pathlib import Path
2
2
 
3
3
  from labnotes.models import ExperimentConfig, LabnotesConfig
4
4
  from labnotes.runner.read_experiment_config import read_experiment_config
5
- from labnotes.runner.runs_parent import runs_parent
6
5
  from labnotes.tables.file_text import file_text
7
6
 
8
7
 
@@ -11,7 +10,7 @@ async def prepare_experiment(
11
10
  name: str,
12
11
  experiment_config: Path | None,
13
12
  concurrency: int | None,
14
- ) -> tuple[ExperimentConfig, int, Path]:
13
+ ) -> tuple[ExperimentConfig, int]:
15
14
  if experiment_config is None:
16
15
  folder = config.experiments.replace("{experiment}", name)
17
16
  experiment_config = config.root / folder / "experiment.toml"
@@ -21,10 +20,4 @@ async def prepare_experiment(
21
20
  experiment = read_experiment_config(
22
21
  experiment_config, source, config.mode, config.concurrency
23
22
  )
24
- runs = runs_parent(config.root, config.runs, name)
25
- runs.mkdir(parents=True, exist_ok=True)
26
- return (
27
- experiment,
28
- experiment.concurrency if concurrency is None else concurrency,
29
- runs,
30
- )
23
+ return experiment, experiment.concurrency if concurrency is None else concurrency
@@ -65,6 +65,7 @@ async def run_experiment(
65
65
  report(
66
66
  {
67
67
  "event": "planned",
68
+ "title": experiment.name or name,
68
69
  "runs": runs,
69
70
  "concurrency": concurrency,
70
71
  "mode": experiment.mode,
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import codecs
2
3
  import signal
3
4
  import time
@@ -104,13 +105,15 @@ class RunApp(App[None]):
104
105
 
105
106
  def listen(self, controls: Controls) -> None:
106
107
  self.controls = controls
107
- stop_then_kill(self.action_interrupt, signal.SIGTERM, signal.SIGHUP)
108
+ stop_then_kill(self.action_interrupt, signal.SIGTERM)
109
+ asyncio.get_running_loop().add_signal_handler(signal.SIGHUP, self.exit)
108
110
 
109
111
  def show_event(self, event: dict[str, object]) -> None:
110
112
  table = self.query_one(DataTable)
111
113
  run = str(event.get("run"))
112
114
  match event["event"]:
113
115
  case "planned":
116
+ self.title = str(event["title"])
114
117
  for planned in event["runs"]: # type: ignore[attr-defined]
115
118
  args = " ".join(param_args(planned["params"]))
116
119
  cells = (planned["run"].rpartition("/")[2], args, PENDING, "")
@@ -242,7 +245,7 @@ class RunApp(App[None]):
242
245
  if self.idle:
243
246
  self.controls.finish()
244
247
  else:
245
- self.show("Runs are still going. Ctrl-C stops them.")
248
+ self.exit()
246
249
 
247
250
  def cursor_run(self) -> str | None:
248
251
  table = self.query_one(DataTable)
@@ -31,11 +31,11 @@ reason = "quiet counts up by one from zero, so it reaches IDLE_EXIT_SECONDS befo
31
31
  [[python.exempt]]
32
32
  path = "src/labnotes/daemon/daemon_app.py"
33
33
  rules = ["mutation"]
34
- lines = [108]
34
+ lines = [117]
35
35
  reason = "start answers 202, 400, 409 or 500, so every status but 202 is above it: != equals >"
36
36
 
37
37
  [[python.exempt]]
38
38
  path = "src/labnotes/daemon/remote_start.py"
39
39
  rules = ["mutation"]
40
- lines = [38]
40
+ lines = [39]
41
41
  reason = "the daemon answers a run with 202, 400, 409 or 500, so every status but 202 is above it: != equals >"
@@ -1,27 +0,0 @@
1
- import asyncio
2
- from collections.abc import Callable
3
- from functools import partial
4
- from pathlib import Path
5
-
6
- from labnotes.models import LabnotesConfig, Start
7
- from labnotes.runner.hold_folder import hold_folder
8
- from labnotes.runner.prepare_experiment import prepare_experiment
9
- from labnotes.runner.run_experiment import run_experiment
10
-
11
-
12
- def start_experiment(
13
- config: LabnotesConfig,
14
- name: str,
15
- experiment_config: Path | None,
16
- concurrency: int | None,
17
- drive: Callable[[Start, str], list[dict[str, object]]],
18
- ) -> list[dict[str, object]]:
19
- experiment, concurrency, runs = asyncio.run(
20
- prepare_experiment(config, name, experiment_config, concurrency)
21
- )
22
- try:
23
- with hold_folder(runs):
24
- start = partial(run_experiment, config, name, experiment, concurrency)
25
- return drive(start, experiment.name or name)
26
- except BlockingIOError as error:
27
- raise ValueError(f"{name} is already running ({runs} is held).") from error
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes