labnotes 0.0.88__tar.gz → 0.0.90__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. {labnotes-0.0.88 → labnotes-0.0.90}/PKG-INFO +1 -1
  2. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/experiment_screen.py +7 -2
  3. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/run_app.py +9 -39
  4. labnotes-0.0.90/src/labnotes/tui/run_output.py +41 -0
  5. labnotes-0.0.90/src/labnotes/tui/run_screen.py +30 -0
  6. {labnotes-0.0.88 → labnotes-0.0.90}/testing-conventions.toml +7 -13
  7. {labnotes-0.0.88 → labnotes-0.0.90}/.gitignore +0 -0
  8. {labnotes-0.0.88 → labnotes-0.0.90}/CHANGELOG.md +0 -0
  9. {labnotes-0.0.88 → labnotes-0.0.90}/MIGRATIONS.md +0 -0
  10. {labnotes-0.0.88 → labnotes-0.0.90}/README.md +0 -0
  11. {labnotes-0.0.88 → labnotes-0.0.90}/pyproject.toml +0 -0
  12. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/__init__.py +0 -0
  13. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/__main__.py +0 -0
  14. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/asset_url.py +0 -0
  15. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/check_counts.py +0 -0
  16. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/cli.py +0 -0
  17. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/context.md +0 -0
  18. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/context_guide.py +0 -0
  19. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/count_groups.py +0 -0
  20. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/count_runs.py +0 -0
  21. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/create_app.py +0 -0
  22. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/create_experiment.py +0 -0
  23. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/create_note.py +0 -0
  24. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/__init__.py +0 -0
  25. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/control_run.py +0 -0
  26. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/daemon_app.py +0 -0
  27. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/daemon_client.py +0 -0
  28. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/daemon_running.py +0 -0
  29. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/daemon_server.py +0 -0
  30. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/describe_status.py +0 -0
  31. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/ensure_daemon.py +0 -0
  32. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/exit_when_idle.py +0 -0
  33. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/remote_start.py +0 -0
  34. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/run_daemon.py +0 -0
  35. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/run_logs.py +0 -0
  36. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/serve_daemon.py +0 -0
  37. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/socket_path.py +0 -0
  38. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/stop_runs.py +0 -0
  39. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/watch_daemon.py +0 -0
  40. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon/watch_status.py +0 -0
  41. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/daemon_command.py +0 -0
  42. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/data_html.py +0 -0
  43. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/default_config_text.py +0 -0
  44. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/default_counts.py +0 -0
  45. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/drain_watch.py +0 -0
  46. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/experiment_notes.py +0 -0
  47. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/experiment_runs.py +0 -0
  48. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/experiments.py +0 -0
  49. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/file_changes.py +0 -0
  50. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/format_duration.py +0 -0
  51. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/format_started.py +0 -0
  52. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/format_value.py +0 -0
  53. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/group_runs.py +0 -0
  54. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/init.py +0 -0
  55. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/invocation_started.py +0 -0
  56. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/is_positive_int.py +0 -0
  57. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/list_experiments.py +0 -0
  58. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/list_runs.py +0 -0
  59. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/load_config.py +0 -0
  60. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/load_experiment.py +0 -0
  61. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/load_run.py +0 -0
  62. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/models.py +0 -0
  63. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/modes.py +0 -0
  64. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/nests_runs.py +0 -0
  65. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/note_path.py +0 -0
  66. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/notebook_kernel.py +0 -0
  67. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/output_html.py +0 -0
  68. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/parse_run_view.py +0 -0
  69. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/print_rows.py +0 -0
  70. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/python_blocks.py +0 -0
  71. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/read_config.py +0 -0
  72. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/read_path.py +0 -0
  73. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/render_markdown.py +0 -0
  74. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/run_command.py +0 -0
  75. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/run_control.py +0 -0
  76. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/run_output.py +0 -0
  77. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/run_status.py +0 -0
  78. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/__init__.py +0 -0
  79. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/describe_event.py +0 -0
  80. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/execute_run.py +0 -0
  81. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/hold_folder.py +0 -0
  82. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/new_param_run_id.py +0 -0
  83. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/new_run_id.py +0 -0
  84. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  85. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/param_args.py +0 -0
  86. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/param_grid.py +0 -0
  87. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/param_run_name.py +0 -0
  88. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/plan_runs.py +0 -0
  89. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/prepare_experiment.py +0 -0
  90. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/read_experiment_config.py +0 -0
  91. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/run_experiment.py +0 -0
  92. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/run_folder.py +0 -0
  93. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/run_headless.py +0 -0
  94. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/runs_parent.py +0 -0
  95. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/shell_command.py +0 -0
  96. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/signal_tree.py +0 -0
  97. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/stop_then_kill.py +0 -0
  98. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/wait_for_tree.py +0 -0
  99. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/runner/write_run_json.py +0 -0
  100. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/save_conflict_copy.py +0 -0
  101. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/save_note.py +0 -0
  102. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/saved_outputs.py +0 -0
  103. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/serve_app.py +0 -0
  104. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/server.py +0 -0
  105. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/shared_params.py +0 -0
  106. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/sort_href.py +0 -0
  107. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/sql_quote.py +0 -0
  108. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/elapsed.js +0 -0
  109. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/experiment.js +0 -0
  110. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/labnotes.css +0 -0
  111. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/local-time.js +0 -0
  112. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/new-note.js +0 -0
  113. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/note-edit.js +0 -0
  114. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/note-editor.js +0 -0
  115. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/row-link.js +0 -0
  116. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/run-cell.js +0 -0
  117. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/run-output.js +0 -0
  118. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/static/view-toggle.js +0 -0
  119. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/summarize_config.py +0 -0
  120. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/__init__.py +0 -0
  121. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/experiment_rows.py +0 -0
  122. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/experiment_run_ids.py +0 -0
  123. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/file_prefix.py +0 -0
  124. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/file_text.py +0 -0
  125. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/iso_time.py +0 -0
  126. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/list_experiment_runs.py +0 -0
  127. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/note_text.py +0 -0
  128. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/open_tables.py +0 -0
  129. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/path_glob.py +0 -0
  130. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/path_pattern.py +0 -0
  131. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/read_experiment.py +0 -0
  132. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/read_file.py +0 -0
  133. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/read_log.py +0 -0
  134. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/read_run.py +0 -0
  135. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tables/summarize_experiments.py +0 -0
  136. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/base.html +0 -0
  137. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/editor_imports.html +0 -0
  138. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/experiment.html +0 -0
  139. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/index.html +0 -0
  140. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/note.html +0 -0
  141. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/templates/run.html +0 -0
  142. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/__init__.py +0 -0
  143. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/browse_app.py +0 -0
  144. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/browse_tui.py +0 -0
  145. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/dim.py +0 -0
  146. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/experiments_screen.py +0 -0
  147. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/fill_table.py +0 -0
  148. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/format_elapsed.py +0 -0
  149. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/group_cells.py +0 -0
  150. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/invocation_columns.py +0 -0
  151. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/invocation_rows.py +0 -0
  152. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/reload_on_change.py +0 -0
  153. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/run_cells.py +0 -0
  154. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/run_tui.py +0 -0
  155. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/tui/runs_table.py +0 -0
  156. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/wait_for_note.py +0 -0
  157. {labnotes-0.0.88 → labnotes-0.0.90}/src/labnotes/watch_rows.py +0 -0
  158. {labnotes-0.0.88 → labnotes-0.0.90}/tests/integration/conftest.py +0 -0
  159. {labnotes-0.0.88 → labnotes-0.0.90}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.88
