labnotes 0.0.10__tar.gz → 0.0.11__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 (51) hide show
  1. {labnotes-0.0.10 → labnotes-0.0.11}/PKG-INFO +20 -1
  2. {labnotes-0.0.10 → labnotes-0.0.11}/README.md +19 -0
  3. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/cli.py +27 -5
  4. labnotes-0.0.11/src/labnotes/context.md +27 -0
  5. labnotes-0.0.11/src/labnotes/context_guide.py +7 -0
  6. labnotes-0.0.11/src/labnotes/query_experiment.py +24 -0
  7. labnotes-0.0.11/src/labnotes/scope_runs.py +14 -0
  8. {labnotes-0.0.10 → labnotes-0.0.11}/.gitignore +0 -0
  9. {labnotes-0.0.10 → labnotes-0.0.11}/CHANGELOG.md +0 -0
  10. {labnotes-0.0.10 → labnotes-0.0.11}/MIGRATIONS.md +0 -0
  11. {labnotes-0.0.10 → labnotes-0.0.11}/pyproject.toml +0 -0
  12. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/__init__.py +0 -0
  13. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/asset_url.py +0 -0
  14. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/cell_progress.py +0 -0
  15. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/count_runs.py +0 -0
  16. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/create_app.py +0 -0
  17. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/data_html.py +0 -0
  18. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/drain_watch.py +0 -0
  19. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/dump_experiments.py +0 -0
  20. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/format_value.py +0 -0
  21. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/list_experiments.py +0 -0
  22. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/list_runs.py +0 -0
  23. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/load_experiment.py +0 -0
  24. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/models.py +0 -0
  25. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/notebook_kernel.py +0 -0
  26. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/open_experiments.py +0 -0
  27. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/open_runs.py +0 -0
  28. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/output_html.py +0 -0
  29. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/parse_run_view.py +0 -0
  30. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/pivot_cells.py +0 -0
  31. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/python_blocks.py +0 -0
  32. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/read_design.py +0 -0
  33. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/read_note.py +0 -0
  34. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/render_markdown.py +0 -0
  35. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/run_columns.py +0 -0
  36. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/saved_outputs.py +0 -0
  37. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/serve_app.py +0 -0
  38. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/sort_href.py +0 -0
  39. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/sql_quote.py +0 -0
  40. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/static/labnotes.css +0 -0
  41. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/static/row-link.js +0 -0
  42. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/static/run-cell.js +0 -0
  43. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/static/view-toggle.js +0 -0
  44. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/stop_idle_kernels.py +0 -0
  45. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/study_progress.py +0 -0
  46. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/summarize_design.py +0 -0
  47. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/templates/base.html +0 -0
  48. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/templates/experiment.html +0 -0
  49. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/templates/index.html +0 -0
  50. {labnotes-0.0.10 → labnotes-0.0.11}/src/labnotes/templates/macros.html +0 -0
  51. {labnotes-0.0.10 → labnotes-0.0.11}/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.11
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>
@@ -46,3 +46,22 @@ through: `--reload`, `--workers`, `--proxy-headers`, `--root-path`, the
46
46
  `--ssl-*` set, and the rest. `labnotes --help` lists them. The folders can also
47
47
  come from `LABNOTES_EXPERIMENTS_FOLDER` and `LABNOTES_DATA_FOLDER`. `--json`
48
48
  prints the experiments as JSON and exits.
49
+
50
+ ## From the command line
51
+
52
+ Name an experiment to print what has run for it as JSON. Add SQL to query
53
+ that experiment's runs: `runs` holds only its runs, `main.runs` every run.
54
+
55
+ ```sh
56
+ uvx labnotes models-effort "SELECT model, COUNT(*) AS n FROM runs GROUP BY 1" \
57
+ --experiments-folder experiments --data-folder data
58
+ ```
59
+
60
+ ## Agent skill
61
+
62
+ ```sh
63
+ npx skills add thekevinscott/labnotes
64
+ ```
65
+
66
+ The skill tells a coding agent to run `labnotes context`, which prints a usage
67
+ guide for the installed version.
@@ -27,3 +27,22 @@ through: `--reload`, `--workers`, `--proxy-headers`, `--root-path`, the
27
27
  `--ssl-*` set, and the rest. `labnotes --help` lists them. The folders can also
28
28
  come from `LABNOTES_EXPERIMENTS_FOLDER` and `LABNOTES_DATA_FOLDER`. `--json`
29
29
  prints the experiments as JSON and exits.
30
+
31
+ ## From the command line
32
+
33
+ Name an experiment to print what has run for it as JSON. Add SQL to query
34
+ that experiment's runs: `runs` holds only its runs, `main.runs` every run.
35
+
36
+ ```sh
37
+ uvx labnotes models-effort "SELECT model, COUNT(*) AS n FROM runs GROUP BY 1" \
38
+ --experiments-folder experiments --data-folder data
39
+ ```
40
+
41
+ ## Agent skill
42
+
43
+ ```sh
44
+ npx skills add thekevinscott/labnotes
45
+ ```
46
+
47
+ The skill tells a coding agent to run `labnotes context`, which prints a usage
48
+ guide for the installed version.
@@ -6,26 +6,32 @@ 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
10
12
  from labnotes.serve_app import DATA_FOLDER, EXPERIMENTS_FOLDER, KERNEL_IDLE_MINUTES
11
13
 
12
14
  # labnotes owns these; the rest of uvicorn's options pass straight through.
13
15
  OWN_OPTIONS = {"app", "factory", "app_dir", "version", "host", "port"}
14
16
 
15
17
 
16
- @click.command(help="Browse experiments and their run data.")
18
+ @click.command(
19
+ help="Browse experiments and their run data. With EXPERIMENT, print what has"
20
+ " run as JSON; with SQL too, query that experiment's runs. `labnotes context`"
21
+ " prints a guide for coding agents."
22
+ )
17
23
  @click.version_option(package_name="labnotes")
24
+ @click.argument("experiment", required=False)
25
+ @click.argument("sql", required=False)
18
26
  @click.option(
19
27
  "--experiments-folder",
20
28
  type=click.Path(path_type=Path),
21
- required=True,
22
29
  envvar=EXPERIMENTS_FOLDER,
23
30
  help="One subfolder per experiment, each with a design.yaml and markdown notes.",
24
31
  )
25
32
  @click.option(
26
33
  "--data-folder",
27
34
  type=click.Path(path_type=Path),
28
- required=True,
29
35
  envvar=DATA_FOLDER,
30
36
  help="Run data, with a .dirsql.toml declaring a runs table that has a study column.",
31
37
  )
@@ -54,12 +60,28 @@ OWN_OPTIONS = {"app", "factory", "app_dir", "version", "host", "port"}
54
60
  @click.pass_context
55
61
  def main(
56
62
  ctx: click.Context,
57
- experiments_folder: Path,
58
- data_folder: Path,
63
+ experiment: str | None,
64
+ sql: str | None,
65
+ experiments_folder: Path | None,
66
+ data_folder: Path | None,
59
67
  kernel_idle_minutes: int,
60
68
  as_json: bool,
61
69
  **server_options: object,
62
70
  ) -> None:
71
+ if experiment == "context" and sql is None:
72
+ click.echo(context_guide(), nl=False)
73
+ return
74
+ if experiments_folder is None or data_folder is None:
75
+ raise click.UsageError("--experiments-folder and --data-folder are required.")
76
+ if experiment is not None:
77
+ try:
78
+ result = asyncio.run(
79
+ query_experiment(experiments_folder, data_folder, experiment, sql)
80
+ )
81
+ except (LookupError, RuntimeError) as error:
82
+ raise click.ClickException(str(error)) from error
83
+ click.echo(json.dumps(result, indent=2))
84
+ return
63
85
  if as_json:
64
86
  experiments = asyncio.run(dump_experiments(experiments_folder, data_folder))
65
87
  click.echo(json.dumps(experiments, indent=2))
@@ -0,0 +1,27 @@
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. Pass both on every call, or set `LABNOTES_EXPERIMENTS_FOLDER` and `LABNOTES_DATA_FOLDER`:
4
+
5
+ ```sh
6
+ uvx labnotes --experiments-folder experiments --data-folder data --json
7
+ ```
8
+
9
+ `--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.
10
+
11
+ ### One experiment
12
+
13
+ Name an experiment to see what has run for it, as JSON:
14
+
15
+ ```sh
16
+ uvx labnotes models-effort --experiments-folder experiments --data-folder data
17
+ ```
18
+
19
+ 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:
20
+
21
+ ```sh
22
+ uvx labnotes models-effort "SELECT model, effort, COUNT(*) AS n FROM runs GROUP BY 1, 2" --experiments-folder experiments --data-folder data
23
+ ```
24
+
25
+ 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`.
26
+
27
+ 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,7 @@
1
+ from importlib.metadata import version
2
+ from importlib.resources import files
3
+
4
+
5
+ def context_guide() -> str:
6
+ guide = files("labnotes").joinpath("context.md").read_text(encoding="utf-8")
7
+ return f"labnotes {version('labnotes')}\n\n{guide}"
@@ -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,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() :]}"
File without changes
File without changes
File without changes
File without changes
File without changes