labnotes 0.0.99__tar.gz → 0.0.100__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.99 → labnotes-0.0.100}/PKG-INFO +1 -1
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/run_output.py +27 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/static/labnotes.css +7 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/static/run-output.js +29 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/open_tables.py +13 -0
- labnotes-0.0.100/src/labnotes/tables/read_blocks.py +17 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/templates/run.html +2 -0
- labnotes-0.0.100/src/labnotes/tui/log_block.py +31 -0
- labnotes-0.0.100/src/labnotes/tui/log_blocks.py +55 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/run_app.py +5 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/run_screen.py +3 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/.gitignore +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/CHANGELOG.md +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/MIGRATIONS.md +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/README.md +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/pyproject.toml +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/__init__.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/__main__.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/asset_url.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/check_counts.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/cli.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/context.md +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/context_guide.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/count_groups.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/count_runs.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/create_app.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/create_experiment.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/create_note.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/__init__.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/control_run.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/daemon_app.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/daemon_client.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/daemon_running.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/daemon_server.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/describe_status.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/ensure_daemon.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/exit_when_idle.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/remote_start.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/run_daemon.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/run_logs.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/serve_daemon.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/socket_path.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/stop_runs.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/watch_daemon.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon/watch_status.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/daemon_command.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/data_html.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/default_config_text.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/default_counts.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/drain_watch.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/experiment_notes.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/experiment_runs.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/experiments.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/file_changes.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/format_duration.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/format_started.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/format_value.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/group_runs.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/init.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/invocation_started.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/is_positive_int.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/list_experiments.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/list_runs.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/load_config.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/load_experiment.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/load_run.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/log.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/log_path.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/models.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/modes.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/nests_runs.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/note_path.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/notebook_kernel.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/output_html.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/parse_run_view.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/print_rows.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/python_blocks.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/read_config.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/read_path.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/render_markdown.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/run_command.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/run_control.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/run_status.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/__init__.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/describe_event.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/execute_run.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/hold_folder.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/new_param_run_id.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/new_run_id.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/new_stamped_run_id.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/param_args.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/param_grid.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/param_run_name.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/plan_runs.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/prepare_experiment.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/read_experiment_config.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/run_experiment.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/run_folder.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/run_headless.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/runs_parent.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/shell_command.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/signal_tree.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/stop_then_kill.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/wait_for_tree.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/runner/write_run_json.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/save_conflict_copy.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/save_note.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/saved_outputs.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/serve_app.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/server.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/shared_params.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/sort_href.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/sql_quote.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/static/elapsed.js +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/static/experiment.js +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/static/local-time.js +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/static/new-note.js +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/static/note-edit.js +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/static/note-editor.js +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/static/row-link.js +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/static/run-cell.js +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/static/view-toggle.js +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/summarize_config.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/__init__.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/experiment_rows.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/experiment_run_ids.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/file_prefix.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/file_text.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/iso_time.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/list_experiment_runs.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/note_text.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/path_glob.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/path_pattern.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/read_experiment.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/read_file.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/read_log.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/read_run.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tables/summarize_experiments.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/templates/base.html +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/templates/editor_imports.html +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/templates/experiment.html +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/templates/index.html +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/templates/note.html +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/__init__.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/browse_app.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/browse_tui.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/dim.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/experiment_screen.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/experiments_screen.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/fill_table.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/format_elapsed.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/group_cells.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/invocation_columns.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/invocation_rows.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/leave_prompt.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/reload_on_change.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/run_cells.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/run_output.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/run_tui.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/runs_table.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/tui/stopped_title.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/wait_for_note.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/src/labnotes/watch_rows.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/testing-conventions.toml +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/tests/integration/conftest.py +0 -0
- {labnotes-0.0.99 → labnotes-0.0.100}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: labnotes
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.100
|
|
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>
|
|
@@ -43,8 +43,30 @@ async def run_output(
|
|
|
43
43
|
last, mid_line = stream, not text.endswith("\n")
|
|
44
44
|
yield f"data: {json.dumps({'stream': stream, 'text': text})}\n\n"
|
|
45
45
|
|
|
46
|
+
blocks_read = 0
|
|
47
|
+
|
|
48
|
+
def read_blocks() -> Iterator[str]:
|
|
49
|
+
nonlocal blocks_read
|
|
50
|
+
path = folder / "log.jsonl"
|
|
51
|
+
if not path.exists():
|
|
52
|
+
return
|
|
53
|
+
with path.open("rb") as log:
|
|
54
|
+
log.seek(blocks_read)
|
|
55
|
+
data = log.read()
|
|
56
|
+
whole = data[: data.rfind(b"\n") + 1]
|
|
57
|
+
blocks_read += len(whole)
|
|
58
|
+
for line in whole.splitlines():
|
|
59
|
+
try:
|
|
60
|
+
record = json.loads(line)
|
|
61
|
+
except json.JSONDecodeError:
|
|
62
|
+
continue
|
|
63
|
+
if isinstance(record, dict):
|
|
64
|
+
yield f"event: block\ndata: {json.dumps(record)}\n\n"
|
|
65
|
+
|
|
46
66
|
for message in read(STREAMS):
|
|
47
67
|
yield message
|
|
68
|
+
for message in read_blocks():
|
|
69
|
+
yield message
|
|
48
70
|
exit_code = run["exit_code"]
|
|
49
71
|
if run["ended"] is None:
|
|
50
72
|
async for event in events:
|
|
@@ -54,6 +76,9 @@ async def run_output(
|
|
|
54
76
|
if event.table == "logs" and row["run"] == run_id:
|
|
55
77
|
for message in read([row["stream"]]):
|
|
56
78
|
yield message
|
|
79
|
+
elif event.table == "blocks" and row["run"] == run_id:
|
|
80
|
+
for message in read_blocks():
|
|
81
|
+
yield message
|
|
57
82
|
elif (
|
|
58
83
|
event.table == "runs"
|
|
59
84
|
and row["id"] == run_id
|
|
@@ -63,4 +88,6 @@ async def run_output(
|
|
|
63
88
|
break
|
|
64
89
|
for message in read(STREAMS):
|
|
65
90
|
yield message
|
|
91
|
+
for message in read_blocks():
|
|
92
|
+
yield message
|
|
66
93
|
yield f"event: end\ndata: {json.dumps(exit_code)}\n\n"
|
|
@@ -302,6 +302,13 @@ h3 {
|
|
|
302
302
|
/* Run output */
|
|
303
303
|
.run-state.good { color: var(--good); }
|
|
304
304
|
.run-state.bad { color: var(--bad); }
|
|
305
|
+
.run-blocks { background: var(--surface); border: 1px solid var(--rule); border-radius: 6px; margin-bottom: 1rem; padding: 0.5rem 0; max-height: 50vh; overflow: auto; }
|
|
306
|
+
.run-blocks[hidden] { display: none; }
|
|
307
|
+
.log-block { display: block; width: 100%; padding: 0.2rem 1.25rem; border: 0; background: none; color: inherit; font: inherit; font-family: var(--mono); font-size: 0.82rem; text-align: left; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
308
|
+
.log-block::before { content: "▸ "; color: var(--ink-3); }
|
|
309
|
+
.log-block[aria-expanded="true"] { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
310
|
+
.log-block[aria-expanded="true"]::before { content: "▾ "; }
|
|
311
|
+
.log-block:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
305
312
|
.run-log { background: var(--surface); border: 1px solid var(--rule); border-radius: 6px; }
|
|
306
313
|
.run-log header { display: flex; gap: 1rem; padding: 0.4rem 1.25rem; border-bottom: 1px solid var(--rule); font-family: var(--mono); font-size: 0.75rem; color: var(--ink-3); }
|
|
307
314
|
.run-log header span::before { content: "■ "; }
|
|
@@ -4,12 +4,41 @@ const output = document.querySelector(".run-output");
|
|
|
4
4
|
const state = document.querySelector(".run-state");
|
|
5
5
|
const source = new EventSource(output.dataset.url);
|
|
6
6
|
|
|
7
|
+
const blocks = document.querySelector(".run-blocks");
|
|
8
|
+
|
|
7
9
|
let parsers = {};
|
|
10
|
+
let streaming = new Map();
|
|
8
11
|
|
|
9
12
|
// The server resends the whole log on each connection, so a reconnect starts clean.
|
|
10
13
|
source.addEventListener("open", () => {
|
|
11
14
|
output.replaceChildren();
|
|
12
15
|
parsers = {};
|
|
16
|
+
blocks.replaceChildren();
|
|
17
|
+
blocks.hidden = true;
|
|
18
|
+
streaming = new Map();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
source.addEventListener("block", (event) => {
|
|
22
|
+
const { id, phase, content = "" } = JSON.parse(event.data);
|
|
23
|
+
const open = streaming.get(id);
|
|
24
|
+
if (open && (phase === "delta" || phase === "stop")) {
|
|
25
|
+
open.textContent += content;
|
|
26
|
+
if (phase === "stop") streaming.delete(id);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (phase === "stop") return;
|
|
30
|
+
const line = document.createElement("button");
|
|
31
|
+
line.type = "button";
|
|
32
|
+
line.className = "log-block";
|
|
33
|
+
line.setAttribute("aria-expanded", "false");
|
|
34
|
+
line.textContent = content;
|
|
35
|
+
line.addEventListener("click", () => {
|
|
36
|
+
const expanded = line.getAttribute("aria-expanded") === "true";
|
|
37
|
+
line.setAttribute("aria-expanded", String(!expanded));
|
|
38
|
+
});
|
|
39
|
+
if (phase === "start" || phase === "delta") streaming.set(id, line);
|
|
40
|
+
blocks.append(line);
|
|
41
|
+
blocks.hidden = false;
|
|
13
42
|
});
|
|
14
43
|
|
|
15
44
|
source.addEventListener("message", (event) => {
|
|
@@ -4,6 +4,7 @@ from dirsql import DirSQL, Table
|
|
|
4
4
|
|
|
5
5
|
from labnotes.models import LabnotesConfig
|
|
6
6
|
from labnotes.tables.path_glob import path_glob
|
|
7
|
+
from labnotes.tables.read_blocks import read_blocks
|
|
7
8
|
from labnotes.tables.read_experiment import read_experiment
|
|
8
9
|
from labnotes.tables.read_file import read_file
|
|
9
10
|
from labnotes.tables.read_log import read_log
|
|
@@ -48,6 +49,12 @@ CREATE TABLE logs (
|
|
|
48
49
|
PRIMARY KEY (experiment, run, stream)
|
|
49
50
|
)
|
|
50
51
|
"""
|
|
52
|
+
BLOCKS = """
|
|
53
|
+
CREATE TABLE blocks (
|
|
54
|
+
experiment TEXT, run TEXT, size INTEGER,
|
|
55
|
+
PRIMARY KEY (experiment, run)
|
|
56
|
+
)
|
|
57
|
+
"""
|
|
51
58
|
|
|
52
59
|
|
|
53
60
|
def open_tables(config: LabnotesConfig) -> DirSQL:
|
|
@@ -73,6 +80,12 @@ def open_tables(config: LabnotesConfig) -> DirSQL:
|
|
|
73
80
|
glob=path_glob(f"{config.runs}/std*.log"),
|
|
74
81
|
on_file=partial(read_log, root, config.runs),
|
|
75
82
|
),
|
|
83
|
+
Table(
|
|
84
|
+
name="blocks",
|
|
85
|
+
ddl=BLOCKS,
|
|
86
|
+
glob=path_glob(f"{config.runs}/log.jsonl"),
|
|
87
|
+
on_file=partial(read_blocks, root, config.runs),
|
|
88
|
+
),
|
|
76
89
|
Table(
|
|
77
90
|
name="experiment_configs",
|
|
78
91
|
ddl=EXPERIMENTS,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from labnotes.tables.path_pattern import path_pattern
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def read_blocks(root: Path, template: str, path: str) -> list[dict[str, object]]:
|
|
7
|
+
relative = Path(path).relative_to(root).as_posix()
|
|
8
|
+
found = path_pattern(f"{template}/log.jsonl").fullmatch(relative)
|
|
9
|
+
if found is None:
|
|
10
|
+
return []
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
"experiment": found["experiment"],
|
|
14
|
+
"run": found["run"],
|
|
15
|
+
"size": Path(path).stat().st_size,
|
|
16
|
+
}
|
|
17
|
+
]
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
</section>
|
|
13
13
|
|
|
14
|
+
<section class="run-blocks" hidden></section>
|
|
15
|
+
|
|
14
16
|
<section class="run-log">
|
|
15
17
|
<header><span class="stdout">stdout</span><span class="stderr">stderr</span></header>
|
|
16
18
|
<pre class="run-output" data-url="{{ url_for('output', name=run.experiment, run=run.id) }}"></pre>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from rich.text import Text
|
|
2
|
+
from textual.events import Click
|
|
3
|
+
from textual.widgets import Static
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LogBlock(Static):
|
|
7
|
+
DEFAULT_CSS = """
|
|
8
|
+
LogBlock { height: auto; }
|
|
9
|
+
LogBlock.-collapsed { height: 1; text-wrap: nowrap; text-overflow: ellipsis; }
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, body: str) -> None:
|
|
13
|
+
super().__init__(classes="-collapsed")
|
|
14
|
+
self.body = body
|
|
15
|
+
self.expanded = False
|
|
16
|
+
self.show()
|
|
17
|
+
|
|
18
|
+
def extend(self, text: str) -> None:
|
|
19
|
+
self.body += text
|
|
20
|
+
self.show()
|
|
21
|
+
|
|
22
|
+
def on_click(self, _: Click) -> None:
|
|
23
|
+
self.expanded = not self.expanded
|
|
24
|
+
self.set_class(not self.expanded, "-collapsed")
|
|
25
|
+
self.show()
|
|
26
|
+
|
|
27
|
+
def show(self) -> None:
|
|
28
|
+
if self.expanded:
|
|
29
|
+
self.update(Text(f"▾ {self.body}"))
|
|
30
|
+
else:
|
|
31
|
+
self.update(Text(f"▸ {self.body}".replace("\n", " ")))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from textual.containers import VerticalScroll
|
|
5
|
+
|
|
6
|
+
from labnotes.tui.log_block import LogBlock
|
|
7
|
+
|
|
8
|
+
STREAMING = ("start", "delta")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LogBlocks(VerticalScroll):
|
|
12
|
+
DEFAULT_CSS = "LogBlocks { height: auto; max-height: 50%; }"
|
|
13
|
+
|
|
14
|
+
def __init__(self, id: str | None = None) -> None:
|
|
15
|
+
super().__init__(id=id)
|
|
16
|
+
self.reset()
|
|
17
|
+
|
|
18
|
+
def reset(self) -> None:
|
|
19
|
+
self.read_to = 0
|
|
20
|
+
self.streaming: dict[object, LogBlock] = {}
|
|
21
|
+
self.remove_children()
|
|
22
|
+
self.display = False
|
|
23
|
+
|
|
24
|
+
def follow(self, folder: Path) -> None:
|
|
25
|
+
path = folder / "log.jsonl"
|
|
26
|
+
if not path.exists():
|
|
27
|
+
return
|
|
28
|
+
with path.open("rb") as log:
|
|
29
|
+
log.seek(self.read_to)
|
|
30
|
+
data = log.read()
|
|
31
|
+
complete = data[: data.rfind(b"\n") + 1]
|
|
32
|
+
self.read_to += len(complete)
|
|
33
|
+
for line in complete.splitlines():
|
|
34
|
+
try:
|
|
35
|
+
record = json.loads(line)
|
|
36
|
+
except json.JSONDecodeError:
|
|
37
|
+
continue
|
|
38
|
+
if isinstance(record, dict):
|
|
39
|
+
self.add(record)
|
|
40
|
+
|
|
41
|
+
def add(self, record: dict[str, object]) -> None:
|
|
42
|
+
phase, key = record.get("phase"), record.get("id")
|
|
43
|
+
body = str(record.get("content", ""))
|
|
44
|
+
if phase == "delta" and key in self.streaming:
|
|
45
|
+
self.streaming[key].extend(body)
|
|
46
|
+
return
|
|
47
|
+
if phase == "stop":
|
|
48
|
+
if key in self.streaming:
|
|
49
|
+
self.streaming.pop(key).extend(body)
|
|
50
|
+
return
|
|
51
|
+
block = LogBlock(body)
|
|
52
|
+
if phase in STREAMING:
|
|
53
|
+
self.streaming[key] = block
|
|
54
|
+
self.mount(block)
|
|
55
|
+
self.display = True
|
|
@@ -15,6 +15,7 @@ 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
17
|
from labnotes.tui.leave_prompt import LeavePrompt
|
|
18
|
+
from labnotes.tui.log_blocks import LogBlocks
|
|
18
19
|
from labnotes.tui.run_output import RunOutput
|
|
19
20
|
from labnotes.tui.runs_table import RunsTable
|
|
20
21
|
|
|
@@ -83,6 +84,7 @@ class RunApp(App[None]):
|
|
|
83
84
|
def compose(self) -> ComposeResult:
|
|
84
85
|
yield Header()
|
|
85
86
|
yield RunsTable(id="runs", cursor_type="row")
|
|
87
|
+
yield LogBlocks(id="blocks")
|
|
86
88
|
yield RunOutput(id="output")
|
|
87
89
|
yield Input(id="filter", placeholder="Filter params")
|
|
88
90
|
yield Static(id="status")
|
|
@@ -379,6 +381,7 @@ class RunApp(App[None]):
|
|
|
379
381
|
|
|
380
382
|
def view(self, run: str) -> None:
|
|
381
383
|
self.viewing = run
|
|
384
|
+
self.query_one(LogBlocks).reset()
|
|
382
385
|
self.query_one(RunOutput).reset()
|
|
383
386
|
self.query_one(DataTable).display = False
|
|
384
387
|
self.query_one(RunOutput).display = True
|
|
@@ -389,11 +392,13 @@ class RunApp(App[None]):
|
|
|
389
392
|
|
|
390
393
|
def follow(self) -> None:
|
|
391
394
|
if self.viewing in self.folders:
|
|
395
|
+
self.query_one(LogBlocks).follow(self.folders[self.viewing])
|
|
392
396
|
self.query_one(RunOutput).follow(self.folders[self.viewing])
|
|
393
397
|
|
|
394
398
|
def action_leave(self) -> None:
|
|
395
399
|
if self.viewing is not None:
|
|
396
400
|
self.viewing = None
|
|
401
|
+
self.query_one(LogBlocks).display = False
|
|
397
402
|
self.query_one(RunOutput).display = False
|
|
398
403
|
self.query_one(DataTable).display = True
|
|
399
404
|
self.sub_title = ""
|
|
@@ -5,6 +5,7 @@ from textual.binding import Binding
|
|
|
5
5
|
from textual.screen import Screen
|
|
6
6
|
from textual.widgets import Footer, Header
|
|
7
7
|
|
|
8
|
+
from labnotes.tui.log_blocks import LogBlocks
|
|
8
9
|
from labnotes.tui.run_output import RunOutput
|
|
9
10
|
|
|
10
11
|
|
|
@@ -18,6 +19,7 @@ class RunScreen(Screen[None]):
|
|
|
18
19
|
|
|
19
20
|
def compose(self) -> ComposeResult:
|
|
20
21
|
yield Header()
|
|
22
|
+
yield LogBlocks(id="blocks")
|
|
21
23
|
yield RunOutput(id="output")
|
|
22
24
|
yield Footer()
|
|
23
25
|
|
|
@@ -27,4 +29,5 @@ class RunScreen(Screen[None]):
|
|
|
27
29
|
self.set_interval(0.25, self.follow)
|
|
28
30
|
|
|
29
31
|
def follow(self) -> None:
|
|
32
|
+
self.query_one(LogBlocks).follow(self.folder)
|
|
30
33
|
self.query_one(RunOutput).follow(self.folder)
|
|
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
|
|
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
|