labnotes 0.0.21__tar.gz → 0.0.22__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 (89) hide show
  1. {labnotes-0.0.21 → labnotes-0.0.22}/PKG-INFO +1 -1
  2. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/context.md +14 -7
  3. labnotes-0.0.22/src/labnotes/default_counts.py +2 -0
  4. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/experiments.py +4 -1
  5. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/models.py +3 -0
  6. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/read_experiment_config.py +6 -2
  7. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/run_experiment.py +2 -6
  8. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/summarize_config.py +3 -3
  9. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/experiment_rows.py +9 -1
  10. labnotes-0.0.22/src/labnotes/tables/list_experiment_runs.py +16 -0
  11. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/open_tables.py +2 -2
  12. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/read_run.py +1 -2
  13. labnotes-0.0.21/src/labnotes/tables/list_experiment_runs.py +0 -11
  14. {labnotes-0.0.21 → labnotes-0.0.22}/.gitignore +0 -0
  15. {labnotes-0.0.21 → labnotes-0.0.22}/CHANGELOG.md +0 -0
  16. {labnotes-0.0.21 → labnotes-0.0.22}/MIGRATIONS.md +0 -0
  17. {labnotes-0.0.21 → labnotes-0.0.22}/README.md +0 -0
  18. {labnotes-0.0.21 → labnotes-0.0.22}/pyproject.toml +0 -0
  19. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/__init__.py +0 -0
  20. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/asset_url.py +0 -0
  21. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/cell_progress.py +0 -0
  22. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/cli.py +0 -0
  23. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/context_guide.py +0 -0
  24. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/count_runs.py +0 -0
  25. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/create_app.py +0 -0
  26. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/create_experiment.py +0 -0
  27. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/data_html.py +0 -0
  28. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/default_config_text.py +0 -0
  29. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/drain_watch.py +0 -0
  30. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/experiment_notes.py +0 -0
  31. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/experiment_progress.py +0 -0
  32. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/experiment_runs.py +0 -0
  33. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/format_value.py +0 -0
  34. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/init.py +0 -0
  35. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/list_experiments.py +0 -0
  36. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/list_runs.py +0 -0
  37. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/load_config.py +0 -0
  38. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/load_experiment.py +0 -0
  39. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/notebook_kernel.py +0 -0
  40. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/output_html.py +0 -0
  41. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/parse_run_view.py +0 -0
  42. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/pivot_cells.py +0 -0
  43. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/print_rows.py +0 -0
  44. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/python_blocks.py +0 -0
  45. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/read_config.py +0 -0
  46. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/read_path.py +0 -0
  47. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/render_markdown.py +0 -0
  48. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/__init__.py +0 -0
  49. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/describe_event.py +0 -0
  50. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/execute_run.py +0 -0
  51. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/lock_path.py +0 -0
  52. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/new_run_id.py +0 -0
  53. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/param_args.py +0 -0
  54. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/param_grid.py +0 -0
  55. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/plan_runs.py +0 -0
  56. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/run_folder.py +0 -0
  57. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/run_headless.py +0 -0
  58. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/shell_command.py +0 -0
  59. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/start_experiment.py +0 -0
  60. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/stop_then_kill.py +0 -0
  61. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/wait_for_group.py +0 -0
  62. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/write_run_json.py +0 -0
  63. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/saved_outputs.py +0 -0
  64. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/serve_app.py +0 -0
  65. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/server.py +0 -0
  66. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/sort_href.py +0 -0
  67. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/sql_quote.py +0 -0
  68. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/static/labnotes.css +0 -0
  69. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/static/row-link.js +0 -0
  70. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/static/run-cell.js +0 -0
  71. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/static/view-toggle.js +0 -0
  72. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/stop_idle_kernels.py +0 -0
  73. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/__init__.py +0 -0
  74. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/file_prefix.py +0 -0
  75. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/iso_time.py +0 -0
  76. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/path_glob.py +0 -0
  77. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/path_pattern.py +0 -0
  78. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/read_experiment.py +0 -0
  79. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/read_file.py +0 -0
  80. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/summarize_experiments.py +0 -0
  81. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/templates/base.html +0 -0
  82. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/templates/experiment.html +0 -0
  83. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/templates/index.html +0 -0
  84. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/templates/macros.html +0 -0
  85. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tui/__init__.py +0 -0
  86. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tui/format_elapsed.py +0 -0
  87. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tui/run_app.py +0 -0
  88. {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tui/run_tui.py +0 -0
  89. {labnotes-0.0.21 → labnotes-0.0.22}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.21
3
+ Version: 0.0.22
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>
@@ -22,8 +22,8 @@ Paths are relative to the root. `{experiment}` and `{run}` stand for folder name
22
22
  `experiment.toml`, files or runs.
23
23
  - `files`: `experiment`, `filename`, `path`, `ctime`, `mtime`.
24
24
  - `runs`: `experiment`, `id`, `folder`, `created`, `ended`, `exit_code`,
25
- `error`, `stdout`, `stderr`, and `params` as JSON, read from each run
26
- folder's `run.json`.
25
+ `error`, `stdout`, `stderr`, `params` as JSON, and `run`, the whole
26
+ `run.json` as JSON, read from each run folder's `run.json`.
27
27
 
28
28
  Keys come from folder names, so renaming a folder renames its rows on the next
29
29
  read. Paths are relative to the root, and times are UTC ISO 8601.
@@ -57,7 +57,8 @@ effort = ["low", "high"]
57
57
  and as `display_name` in `experiments`. The folder name stays the key, so
58
58
  renaming the experiment means editing `name`.
59
59
  Each combination of `[params]` runs `repeats` times, round-robin. Only runs
60
- that exit 0 count, so running again fills in what failed or never ran.
60
+ that count fill a combination, so running again fills in what failed or never
61
+ ran.
61
62
  Each run gets a new run folder. `command` runs in a shell from the root, with
62
63
  `{run_dir}` replaced by the run folder's absolute path. Pass it to the
63
64
  script so it writes its outputs there.
@@ -65,9 +66,15 @@ Each param is appended as `--name value`, with the name as written (use
65
66
  `source-language` for a `--source-language` flag), booleans as 0 or 1, and lists
66
67
  and tables as JSON. labnotes writes `run.json` (params, created, ended, exit
67
68
  code), `stdout.log` and `stderr.log` to the run folder.
68
- To exclude a run, add `"excluded": true` and an optional `"reason"` to its
69
- `run.json`. An excluded run doesn't count toward `repeats`, so the next `run`
70
- replaces it; both show as columns in `runs`.
69
+ A run counts when it exited 0 and its `run.json` doesn't hold
70
+ `"excluded": true`, so excluding a run by hand means adding that key; the next
71
+ `run` replaces it. Other keys you add to `run.json` are kept and show in `run`.
72
+ `counts` in `experiment.toml` replaces the rule with a SQL condition over
73
+ `runs`, for example `counts = "exit_code = 0 AND created > '2026-10-01'"` to
74
+ drop runs from before a script change. The default is
75
+ `exit_code = 0 AND NOT coalesce(run ->> 'excluded', false)`.
76
+ `experiments foo runs` shows the rule's result as `counted`; queries over
77
+ `runs` itself see every run, so apply the rule there too.
71
78
 
72
79
  A script run by labnotes should:
73
80
 
@@ -90,4 +97,4 @@ line per event instead.
90
97
  - A second `run` of the same experiment fails while the first holds the lock.
91
98
  - It exits 1 if any run failed.
92
99
 
93
- `uvx labnotes server` serves the experiments, their markdown files and their runs as a local site, read from the same `.labnotes.toml`. A grid cell is full once it has `repeats` runs that exited 0 and aren't excluded.
100
+ `uvx labnotes server` serves the experiments, their markdown files and their runs as a local site, read from the same `.labnotes.toml`. A grid cell is full once it has `repeats` runs that count.
@@ -0,0 +1,2 @@
1
+ # Which runs count toward repeats, unless experiment.toml sets `counts`.
2
+ DEFAULT_COUNTS = "exit_code = 0 AND NOT coalesce(run ->> 'excluded', false)"
@@ -82,7 +82,10 @@ def experiments(
82
82
  ctx.exit(1 if failed else 0)
83
83
  if action not in (None, "runs"):
84
84
  raise click.UsageError(f"Unknown action {action}. Use runs or run.")
85
- rows = asyncio.run(experiment_rows(config, name, action))
85
+ try:
86
+ rows = asyncio.run(experiment_rows(config, name, action))
87
+ except ValueError as error:
88
+ raise click.ClickException(str(error)) from error
86
89
  if rows is None:
87
90
  raise click.ClickException(f"No experiment named {name}.")
88
91
  print_rows(rows, output_format)
@@ -3,6 +3,8 @@ from dataclasses import dataclass, field
3
3
  from pathlib import Path
4
4
  from typing import Any
5
5
 
6
+ from labnotes.default_counts import DEFAULT_COUNTS
7
+
6
8
  Report = Callable[[dict[str, object]], None]
7
9
  Listen = Callable[[Callable[[], None]], None]
8
10
  Start = Callable[[Report, Listen], Coroutine[Any, Any, list[dict[str, object]]]]
@@ -31,3 +33,4 @@ class ExperimentConfig:
31
33
  repeats: int
32
34
  params: dict[str, list[object]]
33
35
  name: str | None = None
36
+ counts: str = DEFAULT_COUNTS
@@ -2,9 +2,10 @@ import json
2
2
  import tomllib
3
3
  from pathlib import Path
4
4
 
5
+ from labnotes.default_counts import DEFAULT_COUNTS
5
6
  from labnotes.models import ExperimentConfig
6
7
 
7
- SETTINGS = {"name", "command", "repeats", "params"}
8
+ SETTINGS = {"name", "command", "repeats", "params", "counts"}
8
9
 
9
10
 
10
11
  def read_experiment_config(path: Path) -> ExperimentConfig:
@@ -35,4 +36,7 @@ def read_experiment_config(path: Path) -> ExperimentConfig:
35
36
  json.dumps(params)
36
37
  except TypeError as error:
37
38
  raise ValueError(f"{path}: params can't hold dates or times") from error
38
- return ExperimentConfig(command, repeats, params, name)
39
+ counts = values.get("counts", DEFAULT_COUNTS)
40
+ if not isinstance(counts, str) or not counts.strip():
41
+ raise ValueError(f"{path}: counts must be a SQL condition")
42
+ return ExperimentConfig(command, repeats, params, name, counts)
@@ -22,12 +22,8 @@ async def run_experiment(
22
22
  report: Report,
23
23
  listen: Listen,
24
24
  ) -> list[dict[str, object]]:
25
- runs = await list_experiment_runs(open_tables(config), name)
26
- finished = [
27
- json.loads(str(run["params"]))
28
- for run in runs
29
- if run["exit_code"] == 0 and not run["excluded"]
30
- ]
25
+ runs = await list_experiment_runs(open_tables(config), name, experiment.counts)
26
+ finished = [json.loads(str(run["params"])) for run in runs if run["counted"]]
31
27
  plan = plan_runs(param_grid(experiment.params), experiment.repeats, finished)
32
28
  report({"event": "planned", "runs": len(plan), "concurrency": concurrency})
33
29
  running: set[asyncio.subprocess.Process] = set()
@@ -1,7 +1,6 @@
1
1
  from math import prod
2
2
 
3
- # What the runner counts toward repeats.
4
- COMPLETED_RUN = "exit_code = 0 AND NOT coalesce(excluded, 0)"
3
+ from labnotes.default_counts import DEFAULT_COUNTS
5
4
 
6
5
 
7
6
  def summarize_config(config: dict[str, object] | None) -> dict[str, object]:
@@ -15,12 +14,13 @@ def summarize_config(config: dict[str, object] | None) -> dict[str, object]:
15
14
  repeats = repeats if isinstance(repeats, int) else None
16
15
  cells = prod(len(values) for values in varies.values())
17
16
  command = config.get("command")
17
+ counts = config.get("counts")
18
18
  return {
19
19
  "fixed": {},
20
20
  "varies": varies,
21
21
  "cell_count": cells,
22
22
  "runs_per_cell": repeats,
23
23
  "target": cells * repeats if repeats is not None else None,
24
- "completed_run": COMPLETED_RUN,
24
+ "completed_run": counts if isinstance(counts, str) else DEFAULT_COUNTS,
25
25
  "other": {} if command is None else {"command": command},
26
26
  }
@@ -1,4 +1,7 @@
1
+ import json
2
+
1
3
  from labnotes.models import LabnotesConfig
4
+ from labnotes.summarize_config import summarize_config
2
5
  from labnotes.tables.list_experiment_runs import list_experiment_runs
3
6
  from labnotes.tables.open_tables import open_tables
4
7
  from labnotes.tables.summarize_experiments import summarize_experiments
@@ -13,5 +16,10 @@ async def experiment_rows(
13
16
  return None
14
17
  match name, action:
15
18
  case str(), "runs":
16
- return await list_experiment_runs(tables, name)
19
+ [row] = rows
20
+ stored = row["config"]
21
+ summary = summarize_config(json.loads(str(stored)) if stored else None)
22
+ return await list_experiment_runs(
23
+ tables, name, str(summary["completed_run"])
24
+ )
17
25
  return rows
@@ -0,0 +1,16 @@
1
+ from dirsql import DirSQL
2
+
3
+ from labnotes.sql_quote import quote_string
4
+
5
+
6
+ async def list_experiment_runs(
7
+ tables: DirSQL, name: str, counts: str
8
+ ) -> list[dict[str, object]]:
9
+ try:
10
+ return await tables.query(
11
+ f"SELECT id, created, ended, exit_code, error, ({counts}) AS counted,"
12
+ " params, folder FROM runs"
13
+ f" WHERE experiment = {quote_string(name)} ORDER BY created, id"
14
+ )
15
+ except RuntimeError as error:
16
+ raise ValueError(f"{name}: counts doesn't work: {error}") from error
@@ -31,8 +31,8 @@ CREATE TABLE files (
31
31
  RUNS = """
32
32
  CREATE TABLE runs (
33
33
  experiment TEXT, id TEXT, folder TEXT, created TEXT, ended TEXT,
34
- exit_code INTEGER, error TEXT, excluded INTEGER, reason TEXT,
35
- stdout TEXT, stderr TEXT, params TEXT,
34
+ exit_code INTEGER, error TEXT,
35
+ stdout TEXT, stderr TEXT, params TEXT, run TEXT,
36
36
  PRIMARY KEY (experiment, id)
37
37
  )
38
38
  """
@@ -20,10 +20,9 @@ def read_run(root: Path, template: str, path: str) -> list[dict[str, object]]:
20
20
  "ended": run.get("ended"),
21
21
  "exit_code": run.get("exit_code"),
22
22
  "error": run.get("error"),
23
- "excluded": run.get("excluded") is True,
24
- "reason": run.get("reason"),
25
23
  "stdout": f"{folder}/stdout.log",
26
24
  "stderr": f"{folder}/stderr.log",
27
25
  "params": json.dumps(run.get("params", {})),
26
+ "run": json.dumps(run),
28
27
  }
29
28
  ]
@@ -1,11 +0,0 @@
1
- from dirsql import DirSQL
2
-
3
- from labnotes.sql_quote import quote_string
4
-
5
-
6
- async def list_experiment_runs(tables: DirSQL, name: str) -> list[dict[str, object]]:
7
- return await tables.query(
8
- "SELECT id, created, ended, exit_code, error, excluded, reason, params, folder"
9
- " FROM runs"
10
- f" WHERE experiment = {quote_string(name)} ORDER BY created, id"
11
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes