labnotes 0.0.94__tar.gz → 0.0.96__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 (164) hide show
  1. {labnotes-0.0.94 → labnotes-0.0.96}/PKG-INFO +1 -1
  2. labnotes-0.0.96/src/labnotes/__init__.py +3 -0
  3. labnotes-0.0.96/src/labnotes/log.py +23 -0
  4. labnotes-0.0.96/src/labnotes/log_path.py +10 -0
  5. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/execute_run.py +7 -1
  6. labnotes-0.0.96/src/labnotes/tui/run_output.py +61 -0
  7. {labnotes-0.0.94 → labnotes-0.0.96}/testing-conventions.toml +2 -2
  8. labnotes-0.0.94/src/labnotes/tui/__init__.py +0 -0
  9. labnotes-0.0.94/src/labnotes/tui/run_output.py +0 -41
  10. {labnotes-0.0.94 → labnotes-0.0.96}/.gitignore +0 -0
  11. {labnotes-0.0.94 → labnotes-0.0.96}/CHANGELOG.md +0 -0
  12. {labnotes-0.0.94 → labnotes-0.0.96}/MIGRATIONS.md +0 -0
  13. {labnotes-0.0.94 → labnotes-0.0.96}/README.md +0 -0
  14. {labnotes-0.0.94 → labnotes-0.0.96}/pyproject.toml +0 -0
  15. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/__main__.py +0 -0
  16. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/asset_url.py +0 -0
  17. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/check_counts.py +0 -0
  18. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/cli.py +0 -0
  19. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/context.md +0 -0
  20. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/context_guide.py +0 -0
  21. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/count_groups.py +0 -0
  22. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/count_runs.py +0 -0
  23. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/create_app.py +0 -0
  24. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/create_experiment.py +0 -0
  25. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/create_note.py +0 -0
  26. {labnotes-0.0.94/src/labnotes → labnotes-0.0.96/src/labnotes/daemon}/__init__.py +0 -0
  27. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/control_run.py +0 -0
  28. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/daemon_app.py +0 -0
  29. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/daemon_client.py +0 -0
  30. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/daemon_running.py +0 -0
  31. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/daemon_server.py +0 -0
  32. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/describe_status.py +0 -0
  33. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/ensure_daemon.py +0 -0
  34. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/exit_when_idle.py +0 -0
  35. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/remote_start.py +0 -0
  36. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/run_daemon.py +0 -0
  37. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/run_logs.py +0 -0
  38. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/serve_daemon.py +0 -0
  39. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/socket_path.py +0 -0
  40. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/stop_runs.py +0 -0
  41. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/watch_daemon.py +0 -0
  42. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon/watch_status.py +0 -0
  43. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/daemon_command.py +0 -0
  44. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/data_html.py +0 -0
  45. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/default_config_text.py +0 -0
  46. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/default_counts.py +0 -0
  47. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/drain_watch.py +0 -0
  48. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/experiment_notes.py +0 -0
  49. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/experiment_runs.py +0 -0
  50. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/experiments.py +0 -0
  51. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/file_changes.py +0 -0
  52. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/format_duration.py +0 -0
  53. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/format_started.py +0 -0
  54. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/format_value.py +0 -0
  55. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/group_runs.py +0 -0
  56. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/init.py +0 -0
  57. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/invocation_started.py +0 -0
  58. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/is_positive_int.py +0 -0
  59. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/list_experiments.py +0 -0
  60. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/list_runs.py +0 -0
  61. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/load_config.py +0 -0
  62. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/load_experiment.py +0 -0
  63. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/load_run.py +0 -0
  64. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/models.py +0 -0
  65. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/modes.py +0 -0
  66. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/nests_runs.py +0 -0
  67. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/note_path.py +0 -0
  68. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/notebook_kernel.py +0 -0
  69. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/output_html.py +0 -0
  70. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/parse_run_view.py +0 -0
  71. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/print_rows.py +0 -0
  72. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/python_blocks.py +0 -0
  73. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/read_config.py +0 -0
  74. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/read_path.py +0 -0
  75. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/render_markdown.py +0 -0
  76. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/run_command.py +0 -0
  77. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/run_control.py +0 -0
  78. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/run_output.py +0 -0
  79. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/run_status.py +0 -0
  80. {labnotes-0.0.94/src/labnotes/daemon → labnotes-0.0.96/src/labnotes/runner}/__init__.py +0 -0
  81. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/describe_event.py +0 -0
  82. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/hold_folder.py +0 -0
  83. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/new_param_run_id.py +0 -0
  84. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/new_run_id.py +0 -0
  85. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  86. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/param_args.py +0 -0
  87. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/param_grid.py +0 -0
  88. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/param_run_name.py +0 -0
  89. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/plan_runs.py +0 -0
  90. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/prepare_experiment.py +0 -0
  91. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/read_experiment_config.py +0 -0
  92. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/run_experiment.py +0 -0
  93. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/run_folder.py +0 -0
  94. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/run_headless.py +0 -0
  95. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/runs_parent.py +0 -0
  96. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/shell_command.py +0 -0
  97. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/signal_tree.py +0 -0
  98. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/stop_then_kill.py +0 -0
  99. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/wait_for_tree.py +0 -0
  100. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/runner/write_run_json.py +0 -0
  101. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/save_conflict_copy.py +0 -0
  102. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/save_note.py +0 -0
  103. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/saved_outputs.py +0 -0
  104. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/serve_app.py +0 -0
  105. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/server.py +0 -0
  106. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/shared_params.py +0 -0
  107. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/sort_href.py +0 -0
  108. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/sql_quote.py +0 -0
  109. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/static/elapsed.js +0 -0
  110. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/static/experiment.js +0 -0
  111. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/static/labnotes.css +0 -0
  112. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/static/local-time.js +0 -0
  113. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/static/new-note.js +0 -0
  114. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/static/note-edit.js +0 -0
  115. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/static/note-editor.js +0 -0
  116. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/static/row-link.js +0 -0
  117. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/static/run-cell.js +0 -0
  118. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/static/run-output.js +0 -0
  119. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/static/view-toggle.js +0 -0
  120. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/summarize_config.py +0 -0
  121. {labnotes-0.0.94/src/labnotes/runner → labnotes-0.0.96/src/labnotes/tables}/__init__.py +0 -0
  122. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/experiment_rows.py +0 -0
  123. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/experiment_run_ids.py +0 -0
  124. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/file_prefix.py +0 -0
  125. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/file_text.py +0 -0
  126. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/iso_time.py +0 -0
  127. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/list_experiment_runs.py +0 -0
  128. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/note_text.py +0 -0
  129. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/open_tables.py +0 -0
  130. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/path_glob.py +0 -0
  131. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/path_pattern.py +0 -0
  132. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/read_experiment.py +0 -0
  133. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/read_file.py +0 -0
  134. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/read_log.py +0 -0
  135. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/read_run.py +0 -0
  136. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tables/summarize_experiments.py +0 -0
  137. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/templates/base.html +0 -0
  138. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/templates/editor_imports.html +0 -0
  139. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/templates/experiment.html +0 -0
  140. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/templates/index.html +0 -0
  141. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/templates/note.html +0 -0
  142. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/templates/run.html +0 -0
  143. {labnotes-0.0.94/src/labnotes/tables → labnotes-0.0.96/src/labnotes/tui}/__init__.py +0 -0
  144. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/browse_app.py +0 -0
  145. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/browse_tui.py +0 -0
  146. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/dim.py +0 -0
  147. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/experiment_screen.py +0 -0
  148. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/experiments_screen.py +0 -0
  149. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/fill_table.py +0 -0
  150. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/format_elapsed.py +0 -0
  151. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/group_cells.py +0 -0
  152. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/invocation_columns.py +0 -0
  153. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/invocation_rows.py +0 -0
  154. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/reload_on_change.py +0 -0
  155. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/run_app.py +0 -0
  156. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/run_cells.py +0 -0
  157. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/run_screen.py +0 -0
  158. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/run_tui.py +0 -0
  159. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/runs_table.py +0 -0
  160. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/tui/stopped_title.py +0 -0
  161. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/wait_for_note.py +0 -0
  162. {labnotes-0.0.94 → labnotes-0.0.96}/src/labnotes/watch_rows.py +0 -0
  163. {labnotes-0.0.94 → labnotes-0.0.96}/tests/integration/conftest.py +0 -0
  164. {labnotes-0.0.94 → labnotes-0.0.96}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.94
3
+ Version: 0.0.96
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>
@@ -0,0 +1,3 @@
1
+ from labnotes.log import log
2
+
3
+ __all__ = ["log"]
@@ -0,0 +1,23 @@
1
+ import json
2
+ from datetime import UTC, datetime
3
+
4
+ from labnotes.log_path import log_path
5
+
6
+
7
+ def now() -> str:
8
+ return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
9
+
10
+
11
+ def log(block: dict[str, object]) -> None:
12
+ """Write a block to the run's log, or print its content outside a run.
13
+
14
+ Scripts import this, so the module stays standard-library only.
15
+ """
16
+ path = log_path()
17
+ if path is None:
18
+ if "content" in block:
19
+ print(block["content"])
20
+ return
21
+ line = json.dumps({**block, "timestamp": now()})
22
+ with path.open("a", encoding="utf-8") as file:
23
+ file.write(f"{line}\n")
@@ -0,0 +1,10 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ # The one env contract between runner and script; config is otherwise never env.
5
+ LOG_ENV = "LABNOTES_LOG"
6
+
7
+
8
+ def log_path() -> Path | None:
9
+ value = os.environ.get(LOG_ENV)
10
+ return Path(value) if value else None
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
 
6
6
  import psutil
7
7
 
8
+ from labnotes.log_path import LOG_ENV
8
9
  from labnotes.runner.shell_command import shell_command
