labnotes 0.0.78__tar.gz → 0.0.80__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.78 → labnotes-0.0.80}/PKG-INFO +1 -1
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/context.md +6 -3
- labnotes-0.0.80/src/labnotes/daemon/control_run.py +16 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/group_runs.py +6 -1
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/run_command.py +2 -0
- labnotes-0.0.80/src/labnotes/run_control.py +46 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/run_experiment.py +6 -4
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/labnotes.css +1 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/experiment.html +4 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/testing-conventions.toml +6 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/tests/integration/conftest.py +14 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/.gitignore +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/CHANGELOG.md +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/MIGRATIONS.md +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/README.md +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/pyproject.toml +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/daemon_app.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/remote_start.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/init.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/models.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/run_output.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/server.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/run-output.js +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/run.html +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/run_app.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.78 → labnotes-0.0.80}/uv.lock +0 -0
|
@@ -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
|
|
@@ -126,6 +126,9 @@ going. Elsewhere, it prints a line per event instead.
|
|
|
126
126
|
far plus the latest line each running run logged. `-f` keeps following, and
|
|
127
127
|
waits for a daemon when none is up. `--json` prints one JSON object per line.
|
|
128
128
|
Following doesn't keep the daemon up.
|
|
129
|
+
- `labnotes run control NAME ACTION [ARG]` sends one live-view control to a
|
|
130
|
+
running experiment: `kill`, `retry`, `run_next` or `toggle` a run,
|
|
131
|
+
`set_concurrency` to a number, `interrupt` or `finish`.
|
|
129
132
|
- A second `run` of the same experiment fails while the first is going.
|
|
130
133
|
- It exits 0 even if runs fail; the summary line counts failures.
|
|
131
134
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from labnotes.daemon.daemon_client import daemon_client
|
|
4
|
+
from labnotes.daemon.daemon_running import daemon_running
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def control_run(
|
|
8
|
+
root: Path, name: str, action: str, args: list[object]
|
|
9
|
+
) -> str | None:
|
|
10
|
+
if not await daemon_running(root):
|
|
11
|
+
return "Nothing is running."
|
|
12
|
+
async with daemon_client(root) as client:
|
|
13
|
+
reply = await client.post(f"/experiments/{name}/{action}", json={"args": args})
|
|
14
|
+
if reply.is_success:
|
|
15
|
+
return None
|
|
16
|
+
return str(reply.json()["error"])
|
|
@@ -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)),
|
|
@@ -6,6 +6,7 @@ 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
8
|
from labnotes.run_status import run_status
|
|
9
|
+
from labnotes.run_control import run_control
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@click.group(name="run", help="Control the runs going in this project.")
|
|
@@ -28,3 +29,4 @@ def stop(ctx: click.Context) -> None:
|
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
run_command.add_command(run_status)
|
|
32
|
+
run_command.add_command(run_control)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from labnotes.daemon.control_run import control_run
|
|
7
|
+
from labnotes.load_config import load_config
|
|
8
|
+
from labnotes.models import Controls
|
|
9
|
+
from labnotes.read_config import CONFIG_FILE
|
|
10
|
+
|
|
11
|
+
PARAMS = {
|
|
12
|
+
action: typing.get_args(hint)[0]
|
|
13
|
+
for action, hint in typing.get_type_hints(Controls).items()
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command(
|
|
18
|
+
name="control",
|
|
19
|
+
help=(
|
|
20
|
+
"Send one control to an experiment's runs, as the live view's keys do. "
|
|
21
|
+
"kill, retry, run_next and toggle take a run; set_concurrency a number; "
|
|
22
|
+
"interrupt and finish nothing."
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
@click.argument("name")
|
|
26
|
+
@click.argument("action", type=click.Choice(list(PARAMS)))
|
|
27
|
+
@click.argument("args", nargs=-1)
|
|
28
|
+
@click.pass_context
|
|
29
|
+
def run_control(
|
|
30
|
+
ctx: click.Context, name: str, action: str, args: tuple[str, ...]
|
|
31
|
+
) -> None:
|
|
32
|
+
config = load_config(ctx)
|
|
33
|
+
if config is None:
|
|
34
|
+
raise click.ClickException(
|
|
35
|
+
f"No {CONFIG_FILE} here. Run labnotes init, or pass -c PATH."
|
|
36
|
+
)
|
|
37
|
+
kinds = PARAMS[action]
|
|
38
|
+
if len(args) != len(kinds):
|
|
39
|
+
raise click.UsageError(f"{action} takes {len(kinds)} argument(s).")
|
|
40
|
+
try:
|
|
41
|
+
values = [kind(args[i]) for i, kind in enumerate(kinds)]
|
|
42
|
+
except ValueError:
|
|
43
|
+
raise click.UsageError(f"{action} takes a number.") from None
|
|
44
|
+
refused = asyncio.run(control_run(config.root, name, action, values))
|
|
45
|
+
if refused is not None:
|
|
46
|
+
raise click.ClickException(refused)
|
|
@@ -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 %}
|
|
@@ -39,3 +39,9 @@ path = "src/labnotes/daemon/remote_start.py"
|
|
|
39
39
|
rules = ["mutation"]
|
|
40
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 >"
|
|
42
|
+
|
|
43
|
+
[[python.exempt]]
|
|
44
|
+
path = "src/labnotes/run_control.py"
|
|
45
|
+
rules = ["mutation"]
|
|
46
|
+
lines = [38]
|
|
47
|
+
reason = "argument counts are small ints, which CPython caches, so is not equals !="
|
|
@@ -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
|
|
File without changes
|