labnotes 0.0.47__tar.gz → 0.0.48__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 (98) hide show
  1. {labnotes-0.0.47 → labnotes-0.0.48}/PKG-INFO +1 -1
  2. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/context.md +5 -3
  3. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/execute_run.py +0 -2
  4. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/run_experiment.py +10 -5
  5. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/path_glob.py +1 -1
  6. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/path_pattern.py +3 -0
  7. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tui/run_app.py +4 -2
  8. {labnotes-0.0.47 → labnotes-0.0.48}/testing-conventions.toml +7 -1
  9. labnotes-0.0.48/tests/integration/conftest.py +83 -0
  10. labnotes-0.0.47/src/labnotes/runner/new_invocation_id.py +0 -6
  11. {labnotes-0.0.47 → labnotes-0.0.48}/.gitignore +0 -0
  12. {labnotes-0.0.47 → labnotes-0.0.48}/CHANGELOG.md +0 -0
  13. {labnotes-0.0.47 → labnotes-0.0.48}/MIGRATIONS.md +0 -0
  14. {labnotes-0.0.47 → labnotes-0.0.48}/README.md +0 -0
  15. {labnotes-0.0.47 → labnotes-0.0.48}/pyproject.toml +0 -0
  16. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/__init__.py +0 -0
  17. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/asset_url.py +0 -0
  18. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/cell_progress.py +0 -0
  19. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/check_counts.py +0 -0
  20. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/cli.py +0 -0
  21. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/context_guide.py +0 -0
  22. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/count_runs.py +0 -0
  23. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/create_app.py +0 -0
  24. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/create_experiment.py +0 -0
  25. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/data_html.py +0 -0
  26. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/default_config_text.py +0 -0
  27. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/default_counts.py +0 -0
  28. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/drain_watch.py +0 -0
  29. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/experiment_notes.py +0 -0
  30. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/experiment_progress.py +0 -0
  31. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/experiment_runs.py +0 -0
  32. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/experiments.py +0 -0
  33. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/format_value.py +0 -0
  34. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/init.py +0 -0
  35. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/is_positive_int.py +0 -0
  36. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/list_experiments.py +0 -0
  37. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/list_runs.py +0 -0
  38. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/load_config.py +0 -0
  39. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/load_experiment.py +0 -0
  40. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/models.py +0 -0
  41. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/modes.py +0 -0
  42. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/notebook_kernel.py +0 -0
  43. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/output_html.py +0 -0
  44. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/parse_run_view.py +0 -0
  45. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/print_rows.py +0 -0
  46. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/python_blocks.py +0 -0
  47. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/read_config.py +0 -0
  48. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/read_path.py +0 -0
  49. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/render_markdown.py +0 -0
  50. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/__init__.py +0 -0
  51. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/describe_event.py +0 -0
  52. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/hold_folder.py +0 -0
  53. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/new_run_id.py +0 -0
  54. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  55. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/param_args.py +0 -0
  56. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/param_grid.py +0 -0
  57. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/plan_runs.py +0 -0
  58. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/read_experiment_config.py +0 -0
  59. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/run_folder.py +0 -0
  60. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/run_headless.py +0 -0
  61. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/runs_parent.py +0 -0
  62. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/shell_command.py +0 -0
  63. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/start_experiment.py +0 -0
  64. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/stop_then_kill.py +0 -0
  65. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/wait_for_group.py +0 -0
  66. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/runner/write_run_json.py +0 -0
  67. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/saved_outputs.py +0 -0
  68. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/serve_app.py +0 -0
  69. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/server.py +0 -0
  70. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/sort_href.py +0 -0
  71. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/sql_quote.py +0 -0
  72. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/static/experiment.js +0 -0
  73. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/static/labnotes.css +0 -0
  74. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/static/row-link.js +0 -0
  75. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/static/run-cell.js +0 -0
  76. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/static/view-toggle.js +0 -0
  77. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/stop_idle_kernels.py +0 -0
  78. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/summarize_config.py +0 -0
  79. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/__init__.py +0 -0
  80. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/experiment_rows.py +0 -0
  81. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/file_prefix.py +0 -0
  82. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/iso_time.py +0 -0
  83. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/list_experiment_runs.py +0 -0
  84. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/open_tables.py +0 -0
  85. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/read_experiment.py +0 -0
  86. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/read_file.py +0 -0
  87. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/read_run.py +0 -0
  88. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tables/summarize_experiments.py +0 -0
  89. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/templates/base.html +0 -0
  90. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/templates/experiment.html +0 -0
  91. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/templates/index.html +0 -0
  92. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/templates/macros.html +0 -0
  93. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tui/__init__.py +0 -0
  94. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tui/dim.py +0 -0
  95. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tui/format_elapsed.py +0 -0
  96. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tui/run_tui.py +0 -0
  97. {labnotes-0.0.47 → labnotes-0.0.48}/src/labnotes/tui/runs_table.py +0 -0
  98. {labnotes-0.0.47 → labnotes-0.0.48}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.47
