labnotes 0.0.70__tar.gz → 0.0.72__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.
- {labnotes-0.0.70 → labnotes-0.0.72}/PKG-INFO +1 -1
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/context.md +9 -11
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/daemon_app.py +37 -28
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/remote_start.py +2 -1
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/experiments.py +3 -7
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/prepare_experiment.py +2 -9
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/run_experiment.py +1 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/static/labnotes.css +1 -1
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tui/run_app.py +5 -2
- {labnotes-0.0.70 → labnotes-0.0.72}/testing-conventions.toml +2 -2
- labnotes-0.0.70/src/labnotes/runner/start_experiment.py +0 -27
- {labnotes-0.0.70 → labnotes-0.0.72}/.gitignore +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/CHANGELOG.md +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/MIGRATIONS.md +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/README.md +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/pyproject.toml +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/init.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/models.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/server.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.70 → labnotes-0.0.72}/uv.lock +0 -0
|
@@ -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
|
|
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.
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
client = next(ids)
|
|
37
45
|
|
|
38
46
|
async def stream() -> AsyncIterator[str]:
|
|
39
|
-
clients.
|
|
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
|
|
52
|
+
event = await clients[client].get()
|
|
45
53
|
finally:
|
|
46
|
-
clients
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -287,7 +287,7 @@ h3 {
|
|
|
287
287
|
.run-row td { max-width: 24rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
288
288
|
.run-row:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
289
289
|
.group-row { cursor: pointer; background: var(--paper); }
|
|
290
|
-
.group-row td {
|
|
290
|
+
.group-row td { white-space: nowrap; }
|
|
291
291
|
.group-row .good { color: var(--good); }
|
|
292
292
|
.group-row .bad { color: var(--bad); }
|
|
293
293
|
.group-toggle { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
|
|
@@ -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
|
|
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.
|
|
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 = [
|
|
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 = [
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|