labnotes 0.0.15__tar.gz → 0.0.17__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 (88) hide show
  1. {labnotes-0.0.15 → labnotes-0.0.17}/PKG-INFO +7 -2
  2. {labnotes-0.0.15 → labnotes-0.0.17}/README.md +6 -1
  3. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/context.md +14 -1
  4. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/experiments.py +1 -2
  5. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/models.py +1 -0
  6. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/read_experiment_config.py +7 -4
  7. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/run_experiment.py +4 -4
  8. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/run_headless.py +2 -0
  9. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/start_experiment.py +3 -2
  10. labnotes-0.0.17/src/labnotes/runner/stop_then_kill.py +15 -0
  11. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/open_tables.py +4 -1
  12. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/summarize_experiments.py +2 -1
  13. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tui/run_app.py +3 -0
  14. {labnotes-0.0.15 → labnotes-0.0.17}/.gitignore +0 -0
  15. {labnotes-0.0.15 → labnotes-0.0.17}/CHANGELOG.md +0 -0
  16. {labnotes-0.0.15 → labnotes-0.0.17}/MIGRATIONS.md +0 -0
  17. {labnotes-0.0.15 → labnotes-0.0.17}/pyproject.toml +0 -0
  18. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/__init__.py +0 -0
  19. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/asset_url.py +0 -0
  20. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/cell_progress.py +0 -0
  21. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/cli.py +0 -0
  22. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/context_guide.py +0 -0
  23. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/count_runs.py +0 -0
  24. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/create_app.py +0 -0
  25. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/data_html.py +0 -0
  26. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/default_config_text.py +0 -0
  27. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/drain_watch.py +0 -0
  28. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/format_value.py +0 -0
  29. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/init.py +0 -0
  30. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/list_experiments.py +0 -0
  31. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/list_runs.py +0 -0
  32. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/load_config.py +0 -0
  33. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/load_experiment.py +0 -0
  34. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/notebook_kernel.py +0 -0
  35. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/open_experiments.py +0 -0
  36. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/open_runs.py +0 -0
  37. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/output_html.py +0 -0
  38. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/parse_run_view.py +0 -0
  39. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/pivot_cells.py +0 -0
  40. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/print_rows.py +0 -0
  41. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/python_blocks.py +0 -0
  42. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/read_config.py +0 -0
  43. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/read_design.py +0 -0
  44. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/read_note.py +0 -0
  45. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/read_path.py +0 -0
  46. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/render_markdown.py +0 -0
  47. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/run_columns.py +0 -0
  48. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/__init__.py +0 -0
  49. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/describe_event.py +0 -0
  50. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/execute_run.py +0 -0
  51. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/lock_path.py +0 -0
  52. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/new_run_id.py +0 -0
  53. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/param_args.py +0 -0
  54. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/param_grid.py +0 -0
  55. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/plan_runs.py +0 -0
  56. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/run_folder.py +0 -0
  57. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/shell_command.py +0 -0
  58. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/runner/write_run_json.py +0 -0
  59. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/saved_outputs.py +0 -0
  60. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/serve_app.py +0 -0
  61. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/server.py +0 -0
  62. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/sort_href.py +0 -0
  63. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/sql_quote.py +0 -0
  64. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/static/labnotes.css +0 -0
  65. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/static/row-link.js +0 -0
  66. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/static/run-cell.js +0 -0
  67. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/static/view-toggle.js +0 -0
  68. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/stop_idle_kernels.py +0 -0
  69. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/study_progress.py +0 -0
  70. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/summarize_design.py +0 -0
  71. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/__init__.py +0 -0
  72. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/experiment_rows.py +0 -0
  73. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/file_prefix.py +0 -0
  74. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/iso_time.py +0 -0
  75. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/list_experiment_runs.py +0 -0
  76. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/path_glob.py +0 -0
  77. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/path_pattern.py +0 -0
  78. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/read_experiment.py +0 -0
  79. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/read_file.py +0 -0
  80. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tables/read_run.py +0 -0
  81. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/templates/base.html +0 -0
  82. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/templates/experiment.html +0 -0
  83. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/templates/index.html +0 -0
  84. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/templates/macros.html +0 -0
  85. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tui/__init__.py +0 -0
  86. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tui/format_elapsed.py +0 -0
  87. {labnotes-0.0.15 → labnotes-0.0.17}/src/labnotes/tui/run_tui.py +0 -0
  88. {labnotes-0.0.15 → labnotes-0.0.17}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.15
3
+ Version: 0.0.17
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>
@@ -73,6 +73,7 @@ Each takes `--format table` (the default) or `--format json`.
73
73
  experiments folder:
74
74
 
75
75
  ```toml
76
+ name = "Model and effort"
76
77
  command = "uv run scripts/study.py --output {run_dir}"
77
78
  repeats = 5
78
79
 
@@ -81,6 +82,9 @@ model = ["opus", "sonnet"]
81
82
  effort = ["low", "high"]
82
83
  ```
83
84
 
85
+ `name` is optional: a display name for the experiment, shown in the live view
86
+ and as `display_name` in `experiments`. The folder name stays the key, so
87
+ renaming the experiment means editing `name`.
84
88
  Each combination of `[params]` runs `repeats` times, round-robin. Only runs
85
89
  that exit 0 count, so running again fills in what failed or never ran.
86
90
  Each run gets a new run folder. `command` runs in a shell from the root, with
@@ -98,7 +102,8 @@ line per event instead.
98
102
  - `--concurrency N` runs N at a time (default 1).
99
103
  - `-e PATH` reads another file instead of `experiment.toml`.
100
104
  - Ctrl-C stops the running scripts and records their end. A second Ctrl-C
101
- kills them.
105
+ kills them. Closing the terminal or sending SIGTERM stops them the same way,
106
+ then kills any still going 5 seconds later.
102
107
  - A second `run` of the same experiment fails while the first holds the lock.
103
108
  - It exits 1 if any run failed.
104
109
 
@@ -50,6 +50,7 @@ Each takes `--format table` (the default) or `--format json`.
50
50
  experiments folder:
51
51
 
52
52
  ```toml
53
+ name = "Model and effort"
53
54
  command = "uv run scripts/study.py --output {run_dir}"
54
55
  repeats = 5
55
56
 
@@ -58,6 +59,9 @@ model = ["opus", "sonnet"]
58
59
  effort = ["low", "high"]
59
60
  ```
60
61
 
62
+ `name` is optional: a display name for the experiment, shown in the live view
63
+ and as `display_name` in `experiments`. The folder name stays the key, so
64
+ renaming the experiment means editing `name`.
61
65
  Each combination of `[params]` runs `repeats` times, round-robin. Only runs
62
66
  that exit 0 count, so running again fills in what failed or never ran.
63
67
  Each run gets a new run folder. `command` runs in a shell from the root, with
@@ -75,7 +79,8 @@ line per event instead.
75
79
  - `--concurrency N` runs N at a time (default 1).
76
80
  - `-e PATH` reads another file instead of `experiment.toml`.
77
81
  - Ctrl-C stops the running scripts and records their end. A second Ctrl-C
78
- kills them.
82
+ kills them. Closing the terminal or sending SIGTERM stops them the same way,
83
+ then kills any still going 5 seconds later.
79
84
  - A second `run` of the same experiment fails while the first holds the lock.
80
85
  - It exits 1 if any run failed.
81
86
 
@@ -40,6 +40,7 @@ Each takes `--format table` (the default) or `--format json`; use `--format json
40
40
  experiments folder:
41
41
 
42
42
  ```toml
43
+ name = "Model and effort"
43
44
  command = "uv run scripts/study.py --output {run_dir}"
44
45
  repeats = 5
45
46
 
@@ -48,6 +49,9 @@ model = ["opus", "sonnet"]
48
49
  effort = ["low", "high"]
49
50
  ```
50
51
 
52
+ `name` is optional: a display name for the experiment, shown in the live view
53
+ and as `display_name` in `experiments`. The folder name stays the key, so
54
+ renaming the experiment means editing `name`.
51
55
  Each combination of `[params]` runs `repeats` times, round-robin. Only runs
52
56
  that exit 0 count, so running again fills in what failed or never ran.
53
57
  Each run gets a new run folder. `command` runs in a shell from the root, with
@@ -58,6 +62,14 @@ Each param is appended as `--name value`, with the name as written (use
58
62
  and tables as JSON. labnotes writes `run.json` (params, created, ended, exit
59
63
  code), `stdout.log` and `stderr.log` to the run folder.
60
64
 
65
+ A script run by labnotes should:
66
+
67
+ - take each param as a `--name value` flag, parsing lists and tables from JSON;
68
+ - take its output folder as an argument, passed in `command` as `{run_dir}`, and write everything it produces there;
69
+ - exit non-zero when it fails, so the run counts as failed and runs again next time.
70
+
71
+ Runs can take a long time. Start `run` in the background, then check on it with `labnotes experiments foo runs`; a run with no `ended` is still going.
72
+
61
73
  In a terminal, `run` shows a live table of runs (status and time) and stays
62
74
  open when they finish; `q` quits. Elsewhere, or with `--no-tui`, it prints a
63
75
  line per event instead.
@@ -65,7 +77,8 @@ line per event instead.
65
77
  - `--concurrency N` runs N at a time (default 1).
66
78
  - `-e PATH` reads another file instead of `experiment.toml`.
67
79
  - Ctrl-C stops the running scripts and records their end. A second Ctrl-C
68
- kills them.
80
+ kills them. Closing the terminal or sending SIGTERM stops them the same way,
81
+ then kills any still going 5 seconds later.
69
82
  - A second `run` of the same experiment fails while the first holds the lock.
70
83
  - It exits 1 if any run failed.
71
84
 
@@ -1,6 +1,5 @@
1
1
  import asyncio
2
2
  import sys
3
- from functools import partial
4
3
  from pathlib import Path
5
4
 
6
5
  import click
@@ -60,7 +59,7 @@ def experiments(
60
59
  match name, action:
61
60
  case str(), "run":
62
61
  live = sys.stdout.isatty() and sys.stderr.isatty() and not no_tui
63
- drive = partial(run_tui, title=name) if live else run_headless
62
+ drive = run_tui if live else lambda start, title: run_headless(start)
64
63
  try:
65
64
  results = start_experiment(
66
65
  config, name, experiment_config, concurrency, drive
@@ -30,3 +30,4 @@ class ExperimentConfig:
30
30
  command: str
31
31
  repeats: int
32
32
  params: dict[str, list[object]]
33
+ name: str | None = None
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
 
5
5
  from labnotes.models import ExperimentConfig
6
6
 
7
- SETTINGS = {"command", "repeats", "params"}
7
+ SETTINGS = {"name", "command", "repeats", "params"}
8
8
 
9
9
 
10
10
  def read_experiment_config(path: Path) -> ExperimentConfig:
@@ -16,6 +16,9 @@ def read_experiment_config(path: Path) -> ExperimentConfig:
16
16
  unknown = sorted(values.keys() - SETTINGS)
17
17
  if unknown:
18
18
  raise ValueError(f"{path}: unknown keys {', '.join(unknown)}")
19
+ name = values.get("name")
20
+ if name is not None and (not isinstance(name, str) or not name.strip()):
21
+ raise ValueError(f"{path}: name must be a string")
19
22
  command = values.get("command")
20
23
  if not isinstance(command, str) or not command.strip():
21
24
  raise ValueError(f"{path}: command must be a string")
@@ -25,11 +28,11 @@ def read_experiment_config(path: Path) -> ExperimentConfig:
25
28
  params = values.get("params", {})
26
29
  if not isinstance(params, dict):
27
30
  raise ValueError(f"{path}: params must be a table, like [params]")
28
- for name, options in params.items():
31
+ for param, options in params.items():
29
32
  if not isinstance(options, list) or not options:
30
- raise ValueError(f"{path}: params.{name} must be a non-empty list")
33
+ raise ValueError(f"{path}: params.{param} must be a non-empty list")
31
34
  try:
32
35
  json.dumps(params)
33
36
  except TypeError as error:
34
37
  raise ValueError(f"{path}: params can't hold dates or times") from error
35
- return ExperimentConfig(command, repeats, params)
38
+ return ExperimentConfig(command, repeats, params, name)
@@ -38,13 +38,13 @@ async def run_experiment(
38
38
  def interrupt() -> None:
39
39
  nonlocal stopping
40
40
  plan.clear()
41
- if stopping:
42
- report({"event": "killing"})
43
- signal_runs(signal.SIGKILL)
44
- else:
41
+ if not stopping:
45
42
  stopping = True
46
43
  report({"event": "stopping"})
47
44
  signal_runs(signal.SIGTERM)
45
+ elif running:
46
+ report({"event": "killing"})
47
+ signal_runs(signal.SIGKILL)
48
48
 
49
49
  async def work() -> None:
50
50
  while plan:
@@ -6,10 +6,12 @@ import click
6
6
 
7
7
  from labnotes.models import Start
8
8
  from labnotes.runner.describe_event import describe_event
9
+ from labnotes.runner.stop_then_kill import stop_then_kill
9
10
 
10
11
 
11
12
  def run_headless(start: Start) -> list[dict[str, object]]:
12
13
  def listen(interrupt: Callable[[], None]) -> None:
13
14
  asyncio.get_running_loop().add_signal_handler(signal.SIGINT, interrupt)
15
+ stop_then_kill(interrupt, signal.SIGTERM, signal.SIGHUP)
14
16
 
15
17
  return asyncio.run(start(lambda event: click.echo(describe_event(event)), listen))
@@ -15,7 +15,7 @@ def start_experiment(
15
15
  name: str,
16
16
  experiment_config: Path | None,
17
17
  concurrency: int,
18
- drive: Callable[[Start], list[dict[str, object]]],
18
+ drive: Callable[[Start, str], list[dict[str, object]]],
19
19
  ) -> list[dict[str, object]]:
20
20
  if experiment_config is None:
21
21
  folder = config.experiments.replace("{experiment}", name)
@@ -27,6 +27,7 @@ def start_experiment(
27
27
  lock.parent.mkdir(parents=True, exist_ok=True)
28
28
  try:
29
29
  with FileLock(lock, blocking=False):
30
- return drive(partial(run_experiment, config, name, experiment, concurrency))
30
+ start = partial(run_experiment, config, name, experiment, concurrency)
31
+ return drive(start, experiment.name or name)
31
32
  except Timeout as error:
32
33
  raise ValueError(f"{name} is already running ({lock} is held).") from error
@@ -0,0 +1,15 @@
1
+ import asyncio
2
+ from collections.abc import Callable
3
+
4
+ GRACE_SECONDS = 5
5
+
6
+
7
+ def stop_then_kill(interrupt: Callable[[], None], *signals: int) -> None:
8
+ loop = asyncio.get_running_loop()
9
+
10
+ def stop() -> None:
11
+ interrupt()
12
+ loop.call_later(GRACE_SECONDS, interrupt)
13
+
14
+ for signal_number in signals:
15
+ loop.add_signal_handler(signal_number, stop)
@@ -12,7 +12,10 @@ from labnotes.tables.read_run import read_run
12
12
  EXPERIMENTS = """
13
13
  CREATE TABLE experiment_configs (experiment TEXT PRIMARY KEY, config TEXT);
14
14
  CREATE VIEW experiments AS
15
- SELECT names.name, experiment_configs.config
15
+ SELECT
16
+ names.name,
17
+ coalesce(experiment_configs.config ->> 'name', names.name) AS display_name,
18
+ experiment_configs.config
16
19
  FROM (
17
20
  SELECT experiment AS name FROM experiment_configs
18
21
  UNION SELECT experiment FROM files
@@ -4,7 +4,8 @@ from labnotes.sql_quote import quote_string
4
4
 
5
5
  SUMMARY = """
6
6
  SELECT
7
- e.name,{config}
7
+ e.name,
8
+ e.display_name,{config}
8
9
  (SELECT max(created) FROM runs WHERE runs.experiment = e.name) AS last_run,
9
10
  (
10
11
  SELECT min(t) FROM (
@@ -1,3 +1,4 @@
1
+ import signal
1
2
  import time
2
3
  from collections.abc import Callable
3
4
 
@@ -7,6 +8,7 @@ from textual.widgets import DataTable, Footer, Header, Static
7
8
 
8
9
  from labnotes.models import Start
9
10
  from labnotes.runner.describe_event import describe_event
11
+ from labnotes.runner.stop_then_kill import stop_then_kill
10
12
  from labnotes.runner.param_args import param_args
11
13
  from labnotes.tui.format_elapsed import format_elapsed
12
14
 
@@ -50,6 +52,7 @@ class RunApp(App[None]):
50
52
 
51
53
  def listen(self, interrupt: Callable[[], None]) -> None:
52
54
  self.interrupt = interrupt
55
+ stop_then_kill(interrupt, signal.SIGTERM, signal.SIGHUP)
53
56
 
54
57
  def show_event(self, event: dict[str, object]) -> None:
55
58
  table = self.query_one(DataTable)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes