labnotes 0.0.19__tar.gz → 0.0.20__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.19 → labnotes-0.0.20}/PKG-INFO +9 -21
- {labnotes-0.0.19 → labnotes-0.0.20}/README.md +8 -19
- {labnotes-0.0.19 → labnotes-0.0.20}/pyproject.toml +0 -2
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/context.md +1 -9
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/create_app.py +22 -26
- labnotes-0.0.20/src/labnotes/experiment_notes.py +23 -0
- labnotes-0.0.20/src/labnotes/experiment_progress.py +25 -0
- labnotes-0.0.20/src/labnotes/experiment_runs.py +13 -0
- labnotes-0.0.20/src/labnotes/list_experiments.py +26 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/list_runs.py +1 -1
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/load_experiment.py +22 -29
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/serve_app.py +3 -5
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/server.py +11 -20
- labnotes-0.0.20/src/labnotes/summarize_config.py +26 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/templates/experiment.html +7 -12
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/templates/index.html +3 -3
- {labnotes-0.0.19 → labnotes-0.0.20}/uv.lock +0 -59
- labnotes-0.0.19/src/labnotes/list_experiments.py +0 -23
- labnotes-0.0.19/src/labnotes/open_experiments.py +0 -26
- labnotes-0.0.19/src/labnotes/open_runs.py +0 -13
- labnotes-0.0.19/src/labnotes/read_design.py +0 -15
- labnotes-0.0.19/src/labnotes/read_note.py +0 -12
- labnotes-0.0.19/src/labnotes/run_columns.py +0 -6
- labnotes-0.0.19/src/labnotes/study_progress.py +0 -38
- labnotes-0.0.19/src/labnotes/study_runs.py +0 -5
- labnotes-0.0.19/src/labnotes/summarize_design.py +0 -23
- {labnotes-0.0.19 → labnotes-0.0.20}/.gitignore +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/CHANGELOG.md +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/MIGRATIONS.md +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/init.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/models.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/lock_path.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/stop_idle_kernels.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/templates/macros.html +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tui/run_app.py +0 -0
- {labnotes-0.0.19 → labnotes-0.0.20}/src/labnotes/tui/run_tui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labnotes
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.20
|
|
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>
|
|
@@ -14,7 +14,6 @@ Requires-Dist: ipykernel>=7.4.0
|
|
|
14
14
|
Requires-Dist: jinja2>=3.1.6
|
|
15
15
|
Requires-Dist: jupyter-client>=8.10.0
|
|
16
16
|
Requires-Dist: markdown-it-py>=4.2.0
|
|
17
|
-
Requires-Dist: pyyaml>=6.0.3
|
|
18
17
|
Requires-Dist: rich>=14
|
|
19
18
|
Requires-Dist: starlette>=1.8.0
|
|
20
19
|
Requires-Dist: textual>=8.2.8
|
|
@@ -114,29 +113,18 @@ line per event instead.
|
|
|
114
113
|
- A second `run` of the same experiment fails while the first holds the lock.
|
|
115
114
|
- It exits 1 if any run failed.
|
|
116
115
|
|
|
117
|
-
`labnotes server`
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
`study` matches its design's.
|
|
125
|
-
|
|
126
|
-
That serves a local site at http://127.0.0.1:8000: a table of experiments,
|
|
127
|
-
and a page per experiment with its parameters, its notes, progress per cell of
|
|
128
|
-
the design grid, and every run so far. Both folders are watched, so a reload
|
|
129
|
-
shows new runs and edited notes.
|
|
130
|
-
|
|
131
|
-
`design.yaml` keys the site reads: `study`, `fixed`, `varies` (each key a list
|
|
132
|
-
of values), `runs_per_cell`, and `completed_run`, a SQL condition over the
|
|
133
|
-
`runs` table that marks a run as complete. Anything else shows up as-is.
|
|
116
|
+
`labnotes server` serves a local site at http://127.0.0.1:8000 from the same
|
|
117
|
+
`.labnotes.toml`: a table of experiments, and a page per experiment with its
|
|
118
|
+
params, its markdown files, progress per cell of the params grid, and every
|
|
119
|
+
run so far. A cell is full once it has `repeats` runs that exited 0 and aren't
|
|
120
|
+
excluded, the same count `run` uses. The project is watched, so a reload shows
|
|
121
|
+
new runs and edited notes. Python blocks in a note run with the root as the
|
|
122
|
+
working folder.
|
|
134
123
|
|
|
135
124
|
`-h`/`--host` and `-p`/`--port` change where it listens; `-h 0.0.0.0` (or a
|
|
136
125
|
Tailscale IP) serves beyond localhost. Every other uvicorn option passes
|
|
137
126
|
through: `--reload`, `--workers`, `--proxy-headers`, `--root-path`, the
|
|
138
|
-
`--ssl-*` set, and the rest. `labnotes server --help` lists them.
|
|
139
|
-
`LABNOTES_DATA_FOLDER` can stand in for the folder flags.
|
|
127
|
+
`--ssl-*` set, and the rest. `labnotes server --help` lists them.
|
|
140
128
|
|
|
141
129
|
## Agent skill
|
|
142
130
|
|
|
@@ -91,29 +91,18 @@ line per event instead.
|
|
|
91
91
|
- A second `run` of the same experiment fails while the first holds the lock.
|
|
92
92
|
- It exits 1 if any run failed.
|
|
93
93
|
|
|
94
|
-
`labnotes server`
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
`study` matches its design's.
|
|
102
|
-
|
|
103
|
-
That serves a local site at http://127.0.0.1:8000: a table of experiments,
|
|
104
|
-
and a page per experiment with its parameters, its notes, progress per cell of
|
|
105
|
-
the design grid, and every run so far. Both folders are watched, so a reload
|
|
106
|
-
shows new runs and edited notes.
|
|
107
|
-
|
|
108
|
-
`design.yaml` keys the site reads: `study`, `fixed`, `varies` (each key a list
|
|
109
|
-
of values), `runs_per_cell`, and `completed_run`, a SQL condition over the
|
|
110
|
-
`runs` table that marks a run as complete. Anything else shows up as-is.
|
|
94
|
+
`labnotes server` serves a local site at http://127.0.0.1:8000 from the same
|
|
95
|
+
`.labnotes.toml`: a table of experiments, and a page per experiment with its
|
|
96
|
+
params, its markdown files, progress per cell of the params grid, and every
|
|
97
|
+
run so far. A cell is full once it has `repeats` runs that exited 0 and aren't
|
|
98
|
+
excluded, the same count `run` uses. The project is watched, so a reload shows
|
|
99
|
+
new runs and edited notes. Python blocks in a note run with the root as the
|
|
100
|
+
working folder.
|
|
111
101
|
|
|
112
102
|
`-h`/`--host` and `-p`/`--port` change where it listens; `-h 0.0.0.0` (or a
|
|
113
103
|
Tailscale IP) serves beyond localhost. Every other uvicorn option passes
|
|
114
104
|
through: `--reload`, `--workers`, `--proxy-headers`, `--root-path`, the
|
|
115
|
-
`--ssl-*` set, and the rest. `labnotes server --help` lists them.
|
|
116
|
-
`LABNOTES_DATA_FOLDER` can stand in for the folder flags.
|
|
105
|
+
`--ssl-*` set, and the rest. `labnotes server --help` lists them.
|
|
117
106
|
|
|
118
107
|
## Agent skill
|
|
119
108
|
|
|
@@ -19,7 +19,6 @@ dependencies = [
|
|
|
19
19
|
"jinja2>=3.1.6",
|
|
20
20
|
"jupyter-client>=8.10.0",
|
|
21
21
|
"markdown-it-py>=4.2.0",
|
|
22
|
-
"pyyaml>=6.0.3",
|
|
23
22
|
"rich>=14",
|
|
24
23
|
"starlette>=1.8.0",
|
|
25
24
|
"textual>=8.2.8",
|
|
@@ -38,7 +37,6 @@ dev = [
|
|
|
38
37
|
"mypy>=2.4.0",
|
|
39
38
|
"pytest>=8.0",
|
|
40
39
|
"ruff==0.8.4",
|
|
41
|
-
"types-pyyaml>=6.0.12.20260906",
|
|
42
40
|
]
|
|
43
41
|
|
|
44
42
|
[tool.hatch.version]
|
|
@@ -89,12 +89,4 @@ line per event instead.
|
|
|
89
89
|
- A second `run` of the same experiment fails while the first holds the lock.
|
|
90
90
|
- It exits 1 if any run failed.
|
|
91
91
|
|
|
92
|
-
`labnotes server`
|
|
93
|
-
|
|
94
|
-
The run data lives in the data folder. Query it with [dirsql](https://dirsql.dev) and the data folder's `.dirsql.toml`, which declares the `runs` table:
|
|
95
|
-
|
|
96
|
-
```sh
|
|
97
|
-
uvx dirsql -c data/.dirsql.toml "SELECT * FROM runs LIMIT 1" --format json
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
`uvx labnotes server` serves the experiments and their runs as a local site.
|
|
92
|
+
`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.
|
|
@@ -14,17 +14,17 @@ from starlette.templating import Jinja2Templates
|
|
|
14
14
|
|
|
15
15
|
from labnotes.asset_url import asset_url
|
|
16
16
|
from labnotes.drain_watch import drain_watch
|
|
17
|
+
from labnotes.experiment_notes import experiment_notes
|
|
17
18
|
from labnotes.format_value import format_value
|
|
18
19
|
from labnotes.list_experiments import list_experiments
|
|
19
20
|
from labnotes.load_experiment import load_experiment
|
|
21
|
+
from labnotes.models import LabnotesConfig
|
|
20
22
|
from labnotes.notebook_kernel import NotebookKernel
|
|
21
|
-
from labnotes.open_experiments import open_experiments
|
|
22
|
-
from labnotes.open_runs import open_runs
|
|
23
23
|
from labnotes.pivot_cells import pivot_cells
|
|
24
24
|
from labnotes.python_blocks import python_blocks
|
|
25
25
|
from labnotes.saved_outputs import saved_outputs
|
|
26
|
-
from labnotes.sql_quote import quote_string
|
|
27
26
|
from labnotes.stop_idle_kernels import stop_idle_kernels
|
|
27
|
+
from labnotes.tables.open_tables import open_tables
|
|
28
28
|
|
|
29
29
|
HERE = Path(__file__).parent
|
|
30
30
|
templates = Jinja2Templates(directory=HERE / "templates")
|
|
@@ -32,20 +32,15 @@ templates.env.filters["value"] = format_value
|
|
|
32
32
|
templates.env.globals["asset"] = asset_url
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def create_app(
|
|
36
|
-
experiments_folder: Path, data_folder: Path, kernel_idle_minutes: float
|
|
37
|
-
) -> Starlette:
|
|
35
|
+
def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
|
|
38
36
|
kernels: dict[tuple[str, str], NotebookKernel] = {}
|
|
39
37
|
|
|
40
38
|
@asynccontextmanager
|
|
41
39
|
async def lifespan(app: Starlette) -> AsyncIterator[None]:
|
|
42
|
-
app.state.
|
|
43
|
-
app.state.
|
|
44
|
-
await app.state.experiments.ready()
|
|
45
|
-
await app.state.runs.ready()
|
|
40
|
+
app.state.tables = open_tables(config)
|
|
41
|
+
await app.state.tables.ready()
|
|
46
42
|
watchers = [
|
|
47
|
-
asyncio.create_task(drain_watch(app.state.
|
|
48
|
-
asyncio.create_task(drain_watch(app.state.runs)),
|
|
43
|
+
asyncio.create_task(drain_watch(app.state.tables)),
|
|
49
44
|
asyncio.create_task(stop_idle_kernels(kernels, kernel_idle_minutes)),
|
|
50
45
|
]
|
|
51
46
|
yield
|
|
@@ -55,20 +50,18 @@ def create_app(
|
|
|
55
50
|
await kernel.stop()
|
|
56
51
|
|
|
57
52
|
async def index(request: Request) -> Response:
|
|
58
|
-
|
|
59
|
-
experiments = await list_experiments(state.experiments, state.runs)
|
|
53
|
+
experiments = await list_experiments(request.app.state.tables)
|
|
60
54
|
return templates.TemplateResponse(
|
|
61
55
|
request,
|
|
62
56
|
"index.html",
|
|
63
|
-
{"experiments": experiments, "folder":
|
|
57
|
+
{"experiments": experiments, "folder": config.root.name},
|
|
64
58
|
)
|
|
65
59
|
|
|
66
60
|
async def experiment(request: Request) -> Response:
|
|
67
|
-
state = request.app.state
|
|
68
61
|
name = request.path_params["name"]
|
|
69
62
|
loaded = await load_experiment(
|
|
70
|
-
state.
|
|
71
|
-
|
|
63
|
+
request.app.state.tables,
|
|
64
|
+
config.root,
|
|
72
65
|
name,
|
|
73
66
|
request.query_params,
|
|
74
67
|
lambda note, content: saved_outputs(kernels.get((name, note)), content),
|
|
@@ -88,14 +81,13 @@ def create_app(
|
|
|
88
81
|
raise HTTPException(415, "Send JSON")
|
|
89
82
|
name = request.path_params["name"]
|
|
90
83
|
note = request.path_params["note"]
|
|
91
|
-
found = await
|
|
92
|
-
|
|
93
|
-
f" WHERE experiment = {quote_string(name)} AND name = {quote_string(note)}"
|
|
84
|
+
found = await experiment_notes(
|
|
85
|
+
request.app.state.tables, config.root, name, note
|
|
94
86
|
)
|
|
95
87
|
if not found:
|
|
96
88
|
raise HTTPException(404, "No such note")
|
|
97
|
-
[
|
|
98
|
-
return
|
|
89
|
+
[note] = found
|
|
90
|
+
return note["content"]
|
|
99
91
|
|
|
100
92
|
async def run_block(request: Request) -> Response:
|
|
101
93
|
blocks = python_blocks(await note_content(request))
|
|
@@ -104,7 +96,7 @@ def create_app(
|
|
|
104
96
|
if not isinstance(block, int) or not 0 <= block < len(blocks):
|
|
105
97
|
raise HTTPException(400, "No such block")
|
|
106
98
|
if key not in kernels:
|
|
107
|
-
kernels[key] = NotebookKernel(
|
|
99
|
+
kernels[key] = NotebookKernel(config.root)
|
|
108
100
|
return HTMLResponse(await kernels[key].run(blocks[block]))
|
|
109
101
|
|
|
110
102
|
async def restart_kernel(request: Request) -> Response:
|
|
@@ -118,9 +110,13 @@ def create_app(
|
|
|
118
110
|
routes=[
|
|
119
111
|
Route("/", index),
|
|
120
112
|
Route("/experiments/{name}", experiment),
|
|
121
|
-
Route("/experiments/{name}/notes/{note}/run", run_block, methods=["POST"]),
|
|
122
113
|
Route(
|
|
123
|
-
"/experiments/{name}/notes/{note}/
|
|
114
|
+
"/experiments/{name}/notes/{note:path}/run",
|
|
115
|
+
run_block,
|
|
116
|
+
methods=["POST"],
|
|
117
|
+
),
|
|
118
|
+
Route(
|
|
119
|
+
"/experiments/{name}/notes/{note:path}/restart",
|
|
124
120
|
restart_kernel,
|
|
125
121
|
methods=["POST"],
|
|
126
122
|
),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from dirsql import DirSQL
|
|
4
|
+
|
|
5
|
+
from labnotes.sql_quote import quote_string
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def experiment_notes(
|
|
9
|
+
tables: DirSQL, root: Path, experiment: str, note: str | None = None
|
|
10
|
+
) -> list[dict[str, str]]:
|
|
11
|
+
where = f"experiment = {quote_string(experiment)} AND filename LIKE '%.md'"
|
|
12
|
+
if note is not None:
|
|
13
|
+
where += f" AND filename = {quote_string(note)}"
|
|
14
|
+
rows = await tables.query(
|
|
15
|
+
f"SELECT filename, path FROM files WHERE {where} ORDER BY filename"
|
|
16
|
+
)
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
"name": str(row["filename"]),
|
|
20
|
+
"content": (root / str(row["path"])).read_text(encoding="utf-8"),
|
|
21
|
+
}
|
|
22
|
+
for row in rows
|
|
23
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
from dirsql import DirSQL
|
|
4
|
+
|
|
5
|
+
from labnotes.cell_progress import cell_progress
|
|
6
|
+
from labnotes.count_runs import count_runs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def experiment_progress(
|
|
10
|
+
tables: DirSQL, source: str, summary: dict[str, object]
|
|
11
|
+
) -> dict[str, object]:
|
|
12
|
+
varies = cast(dict[str, list[object]], summary["varies"])
|
|
13
|
+
completed_run = str(summary["completed_run"])
|
|
14
|
+
runs_per_cell = summary["runs_per_cell"]
|
|
15
|
+
[totals] = await count_runs(tables, source, completed_run)
|
|
16
|
+
progress: dict[str, object] = {**totals, "filled": None, "cells": []}
|
|
17
|
+
if runs_per_cell is None:
|
|
18
|
+
return progress
|
|
19
|
+
grouped = await count_runs(tables, source, completed_run, group_by=list(varies))
|
|
20
|
+
cells = cell_progress(varies, grouped)
|
|
21
|
+
progress["cells"] = cells
|
|
22
|
+
progress["filled"] = sum(
|
|
23
|
+
min(cast(int, cell["completed"]), cast(int, runs_per_cell)) for cell in cells
|
|
24
|
+
)
|
|
25
|
+
return progress
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from labnotes.sql_quote import quote_identifier, quote_string
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def experiment_runs(experiment: str, params: list[str]) -> str:
|
|
5
|
+
columns = "".join(
|
|
6
|
+
f", params ->> {quote_string(f'$.{quote_identifier(key)}')}"
|
|
7
|
+
f" AS {quote_identifier(key)}"
|
|
8
|
+
for key in params
|
|
9
|
+
)
|
|
10
|
+
return (
|
|
11
|
+
f"(SELECT *{columns} FROM runs"
|
|
12
|
+
f" WHERE experiment = {quote_string(experiment)})"
|
|
13
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
from dirsql import DirSQL
|
|
5
|
+
|
|
6
|
+
from labnotes.experiment_progress import experiment_progress
|
|
7
|
+
from labnotes.experiment_runs import experiment_runs
|
|
8
|
+
from labnotes.summarize_config import summarize_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def list_experiments(tables: DirSQL) -> list[dict[str, object]]:
|
|
12
|
+
rows = await tables.query(
|
|
13
|
+
"SELECT name, display_name, config FROM experiments ORDER BY name"
|
|
14
|
+
)
|
|
15
|
+
listed = []
|
|
16
|
+
for row in rows:
|
|
17
|
+
name = str(row["name"])
|
|
18
|
+
config = None if row["config"] is None else json.loads(str(row["config"]))
|
|
19
|
+
summary = summarize_config(config)
|
|
20
|
+
varies = cast(dict[str, list[object]], summary["varies"])
|
|
21
|
+
source = experiment_runs(name, list(varies))
|
|
22
|
+
progress = await experiment_progress(tables, source, summary)
|
|
23
|
+
listed.append(
|
|
24
|
+
{"name": name, "display_name": row["display_name"], **summary, **progress}
|
|
25
|
+
)
|
|
26
|
+
return listed
|
|
@@ -23,7 +23,7 @@ async def list_runs(
|
|
|
23
23
|
elif view.sort:
|
|
24
24
|
order = f"{quote_identifier(view.sort)} {direction}"
|
|
25
25
|
else:
|
|
26
|
-
order = "
|
|
26
|
+
order = "created DESC, id DESC"
|
|
27
27
|
return await runs.query(
|
|
28
28
|
f"SELECT ({completed_run}) AS labnotes_completed, * FROM {source}"
|
|
29
29
|
f"{filtered} ORDER BY {order}"
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from collections.abc import Callable, Mapping, Sequence
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
from typing import cast
|
|
4
5
|
|
|
5
6
|
from dirsql import DirSQL
|
|
6
7
|
|
|
8
|
+
from labnotes.experiment_notes import experiment_notes
|
|
9
|
+
from labnotes.experiment_progress import experiment_progress
|
|
10
|
+
from labnotes.experiment_runs import experiment_runs
|
|
7
11
|
from labnotes.list_runs import list_runs
|
|
8
12
|
from labnotes.parse_run_view import parse_run_view
|
|
9
13
|
from labnotes.python_blocks import python_blocks
|
|
10
14
|
from labnotes.render_markdown import render_markdown
|
|
11
|
-
from labnotes.run_columns import run_columns
|
|
12
15
|
from labnotes.sort_href import sort_href
|
|
13
16
|
from labnotes.sql_quote import quote_string
|
|
14
|
-
from labnotes.
|
|
15
|
-
from labnotes.study_runs import study_runs
|
|
16
|
-
from labnotes.summarize_design import summarize_design
|
|
17
|
+
from labnotes.summarize_config import summarize_config
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _no_outputs(note: str, content: str) -> Sequence[str]:
|
|
@@ -21,53 +22,45 @@ def _no_outputs(note: str, content: str) -> Sequence[str]:
|
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
async def load_experiment(
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
tables: DirSQL,
|
|
26
|
+
root: Path,
|
|
26
27
|
name: str,
|
|
27
28
|
params: Mapping[str, str],
|
|
28
29
|
outputs_for: Callable[[str, str], Sequence[str]] = _no_outputs,
|
|
29
30
|
) -> dict[str, object] | None:
|
|
30
|
-
found = await
|
|
31
|
-
|
|
31
|
+
found = await tables.query(
|
|
32
|
+
"SELECT name, display_name, config FROM experiments"
|
|
33
|
+
f" WHERE name = {quote_string(name)}"
|
|
32
34
|
)
|
|
33
35
|
if not found:
|
|
34
36
|
return None
|
|
35
37
|
[row] = found
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
f" WHERE experiment = {quote_string(name)} ORDER BY name"
|
|
39
|
-
)
|
|
40
|
-
summary = summarize_design(json.loads(str(row["design"])))
|
|
38
|
+
config = None if row["config"] is None else json.loads(str(row["config"]))
|
|
39
|
+
summary = summarize_config(config)
|
|
41
40
|
varies = cast(dict[str, list[object]], summary["varies"])
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
shown = columns[:1] + [key for key in varies if key in columns[1:]]
|
|
41
|
+
source = experiment_runs(name, list(varies))
|
|
42
|
+
progress = await experiment_progress(tables, source, summary)
|
|
43
|
+
shown = ["id", *(key for key in varies if key != "id")]
|
|
46
44
|
view = parse_run_view(params, shown)
|
|
47
|
-
|
|
48
|
-
[]
|
|
49
|
-
if study is None
|
|
50
|
-
else await list_runs(
|
|
51
|
-
runs, study_runs(study), str(summary["completed_run"]), view
|
|
52
|
-
)
|
|
53
|
-
)
|
|
45
|
+
notes = await experiment_notes(tables, root, name)
|
|
54
46
|
return {
|
|
55
47
|
"name": row["name"],
|
|
56
|
-
"
|
|
48
|
+
"display_name": row["display_name"],
|
|
57
49
|
**summary,
|
|
58
50
|
**progress,
|
|
59
51
|
"notes": [
|
|
60
52
|
{
|
|
61
53
|
"name": note["name"],
|
|
62
54
|
"html": render_markdown(
|
|
63
|
-
|
|
64
|
-
outputs_for(str(note["name"]), str(note["content"])),
|
|
55
|
+
note["content"], outputs_for(note["name"], note["content"])
|
|
65
56
|
),
|
|
66
|
-
"runnable": bool(python_blocks(
|
|
57
|
+
"runnable": bool(python_blocks(note["content"])),
|
|
67
58
|
}
|
|
68
59
|
for note in notes
|
|
69
60
|
],
|
|
70
|
-
"runs_list":
|
|
61
|
+
"runs_list": await list_runs(
|
|
62
|
+
tables, source, str(summary["completed_run"]), view
|
|
63
|
+
),
|
|
71
64
|
"run_columns": shown,
|
|
72
65
|
"run_view": view,
|
|
73
66
|
"sort_hrefs": {
|
|
@@ -4,15 +4,13 @@ from pathlib import Path
|
|
|
4
4
|
from starlette.applications import Starlette
|
|
5
5
|
|
|
6
6
|
from labnotes.create_app import create_app
|
|
7
|
+
from labnotes.read_config import read_config
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
DATA_FOLDER = "LABNOTES_DATA_FOLDER"
|
|
9
|
+
CONFIG = "LABNOTES_CONFIG"
|
|
10
10
|
KERNEL_IDLE_MINUTES = "LABNOTES_KERNEL_IDLE_MINUTES"
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def serve_app() -> Starlette:
|
|
14
14
|
return create_app(
|
|
15
|
-
Path(os.environ[
|
|
16
|
-
Path(os.environ[DATA_FOLDER]),
|
|
17
|
-
int(os.environ[KERNEL_IDLE_MINUTES]),
|
|
15
|
+
read_config(Path(os.environ[CONFIG])), int(os.environ[KERNEL_IDLE_MINUTES])
|
|
18
16
|
)
|
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from pathlib import Path
|
|
3
2
|
|
|
4
3
|
import click
|
|
5
4
|
from uvicorn.main import main as uvicorn_main
|
|
6
5
|
|
|
7
|
-
from labnotes.
|
|
6
|
+
from labnotes.load_config import load_config
|
|
7
|
+
from labnotes.read_config import CONFIG_FILE
|
|
8
|
+
from labnotes.serve_app import CONFIG, KERNEL_IDLE_MINUTES
|
|
8
9
|
|
|
9
10
|
# labnotes owns these; the rest of uvicorn's options pass straight through.
|
|
10
11
|
OWN_OPTIONS = {"app", "factory", "app_dir", "version", "host", "port"}
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
@click.command(
|
|
14
|
-
|
|
15
|
-
"--experiments-folder",
|
|
16
|
-
type=click.Path(path_type=Path),
|
|
17
|
-
envvar=EXPERIMENTS_FOLDER,
|
|
18
|
-
required=True,
|
|
19
|
-
help="One subfolder per experiment, each with a design.yaml and markdown notes.",
|
|
20
|
-
)
|
|
21
|
-
@click.option(
|
|
22
|
-
"--data-folder",
|
|
23
|
-
type=click.Path(path_type=Path),
|
|
24
|
-
envvar=DATA_FOLDER,
|
|
25
|
-
required=True,
|
|
26
|
-
help="Run data, with a .dirsql.toml declaring a runs table that has a study column.",
|
|
14
|
+
@click.command(
|
|
15
|
+
help="Serve the experiments, their notes and their runs as a local site."
|
|
27
16
|
)
|
|
28
17
|
@click.option(
|
|
29
18
|
"--kernel-idle-minutes",
|
|
@@ -44,14 +33,16 @@ OWN_OPTIONS = {"app", "factory", "app_dir", "version", "host", "port"}
|
|
|
44
33
|
@click.pass_context
|
|
45
34
|
def server(
|
|
46
35
|
ctx: click.Context,
|
|
47
|
-
experiments_folder: Path,
|
|
48
|
-
data_folder: Path,
|
|
49
36
|
kernel_idle_minutes: int,
|
|
50
37
|
**server_options: object,
|
|
51
38
|
) -> None:
|
|
39
|
+
config = load_config(ctx)
|
|
40
|
+
if config is None:
|
|
41
|
+
raise click.ClickException(
|
|
42
|
+
f"No {CONFIG_FILE} here. Run labnotes init, or pass -c PATH."
|
|
43
|
+
)
|
|
52
44
|
# Through the environment so --reload and --workers child processes see them.
|
|
53
|
-
os.environ[
|
|
54
|
-
os.environ[DATA_FOLDER] = str(data_folder.resolve())
|
|
45
|
+
os.environ[CONFIG] = str(config.path.resolve())
|
|
55
46
|
os.environ[KERNEL_IDLE_MINUTES] = str(kernel_idle_minutes)
|
|
56
47
|
ctx.invoke(
|
|
57
48
|
uvicorn_main,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from math import prod
|
|
2
|
+
|
|
3
|
+
# What the runner counts toward repeats.
|
|
4
|
+
COMPLETED_RUN = "exit_code = 0 AND NOT coalesce(excluded, 0)"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def summarize_config(config: dict[str, object] | None) -> dict[str, object]:
|
|
8
|
+
config = config or {}
|
|
9
|
+
params = config.get("params")
|
|
10
|
+
varies = {
|
|
11
|
+
key: values if isinstance(values, list) else [values]
|
|
12
|
+
for key, values in (params if isinstance(params, dict) else {}).items()
|
|
13
|
+
}
|
|
14
|
+
repeats = config.get("repeats", 1) if config else None
|
|
15
|
+
repeats = repeats if isinstance(repeats, int) else None
|
|
16
|
+
cells = prod(len(values) for values in varies.values())
|
|
17
|
+
command = config.get("command")
|
|
18
|
+
return {
|
|
19
|
+
"fixed": {},
|
|
20
|
+
"varies": varies,
|
|
21
|
+
"cell_count": cells,
|
|
22
|
+
"runs_per_cell": repeats,
|
|
23
|
+
"target": cells * repeats if repeats is not None else None,
|
|
24
|
+
"completed_run": COMPLETED_RUN,
|
|
25
|
+
"other": {} if command is None else {"command": command},
|
|
26
|
+
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{% extends "base.html" %}
|
|
2
2
|
{% from "macros.html" import meter, percent %}
|
|
3
3
|
{% set e = experiment %}
|
|
4
|
-
{% block title %}{{ e.
|
|
5
|
-
{% block crumbs %}<span class="crumb"><a href="{{ url_for('index') }}">Experiments</a></span><span class="crumb">{{ e.
|
|
4
|
+
{% block title %}{{ e.display_name }} · labnotes{% endblock %}
|
|
5
|
+
{% block crumbs %}<span class="crumb"><a href="{{ url_for('index') }}">Experiments</a></span><span class="crumb">{{ e.display_name }}</span>{% endblock %}
|
|
6
6
|
{% block main %}
|
|
7
7
|
<section class="intro">
|
|
8
|
-
<h1>{{ e.
|
|
9
|
-
{% if e.
|
|
8
|
+
<h1>{{ e.display_name }}</h1>
|
|
9
|
+
{% if e.display_name != e.name %}<p class="study">{{ e.name }}</p>{% endif %}
|
|
10
10
|
<nav class="toc">
|
|
11
11
|
<a href="#design">Design</a>
|
|
12
12
|
{% if pivot %}<a href="#progress">Progress</a>{% endif %}
|
|
13
13
|
{% if e.notes %}<a href="#notes">Notes</a>{% endif %}
|
|
14
|
-
|
|
14
|
+
<a href="#runs">Runs</a>
|
|
15
15
|
</nav>
|
|
16
16
|
</section>
|
|
17
17
|
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
<div class="figure">
|
|
33
33
|
<div class="figure-label">Cells</div>
|
|
34
34
|
<div class="figure-value">{{ e.cell_count }}</div>
|
|
35
|
-
<div class="figure-note">{% if e.runs_per_cell %}<span class="num">{{ e.runs_per_cell }}</span> runs each{% else %}no
|
|
35
|
+
<div class="figure-note">{% if e.runs_per_cell %}<span class="num">{{ e.runs_per_cell }}</span> runs each{% else %}no experiment.toml{% endif %}</div>
|
|
36
36
|
</div>
|
|
37
37
|
</section>
|
|
38
38
|
|
|
@@ -68,9 +68,6 @@
|
|
|
68
68
|
{% endfor %}
|
|
69
69
|
<dt>complete when</dt><dd><code>{{ e.completed_run }}</code></dd>
|
|
70
70
|
</dl>
|
|
71
|
-
{% if e.missing_columns %}
|
|
72
|
-
<p class="notice">The runs table has no column for {{ e.missing_columns | join(', ') }}, so progress per cell can't be shown.</p>
|
|
73
|
-
{% endif %}
|
|
74
71
|
</section>
|
|
75
72
|
|
|
76
73
|
{% if pivot %}
|
|
@@ -120,7 +117,6 @@
|
|
|
120
117
|
<script src="{{ asset('run-cell.js') }}" defer></script>
|
|
121
118
|
{% endif %}
|
|
122
119
|
|
|
123
|
-
{% if e.study %}
|
|
124
120
|
<section id="runs" class="block">
|
|
125
121
|
{% set view = e.run_view %}
|
|
126
122
|
{% set narrowed = view.status or view.filters %}
|
|
@@ -164,8 +160,7 @@
|
|
|
164
160
|
{% endfor %}
|
|
165
161
|
</div>
|
|
166
162
|
{% else %}
|
|
167
|
-
<p class="empty">No runs recorded
|
|
163
|
+
<p class="empty">No runs recorded yet.</p>
|
|
168
164
|
{% endif %}
|
|
169
165
|
</section>
|
|
170
|
-
{% endif %}
|
|
171
166
|
{% endblock %}
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
{% for experiment in experiments %}
|
|
29
29
|
<tr>
|
|
30
30
|
<td>
|
|
31
|
-
<a class="title-link" href="{{ url_for('experiment', name=experiment.name) }}">{{ experiment.
|
|
32
|
-
{% if experiment.
|
|
31
|
+
<a class="title-link" href="{{ url_for('experiment', name=experiment.name) }}">{{ experiment.display_name }}</a>
|
|
32
|
+
{% if experiment.display_name != experiment.name %}<div class="study">{{ experiment.name }}</div>{% endif %}
|
|
33
33
|
</td>
|
|
34
34
|
<td class="design">
|
|
35
35
|
{% for key, values in experiment.varies.items() %}<span class="factor">{{ key }} <b>{{ values | length }}</b>{% if not loop.last or experiment.runs_per_cell %}<span class="times">×</span>{% endif %}</span> {% endfor %}
|
|
@@ -52,6 +52,6 @@
|
|
|
52
52
|
<script src="{{ asset('view-toggle.js') }}" defer></script>
|
|
53
53
|
<script src="{{ asset('row-link.js') }}" defer></script>
|
|
54
54
|
{% else %}
|
|
55
|
-
<p class="empty">No experiments found.
|
|
55
|
+
<p class="empty">No experiments found. Create one with <code>labnotes experiments new</code>.</p>
|
|
56
56
|
{% endif %}
|
|
57
57
|
{% endblock %}
|
|
@@ -479,7 +479,6 @@ dependencies = [
|
|
|
479
479
|
{ name = "jinja2" },
|
|
480
480
|
{ name = "jupyter-client" },
|
|
481
481
|
{ name = "markdown-it-py" },
|
|
482
|
-
{ name = "pyyaml" },
|
|
483
482
|
{ name = "rich" },
|
|
484
483
|
{ name = "starlette" },
|
|
485
484
|
{ name = "textual" },
|
|
@@ -492,7 +491,6 @@ dev = [
|
|
|
492
491
|
{ name = "mypy" },
|
|
493
492
|
{ name = "pytest" },
|
|
494
493
|
{ name = "ruff" },
|
|
495
|
-
{ name = "types-pyyaml" },
|
|
496
494
|
]
|
|
497
495
|
|
|
498
496
|
[package.metadata]
|
|
@@ -505,7 +503,6 @@ requires-dist = [
|
|
|
505
503
|
{ name = "jinja2", specifier = ">=3.1.6" },
|
|
506
504
|
{ name = "jupyter-client", specifier = ">=8.10.0" },
|
|
507
505
|
{ name = "markdown-it-py", specifier = ">=4.2.0" },
|
|
508
|
-
{ name = "pyyaml", specifier = ">=6.0.3" },
|
|
509
506
|
{ name = "rich", specifier = ">=14" },
|
|
510
507
|
{ name = "starlette", specifier = ">=1.8.0" },
|
|
511
508
|
{ name = "textual", specifier = ">=8.2.8" },
|
|
@@ -518,7 +515,6 @@ dev = [
|
|
|
518
515
|
{ name = "mypy", specifier = ">=2.4.0" },
|
|
519
516
|
{ name = "pytest", specifier = ">=8.0" },
|
|
520
517
|
{ name = "ruff", specifier = "==0.8.4" },
|
|
521
|
-
{ name = "types-pyyaml", specifier = ">=6.0.12.20260906" },
|
|
522
518
|
]
|
|
523
519
|
|
|
524
520
|
[[package]]
|
|
@@ -1027,52 +1023,6 @@ wheels = [
|
|
|
1027
1023
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
|
1028
1024
|
]
|
|
1029
1025
|
|
|
1030
|
-
[[package]]
|
|
1031
|
-
name = "pyyaml"
|
|
1032
|
-
version = "6.0.3"
|
|
1033
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1034
|
-
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
|
1035
|
-
wheels = [
|
|
1036
|
-
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
|
1037
|
-
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
|
1038
|
-
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
|
1039
|
-
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
|
1040
|
-
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
|
1041
|
-
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
|
1042
|
-
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
|
1043
|
-
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
|
1044
|
-
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
|
1045
|
-
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
|
1046
|
-
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
|
1047
|
-
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
|
1048
|
-
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
|
1049
|
-
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
|
1050
|
-
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
|
1051
|
-
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
|
1052
|
-
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
|
1053
|
-
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
|
1054
|
-
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
|
1055
|
-
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
|
1056
|
-
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
|
1057
|
-
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
|
1058
|
-
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
|
1059
|
-
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
|
1060
|
-
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
|
1061
|
-
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
|
1062
|
-
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
|
1063
|
-
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
|
1064
|
-
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
|
1065
|
-
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
|
1066
|
-
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
|
1067
|
-
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
|
1068
|
-
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
|
1069
|
-
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
|
1070
|
-
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
|
1071
|
-
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
|
1072
|
-
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
|
1073
|
-
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
|
1074
|
-
]
|
|
1075
|
-
|
|
1076
1026
|
[[package]]
|
|
1077
1027
|
name = "pyzmq"
|
|
1078
1028
|
version = "27.2.0"
|
|
@@ -1247,15 +1197,6 @@ wheels = [
|
|
|
1247
1197
|
{ url = "https://files.pythonhosted.org/packages/51/e9/3a7820be2bb0fe53b6bc9c3be26d3d1158004e4c3ab953aa6840b955b1e9/truststore-0.10.5-py3-none-any.whl", hash = "sha256:9aaaedaefaf06d8b206278cf8b5012bc897f485a874503501e12d776df78951c", size = 19017, upload-time = "2026-10-12T22:27:30.377Z" },
|
|
1248
1198
|
]
|
|
1249
1199
|
|
|
1250
|
-
[[package]]
|
|
1251
|
-
name = "types-pyyaml"
|
|
1252
|
-
version = "6.0.12.20260906"
|
|
1253
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1254
|
-
sdist = { url = "https://files.pythonhosted.org/packages/90/6e/abec85b9013db5b934b0280a6dd104904d84f7bcbaab2e2f3def87ac7463/types_pyyaml-6.0.12.20260906.tar.gz", hash = "sha256:f59c1cc05010b833d2d72287bbaa72610106b28d42d89a907313117faba85212", size = 18649, upload-time = "2026-09-06T06:35:35.362Z" }
|
|
1255
|
-
wheels = [
|
|
1256
|
-
{ url = "https://files.pythonhosted.org/packages/15/c0/fc0644b7ddcfb969e95845837143cb5173ddd6e06ee4ba5fc493cd9329b7/types_pyyaml-6.0.12.20260906-py3-none-any.whl", hash = "sha256:bca893ff0d51df5c9053137d5d0e6ccd36e939a196356f1d5c16372422f5137b", size = 21282, upload-time = "2026-09-06T06:35:34.372Z" },
|
|
1257
|
-
]
|
|
1258
|
-
|
|
1259
1200
|
[[package]]
|
|
1260
1201
|
name = "typing-extensions"
|
|
1261
1202
|
version = "4.16.0"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
from dirsql import DirSQL
|
|
4
|
-
|
|
5
|
-
from labnotes.run_columns import run_columns
|
|
6
|
-
from labnotes.study_progress import study_progress
|
|
7
|
-
from labnotes.summarize_design import summarize_design
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
async def list_experiments(
|
|
11
|
-
experiments: DirSQL, runs: DirSQL
|
|
12
|
-
) -> list[dict[str, object]]:
|
|
13
|
-
rows = await experiments.query(
|
|
14
|
-
"SELECT name, study, design FROM experiments ORDER BY name"
|
|
15
|
-
)
|
|
16
|
-
columns = await run_columns(runs)
|
|
17
|
-
listed = []
|
|
18
|
-
for row in rows:
|
|
19
|
-
summary = summarize_design(json.loads(str(row["design"])))
|
|
20
|
-
study = None if row["study"] is None else str(row["study"])
|
|
21
|
-
progress = await study_progress(runs, study, summary, columns)
|
|
22
|
-
listed.append({"name": row["name"], "study": study, **summary, **progress})
|
|
23
|
-
return listed
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
from dirsql import DirSQL, Table
|
|
4
|
-
|
|
5
|
-
from labnotes.read_design import read_design
|
|
6
|
-
from labnotes.read_note import read_note
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def open_experiments(folder: Path) -> DirSQL:
|
|
10
|
-
return DirSQL(
|
|
11
|
-
str(folder),
|
|
12
|
-
tables=[
|
|
13
|
-
Table(
|
|
14
|
-
name="experiments",
|
|
15
|
-
ddl="CREATE TABLE experiments (name TEXT, study TEXT, design TEXT)",
|
|
16
|
-
glob="*/design.yaml",
|
|
17
|
-
on_file=read_design,
|
|
18
|
-
),
|
|
19
|
-
Table(
|
|
20
|
-
name="notes",
|
|
21
|
-
ddl="CREATE TABLE notes (experiment TEXT, name TEXT, content TEXT)",
|
|
22
|
-
glob="*/*.md",
|
|
23
|
-
on_file=read_note,
|
|
24
|
-
),
|
|
25
|
-
],
|
|
26
|
-
)
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
from dirsql import DirSQL
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def open_runs(folder: Path) -> DirSQL:
|
|
7
|
-
config = folder / ".dirsql.toml"
|
|
8
|
-
if not config.is_file():
|
|
9
|
-
raise FileNotFoundError(
|
|
10
|
-
f"{config} not found; the data folder needs a dirsql config"
|
|
11
|
-
)
|
|
12
|
-
# Run data is usually gitignored (DVC, large outputs), so scan it anyway.
|
|
13
|
-
return DirSQL(str(folder), config=[str(config)], no_ignore=True)
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import yaml
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def read_design(path: str) -> list[dict[str, object]]:
|
|
8
|
-
design = yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {}
|
|
9
|
-
return [
|
|
10
|
-
{
|
|
11
|
-
"name": Path(path).parent.name,
|
|
12
|
-
"study": design.get("study"),
|
|
13
|
-
"design": json.dumps(design),
|
|
14
|
-
}
|
|
15
|
-
]
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
from typing import cast
|
|
2
|
-
|
|
3
|
-
from dirsql import DirSQL
|
|
4
|
-
|
|
5
|
-
from labnotes.cell_progress import cell_progress
|
|
6
|
-
from labnotes.count_runs import count_runs
|
|
7
|
-
from labnotes.study_runs import study_runs
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
async def study_progress(
|
|
11
|
-
runs: DirSQL, study: str | None, summary: dict[str, object], columns: list[str]
|
|
12
|
-
) -> dict[str, object]:
|
|
13
|
-
varies = cast(dict[str, list[object]], summary["varies"])
|
|
14
|
-
completed_run = str(summary["completed_run"])
|
|
15
|
-
runs_per_cell = summary["runs_per_cell"]
|
|
16
|
-
missing = [key for key in varies if key not in columns]
|
|
17
|
-
progress: dict[str, object] = {
|
|
18
|
-
"runs": 0,
|
|
19
|
-
"completed": 0,
|
|
20
|
-
"filled": None,
|
|
21
|
-
"cells": [],
|
|
22
|
-
"missing_columns": missing,
|
|
23
|
-
}
|
|
24
|
-
if study is None:
|
|
25
|
-
return progress
|
|
26
|
-
source = study_runs(study)
|
|
27
|
-
[totals] = await count_runs(runs, source, completed_run)
|
|
28
|
-
progress.update(totals)
|
|
29
|
-
if missing:
|
|
30
|
-
return progress
|
|
31
|
-
grouped = await count_runs(runs, source, completed_run, group_by=list(varies))
|
|
32
|
-
cells = cell_progress(varies, grouped)
|
|
33
|
-
progress["cells"] = cells
|
|
34
|
-
if isinstance(runs_per_cell, int):
|
|
35
|
-
progress["filled"] = sum(
|
|
36
|
-
min(cast(int, cell["completed"]), runs_per_cell) for cell in cells
|
|
37
|
-
)
|
|
38
|
-
return progress
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from math import prod
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def summarize_design(design: dict[str, object]) -> dict[str, object]:
|
|
5
|
-
fixed = design.get("fixed")
|
|
6
|
-
varies = design.get("varies")
|
|
7
|
-
fixed = fixed if isinstance(fixed, dict) else {}
|
|
8
|
-
varies = {
|
|
9
|
-
key: values if isinstance(values, list) else [values]
|
|
10
|
-
for key, values in (varies if isinstance(varies, dict) else {}).items()
|
|
11
|
-
}
|
|
12
|
-
runs_per_cell = design.get("runs_per_cell")
|
|
13
|
-
cells = prod(len(values) for values in varies.values())
|
|
14
|
-
known = {"study", "fixed", "varies", "runs_per_cell", "completed_run"}
|
|
15
|
-
return {
|
|
16
|
-
"fixed": fixed,
|
|
17
|
-
"varies": varies,
|
|
18
|
-
"cell_count": cells,
|
|
19
|
-
"runs_per_cell": runs_per_cell,
|
|
20
|
-
"target": cells * runs_per_cell if isinstance(runs_per_cell, int) else None,
|
|
21
|
-
"completed_run": str(design.get("completed_run") or "1"),
|
|
22
|
-
"other": {key: value for key, value in design.items() if key not in known},
|
|
23
|
-
}
|
|
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
|