3
+ Version: 0.0.90
4
4
  Summary: Browse experiment designs, notes, and run data from a folder.
5
5
  Project-URL: Repository, https://github.com/thekevinscott/labnotes
6
6
  Author-email: Kevin Scott <me@thekevinscott.com>
@@ -14,6 +14,7 @@ from labnotes.tui.fill_table import fill_table
14
14
  from labnotes.tui.invocation_columns import invocation_columns
15
15
  from labnotes.tui.invocation_rows import invocation_rows
16
16
  from labnotes.tui.reload_on_change import reload_on_change
17
+ from labnotes.tui.run_screen import RunScreen
17
18
  from labnotes.tui.runs_table import RunsTable
18
19
  from labnotes.watch_rows import watch_rows
19
20
 
@@ -22,7 +23,7 @@ WATCHED = {"runs", "experiment_configs"}
22
23
 
23
24
  class ExperimentScreen(Screen[None]):
24
25
  BINDINGS = [
25
- Binding("enter", "toggle_invocation", "Expand", priority=True),
26
+ Binding("enter", "open_row", "Open", priority=True),
26
27
  Binding("escape", "app.pop_screen", "Back"),
27
28
  ]
28
29
 
@@ -77,7 +78,7 @@ class ExperimentScreen(Screen[None]):
77
78
  invocation_rows(self.experiment, self.opened),
78
79
  )
79
80
 
80
- def action_toggle_invocation(self) -> None:
81
+ def action_open_row(self) -> None:
81
82
  table = self.query_one(DataTable)
82
83
  if not table.row_count:
83
84
  return
@@ -90,3 +91,7 @@ class ExperimentScreen(Screen[None]):
90
91
  if key in groups:
91
92
  self.opened ^= {key}
92
93
  self.redraw()
94
+ return
95
+ runs = {str(run["id"]): run for run in self.experiment["runs_list"]}
96
+ folder = self.config.root / str(runs[key]["folder"])
97
+ self.app.push_screen(RunScreen(key, folder))
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import codecs
3
2
  import signal
4
3
  import time
5
4
  from pathlib import Path
@@ -7,7 +6,7 @@ from pathlib import Path
7
6
  from rich.text import Text
8
7
  from textual.app import App, ComposeResult
9
8
  from textual.binding import Binding
10
- from textual.widgets import DataTable, Footer, Header, Input, Log, Static
9
+ from textual.widgets import DataTable, Footer, Header, Input, Static
11
10
 
12
11
  from labnotes.models import Controls, Start
13
12
  from labnotes.runner.describe_event import describe_event
@@ -15,10 +14,10 @@ from labnotes.runner.stop_then_kill import stop_then_kill
15
14
  from labnotes.runner.param_args import param_args
16
15
  from labnotes.tui.dim import dim
17
16
  from labnotes.tui.format_elapsed import format_elapsed
17
+ from labnotes.tui.run_output import RunOutput
18
18
  from labnotes.tui.runs_table import RunsTable
19
19
 
20
20
  COLUMNS = ("run", "params", "status", "time")
21
- STREAMS = ("stdout", "stderr")
22
21
  PENDING, PASSED, FAILED = "○", "✓", "✗"
23
22
  SPINNER = "|/-\\"
24
23
  SKIPPED, DESELECTED = "–", "⊘"
@@ -80,18 +79,11 @@ class RunApp(App[None]):
80
79
  self.viewing: str | None = None
81
80
  self.grabbed: str | None = None
82
81
  self.grabbed_from: list[str] = []
83
- self.offsets: dict[str, int]
84
- self.decoders = {
85
- stream: codecs.getincrementaldecoder("utf-8")(errors="replace")
86
- for stream in STREAMS
87
- }
88
- self.last_stream: str | None
89
- self.mid_line: bool
90
82
 
91
83
  def compose(self) -> ComposeResult:
92
84
  yield Header()
93
85
  yield RunsTable(id="runs", cursor_type="row")
94
- yield Log(id="output")
86
+ yield RunOutput(id="output")
95
87
  yield Input(id="filter", placeholder="Filter params")
96
88
  yield Static(id="status")
97
89
  yield Footer()
@@ -101,7 +93,7 @@ class RunApp(App[None]):
101
93
  for column in COLUMNS:
102
94
  table.add_column(column, key=column)
103
95
  self.query_one(Input).display = False
104
- self.query_one(Log).display = False
96
+ self.query_one(RunOutput).display = False
105
97
  self.query_one(Header).mount(Static(id="concurrency"))
106
98
  table.set_interval(0.1, self.spin)
107
99
 
@@ -373,44 +365,22 @@ class RunApp(App[None]):
373
365
 
374
366
  def view(self, run: str) -> None:
375
367
  self.viewing = run
376
- self.offsets = dict.fromkeys(STREAMS, 0)
377
- for decoder in self.decoders.values():
378
- decoder.reset()
379
- self.last_stream = None
380
- self.mid_line = False
381
- self.query_one(Log).clear()
368
+ self.query_one(RunOutput).reset()
382
369
  self.query_one(DataTable).display = False
383
- self.query_one(Log).display = True
370
+ self.query_one(RunOutput).display = True
384
371
  self.sub_title = run
385
372
  self.show("")
386
373
  self.refresh_bindings()
387
374
  self.follow()
388
375
 
389
376
  def follow(self) -> None:
390
- if self.viewing not in self.folders:
391
- return
392
- for stream in STREAMS:
393
- path = self.folders[self.viewing] / f"{stream}.log"
394
- if not path.exists():
395
- continue
396
- with path.open("rb") as log:
397
- log.seek(self.offsets[stream])
398
- data = log.read()
399
- self.offsets[stream] += len(data)
400
- text = self.decoders[stream].decode(data)
401
- if not text:
402
- continue
403
- # Don't glue one stream's unfinished line onto the other's output.
404
- if self.mid_line and stream != self.last_stream:
405
- text = "\n" + text
406
- self.query_one(Log).write(text)
407
- self.last_stream = stream
408
- self.mid_line = not text.endswith("\n")
377
+ if self.viewing in self.folders:
378
+ self.query_one(RunOutput).follow(self.folders[self.viewing])
409
379
 
410
380
  def action_leave(self) -> None:
411
381
  if self.viewing is not None:
412
382
  self.viewing = None
413
- self.query_one(Log).display = False
383
+ self.query_one(RunOutput).display = False
414
384
  self.query_one(DataTable).display = True
415
385
  self.sub_title = ""
416
386
  self.refresh_bindings()
