labnotes 0.0.80__tar.gz → 0.0.82__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.80 → labnotes-0.0.82}/PKG-INFO +3 -2
- {labnotes-0.0.80 → labnotes-0.0.82}/README.md +2 -1
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/context.md +2 -1
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/remote_start.py +11 -1
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/group_runs.py +6 -1
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/list_runs.py +4 -1
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/load_experiment.py +2 -0
- labnotes-0.0.82/src/labnotes/static/elapsed.js +18 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/static/labnotes.css +2 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/templates/experiment.html +4 -3
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tui/run_app.py +5 -1
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tui/run_tui.py +2 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/testing-conventions.toml +1 -1
- {labnotes-0.0.80 → labnotes-0.0.82}/tests/integration/conftest.py +20 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/.gitignore +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/CHANGELOG.md +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/MIGRATIONS.md +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/pyproject.toml +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/daemon_app.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/init.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/models.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/server.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.80 → labnotes-0.0.82}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labnotes
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.82
|
|
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>
|
|
@@ -113,7 +113,8 @@ line per event instead.
|
|
|
113
113
|
kills them. Closing the terminal or sending SIGTERM stops them the same way,
|
|
114
114
|
then kills any still going 5 seconds later.
|
|
115
115
|
- A second `run` of the same experiment fails while the first holds the lock.
|
|
116
|
-
- It exits 0 even if runs fail; the summary line counts failures.
|
|
116
|
+
- It exits 0 even if runs fail; the summary line counts failures. If it can't
|
|
117
|
+
start or reach the daemon, it prints the cause on one line and exits 1.
|
|
117
118
|
|
|
118
119
|
`labnotes server` serves a local site at http://127.0.0.1:8000 from the same
|
|
119
120
|
`.labnotes.toml`: a table of experiments, and a page per experiment with its
|
|
@@ -89,7 +89,8 @@ line per event instead.
|
|
|
89
89
|
kills them. Closing the terminal or sending SIGTERM stops them the same way,
|
|
90
90
|
then kills any still going 5 seconds later.
|
|
91
91
|
- A second `run` of the same experiment fails while the first holds the lock.
|
|
92
|
-
- It exits 0 even if runs fail; the summary line counts failures.
|
|
92
|
+
- It exits 0 even if runs fail; the summary line counts failures. If it can't
|
|
93
|
+
start or reach the daemon, it prints the cause on one line and exits 1.
|
|
93
94
|
|
|
94
95
|
`labnotes server` serves a local site at http://127.0.0.1:8000 from the same
|
|
95
96
|
`.labnotes.toml`: a table of experiments, and a page per experiment with its
|
|
@@ -130,6 +130,7 @@ going. Elsewhere, it prints a line per event instead.
|
|
|
130
130
|
running experiment: `kill`, `retry`, `run_next` or `toggle` a run,
|
|
131
131
|
`set_concurrency` to a number, `interrupt` or `finish`.
|
|
132
132
|
- A second `run` of the same experiment fails while the first is going.
|
|
133
|
-
- It exits 0 even if runs fail; the summary line counts failures.
|
|
133
|
+
- It exits 0 even if runs fail; the summary line counts failures. If it can't
|
|
134
|
+
start or reach the daemon, it prints the cause on one line and exits 1.
|
|
134
135
|
|
|
135
136
|
`uvx labnotes server` serves the experiments, their markdown files and their runs as a local site, read from the same `.labnotes.toml`. An experiment's page shows its `experiment.toml` on one line, then a Runs tab with a table of its runs and a Notebooks tab with one tab per markdown file. Clicking a run opens its `stdout.log` and `stderr.log` together, followed as they're written until the run ends.
|
|
@@ -17,6 +17,13 @@ def remote_start(
|
|
|
17
17
|
concurrency: int | None,
|
|
18
18
|
) -> Start:
|
|
19
19
|
async def start(report: Report, listen: Listen) -> list[dict[str, object]]:
|
|
20
|
+
try:
|
|
21
|
+
return await follow(report, listen)
|
|
22
|
+
except (OSError, httpx2.TransportError) as error:
|
|
23
|
+
cause = str(error) or type(error).__name__
|
|
24
|
+
raise ValueError(f"Can't reach the labnotes daemon: {cause}") from error
|
|
25
|
+
|
|
26
|
+
async def follow(report: Report, listen: Listen) -> list[dict[str, object]]:
|
|
20
27
|
await ensure_daemon(config)
|
|
21
28
|
sending: set[asyncio.Task[httpx2.Response]] = set()
|
|
22
29
|
results: list[dict[str, object]] = []
|
|
@@ -25,7 +32,10 @@ def remote_start(
|
|
|
25
32
|
client.stream("GET", "/events") as response,
|
|
26
33
|
):
|
|
27
34
|
events = aiter(httpx2.EventSource(response))
|
|
28
|
-
|
|
35
|
+
first = await anext(events, None)
|
|
36
|
+
if first is None:
|
|
37
|
+
raise ValueError("Lost the labnotes daemon.")
|
|
38
|
+
connected: dict[str, object] = first.json() # type: ignore[assignment]
|
|
29
39
|
reply = await client.post(
|
|
30
40
|
f"/experiments/{name}/run",
|
|
31
41
|
json={
|
|
@@ -25,7 +25,12 @@ def group_runs(
|
|
|
25
25
|
"total": totals.get(name, len(members)),
|
|
26
26
|
"shared": shared_params(members, params),
|
|
27
27
|
"passed": sum(1 for run in members if run["labnotes_completed"]),
|
|
28
|
-
"failed": sum(
|
|
28
|
+
"failed": sum(
|
|
29
|
+
1
|
|
30
|
+
for run in members
|
|
31
|
+
if not run["labnotes_completed"] and not run["labnotes_unfinished"]
|
|
32
|
+
),
|
|
33
|
+
"unfinished": sum(1 for run in members if run["labnotes_unfinished"]),
|
|
29
34
|
}
|
|
30
35
|
for name, members in sorted(groups.items(), reverse=True)
|
|
31
36
|
]
|
|
@@ -3,6 +3,8 @@ from dirsql import DirSQL
|
|
|
3
3
|
from labnotes.models import RunView
|
|
4
4
|
from labnotes.sql_quote import quote_identifier, quote_string
|
|
5
5
|
|
|
6
|
+
UNFINISHED = "(ended IS NULL AND exit_code IS NULL AND error IS NULL)"
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
async def list_runs(
|
|
8
10
|
runs: DirSQL, source: str, completed_run: str, view: RunView
|
|
@@ -30,6 +32,7 @@ async def list_runs(
|
|
|
30
32
|
else:
|
|
31
33
|
order = "created DESC, id DESC"
|
|
32
34
|
return await runs.query(
|
|
33
|
-
f"SELECT ({completed_run}) AS labnotes_completed,
|
|
35
|
+
f"SELECT ({completed_run}) AS labnotes_completed, {UNFINISHED} AS labnotes_unfinished,"
|
|
36
|
+
f" * FROM {source}"
|
|
34
37
|
f"{filtered} ORDER BY {order}"
|
|
35
38
|
)
|
|
@@ -8,6 +8,7 @@ from dirsql import DirSQL
|
|
|
8
8
|
from labnotes.check_counts import check_counts
|
|
9
9
|
from labnotes.count_groups import count_groups
|
|
10
10
|
from labnotes.count_runs import count_runs
|
|
11
|
+
from labnotes.daemon.daemon_running import daemon_running
|
|
11
12
|
from labnotes.experiment_notes import experiment_notes
|
|
12
13
|
from labnotes.experiment_runs import experiment_runs
|
|
13
14
|
from labnotes.format_value import format_value
|
|
@@ -82,6 +83,7 @@ async def load_experiment(
|
|
|
82
83
|
for note in notes
|
|
83
84
|
],
|
|
84
85
|
"runs_list": runs,
|
|
86
|
+
"runs_live": await daemon_running(root),
|
|
85
87
|
"run_groups": groups,
|
|
86
88
|
"run_columns": shown,
|
|
87
89
|
"run_view": view,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const format = (seconds) => {
|
|
2
|
+
const minutes = Math.floor(seconds / 60);
|
|
3
|
+
const hours = Math.floor(minutes / 60);
|
|
4
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
5
|
+
if (hours) return `${hours}h ${pad(minutes % 60)}m`;
|
|
6
|
+
if (minutes) return `${minutes}m ${pad(seconds % 60)}s`;
|
|
7
|
+
return `${seconds}s`;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const tick = () => {
|
|
11
|
+
for (const cell of document.querySelectorAll(".elapsed[data-since]")) {
|
|
12
|
+
const seconds = Math.max(0, Math.floor((Date.now() - Date.parse(cell.dataset.since)) / 1000));
|
|
13
|
+
cell.textContent = format(seconds);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
tick();
|
|
18
|
+
setInterval(tick, 1000);
|
|
@@ -359,6 +359,8 @@ h3 {
|
|
|
359
359
|
.status { font-size: 0.8rem; font-weight: 700; }
|
|
360
360
|
.status.good { color: var(--good); }
|
|
361
361
|
.status.bad { color: var(--bad); }
|
|
362
|
+
.status.interrupted, .group-row .interrupted { color: var(--ink-3); }
|
|
363
|
+
.status.running, .group-row .running { color: var(--accent); }
|
|
362
364
|
.counts-error { color: var(--bad); }
|
|
363
365
|
|
|
364
366
|
/* Index view toggle */
|
|
@@ -82,8 +82,8 @@
|
|
|
82
82
|
</thead>
|
|
83
83
|
{% macro run_rows(run) %}
|
|
84
84
|
<tr class="run-row">
|
|
85
|
-
<td
|
|
86
|
-
{% for column in e.run_columns %}{% if column == 'id' %}<td class="mono"><a class="run-link" href="{{ url_for('run', name=e.name, run=run.id) }}">{{ run.id.partition('/')[2] or run.id if nested else run.id }}</a></td>{% if nested %}<td></td>{% endif %}{% else %}<td class="{{ 'mono' if loop.first }}{{ 'num' if column == 'duration' }}">{{ run[column] | duration if column == 'duration' else (run[column] | started if column == 'started' else run[column] | value) }}</td>{% endif %}{% endfor %}
|
|
85
|
+
<td>{% if run.labnotes_unfinished and e.runs_live %}<span class="status running" title="running">◐</span>{% elif run.labnotes_unfinished %}<span class="status interrupted" title="interrupted">–</span>{% else %}<span class="status {{ 'good' if run.labnotes_completed else 'bad' }}" title="{{ 'complete' if run.labnotes_completed else 'not complete' }}">{{ '✓' if run.labnotes_completed else '✕' }}</span>{% endif %}</td>
|
|
86
|
+
{% for column in e.run_columns %}{% if column == 'id' %}<td class="mono"><a class="run-link" href="{{ url_for('run', name=e.name, run=run.id) }}">{{ run.id.partition('/')[2] or run.id if nested else run.id }}</a></td>{% if nested %}<td></td>{% endif %}{% else %}<td class="{{ 'mono' if loop.first }}{{ 'num' if column == 'duration' }}">{% if column == 'duration' and run.labnotes_unfinished and e.runs_live %}<span class="elapsed" data-since="{{ run.created }}">—</span>{% else %}{{ run[column] | duration if column == 'duration' else (run[column] | started if column == 'started' else run[column] | value) }}{% endif %}</td>{% endif %}{% endfor %}
|
|
87
87
|
</tr>
|
|
88
88
|
{% endmacro %}
|
|
89
89
|
{% if nested %}
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
{% else %}
|
|
94
94
|
<tbody class="group{{ ' open' if narrowed }}" data-name="{{ group.name }}">
|
|
95
95
|
<tr class="group-row">
|
|
96
|
-
<td class="num" title="{{ group.passed }} passed, {{ group.failed }} failed"><span class="good">{{ group.passed }}✓</span> <span class="bad">{{ group.failed }}✕</span
|
|
96
|
+
<td class="num" title="{{ group.passed }} passed, {{ group.failed }} failed{% if group.unfinished %}, {{ group.unfinished }} {{ 'running' if e.runs_live else 'interrupted' }}{% endif %}"><span class="good">{{ group.passed }}✓</span> <span class="bad">{{ group.failed }}✕</span>{% if group.unfinished %} <span class="{{ 'running' if e.runs_live else 'interrupted' }}">{{ group.unfinished }}{{ '◐' if e.runs_live else '–' }}</span>{% endif %}</td>
|
|
97
97
|
<td class="mono"><button type="button" class="group-toggle" aria-expanded="{{ 'true' if narrowed else 'false' }}">{{ group.name }}</button></td>
|
|
98
98
|
<td class="num">{{ group.runs | length }}{% if narrowed %} of {{ group.total }}{% endif %}</td>
|
|
99
99
|
{% for column in e.run_columns[1:] %}<td>{% if column == 'started' %}{{ group.started | started if group.started }}{% elif column in group.shared %}{{ group.shared[column] | value }}{% endif %}</td>{% endfor %}
|
|
@@ -122,4 +122,5 @@
|
|
|
122
122
|
<script src="{{ asset('experiment.js') }}" defer></script>
|
|
123
123
|
<script src="{{ asset('local-time.js') }}" defer></script>
|
|
124
124
|
<script src="{{ asset('row-link.js') }}" defer></script>
|
|
125
|
+
<script src="{{ asset('elapsed.js') }}" defer></script>
|
|
125
126
|
{% endblock %}
|
|
@@ -47,6 +47,7 @@ class RunApp(App[None]):
|
|
|
47
47
|
self.start = start
|
|
48
48
|
self.title = title
|
|
49
49
|
self.results: list[dict[str, object]] = []
|
|
50
|
+
self.error: Exception | None = None
|
|
50
51
|
self.controls = Controls(
|
|
51
52
|
lambda: None,
|
|
52
53
|
lambda run: None,
|
|
@@ -100,7 +101,10 @@ class RunApp(App[None]):
|
|
|
100
101
|
self.run_worker(self.drive())
|
|
101
102
|
|
|
102
103
|
async def drive(self) -> None:
|
|
103
|
-
|
|
104
|
+
try:
|
|
105
|
+
self.results = await self.start(self.show_event, self.listen)
|
|
106
|
+
except Exception as error:
|
|
107
|
+
self.error = error
|
|
104
108
|
self.exit()
|
|
105
109
|
|
|
106
110
|
def listen(self, controls: Controls) -> None:
|
|
@@ -37,7 +37,7 @@ reason = "start answers 202, 400, 409 or 500, so every status but 202 is above i
|
|
|
37
37
|
[[python.exempt]]
|
|
38
38
|
path = "src/labnotes/daemon/remote_start.py"
|
|
39
39
|
rules = ["mutation"]
|
|
40
|
-
lines = [
|
|
40
|
+
lines = [49]
|
|
41
41
|
reason = "the daemon answers a run with 202, 400, 409 or 500, so every status but 202 is above it: != equals >"
|
|
42
42
|
|
|
43
43
|
[[python.exempt]]
|
|
@@ -10,6 +10,8 @@ from pathlib import Path
|
|
|
10
10
|
import pytest
|
|
11
11
|
from playwright.async_api import Page, async_playwright
|
|
12
12
|
|
|
13
|
+
from labnotes.daemon.socket_path import socket_path
|
|
14
|
+
|
|
13
15
|
CONFIG = (
|
|
14
16
|
'name = "Alpha study"\ncommand = "go"\nrepeats = 2\nmode = "fill"\n'
|
|
15
17
|
'[params]\nsource-language = ["en", "fr"]\n'
|
|
@@ -43,6 +45,24 @@ def free_port() -> int:
|
|
|
43
45
|
return int(sock.getsockname()[1])
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
def daemon(root: Path) -> Iterator[None]:
|
|
49
|
+
labnotes = Path(sys.executable).parent / "labnotes"
|
|
50
|
+
process = subprocess.Popen(
|
|
51
|
+
[labnotes, "-c", root / ".labnotes.toml", "daemon"],
|
|
52
|
+
stdout=subprocess.DEVNULL,
|
|
53
|
+
stderr=subprocess.DEVNULL,
|
|
54
|
+
)
|
|
55
|
+
try:
|
|
56
|
+
for _ in range(100):
|
|
57
|
+
if socket_path(root).exists():
|
|
58
|
+
break
|
|
59
|
+
time.sleep(0.1)
|
|
60
|
+
yield
|
|
61
|
+
finally:
|
|
62
|
+
process.terminate()
|
|
63
|
+
process.wait()
|
|
64
|
+
|
|
65
|
+
|
|
46
66
|
def serve(root: Path) -> Iterator[str]:
|
|
47
67
|
port = free_port()
|
|
48
68
|
labnotes = Path(sys.executable).parent / "labnotes"
|
|
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
|
|
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
|