3
+ Version: 0.0.48
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>
@@ -62,14 +62,16 @@ Each combination of `[params]` runs `repeats` times (1 if omitted), round-robin,
62
62
  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
- Each run gets a new run folder, named for the UTC time it was planned (`20261020-000429-brave-fox`), so folder listings sort by start. `command` runs in a shell from the root, with
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
69
  `{run_dir}` replaced by the run folder's absolute path. Pass it to the
67
70
  script so it writes its outputs there.
68
71
  Each param is appended as `--name value`, with the name as written (use
69
72
  `source-language` for a `--source-language` flag), booleans as 0 or 1, and lists
70
73
  and tables as JSON. labnotes writes `run.json` (params, created, ended, exit
71
- code, and `invocation_id`, shared by every run one `run` command started;
72
- query it as `run ->> 'invocation_id'`), `stdout.log` and `stderr.log` to the
74
+ code), `stdout.log` and `stderr.log` to the
73
75
  run folder.
74
76
  A run counts when it exited 0 and its `run.json` doesn't hold
75
77
  `"excluded": true`, so excluding a run by hand means adding that key; the next
@@ -15,11 +15,9 @@ async def execute_run(
15
15
  params: dict[str, object],
16
16
  running: set[asyncio.subprocess.Process],
17
17
  stopping: asyncio.Event,
18
- invocation: str,
19
18
  ) -> dict[str, object]:
