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.
- {labnotes-0.0.21 → labnotes-0.0.22}/PKG-INFO +1 -1
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/context.md +14 -7
- labnotes-0.0.22/src/labnotes/default_counts.py +2 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/experiments.py +4 -1
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/models.py +3 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/read_experiment_config.py +6 -2
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/run_experiment.py +2 -6
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/summarize_config.py +3 -3
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/experiment_rows.py +9 -1
- labnotes-0.0.22/src/labnotes/tables/list_experiment_runs.py +16 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/open_tables.py +2 -2
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/read_run.py +1 -2
- labnotes-0.0.21/src/labnotes/tables/list_experiment_runs.py +0 -11
- {labnotes-0.0.21 → labnotes-0.0.22}/.gitignore +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/CHANGELOG.md +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/MIGRATIONS.md +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/README.md +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/pyproject.toml +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/experiment_progress.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/init.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/lock_path.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/wait_for_group.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/server.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/stop_idle_kernels.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/templates/macros.html +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tui/run_app.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.21 → labnotes-0.0.22}/uv.lock +0 -0
|
@@ -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`,
|
|
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
|
|
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
|
-
|
|
69
|
-
`
|
|
70
|
-
replaces it
|
|
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
|
|
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.
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|