9
10
  from labnotes.runner.wait_for_tree import wait_for_tree
10
11
  from labnotes.runner.write_run_json import write_run_json
@@ -36,7 +37,12 @@ async def execute_run(
36
37
  process = await asyncio.create_subprocess_shell(
37
38
  shell_command(command, folder, params),
38
39
  cwd=root,
39
- env={"FORCE_COLOR": "1", "CLICOLOR_FORCE": "1", **os.environ},
40
+ env={
41
+ "FORCE_COLOR": "1",
42
+ "CLICOLOR_FORCE": "1",
43
+ **os.environ,
44
+ LOG_ENV: str(folder / "log.jsonl"),
45
+ },
40
46
  stdout=stdout,
41
47
  stderr=stderr,
42
48
  )
@@ -0,0 +1,61 @@
1
+ import codecs
2
+ from pathlib import Path
3
+
4
+ from rich.text import Text
5
+ from textual.geometry import Size
6
+ from textual.widgets import RichLog
7
+
8
+ STREAMS = ("stdout", "stderr")
9
+
10
+
11
+ class RunOutput(RichLog):
12
+ def __init__(self, id: str | None = None) -> None:
13
+ super().__init__(id=id)
14
+ self.reset()
15
+
16
+ def reset(self) -> None:
17
+ self.offsets = dict.fromkeys(STREAMS, 0)
18
+ self.decoders = {
19
+ stream: codecs.getincrementaldecoder("utf-8")(errors="replace")
20
+ for stream in STREAMS
21
+ }
22
+ self.last_stream: str | None = None
23
+ self.unfinished = ""
24
+ self.clear()
25
+
26
+ @property
27
+ def mid_line(self) -> bool:
28
+ return bool(self.unfinished)
29
+
30
+ def follow(self, folder: Path) -> None:
31
+ for stream in STREAMS:
32
+ path = folder / f"{stream}.log"
33
+ if not path.exists():
34
+ continue
35
+ with path.open("rb") as log:
36
+ log.seek(self.offsets[stream])
37
+ data = log.read()
38
+ self.offsets[stream] += len(data)
39
+ text = self.decoders[stream].decode(data)
40
+ if not text:
41
+ continue
42
+ # A stream's unfinished line is redrawn as it grows, but the
43
+ # other stream's output starts below it.
44
+ if self.mid_line and stream == self.last_stream:
45
+ self.drop_last_line()
46
+ text = self.unfinished + text
47
+ *complete, self.unfinished = text.split("\n")
48
+ if complete:
49
+ self.write(Text.from_ansi("\n".join(complete)))
50
+ if self.unfinished:
51
+ self.write(Text.from_ansi(self.unfinished))
52
+ self.last_stream = stream
53
+
54
+ def drop_last_line(self) -> None:
55
+ if not self._size_known:
56
+ self._deferred_renders.pop()
57
+ return
58
+ del self.lines[-1]
59
+ self._line_cache.clear()
60
+ self.virtual_size = Size(self.virtual_size.width, len(self.lines))
61
+ self.refresh()
@@ -7,8 +7,8 @@ reason = "LOCK_EX and LOCK_NB are disjoint bits, so | equals + and ^"
7
7
  [[python.exempt]]
8
8
  path = "src/labnotes/tui/run_output.py"
9
9
  rules = ["mutation"]
10
- lines = [37]
11
- reason = "last_stream is always the same str object taken from STREAMS, so is not equals !="
10
+ lines = [44]
11
+ reason = "last_stream is always the same str object taken from STREAMS, so is equals =="
12
12
 
13
13
  [[python.exempt]]
14
14
  path = "src/labnotes/load_experiment.py"
File without changes
@@ -1,41 +0,0 @@
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")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes