labnotes 0.0.10__tar.gz → 0.0.12__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 (53) hide show
  1. {labnotes-0.0.10 → labnotes-0.0.12}/PKG-INFO +36 -6
  2. labnotes-0.0.12/README.md +59 -0
  3. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/cli.py +47 -5
  4. labnotes-0.0.12/src/labnotes/context.md +34 -0
  5. labnotes-0.0.12/src/labnotes/context_guide.py +22 -0
  6. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/models.py +8 -0
  7. labnotes-0.0.12/src/labnotes/query_experiment.py +24 -0
  8. labnotes-0.0.12/src/labnotes/read_config.py +23 -0
  9. labnotes-0.0.12/src/labnotes/scope_runs.py +14 -0
  10. labnotes-0.0.10/README.md +0 -29
  11. {labnotes-0.0.10 → labnotes-0.0.12}/.gitignore +0 -0
  12. {labnotes-0.0.10 → labnotes-0.0.12}/CHANGELOG.md +0 -0
  13. {labnotes-0.0.10 → labnotes-0.0.12}/MIGRATIONS.md +0 -0
  14. {labnotes-0.0.10 → labnotes-0.0.12}/pyproject.toml +0 -0
  15. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/__init__.py +0 -0
  16. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/asset_url.py +0 -0
  17. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/cell_progress.py +0 -0
  18. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/count_runs.py +0 -0
  19. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/create_app.py +0 -0
  20. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/data_html.py +0 -0
  21. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/drain_watch.py +0 -0
  22. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/dump_experiments.py +0 -0
  23. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/format_value.py +0 -0
  24. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/list_experiments.py +0 -0
  25. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/list_runs.py +0 -0
  26. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/load_experiment.py +0 -0
  27. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/notebook_kernel.py +0 -0
  28. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/open_experiments.py +0 -0
  29. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/open_runs.py +0 -0
  30. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/output_html.py +0 -0
  31. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/parse_run_view.py +0 -0
  32. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/pivot_cells.py +0 -0
  33. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/python_blocks.py +0 -0
  34. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/read_design.py +0 -0
  35. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/read_note.py +0 -0
  36. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/render_markdown.py +0 -0
  37. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/run_columns.py +0 -0
  38. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/saved_outputs.py +0 -0
  39. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/serve_app.py +0 -0
  40. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/sort_href.py +0 -0
  41. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/sql_quote.py +0 -0
  42. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/static/labnotes.css +0 -0
  43. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/static/row-link.js +0 -0
  44. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/static/run-cell.js +0 -0
  45. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/static/view-toggle.js +0 -0
  46. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/stop_idle_kernels.py +0 -0
  47. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/study_progress.py +0 -0
  48. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/summarize_design.py +0 -0
  49. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/templates/base.html +0 -0
  50. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/templates/experiment.html +0 -0
  51. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/templates/index.html +0 -0
  52. {labnotes-0.0.10 → labnotes-0.0.12}/src/labnotes/templates/macros.html +0 -0
  53. {labnotes-0.0.10 → labnotes-0.0.12}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.10
3
+ Version: 0.0.12
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,12 +22,23 @@ Description-Content-Type: text/markdown
22
22
  Browse experiment designs, notes, and run data from a folder.
23
23
 
24
24
  ```sh
25
- uvx labnotes --experiments-folder experiments --data-folder data
25
+ uvx labnotes
26
26
  ```
27
27
 
28
- - `--experiments-folder` holds one subfolder per experiment, each with a
28
+ labnotes reads two folders, named in a `.labnotes.toml` at the project root:
29
+
30
+ ```toml
31
+ experiments = "experiments"
32
+ data = "data"
33
+ ```
34
+
35
+ Both keys are optional and default to the values above. Paths are relative to
36
+ `.labnotes.toml`, or absolute, or start with `~`. labnotes reads the file from
37
+ the working folder, or from the path given with `-c`.
38
+
39
+ - `experiments` holds one subfolder per experiment, each with a
29
40
  `design.yaml` (with a `study` key) and any number of markdown notes.
30
- - `--data-folder` holds the run data and a `.dirsql.toml` that declares a
41
+ - `data` holds the run data and a `.dirsql.toml` that declares a
31
42
  `runs` table with a `study` column. An experiment's runs are the rows whose
32
43
  `study` matches its design's.
33
44
 
@@ -43,6 +54,25 @@ of values), `runs_per_cell`, and `completed_run`, a SQL condition over the
43
54
  `-h`/`--host` and `-p`/`--port` change where it listens; `-h 0.0.0.0` (or a
44
55
  Tailscale IP) serves beyond localhost. Every other uvicorn option passes
45
56
  through: `--reload`, `--workers`, `--proxy-headers`, `--root-path`, the
46
- `--ssl-*` set, and the rest. `labnotes --help` lists them. The folders can also
47
- come from `LABNOTES_EXPERIMENTS_FOLDER` and `LABNOTES_DATA_FOLDER`. `--json`
57
+ `--ssl-*` set, and the rest. `labnotes --help` lists them. `--experiments-folder`
58
+ and `--data-folder`, or `LABNOTES_EXPERIMENTS_FOLDER` and
59
+ `LABNOTES_DATA_FOLDER`, override `.labnotes.toml`. `--json`
48
60
  prints the experiments as JSON and exits.
61
+
62
+ ## From the command line
63
+
64
+ Name an experiment to print what has run for it as JSON. Add SQL to query
65
+ that experiment's runs: `runs` holds only its runs, `main.runs` every run.
66
+
67
+ ```sh
68
+ uvx labnotes models-effort "SELECT model, COUNT(*) AS n FROM runs GROUP BY 1"
69
+ ```
70
+
71
+ ## Agent skill
72
+
73
+ ```sh
74
+ npx skills add thekevinscott/labnotes
75
+ ```
76
+
77
+ The skill tells a coding agent to run `labnotes context`, which prints a usage
78
+ guide for the installed version.
@@ -0,0 +1,59 @@
1
+ # labnotes
2
+
3
+ Browse experiment designs, notes, and run data from a folder.
4
+
5
+ ```sh
6
+ uvx labnotes
7
+ ```
8
+
9
+ labnotes reads two folders, named in a `.labnotes.toml` at the project root:
10
+
11
+ ```toml
12
+ experiments = "experiments"
13
+ data = "data"
14
+ ```
15
+
16
+ Both keys are optional and default to the values above. Paths are relative to
17
+ `.labnotes.toml`, or absolute, or start with `~`. labnotes reads the file from
18
+ the working folder, or from the path given with `-c`.
19
+
20
+ - `experiments` holds one subfolder per experiment, each with a
21
+ `design.yaml` (with a `study` key) and any number of markdown notes.
22
+ - `data` holds the run data and a `.dirsql.toml` that declares a
23
+ `runs` table with a `study` column. An experiment's runs are the rows whose
24
+ `study` matches its design's.
25
+
26
+ That serves a local site at http://127.0.0.1:8000: a table of experiments,
27
+ and a page per experiment with its parameters, its notes, progress per cell of
28
+ the design grid, and every run so far. Both folders are watched, so a reload
29
+ shows new runs and edited notes.
30
+
31
+ `design.yaml` keys the site reads: `study`, `fixed`, `varies` (each key a list
32
+ of values), `runs_per_cell`, and `completed_run`, a SQL condition over the
33
+ `runs` table that marks a run as complete. Anything else shows up as-is.
34
+
35
+ `-h`/`--host` and `-p`/`--port` change where it listens; `-h 0.0.0.0` (or a
36
+ Tailscale IP) serves beyond localhost. Every other uvicorn option passes
37
+ through: `--reload`, `--workers`, `--proxy-headers`, `--root-path`, the
38
+ `--ssl-*` set, and the rest. `labnotes --help` lists them. `--experiments-folder`
39
+ and `--data-folder`, or `LABNOTES_EXPERIMENTS_FOLDER` and
40
+ `LABNOTES_DATA_FOLDER`, override `.labnotes.toml`. `--json`
41
+ prints the experiments as JSON and exits.
42
+
43
+ ## From the command line
44
+
45
+ Name an experiment to print what has run for it as JSON. Add SQL to query
46
+ that experiment's runs: `runs` holds only its runs, `main.runs` every run.
47
+
48
+ ```sh
49
+ uvx labnotes models-effort "SELECT model, COUNT(*) AS n FROM runs GROUP BY 1"
50
+ ```
51
+
52
+ ## Agent skill
53
+
54
+ ```sh
55
+ npx skills add thekevinscott/labnotes
56
+ ```
57
+
58
+ The skill tells a coding agent to run `labnotes context`, which prints a usage
59
+ guide for the installed version.
@@ -6,26 +6,40 @@ from pathlib import Path
6
6
  import click
7
7
  from uvicorn.main import main as uvicorn_main
8
8
 
9
+ from labnotes.context_guide import context_guide
9
10
  from labnotes.dump_experiments import dump_experiments
11
+ from labnotes.query_experiment import query_experiment
12
+ from labnotes.read_config import CONFIG_FILE, read_config
10
13
  from labnotes.serve_app import DATA_FOLDER, EXPERIMENTS_FOLDER, KERNEL_IDLE_MINUTES
11
14
 
12
15
  # labnotes owns these; the rest of uvicorn's options pass straight through.
13
16
  OWN_OPTIONS = {"app", "factory", "app_dir", "version", "host", "port"}
14
17
 
15
18
 
16
- @click.command(help="Browse experiments and their run data.")
19
+ @click.command(
20
+ help="Browse experiments and their run data. With EXPERIMENT, print what has"
21
+ " run as JSON; with SQL too, query that experiment's runs. `labnotes context`"
22
+ " prints a guide for coding agents."
23
+ )
17
24
  @click.version_option(package_name="labnotes")
25
+ @click.argument("experiment", required=False)
26
+ @click.argument("sql", required=False)
27
+ @click.option(
28
+ "-c",
29
+ "--config",
30
+ "config_path",
31
+ type=click.Path(path_type=Path, dir_okay=False, exists=True),
32
+ help=f"Project config. Default: {CONFIG_FILE} in the working folder, if there.",
33
+ )
18
34
  @click.option(
19
35
  "--experiments-folder",
20
36
  type=click.Path(path_type=Path),
21
- required=True,
22
37
  envvar=EXPERIMENTS_FOLDER,
23
38
  help="One subfolder per experiment, each with a design.yaml and markdown notes.",
24
39
  )
25
40
  @click.option(
26
41
  "--data-folder",
27
42
  type=click.Path(path_type=Path),
28
- required=True,
29
43
  envvar=DATA_FOLDER,
30
44
  help="Run data, with a .dirsql.toml declaring a runs table that has a study column.",
31
45
  )
@@ -54,12 +68,40 @@ OWN_OPTIONS = {"app", "factory", "app_dir", "version", "host", "port"}
54
68
  @click.pass_context
55
69
  def main(
56
70
  ctx: click.Context,
57
- experiments_folder: Path,
58
- data_folder: Path,
71
+ experiment: str | None,
72
+ sql: str | None,
73
+ config_path: Path | None,
74
+ experiments_folder: Path | None,
75
+ data_folder: Path | None,
59
76
  kernel_idle_minutes: int,
60
77
  as_json: bool,
61
78
  **server_options: object,
62
79
  ) -> None:
80
+ cwd = Path.cwd()
81
+ config_path = config_path or cwd / CONFIG_FILE
82
+ try:
83
+ config = read_config(config_path) if config_path.is_file() else None
84
+ except ValueError as error:
85
+ raise click.ClickException(str(error)) from error
86
+ if experiment == "context" and sql is None:
87
+ click.echo(context_guide(config, cwd), nl=False)
88
+ return
89
+ if config is not None:
90
+ experiments_folder = experiments_folder or config.experiments_folder
91
+ data_folder = data_folder or config.data_folder
92
+ if experiments_folder is None or data_folder is None:
93
+ raise click.UsageError(
94
+ f"--experiments-folder and --data-folder are required without a {CONFIG_FILE}."
95
+ )
96
+ if experiment is not None:
97
+ try:
98
+ result = asyncio.run(
99
+ query_experiment(experiments_folder, data_folder, experiment, sql)
100
+ )
101
+ except (LookupError, RuntimeError) as error:
102
+ raise click.ClickException(str(error)) from error
103
+ click.echo(json.dumps(result, indent=2))
104
+ return
63
105
  if as_json:
64
106
  experiments = asyncio.run(dump_experiments(experiments_folder, data_folder))
65
107
  click.echo(json.dumps(experiments, indent=2))
@@ -0,0 +1,34 @@
1
+ ## Usage
2
+
3
+ labnotes reads two folders. The experiments folder has one subfolder per experiment, each with a `design.yaml` and markdown notes. The data folder holds run data and a `.dirsql.toml` declaring a `runs` table with a `study` column. A project that uses labnotes has a `.labnotes.toml` at its root; labnotes reads it from the working folder, so run labnotes from the project root, or pass `-c PATH`. The lines above this guide name the one found and the folders it resolves to, or say there is none. Its keys are optional:
4
+
5
+ ```toml
6
+ experiments = "experiments" # the default
7
+ data = "data" # relative to .labnotes.toml, absolute, or ~
8
+ ```
9
+
10
+ `--experiments-folder` and `--data-folder`, or `LABNOTES_EXPERIMENTS_FOLDER` and `LABNOTES_DATA_FOLDER`, override either key. Without a `.labnotes.toml`, pass both.
11
+
12
+ ```sh
13
+ uvx labnotes --json
14
+ ```
15
+
16
+ `--json` lists every experiment: its name, study, the design's fixed and varied conditions, the target run count, and how many runs have run and completed, overall and per cell.
17
+
18
+ ### One experiment
19
+
20
+ Name an experiment to see what has run for it, as JSON:
21
+
22
+ ```sh
23
+ uvx labnotes models-effort
24
+ ```
25
+
26
+ Add SQL to query its runs. `runs` holds only that experiment's runs, matched on the study in its `design.yaml`; `main.runs` holds every run in the data folder. The query goes through [dirsql](https://dirsql.dev), so it is SQLite and read-only:
27
+
28
+ ```sh
29
+ uvx labnotes models-effort "SELECT model, effort, COUNT(*) AS n FROM runs GROUP BY 1, 2"
30
+ ```
31
+
32
+ stdout is one JSON array of row objects. The columns are whatever the data folder's `.dirsql.toml` declares; `SELECT * FROM runs LIMIT 1` shows them. A run counts as completed when the design's `completed_run` SQL expression is true; the one-experiment JSON prints it, so reuse it in a `WHERE`.
33
+
34
+ An unknown experiment, an experiment without a `study`, or bad SQL prints an error to stderr and exits `1`. An experiment named `context` cannot be queried this way.
@@ -0,0 +1,22 @@
1
+ from importlib.metadata import version
2
+ from importlib.resources import files
3
+ from pathlib import Path
4
+
5
+ from labnotes.read_config import CONFIG_FILE
6
+ from labnotes.models import LabnotesConfig
7
+
8
+
9
+ def context_guide(config: LabnotesConfig | None, cwd: Path) -> str:
10
+ guide = files("labnotes").joinpath("context.md").read_text(encoding="utf-8")
11
+ if config is None:
12
+ project = (
13
+ f"No {CONFIG_FILE} in {cwd}. If this project uses labnotes, run it"
14
+ f" from the folder that holds {CONFIG_FILE}, or pass -c PATH."
15
+ )
16
+ else:
17
+ project = (
18
+ f"Config: {config.path}\n"
19
+ f"Experiments folder: {config.experiments_folder}\n"
20
+ f"Data folder: {config.data_folder}"
21
+ )
22
+ return f"labnotes {version('labnotes')}\n\n{project}\n\n{guide}"
@@ -1,4 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
+ from pathlib import Path
2
3
 
3
4
 
4
5
  @dataclass(frozen=True)
@@ -7,3 +8,10 @@ class RunView:
7
8
  descending: bool = False
8
9
  status: str | None = None
9
10
  filters: dict[str, str] = field(default_factory=dict)
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class LabnotesConfig:
15
+ path: Path
16
+ experiments_folder: Path
17
+ data_folder: Path
@@ -0,0 +1,24 @@
1
+ from pathlib import Path
2
+
3
+ from labnotes.list_experiments import list_experiments
4
+ from labnotes.open_experiments import open_experiments
5
+ from labnotes.open_runs import open_runs
6
+ from labnotes.scope_runs import scope_runs
7
+
8
+
9
+ async def query_experiment(
10
+ experiments_folder: Path, data_folder: Path, name: str, sql: str | None
11
+ ) -> object:
12
+ experiments = open_experiments(experiments_folder)
13
+ runs = open_runs(data_folder)
14
+ await experiments.ready()
15
+ await runs.ready()
16
+ listed = await list_experiments(experiments, runs)
17
+ experiment = next((row for row in listed if row["name"] == name), None)
18
+ if experiment is None:
19
+ raise LookupError(f"no experiment named {name!r}")
20
+ if sql is None:
21
+ return experiment
22
+ if experiment["study"] is None:
23
+ raise LookupError(f"{name!r} has no study in its design.yaml")
24
+ return await runs.query(scope_runs(sql, str(experiment["study"])))
@@ -0,0 +1,23 @@
1
+ import tomllib
2
+ from pathlib import Path
3
+
4
+ from labnotes.models import LabnotesConfig
5
+
6
+ CONFIG_FILE = ".labnotes.toml"
7
+ DEFAULTS = {"experiments": "experiments", "data": "data"}
8
+
9
+
10
+ def read_config(path: Path) -> LabnotesConfig:
11
+ with path.open("rb") as file:
12
+ values = tomllib.load(file)
13
+ unknown = sorted(values.keys() - DEFAULTS.keys())
14
+ if unknown:
15
+ raise ValueError(f"{path}: unknown keys {', '.join(unknown)}")
16
+ folder = path.resolve().parent
17
+ folders = {}
18
+ for key, default in DEFAULTS.items():
19
+ value = values.get(key, default)
20
+ if not isinstance(value, str):
21
+ raise ValueError(f"{path}: {key} must be a string")
22
+ folders[key] = folder / Path(value).expanduser()
23
+ return LabnotesConfig(path.resolve(), folders["experiments"], folders["data"])
@@ -0,0 +1,14 @@
1
+ import re
2
+
3
+ from labnotes.sql_quote import quote_string
4
+
5
+ LEADING_WITH = re.compile(r"\s*WITH(\s+RECURSIVE)?\s+", re.IGNORECASE)
6
+
7
+
8
+ def scope_runs(sql: str, study: str) -> str:
9
+ runs = f"runs AS (SELECT * FROM main.runs WHERE study = {quote_string(study)})"
10
+ leading_with = LEADING_WITH.match(sql)
11
+ if leading_with is None:
12
+ return f"WITH {runs} {sql}"
13
+ recursive = " RECURSIVE" if leading_with.group(1) else ""
14
+ return f"WITH{recursive} {runs}, {sql[leading_with.end() :]}"
labnotes-0.0.10/README.md DELETED
@@ -1,29 +0,0 @@
1
- # labnotes
2
-
3
- Browse experiment designs, notes, and run data from a folder.
4
-
5
- ```sh
6
- uvx labnotes --experiments-folder experiments --data-folder data
7
- ```
8
-
9
- - `--experiments-folder` holds one subfolder per experiment, each with a
10
- `design.yaml` (with a `study` key) and any number of markdown notes.
11
- - `--data-folder` holds the run data and a `.dirsql.toml` that declares a
12
- `runs` table with a `study` column. An experiment's runs are the rows whose
13
- `study` matches its design's.
14
-
15
- That serves a local site at http://127.0.0.1:8000: a table of experiments,
16
- and a page per experiment with its parameters, its notes, progress per cell of
17
- the design grid, and every run so far. Both folders are watched, so a reload
18
- shows new runs and edited notes.
19
-
20
- `design.yaml` keys the site reads: `study`, `fixed`, `varies` (each key a list
21
- of values), `runs_per_cell`, and `completed_run`, a SQL condition over the
22
- `runs` table that marks a run as complete. Anything else shows up as-is.
23
-
24
- `-h`/`--host` and `-p`/`--port` change where it listens; `-h 0.0.0.0` (or a
25
- Tailscale IP) serves beyond localhost. Every other uvicorn option passes
26
- through: `--reload`, `--workers`, `--proxy-headers`, `--root-path`, the
27
- `--ssl-*` set, and the rest. `labnotes --help` lists them. The folders can also
28
- come from `LABNOTES_EXPERIMENTS_FOLDER` and `LABNOTES_DATA_FOLDER`. `--json`
29
- prints the experiments as JSON and exits.
File without changes
File without changes
File without changes
File without changes
File without changes