@@ -0,0 +1,41 @@
1
+ import codecs
2
+ from pathlib import Path
3
+
4
+ from textual.widgets import Log
5
+
6
+ STREAMS = ("stdout", "stderr")
7
+
8
+
9
+ class RunOutput(Log):
10
+ def __init__(self, id: str | None = None) -> None:
11
+ super().__init__(id=id)
12
+ self.reset()
13
+
14
+ def reset(self) -> None:
15
+ self.offsets = dict.fromkeys(STREAMS, 0)
16
+ self.decoders = {
17
+ stream: codecs.getincrementaldecoder("utf-8")(errors="replace")
18
+ for stream in STREAMS
19
+ }
20
+ self.last_stream: str | None = None
21
+ self.mid_line = False
22
+ self.clear()
23
+
24
+ def follow(self, folder: Path) -> None:
25
+ for stream in STREAMS:
26
+ path = folder / f"{stream}.log"
27
+ if not path.exists():
28
+ continue
29
+ with path.open("rb") as log:
30
+ log.seek(self.offsets[stream])
31
+ data = log.read()
32
+ self.offsets[stream] += len(data)
33
+ text = self.decoders[stream].decode(data)
34
+ if not text:
35
+ continue
36
+ # Don't glue one stream's unfinished line onto the other's output.
37
+ if self.mid_line and stream != self.last_stream:
38
+ text = "\n" + text
39
+ self.write(text)
40
+ self.last_stream = stream
41
+ self.mid_line = not text.endswith("\n")
@@ -0,0 +1,30 @@
1
+ from pathlib import Path
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.binding import Binding
5
+ from textual.screen import Screen
6
+ from textual.widgets import Footer, Header
7
+
8
+ from labnotes.tui.run_output import RunOutput
9
+
10
+
11
+ class RunScreen(Screen[None]):
12
+ BINDINGS = [Binding("escape", "app.pop_screen", "Back")]
13
+
14
+ def __init__(self, run_id: str, folder: Path) -> None:
15
+ super().__init__()
16
+ self.run_id = run_id
17
+ self.folder = folder
18
+
19
+ def compose(self) -> ComposeResult:
20
+ yield Header()
21
+ yield RunOutput(id="output")
22
+ yield Footer()
23
+
24
+ def on_mount(self) -> None:
25
+ self.sub_title = self.run_id
26
+ self.follow()
27
+ self.set_interval(0.25, self.follow)
28
+
29
+ def follow(self) -> None:
30
+ self.query_one(RunOutput).follow(self.folder)
@@ -5,21 +5,15 @@ lines = [12]
5
5
  reason = "LOCK_EX and LOCK_NB are disjoint bits, so | equals + and ^"
6
6
 
7
7
  [[python.exempt]]
8
- path = "src/labnotes/tui/run_app.py"
8
+ path = "src/labnotes/tui/run_output.py"
9
9
  rules = ["mutation"]
10
- lines = [311]
10
+ lines = [37]
11
11
  reason = "last_stream is always the same str object taken from STREAMS, so is not equals !="
12
12
 
13
- [[python.exempt]]
14
- path = "src/labnotes/runner/run_experiment.py"
15
- rules = ["mutation"]
16
- lines = [47]
17
- reason = "mode is validated to matrix or adhoc; \"matrix\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
18
-
19
13
  [[python.exempt]]
20
14
  path = "src/labnotes/load_experiment.py"
21
15
  rules = ["mutation"]
22
- lines = [59]
16
+ lines = [61]
23
17
  reason = "summarize_config returns mode matrix or adhoc; \"matrix\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
24
18
 
25
19
  [[python.exempt]]
@@ -43,7 +37,7 @@ reason = "the daemon answers a run with 202, 400, 409 or 500, so every status bu
43
37
  [[python.exempt]]
44
38
  path = "src/labnotes/run_control.py"
45
39
  rules = ["mutation"]
46
- lines = [38]
40
+ lines = [39]
47
41
  reason = "argument counts are small ints, which CPython caches, so is not equals !="
48
42
 
49
43
  [[python.exempt]]
@@ -61,11 +55,11 @@ reason = "run columns come from string literals in load_experiment, which CPytho
61
55
  [[python.exempt]]
62
56
  path = "src/labnotes/tui/experiment_screen.py"
63
57
  rules = ["mutation"]
64
- lines = [42]
58
+ lines = [53]
65
59
  reason = "WorkerState members are singletons, so is equals =="
66
60
 
67
61
  [[python.exempt]]
68
62
  path = "src/labnotes/tui/experiments_screen.py"
69
63
  rules = ["mutation"]
70
- lines = [28, 37]
71
- reason = "WorkerState members are singletons, so is equals ==; every row of one query has the same keys, so any row names the columns"
64
+ lines = [36]
65
+ reason = "WorkerState members are singletons, so is equals =="
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes