labnotes 0.0.43__tar.gz → 0.0.45__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.43 → labnotes-0.0.45}/PKG-INFO +1 -1
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/context.md +12 -7
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/create_app.py +2 -1
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/experiments.py +3 -4
- labnotes-0.0.45/src/labnotes/is_positive_int.py +2 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/list_experiments.py +2 -2
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/load_experiment.py +2 -1
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/models.py +5 -1
- labnotes-0.0.45/src/labnotes/modes.py +2 -0
- labnotes-0.0.45/src/labnotes/read_config.py +36 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/read_path.py +3 -1
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/read_experiment_config.py +10 -6
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/start_experiment.py +6 -2
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/summarize_config.py +2 -2
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/experiment_rows.py +3 -1
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tui/run_app.py +5 -4
- labnotes-0.0.45/src/labnotes/tui/runs_table.py +11 -0
- labnotes-0.0.43/src/labnotes/read_config.py +0 -27
- {labnotes-0.0.43 → labnotes-0.0.45}/.gitignore +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/CHANGELOG.md +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/MIGRATIONS.md +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/README.md +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/pyproject.toml +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/experiment_progress.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/init.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/pivot_cells.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/new_invocation_id.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/wait_for_group.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/server.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/static/labnotes.css +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/stop_idle_kernels.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/open_tables.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/templates/macros.html +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/testing-conventions.toml +0 -0
- {labnotes-0.0.43 → labnotes-0.0.45}/uv.lock +0 -0
|
@@ -59,7 +59,9 @@ renaming the experiment means editing `name`.
|
|
|
59
59
|
Each combination of `[params]` runs `repeats` times (1 if omitted), round-robin, on every
|
|
60
60
|
`run`, whatever is already on disk. That is `mode = "adhoc"`, the default.
|
|
61
61
|
`mode = "fill"` counts what is there: only runs that count fill a combination,
|
|
62
|
-
so running again fills in what failed or never ran.
|
|
62
|
+
so running again fills in what failed or never ran. `mode` under
|
|
63
|
+
`[experiments]` in `.labnotes.toml` sets it for every experiment that doesn't
|
|
64
|
+
set its own.
|
|
63
65
|
Each run gets a new run folder, named for the UTC time it was planned (`20261020-000429-brave-fox`), so folder listings sort by start. `command` runs in a shell from the root, with
|
|
64
66
|
`{run_dir}` replaced by the run folder's absolute path. Pass it to the
|
|
65
67
|
script so it writes its outputs there.
|
|
@@ -90,20 +92,23 @@ A script run by labnotes should:
|
|
|
90
92
|
Runs can take a long time. Start `run` in the background, then check on it with `labnotes experiments foo runs`; a run with no `ended` is still going.
|
|
91
93
|
|
|
92
94
|
In a terminal, `run` shows a live table of runs (status and time) and stays
|
|
93
|
-
open when they finish. Failed runs show their status in red; `
|
|
94
|
-
runs it again under a new id.
|
|
95
|
+
open when they finish. Failed runs show their status in red; `f` hides or shows them, and `r` on one
|
|
96
|
+
runs it again under a new id. `j` and `k` move the cursor, `g` and `G` jump to the
|
|
97
|
+
first and last row. Runs not started yet show grayed as `pending`, and as
|
|
95
98
|
`skipped` once stopped. Move the cursor to a pending run and press `n` to run it
|
|
96
|
-
next.
|
|
99
|
+
next. Space deselects a pending run so it won't run; Space again puts it back at
|
|
97
100
|
the end of the queue. `/` filters rows by params text; Enter keeps the filter,
|
|
98
|
-
Escape clears it. `D` deselects every pending run the filter shows. `
|
|
99
|
-
running run under the cursor; `
|
|
101
|
+
Escape clears it. `D` deselects every pending run the filter shows. `x` stops the
|
|
102
|
+
running run under the cursor; `x` again kills it. `+` and
|
|
100
103
|
`-` change how many run at once; lowering it lets running runs
|
|
101
104
|
finish. Enter on a run follows its `stdout.log`; `e` switches to
|
|
102
105
|
`stderr.log` and back, and Escape returns to the table. An `adhoc` experiment that
|
|
103
106
|
plans a single run opens straight on its output. `q` quits. Elsewhere, or with `--no-tui`, it prints a
|
|
104
107
|
line per event instead.
|
|
105
108
|
|
|
106
|
-
- `--concurrency N` runs N at a time to start with
|
|
109
|
+
- `--concurrency N` runs N at a time to start with. Without it, `concurrency`
|
|
110
|
+
in `experiment.toml` sets it, then `concurrency` under `[experiments]` in
|
|
111
|
+
`.labnotes.toml`, then 1.
|
|
107
112
|
- `-e PATH` reads another file instead of `experiment.toml`.
|
|
108
113
|
- Ctrl-C stops the running scripts and records their end. A second Ctrl-C
|
|
109
114
|
kills them. Closing the terminal or sending SIGTERM stops them the same way,
|
|
@@ -50,7 +50,7 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
|
|
|
50
50
|
await kernel.stop()
|
|
51
51
|
|
|
52
52
|
async def index(request: Request) -> Response:
|
|
53
|
-
experiments = await list_experiments(request.app.state.tables)
|
|
53
|
+
experiments = await list_experiments(request.app.state.tables, config.mode)
|
|
54
54
|
return templates.TemplateResponse(
|
|
55
55
|
request,
|
|
56
56
|
"index.html",
|
|
@@ -62,6 +62,7 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
|
|
|
62
62
|
loaded = await load_experiment(
|
|
63
63
|
request.app.state.tables,
|
|
64
64
|
config.root,
|
|
65
|
+
config.mode,
|
|
65
66
|
name,
|
|
66
67
|
request.query_params,
|
|
67
68
|
lambda note, content: saved_outputs(kernels.get((name, note)), content),
|
|
@@ -30,9 +30,8 @@ from labnotes.tui.run_tui import run_tui
|
|
|
30
30
|
@click.option(
|
|
31
31
|
"--concurrency",
|
|
32
32
|
type=click.IntRange(min=1),
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
help="With run: how many runs at once.",
|
|
33
|
+
help="With run: how many runs at once. Overrides experiment.toml and"
|
|
34
|
+
" .labnotes.toml; 1 if none sets it.",
|
|
36
35
|
)
|
|
37
36
|
@click.option(
|
|
38
37
|
"-e",
|
|
@@ -51,7 +50,7 @@ def experiments(
|
|
|
51
50
|
name: str | None,
|
|
52
51
|
action: str | None,
|
|
53
52
|
output_format: str,
|
|
54
|
-
concurrency: int,
|
|
53
|
+
concurrency: int | None,
|
|
55
54
|
experiment_config: Path | None,
|
|
56
55
|
no_tui: bool,
|
|
57
56
|
) -> None:
|
|
@@ -9,7 +9,7 @@ from labnotes.experiment_runs import experiment_runs
|
|
|
9
9
|
from labnotes.summarize_config import summarize_config
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
async def list_experiments(tables: DirSQL) -> list[dict[str, object]]:
|
|
12
|
+
async def list_experiments(tables: DirSQL, mode: str) -> list[dict[str, object]]:
|
|
13
13
|
rows = await tables.query(
|
|
14
14
|
"SELECT name, display_name, config FROM experiments ORDER BY name"
|
|
15
15
|
)
|
|
@@ -17,7 +17,7 @@ async def list_experiments(tables: DirSQL) -> list[dict[str, object]]:
|
|
|
17
17
|
for row in rows:
|
|
18
18
|
name = str(row["name"])
|
|
19
19
|
config = None if row["config"] is None else json.loads(str(row["config"]))
|
|
20
|
-
summary = summarize_config(config)
|
|
20
|
+
summary = summarize_config(config, mode)
|
|
21
21
|
varies = cast(dict[str, list[object]], summary["varies"])
|
|
22
22
|
source = experiment_runs(name, list(varies))
|
|
23
23
|
summary = await check_counts(tables, source, summary)
|
|
@@ -25,6 +25,7 @@ def _no_outputs(note: str, content: str) -> Sequence[str]:
|
|
|
25
25
|
async def load_experiment(
|
|
26
26
|
tables: DirSQL,
|
|
27
27
|
root: Path,
|
|
28
|
+
mode: str,
|
|
28
29
|
name: str,
|
|
29
30
|
params: Mapping[str, str],
|
|
30
31
|
outputs_for: Callable[[str, str], Sequence[str]] = _no_outputs,
|
|
@@ -37,7 +38,7 @@ async def load_experiment(
|
|
|
37
38
|
return None
|
|
38
39
|
[row] = found
|
|
39
40
|
config = None if row["config"] is None else json.loads(str(row["config"]))
|
|
40
|
-
summary = summarize_config(config)
|
|
41
|
+
summary = summarize_config(config, mode)
|
|
41
42
|
varies = cast(dict[str, list[object]], summary["varies"])
|
|
42
43
|
source = experiment_runs(name, list(varies))
|
|
43
44
|
summary = await check_counts(tables, source, summary)
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from labnotes.default_counts import DEFAULT_COUNTS
|
|
7
|
+
from labnotes.modes import DEFAULT_MODE
|
|
7
8
|
|
|
8
9
|
Report = Callable[[dict[str, object]], None]
|
|
9
10
|
|
|
@@ -38,6 +39,8 @@ class LabnotesConfig:
|
|
|
38
39
|
experiments: str
|
|
39
40
|
files: str
|
|
40
41
|
runs: str
|
|
42
|
+
mode: str = DEFAULT_MODE
|
|
43
|
+
concurrency: int = 1
|
|
41
44
|
|
|
42
45
|
|
|
43
46
|
@dataclass(frozen=True)
|
|
@@ -47,4 +50,5 @@ class ExperimentConfig:
|
|
|
47
50
|
params: dict[str, list[object]]
|
|
48
51
|
name: str | None = None
|
|
49
52
|
counts: str = DEFAULT_COUNTS
|
|
50
|
-
mode: str =
|
|
53
|
+
mode: str = DEFAULT_MODE
|
|
54
|
+
concurrency: int = 1
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import tomllib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from labnotes.models import LabnotesConfig
|
|
5
|
+
from labnotes.is_positive_int import is_positive_int
|
|
6
|
+
from labnotes.modes import DEFAULT_MODE, MODES
|
|
7
|
+
from labnotes.read_path import DEFAULT_PATHS, read_path
|
|
8
|
+
|
|
9
|
+
CONFIG_FILE = ".labnotes.toml"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def read_config(path: Path) -> LabnotesConfig:
|
|
13
|
+
try:
|
|
14
|
+
with path.open("rb") as file:
|
|
15
|
+
values = tomllib.load(file)
|
|
16
|
+
except tomllib.TOMLDecodeError as error:
|
|
17
|
+
raise ValueError(f"{path}: {error}") from error
|
|
18
|
+
unknown = sorted(values.keys() - DEFAULT_PATHS.keys())
|
|
19
|
+
if unknown:
|
|
20
|
+
raise ValueError(f"{path}: unknown tables {', '.join(unknown)}")
|
|
21
|
+
paths = {
|
|
22
|
+
table: read_path(path, table, values.get(table, {})) for table in DEFAULT_PATHS
|
|
23
|
+
}
|
|
24
|
+
experiments = values.get("experiments", {})
|
|
25
|
+
mode = experiments.get("mode", DEFAULT_MODE)
|
|
26
|
+
if mode not in MODES:
|
|
27
|
+
raise ValueError(f"{path}: [experiments] mode must be fill or adhoc")
|
|
28
|
+
concurrency = experiments.get("concurrency", 1)
|
|
29
|
+
if not is_positive_int(concurrency):
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"{path}: [experiments] concurrency must be a positive integer"
|
|
32
|
+
)
|
|
33
|
+
path = path.resolve()
|
|
34
|
+
return LabnotesConfig(
|
|
35
|
+
path, path.parent, **paths, mode=mode, concurrency=concurrency
|
|
36
|
+
)
|
|
@@ -11,13 +11,15 @@ PLACEHOLDERS = {
|
|
|
11
11
|
"files": {"experiment"},
|
|
12
12
|
"runs": {"experiment", "run"},
|
|
13
13
|
}
|
|
14
|
+
# Keys other readers own, so the path check lets them through.
|
|
15
|
+
OTHER_KEYS = {"experiments": {"mode", "concurrency"}}
|
|
14
16
|
PLACEHOLDER = re.compile(r"\{([^}]*)\}")
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
def read_path(config: Path, table: str, section: object) -> str:
|
|
18
20
|
if not isinstance(section, dict):
|
|
19
21
|
raise ValueError(f"{config}: {table} must be a table, like [{table}]")
|
|
20
|
-
unknown = sorted(section.keys() - {"path"})
|
|
22
|
+
unknown = sorted(section.keys() - {"path", *OTHER_KEYS.get(table, ())})
|
|
21
23
|
if unknown:
|
|
22
24
|
raise ValueError(f"{config}: unknown keys in [{table}]: {', '.join(unknown)}")
|
|
23
25
|
template = section.get("path", DEFAULT_PATHS[table])
|
|
@@ -4,12 +4,13 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
from labnotes.default_counts import DEFAULT_COUNTS
|
|
6
6
|
from labnotes.models import ExperimentConfig
|
|
7
|
+
from labnotes.is_positive_int import is_positive_int
|
|
8
|
+
from labnotes.modes import MODES
|
|
7
9
|
|
|
8
|
-
SETTINGS = {"name", "command", "repeats", "params", "counts", "mode"}
|
|
9
|
-
MODES = ("fill", "adhoc")
|
|
10
|
+
SETTINGS = {"name", "command", "repeats", "params", "counts", "mode", "concurrency"}
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def read_experiment_config(path: Path) -> ExperimentConfig:
|
|
13
|
+
def read_experiment_config(path: Path, mode: str, concurrency: int) -> ExperimentConfig:
|
|
13
14
|
try:
|
|
14
15
|
with path.open("rb") as file:
|
|
15
16
|
values = tomllib.load(file)
|
|
@@ -25,7 +26,7 @@ def read_experiment_config(path: Path) -> ExperimentConfig:
|
|
|
25
26
|
if not isinstance(command, str) or not command.strip():
|
|
26
27
|
raise ValueError(f"{path}: command must be a string")
|
|
27
28
|
repeats = values.get("repeats", 1)
|
|
28
|
-
if
|
|
29
|
+
if not is_positive_int(repeats):
|
|
29
30
|
raise ValueError(f"{path}: repeats must be a positive integer")
|
|
30
31
|
params = values.get("params", {})
|
|
31
32
|
if not isinstance(params, dict):
|
|
@@ -40,7 +41,10 @@ def read_experiment_config(path: Path) -> ExperimentConfig:
|
|
|
40
41
|
counts = values.get("counts", DEFAULT_COUNTS)
|
|
41
42
|
if not isinstance(counts, str) or not counts.strip():
|
|
42
43
|
raise ValueError(f"{path}: counts must be a SQL condition")
|
|
43
|
-
mode = values.get("mode",
|
|
44
|
+
mode = values.get("mode", mode)
|
|
44
45
|
if mode not in MODES:
|
|
45
46
|
raise ValueError(f"{path}: mode must be fill or adhoc")
|
|
46
|
-
|
|
47
|
+
concurrency = values.get("concurrency", concurrency)
|
|
48
|
+
if not is_positive_int(concurrency):
|
|
49
|
+
raise ValueError(f"{path}: concurrency must be a positive integer")
|
|
50
|
+
return ExperimentConfig(command, repeats, params, name, counts, mode, concurrency)
|
|
@@ -13,7 +13,7 @@ def start_experiment(
|
|
|
13
13
|
config: LabnotesConfig,
|
|
14
14
|
name: str,
|
|
15
15
|
experiment_config: Path | None,
|
|
16
|
-
concurrency: int,
|
|
16
|
+
concurrency: int | None,
|
|
17
17
|
drive: Callable[[Start, str], list[dict[str, object]]],
|
|
18
18
|
) -> list[dict[str, object]]:
|
|
19
19
|
if experiment_config is None:
|
|
@@ -21,7 +21,11 @@ def start_experiment(
|
|
|
21
21
|
experiment_config = config.root / folder / "experiment.toml"
|
|
22
22
|
if not experiment_config.is_file():
|
|
23
23
|
raise ValueError(f"No experiment.toml for {name} at {experiment_config}.")
|
|
24
|
-
experiment = read_experiment_config(
|
|
24
|
+
experiment = read_experiment_config(
|
|
25
|
+
experiment_config, config.mode, config.concurrency
|
|
26
|
+
)
|
|
27
|
+
if concurrency is None:
|
|
28
|
+
concurrency = experiment.concurrency
|
|
25
29
|
runs = runs_parent(config.root, config.runs, name)
|
|
26
30
|
runs.mkdir(parents=True, exist_ok=True)
|
|
27
31
|
try:
|
|
@@ -3,7 +3,7 @@ from math import prod
|
|
|
3
3
|
from labnotes.default_counts import DEFAULT_COUNTS
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def summarize_config(config: dict[str, object] | None) -> dict[str, object]:
|
|
6
|
+
def summarize_config(config: dict[str, object] | None, mode: str) -> dict[str, object]:
|
|
7
7
|
config = config or {}
|
|
8
8
|
params = config.get("params")
|
|
9
9
|
varies = {
|
|
@@ -15,7 +15,7 @@ def summarize_config(config: dict[str, object] | None) -> dict[str, object]:
|
|
|
15
15
|
cells = prod(len(values) for values in varies.values())
|
|
16
16
|
command = config.get("command")
|
|
17
17
|
counts = config.get("counts")
|
|
18
|
-
fill = config.get("mode") == "fill"
|
|
18
|
+
fill = config.get("mode", mode) == "fill"
|
|
19
19
|
return {
|
|
20
20
|
"fixed": {},
|
|
21
21
|
"varies": varies,
|
|
@@ -18,7 +18,9 @@ async def experiment_rows(
|
|
|
18
18
|
case str(), "runs":
|
|
19
19
|
[row] = rows
|
|
20
20
|
stored = row["config"]
|
|
21
|
-
summary = summarize_config(
|
|
21
|
+
summary = summarize_config(
|
|
22
|
+
json.loads(str(stored)) if stored else None, config.mode
|
|
23
|
+
)
|
|
22
24
|
return await list_experiment_runs(
|
|
23
25
|
tables, name, str(summary["completed_run"])
|
|
24
26
|
)
|
|
@@ -14,6 +14,7 @@ from labnotes.runner.stop_then_kill import stop_then_kill
|
|
|
14
14
|
from labnotes.runner.param_args import param_args
|
|
15
15
|
from labnotes.tui.dim import dim
|
|
16
16
|
from labnotes.tui.format_elapsed import format_elapsed
|
|
17
|
+
from labnotes.tui.runs_table import RunsTable
|
|
17
18
|
|
|
18
19
|
COLUMNS = ("run", "params", "status", "time")
|
|
19
20
|
OTHER_STREAM = {"stdout": "stderr", "stderr": "stdout"}
|
|
@@ -26,10 +27,10 @@ class RunApp(App[None]):
|
|
|
26
27
|
BINDINGS = [
|
|
27
28
|
Binding("ctrl+c", "interrupt", "Stop runs"),
|
|
28
29
|
Binding("n", "run_next", "Run next"),
|
|
29
|
-
Binding("
|
|
30
|
+
Binding("space", "toggle_run", "Deselect"),
|
|
30
31
|
Binding("D", "deselect_shown", "Deselect shown"),
|
|
31
|
-
Binding("
|
|
32
|
-
Binding("
|
|
32
|
+
Binding("x", "kill_run", "Stop run"),
|
|
33
|
+
Binding("f", "hide_failed", "Hide failed"),
|
|
33
34
|
Binding("r", "retry", "Retry"),
|
|
34
35
|
Binding("slash", "filter", "Filter", key_display="/"),
|
|
35
36
|
Binding("e", "switch_output", "stdout/stderr"),
|
|
@@ -72,7 +73,7 @@ class RunApp(App[None]):
|
|
|
72
73
|
|
|
73
74
|
def compose(self) -> ComposeResult:
|
|
74
75
|
yield Header()
|
|
75
|
-
yield
|
|
76
|
+
yield RunsTable(id="runs", cursor_type="row")
|
|
76
77
|
yield Log(id="output")
|
|
77
78
|
yield Input(id="filter", placeholder="Filter params")
|
|
78
79
|
yield Static(id="status")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from textual.binding import Binding
|
|
2
|
+
from textual.widgets import DataTable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RunsTable(DataTable[object]):
|
|
6
|
+
BINDINGS = [
|
|
7
|
+
Binding("j", "cursor_down", show=False),
|
|
8
|
+
Binding("k", "cursor_up", show=False),
|
|
9
|
+
Binding("g", "scroll_top", show=False),
|
|
10
|
+
Binding("G", "scroll_bottom", show=False),
|
|
11
|
+
]
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import tomllib
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
from labnotes.models import LabnotesConfig
|
|
5
|
-
from labnotes.read_path import DEFAULT_PATHS, read_path
|
|
6
|
-
|
|
7
|
-
CONFIG_FILE = ".labnotes.toml"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def read_config(path: Path) -> LabnotesConfig:
|
|
11
|
-
try:
|
|
12
|
-
with path.open("rb") as file:
|
|
13
|
-
values = tomllib.load(file)
|
|
14
|
-
except tomllib.TOMLDecodeError as error:
|
|
15
|
-
raise ValueError(f"{path}: {error}") from error
|
|
16
|
-
unknown = sorted(values.keys() - DEFAULT_PATHS.keys())
|
|
17
|
-
if unknown:
|
|
18
|
-
raise ValueError(f"{path}: unknown tables {', '.join(unknown)}")
|
|
19
|
-
path = path.resolve()
|
|
20
|
-
return LabnotesConfig(
|
|
21
|
-
path,
|
|
22
|
-
path.parent,
|
|
23
|
-
**{
|
|
24
|
-
table: read_path(path, table, values.get(table, {}))
|
|
25
|
-
for table in DEFAULT_PATHS
|
|
26
|
-
},
|
|
27
|
-
)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|