labnotes 0.0.79__tar.gz → 0.0.81__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.79 → labnotes-0.0.81}/PKG-INFO +3 -2
- {labnotes-0.0.79 → labnotes-0.0.81}/README.md +2 -1
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/context.md +5 -4
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/remote_start.py +11 -1
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/group_runs.py +6 -1
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/run_experiment.py +6 -4
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/static/labnotes.css +1 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/templates/experiment.html +4 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tui/run_app.py +5 -1
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tui/run_tui.py +2 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/testing-conventions.toml +1 -1
- {labnotes-0.0.79 → labnotes-0.0.81}/tests/integration/conftest.py +14 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/.gitignore +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/CHANGELOG.md +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/MIGRATIONS.md +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/pyproject.toml +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/daemon_app.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/init.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/models.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/server.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.79 → labnotes-0.0.81}/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.81
|
|
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
|
|
@@ -63,9 +63,9 @@ so running again fills in what failed or never ran. `mode` under
|
|
|
63
63
|
`[experiments]` in `.labnotes.toml` sets it for every experiment that doesn't
|
|
64
64
|
set its own.
|
|
65
65
|
Each run gets a new run folder. In `fill` mode it is named for the UTC time it was planned
|
|
66
|
-
(`runs/20261020-000429-brave-fox`). In `adhoc` mode
|
|
67
|
-
that way, and its runs nest
|
|
68
|
-
is that path under `runs/`. Either way, folder listings sort by start. `command` runs in a shell from the root, with
|
|
66
|
+
(`runs/20261020-000429-brave-fox`). In `adhoc` mode a `run` command that plans one run names
|
|
67
|
+
its folder the same way. One that plans more gets one folder named that way, and its runs nest
|
|
68
|
+
inside it (`runs/20261020-000429-brave-fox/quiet-river`); the run id is that path under `runs/`. Either way, folder listings sort by start. `command` runs in a shell from the root, with
|
|
69
69
|
`{run_dir}` replaced by the run folder's absolute path. Pass it to the
|
|
70
70
|
script so it writes its outputs there.
|
|
71
71
|
Each param is appended as `--name value`, with the name as written (use
|
|
@@ -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={
|
|
@@ -10,11 +10,16 @@ def group_runs(
|
|
|
10
10
|
totals: Mapping[str, int],
|
|
11
11
|
) -> list[dict[str, object]]:
|
|
12
12
|
groups: dict[str, list[dict[str, object]]] = {}
|
|
13
|
+
flat: set[str] = set()
|
|
13
14
|
for run in runs:
|
|
14
|
-
|
|
15
|
+
name, nested, _ = str(run["id"]).partition("/")
|
|
16
|
+
groups.setdefault(name, []).append(run)
|
|
17
|
+
if not nested:
|
|
18
|
+
flat.add(name)
|
|
15
19
|
return [
|
|
16
20
|
{
|
|
17
21
|
"name": name,
|
|
22
|
+
"flat": name in flat,
|
|
18
23
|
"started": invocation_started(name),
|
|
19
24
|
"runs": members,
|
|
20
25
|
"total": totals.get(name, len(members)),
|
|
@@ -45,7 +45,11 @@ async def run_experiment(
|
|
|
45
45
|
def taken(run: str) -> bool:
|
|
46
46
|
return run in planned or run in existing
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
cells = plan_runs(param_grid(experiment.params), experiment.repeats, finished)
|
|
49
|
+
group = None
|
|
50
|
+
match experiment.mode:
|
|
51
|
+
case "adhoc" if len(cells) > 1:
|
|
52
|
+
group = new_stamped_run_id(taken)
|
|
49
53
|
|
|
50
54
|
def new_id(params: dict[str, object]) -> str:
|
|
51
55
|
if group is None:
|
|
@@ -55,9 +59,7 @@ async def run_experiment(
|
|
|
55
59
|
)
|
|
56
60
|
|
|
57
61
|
plan: list[Entry] = []
|
|
58
|
-
for params in
|
|
59
|
-
param_grid(experiment.params), experiment.repeats, finished
|
|
60
|
-
):
|
|
62
|
+
for params in cells:
|
|
61
63
|
run = new_id(params)
|
|
62
64
|
planned.add(run)
|
|
63
65
|
plan.append((run, params))
|
|
@@ -295,6 +295,7 @@ h3 {
|
|
|
295
295
|
.group-toggle[aria-expanded="true"]::before { content: "▾"; }
|
|
296
296
|
.group:not(.open) > :not(.group-row) { display: none; }
|
|
297
297
|
.group.open .run-row td:nth-child(2) { padding-left: 2rem; }
|
|
298
|
+
.lone .run-row td:nth-child(2) { padding-left: 1.75rem; }
|
|
298
299
|
.run-row { cursor: pointer; }
|
|
299
300
|
.run-link { text-decoration: none; }
|
|
300
301
|
|
|
@@ -88,6 +88,9 @@
|
|
|
88
88
|
{% endmacro %}
|
|
89
89
|
{% if nested %}
|
|
90
90
|
{% for group in e.run_groups %}
|
|
91
|
+
{% if group.flat %}
|
|
92
|
+
<tbody class="lone">{{ run_rows(group.runs[0]) }}</tbody>
|
|
93
|
+
{% else %}
|
|
91
94
|
<tbody class="group{{ ' open' if narrowed }}" data-name="{{ group.name }}">
|
|
92
95
|
<tr class="group-row">
|
|
93
96
|
<td class="num" title="{{ group.passed }} passed, {{ group.failed }} failed"><span class="good">{{ group.passed }}✓</span> <span class="bad">{{ group.failed }}✕</span></td>
|
|
@@ -97,6 +100,7 @@
|
|
|
97
100
|
</tr>
|
|
98
101
|
{% for run in group.runs %}{{ run_rows(run) }}{% endfor %}
|
|
99
102
|
</tbody>
|
|
103
|
+
{% endif %}
|
|
100
104
|
{% else %}
|
|
101
105
|
<tbody><tr><td class="empty" colspan="{{ width }}">No runs match these filters.</td></tr></tbody>
|
|
102
106
|
{% endfor %}
|
|
@@ -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]]
|
|
@@ -83,6 +83,20 @@ def browse(
|
|
|
83
83
|
asyncio.run(drive())
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
@pytest.fixture
|
|
87
|
+
def tmp_path(tmp_path: Path) -> Path:
|
|
88
|
+
# Real project roots run long; pytest's short default hides path-length
|
|
89
|
+
# limits such as the 107-byte AF_UNIX socket path.
|
|
90
|
+
deep = (
|
|
91
|
+
tmp_path
|
|
92
|
+
/ "a-fairly-long-folder-name-for-research"
|
|
93
|
+
/ "and-another-nested-one"
|
|
94
|
+
/ "experiments-and-their-notes-live-here"
|
|
95
|
+
)
|
|
96
|
+
deep.mkdir(parents=True)
|
|
97
|
+
return deep
|
|
98
|
+
|
|
99
|
+
|
|
86
100
|
@pytest.fixture
|
|
87
101
|
def site(tmp_path: Path) -> Iterator[str]:
|
|
88
102
|
yield from serve(project(tmp_path))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|