labnotes 0.0.72__tar.gz → 0.0.74__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.72 → labnotes-0.0.74}/PKG-INFO +1 -1
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/context.md +4 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/daemon_app.py +19 -2
- labnotes-0.0.74/src/labnotes/daemon/describe_status.py +14 -0
- labnotes-0.0.74/src/labnotes/daemon/run_logs.py +54 -0
- labnotes-0.0.74/src/labnotes/daemon/watch_daemon.py +52 -0
- labnotes-0.0.74/src/labnotes/daemon/watch_status.py +19 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/run_command.py +4 -0
- labnotes-0.0.74/src/labnotes/run_status.py +29 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/static/experiment.js +30 -8
- {labnotes-0.0.72 → labnotes-0.0.74}/testing-conventions.toml +1 -1
- {labnotes-0.0.72 → labnotes-0.0.74}/.gitignore +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/CHANGELOG.md +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/MIGRATIONS.md +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/README.md +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/pyproject.toml +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/remote_start.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/init.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/models.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/server.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tui/run_app.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.72 → labnotes-0.0.74}/uv.lock +0 -0
|
@@ -122,6 +122,10 @@ going. Elsewhere, it prints a line per event instead.
|
|
|
122
122
|
seconds later. Once the runs are done, either one closes the live view.
|
|
123
123
|
- `labnotes run stop` stops every run the daemon has going: SIGTERM, then
|
|
124
124
|
SIGKILL 5 seconds later.
|
|
125
|
+
- `labnotes run status` prints what the daemon has going, a line per event so
|
|
126
|
+
far plus the latest line each running run logged. `-f` keeps following, and
|
|
127
|
+
waits for a daemon when none is up. `--json` prints one JSON object per line.
|
|
128
|
+
Following doesn't keep the daemon up.
|
|
125
129
|
- A second `run` of the same experiment fails while the first is going.
|
|
126
130
|
- It exits 0 even if runs fail; the summary line counts failures.
|
|
127
131
|
|
|
@@ -23,6 +23,8 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
|
|
|
23
23
|
sessions: dict[str, Controls | None] = {}
|
|
24
24
|
owners: dict[str, int] = {}
|
|
25
25
|
clients: dict[int, asyncio.Queue[Event]] = {}
|
|
26
|
+
watchers: set[int] = set()
|
|
27
|
+
history: dict[str, list[Event]] = {}
|
|
26
28
|
ids = itertools.count(1)
|
|
27
29
|
tasks: set[asyncio.Task[str | None]] = set()
|
|
28
30
|
|
|
@@ -40,11 +42,22 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
|
|
|
40
42
|
del owners[name]
|
|
41
43
|
controls.finish()
|
|
42
44
|
|
|
45
|
+
def record(event: Event) -> None:
|
|
46
|
+
history.setdefault(str(event["experiment"]), []).append(event)
|
|
47
|
+
broadcast(event)
|
|
48
|
+
|
|
43
49
|
async def events(request: Request) -> Response:
|
|
44
50
|
client = next(ids)
|
|
51
|
+
watching = request.query_params.get("watch") == "1"
|
|
45
52
|
|
|
46
53
|
async def stream() -> AsyncIterator[str]:
|
|
47
54
|
clients[client] = asyncio.Queue()
|
|
55
|
+
if watching:
|
|
56
|
+
watchers.add(client)
|
|
57
|
+
for past in history.values():
|
|
58
|
+
for old in past:
|
|
59
|
+
clients[client].put_nowait(old)
|
|
60
|
+
clients[client].put_nowait({"event": "replayed"})
|
|
48
61
|
try:
|
|
49
62
|
event: Event = {"event": "connected", "client": client}
|
|
50
63
|
while True:
|
|
@@ -52,6 +65,7 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
|
|
|
52
65
|
event = await clients[client].get()
|
|
53
66
|
finally:
|
|
54
67
|
del clients[client]
|
|
68
|
+
watchers.discard(client)
|
|
55
69
|
release(client)
|
|
56
70
|
|
|
57
71
|
return StreamingResponse(stream(), media_type="text/event-stream")
|
|
@@ -86,7 +100,7 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
|
|
|
86
100
|
name,
|
|
87
101
|
experiment,
|
|
88
102
|
concurrency,
|
|
89
|
-
lambda event:
|
|
103
|
+
lambda event: record({**event, "experiment": name}),
|
|
90
104
|
listen,
|
|
91
105
|
)
|
|
92
106
|
# Clients hear why the session ended; the daemon keeps serving.
|
|
@@ -94,6 +108,7 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
|
|
|
94
108
|
error = str(failure)
|
|
95
109
|
del sessions[name]
|
|
96
110
|
owners.pop(name, None)
|
|
111
|
+
history.pop(name, None)
|
|
97
112
|
broadcast({"event": "done", "experiment": name, "error": error})
|
|
98
113
|
return error
|
|
99
114
|
|
|
@@ -149,6 +164,8 @@ def daemon_app(config: LabnotesConfig) -> Starlette:
|
|
|
149
164
|
Route("/experiments/{name}/{action}", control, methods=["POST"]),
|
|
150
165
|
]
|
|
151
166
|
)
|
|
152
|
-
app.state.idle =
|
|
167
|
+
app.state.idle = (
|
|
168
|
+
lambda: all(client in watchers for client in clients) and not sessions
|
|
169
|
+
)
|
|
153
170
|
app.state.stop = stop_all
|
|
154
171
|
return app
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from labnotes.runner.describe_event import describe_event
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def describe_status(event: dict[str, object]) -> str:
|
|
5
|
+
match event:
|
|
6
|
+
case {"event": "output"}:
|
|
7
|
+
said = f"{event['run']}: {event['line']}"
|
|
8
|
+
case {"event": "done", "error": None}:
|
|
9
|
+
said = "Done."
|
|
10
|
+
case {"event": "done"}:
|
|
11
|
+
said = f"Stopped: {event['error']}"
|
|
12
|
+
case _:
|
|
13
|
+
said = describe_event(event)
|
|
14
|
+
return f"{event['experiment']}: {said}"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
STREAMS = ("stdout", "stderr")
|
|
4
|
+
Key = tuple[str, str]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RunLogs:
|
|
8
|
+
"""The latest line each running run wrote, read from its logs by byte offset."""
|
|
9
|
+
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.folders: dict[Key, Path] = {}
|
|
12
|
+
self.offsets: dict[tuple[Key, str], int] = {}
|
|
13
|
+
self.partial: dict[tuple[Key, str], bytes] = {}
|
|
14
|
+
|
|
15
|
+
def add(self, experiment: str, run: str, folder: Path) -> None:
|
|
16
|
+
self.folders[experiment, run] = folder
|
|
17
|
+
|
|
18
|
+
def drop(self, experiment: str, run: str) -> None:
|
|
19
|
+
self.folders.pop((experiment, run), None)
|
|
20
|
+
|
|
21
|
+
def latest(self) -> list[dict[str, object]]:
|
|
22
|
+
found: list[dict[str, object]] = []
|
|
23
|
+
for key, folder in self.folders.items():
|
|
24
|
+
for stream in STREAMS:
|
|
25
|
+
line = self.read(key, stream, folder / f"{stream}.log")
|
|
26
|
+
if line is not None:
|
|
27
|
+
experiment, run = key
|
|
28
|
+
found.append(
|
|
29
|
+
{
|
|
30
|
+
"event": "output",
|
|
31
|
+
"experiment": experiment,
|
|
32
|
+
"run": run,
|
|
33
|
+
"stream": stream,
|
|
34
|
+
"line": line,
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
return found
|
|
38
|
+
|
|
39
|
+
def read(self, key: Key, stream: str, path: Path) -> str | None:
|
|
40
|
+
try:
|
|
41
|
+
with path.open("rb") as log:
|
|
42
|
+
log.seek(self.offsets.get((key, stream), 0))
|
|
43
|
+
data = log.read()
|
|
44
|
+
except FileNotFoundError:
|
|
45
|
+
return None
|
|
46
|
+
self.offsets[key, stream] = self.offsets.get((key, stream), 0) + len(data)
|
|
47
|
+
*complete, rest = (self.partial.get((key, stream), b"") + data).split(b"\n")
|
|
48
|
+
self.partial[key, stream] = rest
|
|
49
|
+
shown = [
|
|
50
|
+
text
|
|
51
|
+
for line in complete
|
|
52
|
+
if (text := line.rsplit(b"\r")[-1].decode(errors="replace").strip())
|
|
53
|
+
]
|
|
54
|
+
return shown[-1] if shown else None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import httpx2
|
|
6
|
+
|
|
7
|
+
from labnotes.daemon.daemon_client import daemon_client
|
|
8
|
+
from labnotes.daemon.run_logs import RunLogs
|
|
9
|
+
|
|
10
|
+
Event = dict[str, object]
|
|
11
|
+
TAIL_SECONDS = 1.0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def watch_daemon(root: Path, follow: bool, emit: Callable[[Event], None]) -> None:
|
|
15
|
+
logs = RunLogs()
|
|
16
|
+
|
|
17
|
+
def tail() -> None:
|
|
18
|
+
for line in logs.latest():
|
|
19
|
+
emit(line)
|
|
20
|
+
|
|
21
|
+
async def keep_tailing() -> None:
|
|
22
|
+
while True:
|
|
23
|
+
await asyncio.sleep(TAIL_SECONDS)
|
|
24
|
+
tail()
|
|
25
|
+
|
|
26
|
+
tailing = asyncio.create_task(keep_tailing()) if follow else None
|
|
27
|
+
try:
|
|
28
|
+
async with (
|
|
29
|
+
daemon_client(root) as client,
|
|
30
|
+
client.stream("GET", "/events", params={"watch": "1"}) as response,
|
|
31
|
+
):
|
|
32
|
+
events = aiter(httpx2.EventSource(response))
|
|
33
|
+
await anext(events)
|
|
34
|
+
async for message in events:
|
|
35
|
+
event: Event = message.json() # type: ignore[assignment]
|
|
36
|
+
match event:
|
|
37
|
+
case {"event": "replayed"}:
|
|
38
|
+
tail()
|
|
39
|
+
if not follow:
|
|
40
|
+
return
|
|
41
|
+
continue
|
|
42
|
+
case {"event": "started", "experiment": str(name), "run": str(run)}:
|
|
43
|
+
logs.add(name, run, Path(str(event["folder"])))
|
|
44
|
+
case {"event": "ended", "experiment": str(name), "run": str(run)}:
|
|
45
|
+
tail()
|
|
46
|
+
logs.drop(name, run)
|
|
47
|
+
emit(event)
|
|
48
|
+
except httpx2.TransportError:
|
|
49
|
+
pass
|
|
50
|
+
finally:
|
|
51
|
+
if tailing is not None:
|
|
52
|
+
tailing.cancel()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from labnotes.daemon.daemon_running import daemon_running
|
|
6
|
+
from labnotes.daemon.watch_daemon import watch_daemon
|
|
7
|
+
|
|
8
|
+
POLL_SECONDS = 1.0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def watch_status(
|
|
12
|
+
root: Path, follow: bool, emit: Callable[[dict[str, object]], None]
|
|
13
|
+
) -> None:
|
|
14
|
+
while True:
|
|
15
|
+
if await daemon_running(root):
|
|
16
|
+
await watch_daemon(root, follow, emit)
|
|
17
|
+
if not follow:
|
|
18
|
+
return
|
|
19
|
+
await asyncio.sleep(POLL_SECONDS)
|
|
@@ -5,6 +5,7 @@ import click
|
|
|
5
5
|
from labnotes.daemon.stop_runs import stop_runs
|
|
6
6
|
from labnotes.load_config import load_config
|
|
7
7
|
from labnotes.read_config import CONFIG_FILE
|
|
8
|
+
from labnotes.run_status import run_status
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@click.group(name="run", help="Control the runs going in this project.")
|
|
@@ -24,3 +25,6 @@ def stop(ctx: click.Context) -> None:
|
|
|
24
25
|
)
|
|
25
26
|
stopping = asyncio.run(stop_runs(config.root))
|
|
26
27
|
click.echo("Stopping runs." if stopping else "Nothing is running.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
run_command.add_command(run_status)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from labnotes.daemon.describe_status import describe_status
|
|
8
|
+
from labnotes.daemon.watch_status import watch_status
|
|
9
|
+
from labnotes.load_config import load_config
|
|
10
|
+
from labnotes.read_config import CONFIG_FILE
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command(name="status", help="Show what the runs in this project are doing.")
|
|
14
|
+
@click.option("--follow", "-f", is_flag=True, help="Keep reporting until interrupted.")
|
|
15
|
+
@click.option("--json", "as_json", is_flag=True, help="One JSON event per line.")
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def run_status(ctx: click.Context, follow: bool, as_json: bool) -> None:
|
|
18
|
+
config = load_config(ctx)
|
|
19
|
+
if config is None:
|
|
20
|
+
raise click.ClickException(
|
|
21
|
+
f"No {CONFIG_FILE} here. Run labnotes init, or pass -c PATH."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def emit(event: dict[str, object]) -> None:
|
|
25
|
+
line = json.dumps(event, default=str) if as_json else describe_status(event)
|
|
26
|
+
click.echo(line)
|
|
27
|
+
|
|
28
|
+
with contextlib.suppress(KeyboardInterrupt):
|
|
29
|
+
asyncio.run(watch_status(config.root, follow, emit))
|
|
@@ -28,24 +28,26 @@ if (filters) {
|
|
|
28
28
|
let pending;
|
|
29
29
|
let latest;
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const load = async (url) => {
|
|
32
32
|
clearTimeout(pending);
|
|
33
|
-
const params = new URLSearchParams();
|
|
34
|
-
for (const [key, value] of new FormData(filters)) {
|
|
35
|
-
if (value) params.append(key, value);
|
|
36
|
-
}
|
|
37
|
-
const url = new URL(location.href);
|
|
38
|
-
url.search = params;
|
|
39
33
|
latest = url.href;
|
|
40
34
|
const response = await fetch(url);
|
|
41
35
|
if (latest !== url.href) return;
|
|
42
36
|
const fresh = new DOMParser()
|
|
43
37
|
.parseFromString(await response.text(), "text/html")
|
|
44
38
|
.querySelector("#runs");
|
|
39
|
+
const scroller = runs.querySelector(".scroll");
|
|
40
|
+
const { scrollX, scrollY } = window;
|
|
41
|
+
const { scrollLeft } = scroller;
|
|
42
|
+
const header = runs.querySelector("thead tr");
|
|
43
|
+
const focused = [...header.querySelectorAll("a.sort")].indexOf(document.activeElement);
|
|
45
44
|
runs.querySelector(".narrowed")?.remove();
|
|
46
45
|
const narrowed = fresh.querySelector(".narrowed");
|
|
47
46
|
if (narrowed) runs.prepend(narrowed);
|
|
48
|
-
|
|
47
|
+
const freshHeader = fresh.querySelector("thead tr");
|
|
48
|
+
header.replaceWith(freshHeader);
|
|
49
|
+
for (const input of filters.querySelectorAll("input[type=hidden]")) input.remove();
|
|
50
|
+
filters.prepend(...fresh.querySelectorAll("#run-filters input[type=hidden]"));
|
|
49
51
|
const table = runs.querySelector("table");
|
|
50
52
|
const opened = new Set([...table.querySelectorAll(".group.open")].map((group) => group.dataset.name));
|
|
51
53
|
for (const body of table.querySelectorAll("tbody")) body.remove();
|
|
@@ -58,8 +60,28 @@ if (filters) {
|
|
|
58
60
|
summary.textContent = summaries[index].textContent;
|
|
59
61
|
});
|
|
60
62
|
history.replaceState(null, "", url);
|
|
63
|
+
scroller.scrollLeft = scrollLeft;
|
|
64
|
+
window.scrollTo(scrollX, scrollY);
|
|
65
|
+
if (focused >= 0) freshHeader.querySelectorAll("a.sort")[focused].focus({ preventScroll: true });
|
|
61
66
|
};
|
|
62
67
|
|
|
68
|
+
const refresh = () => {
|
|
69
|
+
const params = new URLSearchParams();
|
|
70
|
+
for (const [key, value] of new FormData(filters)) {
|
|
71
|
+
if (value) params.append(key, value);
|
|
72
|
+
}
|
|
73
|
+
const url = new URL(location.href);
|
|
74
|
+
url.search = params;
|
|
75
|
+
return load(url);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
runs.addEventListener("click", (event) => {
|
|
79
|
+
const link = event.target.closest("a.sort");
|
|
80
|
+
if (!link || event.button || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
load(new URL(link.href));
|
|
83
|
+
});
|
|
84
|
+
|
|
63
85
|
filters.addEventListener("submit", (event) => {
|
|
64
86
|
event.preventDefault();
|
|
65
87
|
refresh();
|
|
@@ -31,7 +31,7 @@ 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 = [132]
|
|
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]]
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|