20
19
  run: dict[str, object] = {
21
20
  "params": params,
22
- "invocation_id": invocation,
23
21
  "created": iso_time(time.time()),
24
22
  "ended": None,
25
23
  "exit_code": None,
@@ -12,7 +12,7 @@ from labnotes.models import (
12
12
  Report,
13
13
  )
14
14
  from labnotes.runner.execute_run import execute_run
15
- from labnotes.runner.new_invocation_id import new_invocation_id
15
+ from labnotes.runner.new_run_id import new_run_id
16
16
  from labnotes.runner.new_stamped_run_id import new_stamped_run_id
17
17
  from labnotes.runner.param_grid import param_grid
18
18
  from labnotes.runner.plan_runs import plan_runs
@@ -44,12 +44,18 @@ async def run_experiment(
44
44
  run in planned or run_folder(config.root, config.runs, name, run).exists()
45
45
  )
46
46
 
47
- invocation = new_invocation_id()
47
+ group = new_stamped_run_id(taken) if experiment.mode == "adhoc" else None
48
+
49
+ def new_id() -> str:
50
+ if group is None:
51
+ return new_stamped_run_id(taken)
52
+ return f"{group}/" + new_run_id(lambda run: taken(f"{group}/{run}"))
53
+
48
54
  plan: list[Entry] = []
49
55
  for params in plan_runs(
50
56
  param_grid(experiment.params), experiment.repeats, finished
51
57
  ):
52
- run = new_stamped_run_id(taken)
58
+ run = new_id()
53
59
  planned.add(run)
54
60
  plan.append((run, params))
55
61
  runs = [{"run": run, "params": params} for run, params in plan]
@@ -131,7 +137,7 @@ async def run_experiment(
131
137
  def retry(run: str) -> None:
132
138
  if stopping.is_set() or run not in ended:
133
139
  return
134
- again = new_stamped_run_id(taken)
140
+ again = new_id()
135
141
  planned.add(again)
136
142
  plan.append((again, ended[run]))
137
143
  report({"event": "retried", "run": run, "retry": again, "params": ended[run]})
@@ -182,7 +188,6 @@ async def run_experiment(
182
188
  params,
183
189
  running[run],
184
190
  stopping,
185
- invocation,
186
191
  )
187
192
  del running[run]
188
193
  report({"event": "ended", "run": run, **result})
@@ -4,5 +4,5 @@ PLACEHOLDER = re.compile(r"\{[^}]*\}")
4
4
 
5
5
 
6
6
  def path_glob(template: str) -> str:
7
- glob = PLACEHOLDER.sub("*", template)
7
+ glob = PLACEHOLDER.sub("*", template.replace("{run}", "**"))
8
8
  return f"{glob}**" if glob.endswith("/") else glob
@@ -6,6 +6,9 @@ TOKEN = re.compile(r"\{([^}]*)\}|\*\*|\*|\?|[^{*?]+")
6
6
  def path_pattern(template: str) -> re.Pattern[str]:
7
7
  parts = []
8
8
  for found in TOKEN.finditer(template):
9
+ if found.group(1) == "run":
10
+ parts.append("(?P<run>[^/]+(?:/[^/]+)?)")
11
+ continue
9
12
  if found.group(1) is not None:
10
13
  parts.append(f"(?P<{found.group(1)}>[^/]+)")
11
14
  continue
@@ -110,7 +110,7 @@ class RunApp(App[None]):
110
110
  case "planned":
111
111
  for planned in event["runs"]: # type: ignore[attr-defined]
112
112
  args = " ".join(param_args(planned["params"]))
113
- cells = (planned["run"], args, PENDING, "")
113
+ cells = (planned["run"].rpartition("/")[2], args, PENDING, "")
114
114
  self.cells[planned["run"]] = [*map(dim, cells)]
115
115
  self.pending.add(planned["run"])
116
116
  self.redraw(table.cursor_row)
@@ -161,7 +161,9 @@ class RunApp(App[None]):
161
161
  case "retried":
162
162
  again = str(event["retry"])
163
163
  args = " ".join(param_args(event["params"])) # type: ignore[arg-type]
164
- self.cells[again] = [*map(dim, (again, args, PENDING, ""))]
164
+ self.cells[again] = [
165
+ *map(dim, (again.rpartition("/")[2], args, PENDING, ""))
166
+ ]
165
167
  self.pending.add(again)
166
168
  self.redraw(table.cursor_row)
167
169
  self.show(describe_event(event))
@@ -7,5 +7,11 @@ reason = "LOCK_EX and LOCK_NB are disjoint bits, so | equals + and ^"
7
7
  [[python.exempt]]
8
8
  path = "src/labnotes/tui/run_app.py"
9
9
  rules = ["mutation"]
10
- lines = [309]
10
+ lines = [311]
11
11
  reason = "last_stream is always the same str object taken from STREAMS, so is not equals !="
12
+
13
+ [[python.exempt]]
14
+ path = "src/labnotes/runner/run_experiment.py"
15
+ rules = ["mutation"]
16
+ lines = [47]
17
+ reason = "mode is validated to fill or adhoc; \"fill\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
@@ -0,0 +1,83 @@
1
+ import asyncio
2
+ import json
3
+ import socket
4
+ import subprocess
5
+ import sys
6
+ import time
7
+ from collections.abc import Awaitable, Callable, Iterator
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+ from playwright.async_api import Page, async_playwright
12
+
13
+ CONFIG = (
14
+ 'name = "Alpha study"\ncommand = "go"\nrepeats = 2\nmode = "fill"\n'
15
+ '[params]\nsource-language = ["en", "fr"]\n'
16
+ )
17
+
18
+
19
+ def write(path: Path, text: str) -> None:
20
+ path.parent.mkdir(parents=True, exist_ok=True)
21
+ path.write_text(text, encoding="utf-8")
22
+
23
+
24
+ def run(project: Path, name: str, params: dict[str, object], **fields: object) -> None:
25
+ record = {"params": params, "created": f"2026-10-17T00:00:0{name[-1]}Z", **fields}
26
+ write(project / f"alpha/runs/{name}/run.json", json.dumps(record))
27
+
28
+
29
+ def project(root: Path) -> Path:
30
+ write(root / "beta/files/notes.md", "beta notes")
31
+ write(root / ".labnotes.toml", "")
32
+ write(root / "alpha/experiment.toml", CONFIG)
33
+ write(root / "alpha/files/plan.md", "# The plan\n\n```python\n1 + 1\n```\n")
34
+ write(root / "alpha/files/results.md", "# Results\n\nAll good.\n")
35
+ run(root, "run1", {"source-language": "en"}, exit_code=0)
36
+ run(root, "run2", {"source-language": "fr"}, exit_code=1, stdout="boom")
37
+ return root
38
+
39
+
40
+ def free_port() -> int:
41
+ with socket.socket() as sock:
42
+ sock.bind(("127.0.0.1", 0))
43
+ return int(sock.getsockname()[1])
44
+
45
+
46
+ def serve(root: Path) -> Iterator[str]:
47
+ port = free_port()
48
+ labnotes = Path(sys.executable).parent / "labnotes"
49
+ process = subprocess.Popen(
50
+ [labnotes, "-c", root / ".labnotes.toml", "server", "-p", str(port)],
51
+ stdout=subprocess.DEVNULL,
52
+ stderr=subprocess.DEVNULL,
53
+ )
54
+ try:
55
+ for _ in range(100):
56
+ try:
57
+ socket.create_connection(("127.0.0.1", port)).close()
58
+ break
59
+ except OSError:
60
+ time.sleep(0.1)
61
+ yield f"http://127.0.0.1:{port}"
62
+ finally:
63
+ process.terminate()
64
+ process.wait()
65
+
66
+
67
+ def browse(
68
+ url: str, check: Callable[[Page], Awaitable[None]], width: int = 1280
69
+ ) -> None:
70
+ async def drive() -> None:
71
+ async with async_playwright() as playwright:
72
+ browser = await playwright.chromium.launch()
73
+ page = await browser.new_page(viewport={"width": width, "height": 800})
74
+ await page.goto(url)
75
+ await check(page)
76
+ await browser.close()
77
+
78
+ asyncio.run(drive())
79
+
80
+
81
+ @pytest.fixture
82
+ def site(tmp_path: Path) -> Iterator[str]:
83
+ yield from serve(project(tmp_path))
@@ -1,6 +0,0 @@
1
- import hashlib
2
- import time
3
-
4
-
5
- def new_invocation_id() -> str:
6
- return hashlib.sha256(str(time.time_ns()).encode()).hexdigest()[:8]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes