labnotes 0.0.46__tar.gz → 0.0.47__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.46 → labnotes-0.0.47}/PKG-INFO +1 -1
- {labnotes-0.0.46 → labnotes-0.0.47}/pyproject.toml +1 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/context.md +3 -3
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/create_app.py +1 -7
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/load_experiment.py +7 -5
- labnotes-0.0.47/src/labnotes/static/experiment.js +21 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/static/labnotes.css +93 -105
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/open_tables.py +5 -2
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/read_experiment.py +9 -3
- labnotes-0.0.47/src/labnotes/templates/experiment.html +106 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tui/run_app.py +34 -24
- labnotes-0.0.47/testing-conventions.toml +11 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/uv.lock +100 -0
- labnotes-0.0.46/src/labnotes/pivot_cells.py +0 -21
- labnotes-0.0.46/src/labnotes/templates/experiment.html +0 -167
- labnotes-0.0.46/testing-conventions.toml +0 -5
- {labnotes-0.0.46 → labnotes-0.0.47}/.gitignore +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/CHANGELOG.md +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/MIGRATIONS.md +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/README.md +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/cell_progress.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/experiment_progress.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/init.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/models.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/new_invocation_id.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/start_experiment.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/wait_for_group.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/server.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/stop_idle_kernels.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/templates/macros.html +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.46 → labnotes-0.0.47}/src/labnotes/tui/runs_table.py +0 -0
|
@@ -101,8 +101,8 @@ the end of the queue. `/` filters rows by params text; Enter keeps the filter,
|
|
|
101
101
|
Escape clears it. `D` deselects every pending run the filter shows. `x` stops the
|
|
102
102
|
running run under the cursor; `x` again kills it. `+` and
|
|
103
103
|
`-` change how many run at once; lowering it lets running runs
|
|
104
|
-
finish. Enter on a run follows its `stdout.log
|
|
105
|
-
|
|
104
|
+
finish. Enter on a run follows its `stdout.log` and `stderr.log` together, as
|
|
105
|
+
they're written, and Escape returns to the table. An `adhoc` experiment that
|
|
106
106
|
plans a single run opens straight on its output. `q` quits. Elsewhere, or with `--no-tui`, it prints a
|
|
107
107
|
line per event instead.
|
|
108
108
|
|
|
@@ -117,4 +117,4 @@ line per event instead.
|
|
|
117
117
|
- A second `run` of the same experiment fails while the first holds the lock.
|
|
118
118
|
- It exits 1 if any run failed.
|
|
119
119
|
|
|
120
|
-
`uvx labnotes server` serves the experiments, their markdown files and their runs as a local site, read from the same `.labnotes.toml`.
|
|
120
|
+
`uvx labnotes server` serves the experiments, their markdown files and their runs as a local site, read from the same `.labnotes.toml`. An experiment's page shows its `experiment.toml` on one line, then a Runs tab with a table of its runs and a Notebooks tab with one tab per markdown file.
|
|
@@ -2,7 +2,6 @@ import asyncio
|
|
|
2
2
|
from collections.abc import AsyncIterator
|
|
3
3
|
from contextlib import asynccontextmanager
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import cast
|
|
6
5
|
|
|
7
6
|
from starlette.applications import Starlette
|
|
8
7
|
from starlette.exceptions import HTTPException
|
|
@@ -20,7 +19,6 @@ from labnotes.list_experiments import list_experiments
|
|
|
20
19
|
from labnotes.load_experiment import load_experiment
|
|
21
20
|
from labnotes.models import LabnotesConfig
|
|
22
21
|
from labnotes.notebook_kernel import NotebookKernel
|
|
23
|
-
from labnotes.pivot_cells import pivot_cells
|
|
24
22
|
from labnotes.python_blocks import python_blocks
|
|
25
23
|
from labnotes.saved_outputs import saved_outputs
|
|
26
24
|
from labnotes.stop_idle_kernels import stop_idle_kernels
|
|
@@ -69,12 +67,8 @@ def create_app(config: LabnotesConfig, kernel_idle_minutes: float) -> Starlette:
|
|
|
69
67
|
)
|
|
70
68
|
if loaded is None:
|
|
71
69
|
raise HTTPException(404, "No such experiment")
|
|
72
|
-
varies = cast(dict[str, list[object]], loaded["varies"])
|
|
73
|
-
cells = cast(list[dict[str, object]], loaded["cells"])
|
|
74
70
|
return templates.TemplateResponse(
|
|
75
|
-
request,
|
|
76
|
-
"experiment.html",
|
|
77
|
-
{"experiment": loaded, "pivot": pivot_cells(varies, cells)},
|
|
71
|
+
request, "experiment.html", {"experiment": loaded}
|
|
78
72
|
)
|
|
79
73
|
|
|
80
74
|
async def note_content(request: Request) -> str:
|
|
@@ -5,9 +5,9 @@ from typing import cast
|
|
|
5
5
|
|
|
6
6
|
from dirsql import DirSQL
|
|
7
7
|
|
|
8
|
-
from labnotes.experiment_notes import experiment_notes
|
|
9
8
|
from labnotes.check_counts import check_counts
|
|
10
|
-
from labnotes.
|
|
9
|
+
from labnotes.count_runs import count_runs
|
|
10
|
+
from labnotes.experiment_notes import experiment_notes
|
|
11
11
|
from labnotes.experiment_runs import experiment_runs
|
|
12
12
|
from labnotes.list_runs import list_runs
|
|
13
13
|
from labnotes.parse_run_view import parse_run_view
|
|
@@ -31,7 +31,7 @@ async def load_experiment(
|
|
|
31
31
|
outputs_for: Callable[[str, str], Sequence[str]] = _no_outputs,
|
|
32
32
|
) -> dict[str, object] | None:
|
|
33
33
|
found = await tables.query(
|
|
34
|
-
"SELECT name, display_name, config FROM experiments"
|
|
34
|
+
"SELECT name, display_name, config, source FROM experiments"
|
|
35
35
|
f" WHERE name = {quote_string(name)}"
|
|
36
36
|
)
|
|
37
37
|
if not found:
|
|
@@ -42,15 +42,17 @@ async def load_experiment(
|
|
|
42
42
|
varies = cast(dict[str, list[object]], summary["varies"])
|
|
43
43
|
source = experiment_runs(name, list(varies))
|
|
44
44
|
summary = await check_counts(tables, source, summary)
|
|
45
|
-
progress = await experiment_progress(tables, source, summary)
|
|
46
45
|
shown = ["id", *(key for key in varies if key != "id")]
|
|
47
46
|
view = parse_run_view(params, shown)
|
|
47
|
+
[totals] = await count_runs(tables, source, str(summary["completed_run"]))
|
|
48
48
|
notes = await experiment_notes(tables, root, name)
|
|
49
49
|
return {
|
|
50
50
|
"name": row["name"],
|
|
51
51
|
"display_name": row["display_name"],
|
|
52
|
+
"config_source": row["source"],
|
|
53
|
+
"counts_set": "counts" in (config or {}),
|
|
52
54
|
**summary,
|
|
53
|
-
**
|
|
55
|
+
**totals,
|
|
54
56
|
"notes": [
|
|
55
57
|
{
|
|
56
58
|
"name": note["name"],
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const summary = document.querySelector(".config-summary");
|
|
2
|
+
const open = document.querySelector(".config-open");
|
|
3
|
+
const dialog = document.querySelector(".config-file");
|
|
4
|
+
|
|
5
|
+
if (summary) {
|
|
6
|
+
const fit = () => {
|
|
7
|
+
open.hidden = summary.scrollWidth <= summary.clientWidth;
|
|
8
|
+
};
|
|
9
|
+
new ResizeObserver(fit).observe(summary);
|
|
10
|
+
open.addEventListener("click", () => dialog.showModal());
|
|
11
|
+
dialog.querySelector(".config-close").addEventListener("click", () => dialog.close());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
for (const row of document.querySelectorAll(".run-row")) {
|
|
15
|
+
row.addEventListener("click", () => {
|
|
16
|
+
if (getSelection().toString()) return;
|
|
17
|
+
const record = row.nextElementSibling;
|
|
18
|
+
record.hidden = !record.hidden;
|
|
19
|
+
row.setAttribute("aria-expanded", String(!record.hidden));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -112,10 +112,6 @@ h3 {
|
|
|
112
112
|
.study { font-family: var(--mono); font-size: 0.82rem; color: var(--ink-3); margin: 0.35rem 0 0; }
|
|
113
113
|
.intro .study { font-size: 0.9rem; margin-top: 0.6rem; }
|
|
114
114
|
|
|
115
|
-
.toc { display: flex; gap: 1.4rem; margin-top: 1.4rem; font-size: 0.88rem; }
|
|
116
|
-
.toc a { color: var(--ink-2); text-decoration: none; }
|
|
117
|
-
.toc a:hover { color: var(--accent); }
|
|
118
|
-
|
|
119
115
|
.empty, .notice { color: var(--ink-2); }
|
|
120
116
|
.notice {
|
|
121
117
|
margin-top: 1.25rem;
|
|
@@ -181,31 +177,6 @@ h3 {
|
|
|
181
177
|
.progress-line .num { text-align: right; }
|
|
182
178
|
.pct { color: var(--ink-2); }
|
|
183
179
|
|
|
184
|
-
/* Headline figures */
|
|
185
|
-
.figures {
|
|
186
|
-
display: grid;
|
|
187
|
-
grid-template-columns: 2fr 1fr 1fr;
|
|
188
|
-
gap: 1px;
|
|
189
|
-
background: var(--rule);
|
|
190
|
-
border: 1px solid var(--rule);
|
|
191
|
-
border-radius: 10px;
|
|
192
|
-
overflow: hidden;
|
|
193
|
-
margin-bottom: 3.5rem;
|
|
194
|
-
}
|
|
195
|
-
.figure { background: var(--surface); padding: 1.3rem 1.5rem 1.4rem; }
|
|
196
|
-
.figure-label { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); }
|
|
197
|
-
.figure-value { font-size: 2.4rem; font-weight: 500; letter-spacing: -0.03em; line-height: 1.15; margin: 0.35rem 0 0.6rem; font-variant-numeric: tabular-nums; }
|
|
198
|
-
.figure .meter { height: 6px; border-radius: 3px; margin-bottom: 0.7rem; }
|
|
199
|
-
.figure-note { color: var(--ink-2); font-size: 0.86rem; }
|
|
200
|
-
|
|
201
|
-
.block { margin-bottom: 3.5rem; }
|
|
202
|
-
|
|
203
|
-
/* Design */
|
|
204
|
-
.design-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 2.5rem; margin-bottom: 1.75rem; }
|
|
205
|
-
.factors { display: grid; grid-template-columns: max-content 1fr; gap: 0.6rem 1.25rem; margin: 0; align-items: baseline; }
|
|
206
|
-
.factors dt { font-family: var(--mono); font-size: 0.82rem; color: var(--ink-2); }
|
|
207
|
-
.factors dd { margin: 0; display: flex; flex-wrap: wrap; gap: 0.35rem; }
|
|
208
|
-
|
|
209
180
|
.chip {
|
|
210
181
|
font-family: var(--mono);
|
|
211
182
|
font-size: 0.8rem;
|
|
@@ -217,30 +188,100 @@ h3 {
|
|
|
217
188
|
}
|
|
218
189
|
.chip.quiet { background: var(--chip); border-color: var(--rule); }
|
|
219
190
|
|
|
220
|
-
|
|
221
|
-
.
|
|
222
|
-
.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
.
|
|
233
|
-
.
|
|
234
|
-
.
|
|
235
|
-
|
|
236
|
-
|
|
191
|
+
/* experiment.toml line */
|
|
192
|
+
.config-line { display: flex; align-items: baseline; gap: 0.75rem; margin-top: 1.1rem; }
|
|
193
|
+
.config-summary {
|
|
194
|
+
flex: 1;
|
|
195
|
+
min-width: 0;
|
|
196
|
+
margin: 0;
|
|
197
|
+
overflow: hidden;
|
|
198
|
+
white-space: nowrap;
|
|
199
|
+
text-overflow: ellipsis;
|
|
200
|
+
font-size: 0.88rem;
|
|
201
|
+
color: var(--ink-3);
|
|
202
|
+
}
|
|
203
|
+
.setting { margin-left: 1.1rem; }
|
|
204
|
+
.setting b { font-weight: 600; color: var(--ink); }
|
|
205
|
+
.config-open, .config-close {
|
|
206
|
+
font: inherit;
|
|
207
|
+
font-family: var(--mono);
|
|
208
|
+
font-size: 0.75rem;
|
|
209
|
+
padding: 0.15rem 0.6rem;
|
|
210
|
+
border: 1px solid var(--rule-strong);
|
|
211
|
+
border-radius: 5px;
|
|
212
|
+
background: var(--surface);
|
|
213
|
+
color: var(--ink-2);
|
|
214
|
+
cursor: pointer;
|
|
215
|
+
white-space: nowrap;
|
|
216
|
+
}
|
|
217
|
+
.config-open:hover, .config-close:hover { color: var(--accent); border-color: var(--accent); }
|
|
218
|
+
.config-file {
|
|
219
|
+
width: min(48rem, 90vw);
|
|
220
|
+
padding: 0;
|
|
221
|
+
border: 1px solid var(--rule);
|
|
222
|
+
border-radius: 10px;
|
|
223
|
+
background: var(--surface);
|
|
224
|
+
color: var(--ink);
|
|
225
|
+
}
|
|
226
|
+
.config-file::backdrop { background: color-mix(in srgb, var(--ink) 30%, transparent); }
|
|
227
|
+
.config-file header {
|
|
228
|
+
display: flex;
|
|
229
|
+
justify-content: space-between;
|
|
230
|
+
align-items: center;
|
|
231
|
+
padding: 0.6rem 1.25rem;
|
|
232
|
+
border-bottom: 1px solid var(--rule);
|
|
233
|
+
font-family: var(--mono);
|
|
234
|
+
font-size: 0.78rem;
|
|
235
|
+
color: var(--ink-3);
|
|
236
|
+
}
|
|
237
|
+
.config-file pre { margin: 0; padding: 1rem 1.25rem; max-height: 70vh; overflow: auto; font-size: 0.82rem; }
|
|
238
|
+
|
|
239
|
+
/* Tabs */
|
|
240
|
+
.tabs { display: flex; gap: 1.6rem; border-bottom: 1px solid var(--rule); margin-bottom: 1.75rem; font-size: 0.95rem; }
|
|
241
|
+
.tabs a { color: var(--ink-2); text-decoration: none; padding: 0.5rem 0; margin-bottom: -1px; border-bottom: 2px solid transparent; }
|
|
242
|
+
.tabs a:hover { color: var(--ink); }
|
|
243
|
+
.tabs a[aria-current="page"] { color: var(--ink); border-bottom-color: var(--accent); }
|
|
244
|
+
.tabs .count { font-size: 0.8rem; color: var(--ink-3); margin-left: 0.3rem; }
|
|
245
|
+
.tabs.sub { gap: 1.2rem; margin-top: -1rem; font-family: var(--mono); font-size: 0.8rem; }
|
|
246
|
+
|
|
247
|
+
/* Runs table */
|
|
248
|
+
.narrowed { color: var(--ink-2); font-size: 0.88rem; margin: 0 0 0.75rem; }
|
|
249
|
+
.narrowed .clear { margin-left: 0.8rem; color: var(--ink-3); }
|
|
250
|
+
.runs-table {
|
|
251
|
+
width: 100%;
|
|
252
|
+
border-collapse: separate;
|
|
253
|
+
border-spacing: 0;
|
|
237
254
|
background: var(--surface);
|
|
238
255
|
border: 1px solid var(--rule);
|
|
239
|
-
border-radius:
|
|
256
|
+
border-radius: 10px;
|
|
257
|
+
font-size: 0.86rem;
|
|
240
258
|
}
|
|
241
|
-
.
|
|
242
|
-
.
|
|
243
|
-
.
|
|
259
|
+
.runs-table th, .runs-table td { padding: 0.55rem 1rem; text-align: left; border-bottom: 1px solid var(--rule); }
|
|
260
|
+
.runs-table th { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); white-space: nowrap; }
|
|
261
|
+
.runs-table th:first-child, .runs-table .run-row td:first-child { width: 4rem; }
|
|
262
|
+
.runs-table .sort { color: inherit; text-decoration: none; }
|
|
263
|
+
.runs-table .sort:hover, .runs-table .sorted { color: var(--ink); }
|
|
264
|
+
.runs-table .arrow { margin-left: 0.3rem; color: var(--accent); }
|
|
265
|
+
.runs-table .filters td { padding-top: 0.4rem; padding-bottom: 0.4rem; background: var(--paper); }
|
|
266
|
+
.runs-table .filters input, .runs-table .filters select {
|
|
267
|
+
width: 100%;
|
|
268
|
+
min-width: 0;
|
|
269
|
+
font: inherit;
|
|
270
|
+
font-size: 0.82rem;
|
|
271
|
+
padding: 0.2rem 0.45rem;
|
|
272
|
+
color: var(--ink);
|
|
273
|
+
background: var(--surface);
|
|
274
|
+
border: 1px solid var(--rule);
|
|
275
|
+
border-radius: 5px;
|
|
276
|
+
}
|
|
277
|
+
.runs-table .filters select { padding: 0.2rem 0; }
|
|
278
|
+
.runs-table .filters input:focus, .runs-table .filters select:focus { outline: none; border-color: var(--accent); }
|
|
279
|
+
.runs-table tbody tr:last-child td { border-bottom: 0; }
|
|
280
|
+
.run-row { cursor: pointer; }
|
|
281
|
+
.run-row td { max-width: 24rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
282
|
+
.run-row:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
283
|
+
.run-row[aria-expanded="true"] { background: color-mix(in srgb, var(--accent) 6%, transparent); }
|
|
284
|
+
.record-row td { background: var(--paper); }
|
|
244
285
|
|
|
245
286
|
/* Notes */
|
|
246
287
|
.note {
|
|
@@ -288,56 +329,6 @@ h3 {
|
|
|
288
329
|
.prose th { font-weight: 600; border-bottom-color: var(--rule-strong); }
|
|
289
330
|
.prose hr { border: 0; border-top: 1px solid var(--rule); margin: 2em 0; }
|
|
290
331
|
|
|
291
|
-
/* Runs */
|
|
292
|
-
.runs {
|
|
293
|
-
--row: 2.75rem repeat(var(--columns), minmax(0, 1fr));
|
|
294
|
-
background: var(--surface);
|
|
295
|
-
border: 1px solid var(--rule);
|
|
296
|
-
border-radius: 10px;
|
|
297
|
-
overflow: hidden;
|
|
298
|
-
font-size: 0.86rem;
|
|
299
|
-
}
|
|
300
|
-
.run-head, .run summary, .run-filters {
|
|
301
|
-
display: grid;
|
|
302
|
-
grid-template-columns: var(--row);
|
|
303
|
-
gap: 1rem;
|
|
304
|
-
padding: 0.6rem 1.25rem;
|
|
305
|
-
align-items: center;
|
|
306
|
-
}
|
|
307
|
-
.run-head {
|
|
308
|
-
font-size: 0.72rem;
|
|
309
|
-
font-weight: 600;
|
|
310
|
-
letter-spacing: 0.08em;
|
|
311
|
-
text-transform: uppercase;
|
|
312
|
-
color: var(--ink-3);
|
|
313
|
-
border-bottom: 1px solid var(--rule);
|
|
314
|
-
}
|
|
315
|
-
.run-head .sort { color: inherit; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
316
|
-
.run-head .sort:hover, .run-head .sorted { color: var(--ink); }
|
|
317
|
-
.run-head .arrow { margin-left: 0.3rem; color: var(--accent); }
|
|
318
|
-
.run-filters { padding-top: 0.5rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--rule); background: var(--paper); }
|
|
319
|
-
.run-filters input, .run-filters select {
|
|
320
|
-
width: 100%;
|
|
321
|
-
min-width: 0;
|
|
322
|
-
font: inherit;
|
|
323
|
-
font-size: 0.82rem;
|
|
324
|
-
padding: 0.2rem 0.45rem;
|
|
325
|
-
color: var(--ink);
|
|
326
|
-
background: var(--surface);
|
|
327
|
-
border: 1px solid var(--rule);
|
|
328
|
-
border-radius: 5px;
|
|
329
|
-
}
|
|
330
|
-
.run-filters select { padding: 0.2rem 0; }
|
|
331
|
-
.run-filters input:focus, .run-filters select:focus { outline: none; border-color: var(--accent); }
|
|
332
|
-
.run-empty { margin: 0; padding: 1rem 1.25rem; }
|
|
333
|
-
h2 .clear { font-family: var(--sans); font-size: 0.82rem; margin-left: 0.8rem; color: var(--ink-3); }
|
|
334
|
-
.run { border-bottom: 1px solid var(--rule); }
|
|
335
|
-
.run:last-child { border-bottom: 0; }
|
|
336
|
-
.run summary { cursor: pointer; list-style: none; }
|
|
337
|
-
.run summary::-webkit-details-marker { display: none; }
|
|
338
|
-
.run summary span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
339
|
-
.run summary:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
340
|
-
.run[open] summary { background: color-mix(in srgb, var(--accent) 6%, transparent); }
|
|
341
332
|
.status { font-size: 0.8rem; font-weight: 700; }
|
|
342
333
|
.status.good { color: var(--good); }
|
|
343
334
|
.status.bad { color: var(--bad); }
|
|
@@ -348,9 +339,7 @@ h2 .clear { font-family: var(--sans); font-size: 0.82rem; margin-left: 0.8rem; c
|
|
|
348
339
|
grid-template-columns: max-content minmax(0, 1fr);
|
|
349
340
|
gap: 0.45rem 1.5rem;
|
|
350
341
|
margin: 0;
|
|
351
|
-
padding:
|
|
352
|
-
border-top: 1px dashed var(--rule);
|
|
353
|
-
background: var(--paper);
|
|
342
|
+
padding: 0.5rem 0 0.75rem 2.25rem;
|
|
354
343
|
}
|
|
355
344
|
.record dt { font-family: var(--mono); font-size: 0.78rem; color: var(--ink-3); }
|
|
356
345
|
.record dd { margin: 0; min-width: 0; }
|
|
@@ -403,7 +392,6 @@ h2 .clear { font-family: var(--sans); font-size: 0.82rem; margin-left: 0.8rem; c
|
|
|
403
392
|
@media (max-width: 52rem) {
|
|
404
393
|
main, .masthead { padding-left: 1.1rem; padding-right: 1.1rem; }
|
|
405
394
|
h1 { font-size: 2rem; }
|
|
406
|
-
.figures { grid-template-columns: 1fr; }
|
|
407
395
|
.ledger .design { display: none; }
|
|
408
396
|
.progress-col { width: auto; }
|
|
409
397
|
}
|
|
@@ -429,7 +417,7 @@ h2 .clear { font-family: var(--sans); font-size: 0.82rem; margin-left: 0.8rem; c
|
|
|
429
417
|
}
|
|
430
418
|
.cell .run:hover, .note-name .restart:hover { color: var(--accent); border-color: var(--accent); }
|
|
431
419
|
.cell .run:disabled, .note-name .restart:disabled { cursor: progress; opacity: 0.6; }
|
|
432
|
-
.note-name:has(.restart) { display: flex; align-items: center; justify-content:
|
|
420
|
+
.note-name:has(.restart) { display: flex; align-items: center; justify-content: flex-end; padding-top: 0.45rem; padding-bottom: 0.45rem; }
|
|
433
421
|
.cell-output { display: block; }
|
|
434
422
|
.cell-output:not(:empty) { margin-top: 0.4rem; padding-left: 0.9rem; border-left: 2px solid var(--accent-soft); overflow-x: auto; }
|
|
435
423
|
.cell-output pre { background: none; border: 0; padding: 0.3rem 0; }
|
|
@@ -10,12 +10,15 @@ from labnotes.tables.read_run import read_run
|
|
|
10
10
|
|
|
11
11
|
# experiments is a view, so files and runs can't declare a foreign key to it.
|
|
12
12
|
EXPERIMENTS = """
|
|
13
|
-
CREATE TABLE experiment_configs (
|
|
13
|
+
CREATE TABLE experiment_configs (
|
|
14
|
+
experiment TEXT PRIMARY KEY, config TEXT, source TEXT
|
|
15
|
+
);
|
|
14
16
|
CREATE VIEW experiments AS
|
|
15
17
|
SELECT
|
|
16
18
|
names.name,
|
|
17
19
|
coalesce(experiment_configs.config ->> 'name', names.name) AS display_name,
|
|
18
|
-
experiment_configs.config
|
|
20
|
+
experiment_configs.config,
|
|
21
|
+
experiment_configs.source
|
|
19
22
|
FROM (
|
|
20
23
|
SELECT experiment AS name FROM experiment_configs
|
|
21
24
|
UNION SELECT experiment FROM files
|
|
@@ -10,6 +10,12 @@ def read_experiment(root: Path, template: str, path: str) -> list[dict[str, obje
|
|
|
10
10
|
found = path_pattern(f"{template}/experiment.toml").fullmatch(relative)
|
|
11
11
|
if found is None:
|
|
12
12
|
return []
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return [
|
|
13
|
+
source = Path(path).read_text(encoding="utf-8")
|
|
14
|
+
config = tomllib.loads(source)
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
"experiment": found["experiment"],
|
|
18
|
+
"config": json.dumps(config),
|
|
19
|
+
"source": source,
|
|
20
|
+
}
|
|
21
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
{% set e = experiment %}
|
|
3
|
+
{% set tab = 'notebooks' if request.query_params.get('tab') == 'notebooks' else 'runs' %}
|
|
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
|
+
{% block main %}
|
|
7
|
+
<section class="intro">
|
|
8
|
+
<h1>{{ e.display_name }}</h1>
|
|
9
|
+
{% if e.display_name != e.name %}<p class="study">{{ e.name }}</p>{% endif %}
|
|
10
|
+
{% if e.config_source is not none %}
|
|
11
|
+
<div class="config-line">
|
|
12
|
+
<p class="config-summary">
|
|
13
|
+
<span class="chip quiet">{{ e.mode }}</span>
|
|
14
|
+
{% for key, value in e.other.items() %}<span class="setting">{{ key }} <code>{{ value | value }}</code></span>{% endfor %}
|
|
15
|
+
{% if e.runs_per_cell %}<span class="setting">repeats <b class="num">{{ e.runs_per_cell }}</b></span>{% endif %}
|
|
16
|
+
{% for key, values in e.varies.items() %}<span class="setting">{{ key }}{% for value in values %} <span class="chip">{{ value | value }}</span>{% endfor %}</span>{% endfor %}
|
|
17
|
+
{% if e.counts_error %}<span class="setting counts-error">{{ e.counts_error }}</span>{% elif e.counts_set %}<span class="setting">counts <code>{{ e.completed_run }}</code></span>{% endif %}
|
|
18
|
+
</p>
|
|
19
|
+
<button type="button" class="config-open">experiment.toml</button>
|
|
20
|
+
</div>
|
|
21
|
+
<dialog class="config-file">
|
|
22
|
+
<header>experiment.toml<button type="button" class="config-close">Close</button></header>
|
|
23
|
+
<pre>{{ e.config_source }}</pre>
|
|
24
|
+
</dialog>
|
|
25
|
+
{% endif %}
|
|
26
|
+
</section>
|
|
27
|
+
|
|
28
|
+
<nav class="tabs">
|
|
29
|
+
<a href="?"{% if tab == 'runs' %} aria-current="page"{% endif %}>Runs <span class="count num">{{ e.runs }}</span></a>
|
|
30
|
+
<a href="?tab=notebooks"{% if tab == 'notebooks' %} aria-current="page"{% endif %}>Notebooks <span class="count num">{{ e.notes | length }}</span></a>
|
|
31
|
+
</nav>
|
|
32
|
+
|
|
33
|
+
{% if tab == 'notebooks' %}
|
|
34
|
+
{% set chosen = request.query_params.get('note') %}
|
|
35
|
+
{% set note = (e.notes | selectattr('name', 'equalto', chosen) | first) or (e.notes | first) %}
|
|
36
|
+
{% if note %}
|
|
37
|
+
<nav class="tabs sub">
|
|
38
|
+
{% for each in e.notes %}<a href="?tab=notebooks&note={{ each.name | urlencode }}"{% if each.name == note.name %} aria-current="page"{% endif %}>{{ each.name }}</a>{% endfor %}
|
|
39
|
+
</nav>
|
|
40
|
+
<article class="note" data-run-url="{{ url_for('run_block', name=e.name, note=note.name) }}" data-restart-url="{{ url_for('restart_kernel', name=e.name, note=note.name) }}">
|
|
41
|
+
{% if note.runnable %}<header class="note-name"><button type="button" class="restart" title="Stop this note's Python kernel. The next Run starts a fresh one.">Restart</button></header>{% endif %}
|
|
42
|
+
<div class="prose">{{ note.html | safe }}</div>
|
|
43
|
+
</article>
|
|
44
|
+
<script src="{{ asset('run-cell.js') }}" defer></script>
|
|
45
|
+
{% else %}
|
|
46
|
+
<p class="empty">No notebooks yet.</p>
|
|
47
|
+
{% endif %}
|
|
48
|
+
{% else %}
|
|
49
|
+
<section id="runs">
|
|
50
|
+
{% set view = e.run_view %}
|
|
51
|
+
{% set narrowed = view.status or view.filters %}
|
|
52
|
+
{% if narrowed %}<p class="narrowed"><span class="num">{{ e.runs_list | length }}</span> of <span class="num">{{ e.runs }}</span> <a class="clear" href="{{ '?sort=' ~ (view.sort | urlencode) ~ ('&order=desc' if view.descending else '') if view.sort else '?' }}#runs">Clear filters</a></p>{% endif %}
|
|
53
|
+
{% if e.runs %}
|
|
54
|
+
<form id="run-filters" method="get" action="#runs">
|
|
55
|
+
{% if view.sort %}<input type="hidden" name="sort" value="{{ view.sort }}">{% endif %}
|
|
56
|
+
{% if view.descending %}<input type="hidden" name="order" value="desc">{% endif %}
|
|
57
|
+
<button type="submit" hidden>Filter</button>
|
|
58
|
+
</form>
|
|
59
|
+
<div class="scroll">
|
|
60
|
+
<table class="runs-table">
|
|
61
|
+
<thead>
|
|
62
|
+
<tr>
|
|
63
|
+
{% for column in ['status'] + e.run_columns %}
|
|
64
|
+
{% set sorted = view.sort == column %}
|
|
65
|
+
<th scope="col"{% if sorted %} aria-sort="{{ 'descending' if view.descending else 'ascending' }}"{% endif %}><a class="sort{{ ' sorted' if sorted }}" href="{{ e.sort_hrefs[column] }}" title="Sort by {{ column }}">{{ '' if column == 'status' else column }}{% if sorted %}<span class="arrow">{{ '↓' if view.descending else '↑' }}</span>{% endif %}</a></th>
|
|
66
|
+
{% endfor %}
|
|
67
|
+
</tr>
|
|
68
|
+
<tr class="filters">
|
|
69
|
+
<td><select form="run-filters" name="status" aria-label="Filter by status" onchange="this.form.requestSubmit()">
|
|
70
|
+
<option value=""{{ ' selected' if not view.status }}>all</option>
|
|
71
|
+
<option value="complete"{{ ' selected' if view.status == 'complete' }}>✓</option>
|
|
72
|
+
<option value="incomplete"{{ ' selected' if view.status == 'incomplete' }}>✕</option>
|
|
73
|
+
</select></td>
|
|
74
|
+
{% for column in e.run_columns %}<td><input form="run-filters" type="search" name="f.{{ column }}" value="{{ view.filters.get(column, '') }}" placeholder="filter" aria-label="Filter {{ column }}"></td>{% endfor %}
|
|
75
|
+
</tr>
|
|
76
|
+
</thead>
|
|
77
|
+
<tbody>
|
|
78
|
+
{% for run in e.runs_list %}
|
|
79
|
+
<tr class="run-row" aria-expanded="false">
|
|
80
|
+
<td><span class="status {{ 'good' if run.labnotes_completed else 'bad' }}" title="{{ 'complete' if run.labnotes_completed else 'not complete' }}">{{ '✓' if run.labnotes_completed else '✕' }}</span></td>
|
|
81
|
+
{% for column in e.run_columns %}<td class="{{ 'mono' if loop.first }}">{{ run[column] | value }}</td>{% endfor %}
|
|
82
|
+
</tr>
|
|
83
|
+
<tr class="record-row" hidden>
|
|
84
|
+
<td colspan="{{ e.run_columns | length + 1 }}">
|
|
85
|
+
<dl class="record">
|
|
86
|
+
{% for key, value in run.items() if key != 'labnotes_completed' %}
|
|
87
|
+
<dt>{{ key }}</dt>
|
|
88
|
+
{% set shown = value | value %}
|
|
89
|
+
<dd>{% if shown | length > 80 %}<pre>{{ shown }}</pre>{% else %}<code>{{ shown }}</code>{% endif %}</dd>
|
|
90
|
+
{% endfor %}
|
|
91
|
+
</dl>
|
|
92
|
+
</td>
|
|
93
|
+
</tr>
|
|
94
|
+
{% else %}
|
|
95
|
+
<tr><td class="empty" colspan="{{ e.run_columns | length + 1 }}">No runs match these filters.</td></tr>
|
|
96
|
+
{% endfor %}
|
|
97
|
+
</tbody>
|
|
98
|
+
</table>
|
|
99
|
+
</div>
|
|
100
|
+
{% else %}
|
|
101
|
+
<p class="empty">No runs recorded yet.</p>
|
|
102
|
+
{% endif %}
|
|
103
|
+
</section>
|
|
104
|
+
{% endif %}
|
|
105
|
+
<script src="{{ asset('experiment.js') }}" defer></script>
|
|
106
|
+
{% endblock %}
|
|
@@ -17,7 +17,7 @@ from labnotes.tui.format_elapsed import format_elapsed
|
|
|
17
17
|
from labnotes.tui.runs_table import RunsTable
|
|
18
18
|
|
|
19
19
|
COLUMNS = ("run", "params", "status", "time")
|
|
20
|
-
|
|
20
|
+
STREAMS = ("stdout", "stderr")
|
|
21
21
|
PENDING, PASSED, FAILED = "○", "✓", "✗"
|
|
22
22
|
SPINNER = "|/-\\"
|
|
23
23
|
SKIPPED, DESELECTED = "–", "⊘"
|
|
@@ -33,7 +33,6 @@ class RunApp(App[None]):
|
|
|
33
33
|
Binding("f", "hide_failed", "Hide failed"),
|
|
34
34
|
Binding("r", "retry", "Retry"),
|
|
35
35
|
Binding("slash", "filter", "Filter", key_display="/"),
|
|
36
|
-
Binding("e", "switch_output", "stdout/stderr"),
|
|
37
36
|
Binding("escape", "leave", "Back"),
|
|
38
37
|
Binding("plus,equals_sign", "concurrency(1)", "More at once", key_display="+"),
|
|
39
38
|
Binding("minus", "concurrency(-1)", "Fewer at once", key_display="-"),
|
|
@@ -67,9 +66,13 @@ class RunApp(App[None]):
|
|
|
67
66
|
self.hiding = False
|
|
68
67
|
self.folders: dict[str, Path] = {}
|
|
69
68
|
self.viewing: str | None = None
|
|
70
|
-
self.
|
|
71
|
-
self.
|
|
72
|
-
|
|
69
|
+
self.offsets: dict[str, int]
|
|
70
|
+
self.decoders = {
|
|
71
|
+
stream: codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
72
|
+
for stream in STREAMS
|
|
73
|
+
}
|
|
74
|
+
self.last_stream: str | None
|
|
75
|
+
self.mid_line: bool
|
|
73
76
|
|
|
74
77
|
def compose(self) -> ComposeResult:
|
|
75
78
|
yield Header()
|
|
@@ -115,7 +118,7 @@ class RunApp(App[None]):
|
|
|
115
118
|
self.show(describe_event(event))
|
|
116
119
|
match event["mode"], event["runs"]:
|
|
117
120
|
case "adhoc", [only]:
|
|
118
|
-
self.view(only["run"]
|
|
121
|
+
self.view(only["run"]) # type: ignore[index]
|
|
119
122
|
case "started":
|
|
120
123
|
self.idle = False
|
|
121
124
|
self.pending.discard(run)
|
|
@@ -273,34 +276,41 @@ class RunApp(App[None]):
|
|
|
273
276
|
field.focus()
|
|
274
277
|
|
|
275
278
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
276
|
-
self.view(str(event.row_key.value)
|
|
279
|
+
self.view(str(event.row_key.value))
|
|
277
280
|
|
|
278
|
-
def view(self, run: str
|
|
281
|
+
def view(self, run: str) -> None:
|
|
279
282
|
self.viewing = run
|
|
280
|
-
self.
|
|
281
|
-
self.
|
|
282
|
-
|
|
283
|
+
self.offsets = dict.fromkeys(STREAMS, 0)
|
|
284
|
+
for decoder in self.decoders.values():
|
|
285
|
+
decoder.reset()
|
|
286
|
+
self.last_stream = None
|
|
287
|
+
self.mid_line = False
|
|
283
288
|
self.query_one(Log).clear()
|
|
284
289
|
self.query_one(DataTable).display = False
|
|
285
290
|
self.query_one(Log).display = True
|
|
286
|
-
self.show(f"{run}
|
|
291
|
+
self.show(f"{run} stdout and stderr. Escape goes back.")
|
|
287
292
|
self.follow()
|
|
288
293
|
|
|
289
294
|
def follow(self) -> None:
|
|
290
295
|
if self.viewing not in self.folders:
|
|
291
296
|
return
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
297
|
+
for stream in STREAMS:
|
|
298
|
+
path = self.folders[self.viewing] / f"{stream}.log"
|
|
299
|
+
if not path.exists():
|
|
300
|
+
continue
|
|
301
|
+
with path.open("rb") as log:
|
|
302
|
+
log.seek(self.offsets[stream])
|
|
303
|
+
data = log.read()
|
|
304
|
+
self.offsets[stream] += len(data)
|
|
305
|
+
text = self.decoders[stream].decode(data)
|
|
306
|
+
if not text:
|
|
307
|
+
continue
|
|
308
|
+
# Don't glue one stream's unfinished line onto the other's output.
|
|
309
|
+
if self.mid_line and stream != self.last_stream:
|
|
310
|
+
text = "\n" + text
|
|
311
|
+
self.query_one(Log).write(text)
|
|
312
|
+
self.last_stream = stream
|
|
313
|
+
self.mid_line = not text.endswith("\n")
|
|
304
314
|
|
|
305
315
|
def action_leave(self) -> None:
|
|
306
316
|
if self.viewing is not None:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[[python.exempt]]
|
|
2
|
+
path = "src/labnotes/runner/hold_folder.py"
|
|
3
|
+
rules = ["mutation"]
|
|
4
|
+
lines = [12]
|
|
5
|
+
reason = "LOCK_EX and LOCK_NB are disjoint bits, so | equals + and ^"
|
|
6
|
+
|
|
7
|
+
[[python.exempt]]
|
|
8
|
+
path = "src/labnotes/tui/run_app.py"
|
|
9
|
+
rules = ["mutation"]
|
|
10
|
+
lines = [309]
|
|
11
|
+
reason = "last_stream is always the same str object taken from STREAMS, so is not equals !="
|