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.
Files changed (145) hide show
  1. {labnotes-0.0.78 → labnotes-0.0.80}/PKG-INFO +1 -1
  2. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/context.md +6 -3
  3. labnotes-0.0.80/src/labnotes/daemon/control_run.py +16 -0
  4. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/group_runs.py +6 -1
  5. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/run_command.py +2 -0
  6. labnotes-0.0.80/src/labnotes/run_control.py +46 -0
  7. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/run_experiment.py +6 -4
  8. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/labnotes.css +1 -0
  9. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/experiment.html +4 -0
  10. {labnotes-0.0.78 → labnotes-0.0.80}/testing-conventions.toml +6 -0
  11. {labnotes-0.0.78 → labnotes-0.0.80}/tests/integration/conftest.py +14 -0
  12. {labnotes-0.0.78 → labnotes-0.0.80}/.gitignore +0 -0
  13. {labnotes-0.0.78 → labnotes-0.0.80}/CHANGELOG.md +0 -0
  14. {labnotes-0.0.78 → labnotes-0.0.80}/MIGRATIONS.md +0 -0
  15. {labnotes-0.0.78 → labnotes-0.0.80}/README.md +0 -0
  16. {labnotes-0.0.78 → labnotes-0.0.80}/pyproject.toml +0 -0
  17. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/__init__.py +0 -0
  18. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/__main__.py +0 -0
  19. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/asset_url.py +0 -0
  20. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/check_counts.py +0 -0
  21. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/cli.py +0 -0
  22. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/context_guide.py +0 -0
  23. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/count_groups.py +0 -0
  24. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/count_runs.py +0 -0
  25. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/create_app.py +0 -0
  26. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/create_experiment.py +0 -0
  27. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/create_note.py +0 -0
  28. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/__init__.py +0 -0
  29. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/daemon_app.py +0 -0
  30. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/daemon_client.py +0 -0
  31. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/daemon_running.py +0 -0
  32. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/daemon_server.py +0 -0
  33. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/describe_status.py +0 -0
  34. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/ensure_daemon.py +0 -0
  35. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/exit_when_idle.py +0 -0
  36. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/remote_start.py +0 -0
  37. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/run_daemon.py +0 -0
  38. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/run_logs.py +0 -0
  39. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/serve_daemon.py +0 -0
  40. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/socket_path.py +0 -0
  41. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/stop_runs.py +0 -0
  42. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/watch_daemon.py +0 -0
  43. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon/watch_status.py +0 -0
  44. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/daemon_command.py +0 -0
  45. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/data_html.py +0 -0
  46. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/default_config_text.py +0 -0
  47. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/default_counts.py +0 -0
  48. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/drain_watch.py +0 -0
  49. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/experiment_notes.py +0 -0
  50. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/experiment_runs.py +0 -0
  51. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/experiments.py +0 -0
  52. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/file_changes.py +0 -0
  53. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/format_duration.py +0 -0
  54. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/format_started.py +0 -0
  55. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/format_value.py +0 -0
  56. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/init.py +0 -0
  57. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/invocation_started.py +0 -0
  58. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/is_positive_int.py +0 -0
  59. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/list_experiments.py +0 -0
  60. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/list_runs.py +0 -0
  61. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/load_config.py +0 -0
  62. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/load_experiment.py +0 -0
  63. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/load_run.py +0 -0
  64. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/models.py +0 -0
  65. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/modes.py +0 -0
  66. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/nests_runs.py +0 -0
  67. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/note_path.py +0 -0
  68. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/notebook_kernel.py +0 -0
  69. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/output_html.py +0 -0
  70. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/parse_run_view.py +0 -0
  71. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/print_rows.py +0 -0
  72. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/python_blocks.py +0 -0
  73. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/read_config.py +0 -0
  74. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/read_path.py +0 -0
  75. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/render_markdown.py +0 -0
  76. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/run_output.py +0 -0
  77. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/run_status.py +0 -0
  78. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/__init__.py +0 -0
  79. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/describe_event.py +0 -0
  80. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/execute_run.py +0 -0
  81. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/hold_folder.py +0 -0
  82. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/new_param_run_id.py +0 -0
  83. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/new_run_id.py +0 -0
  84. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  85. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/param_args.py +0 -0
  86. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/param_grid.py +0 -0
  87. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/param_run_name.py +0 -0
  88. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/plan_runs.py +0 -0
  89. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/prepare_experiment.py +0 -0
  90. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/read_experiment_config.py +0 -0
  91. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/run_folder.py +0 -0
  92. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/run_headless.py +0 -0
  93. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/runs_parent.py +0 -0
  94. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/shell_command.py +0 -0
  95. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/signal_tree.py +0 -0
  96. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/stop_then_kill.py +0 -0
  97. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/wait_for_tree.py +0 -0
  98. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/runner/write_run_json.py +0 -0
  99. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/save_conflict_copy.py +0 -0
  100. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/save_note.py +0 -0
  101. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/saved_outputs.py +0 -0
  102. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/serve_app.py +0 -0
  103. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/server.py +0 -0
  104. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/shared_params.py +0 -0
  105. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/sort_href.py +0 -0
  106. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/sql_quote.py +0 -0
  107. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/experiment.js +0 -0
  108. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/local-time.js +0 -0
  109. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/new-note.js +0 -0
  110. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/note-edit.js +0 -0
  111. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/note-editor.js +0 -0
  112. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/row-link.js +0 -0
  113. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/run-cell.js +0 -0
  114. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/run-output.js +0 -0
  115. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/static/view-toggle.js +0 -0
  116. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/summarize_config.py +0 -0
  117. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/__init__.py +0 -0
  118. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/experiment_rows.py +0 -0
  119. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/experiment_run_ids.py +0 -0
  120. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/file_prefix.py +0 -0
  121. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/file_text.py +0 -0
  122. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/iso_time.py +0 -0
  123. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/list_experiment_runs.py +0 -0
  124. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/note_text.py +0 -0
  125. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/open_tables.py +0 -0
  126. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/path_glob.py +0 -0
  127. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/path_pattern.py +0 -0
  128. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/read_experiment.py +0 -0
  129. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/read_file.py +0 -0
  130. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/read_log.py +0 -0
  131. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/read_run.py +0 -0
  132. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tables/summarize_experiments.py +0 -0
  133. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/base.html +0 -0
  134. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/editor_imports.html +0 -0
  135. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/index.html +0 -0
  136. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/note.html +0 -0
  137. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/templates/run.html +0 -0
  138. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/__init__.py +0 -0
  139. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/dim.py +0 -0
  140. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/format_elapsed.py +0 -0
  141. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/run_app.py +0 -0
  142. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/run_tui.py +0 -0
  143. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/tui/runs_table.py +0 -0
  144. {labnotes-0.0.78 → labnotes-0.0.80}/src/labnotes/wait_for_note.py +0 -0
  145. {labnotes-0.0.78 → labnotes-0.0.80}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.78
3
+ Version: 0.0.80
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>
@@ -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 each `run` command gets one folder named
67
- that way, and its runs nest inside it (`runs/20261020-000429-brave-fox/quiet-river`); the run id
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
- groups.setdefault(str(run["id"]).partition("/")[0], []).append(run)
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
- group = new_stamped_run_id(taken) if experiment.mode == "adhoc" else None
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 plan_runs(
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