starbash 0.2.6__tar.gz → 0.2.7__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.
- {starbash-0.2.6 → starbash-0.2.7}/PKG-INFO +2 -2
- {starbash-0.2.6 → starbash-0.2.7}/pyproject.toml +2 -2
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/filtering.py +13 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/processing.py +27 -3
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/tool/base.py +111 -41
- {starbash-0.2.6 → starbash-0.2.7}/LICENSE +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/README.md +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/__init__.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/aliases.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/analytics.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/app.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/check_version.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/commands/__init__.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/commands/info.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/commands/process.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/commands/repo.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/commands/select.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/commands/user.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/database.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/defaults/__init__.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/defaults/starbash.toml +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/doit.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/doit_types.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/dwarf3.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/exception.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/icon.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/linux.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/main.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/os.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/parameters.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/paths.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/processed_target.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/processing_like.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/recipes/README.md +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/recipes/__init__.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/recipes/osc.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/rich.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/safety.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/score.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/selection.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/sim_siril/__init__.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/sim_siril/connection.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/sim_siril/enums.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/sim_siril/utility.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/stage_utils.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/stages.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/templates/__init__.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/templates/repo/master.toml +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/templates/repo/processed.toml +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/templates/target/master.toml +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/templates/target/processed.toml +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/templates/userconfig.toml +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/toml.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/tool/__init__.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/tool/context.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/tool/graxpert.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/tool/python.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/tool/rcastro.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/tool/siril.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/ui/main.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/url.py +0 -0
- {starbash-0.2.6 → starbash-0.2.7}/src/starbash/windows.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: starbash
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: A tool for automating/standardizing/sharing astrophotography workflows.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Kevin Hester
|
|
@@ -20,7 +20,7 @@ Requires-Dist: restrictedpython (>=8.1,<9.0)
|
|
|
20
20
|
Requires-Dist: rich (>=14.2.0,<15.0.0)
|
|
21
21
|
Requires-Dist: sentry-sdk (>=2.42.1,<3.0.0)
|
|
22
22
|
Requires-Dist: textual (>=6.8.0,<7.0.0)
|
|
23
|
-
Requires-Dist: toml-repo (>=0.1.
|
|
23
|
+
Requires-Dist: toml-repo (>=0.1.6,<0.2.0)
|
|
24
24
|
Requires-Dist: typer (>=0.20.0,<0.21.0)
|
|
25
25
|
Requires-Dist: update-checker (>=0.18.0,<0.19.0)
|
|
26
26
|
Project-URL: Documentation, https://github.com/geeksville/starbash/blob/main/README.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "starbash"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.7"
|
|
4
4
|
description = "A tool for automating/standardizing/sharing astrophotography workflows."
|
|
5
5
|
authors = ["Kevin Hester <kevinh@geeksville.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -34,7 +34,7 @@ graxpert = {version = "^3.2.0a2", extras = ["cpuonly"]}
|
|
|
34
34
|
# experimenting with siril-scripts/processing/VeraLux_HyperMetric_Stretch.py
|
|
35
35
|
numpy = "2.2.6"
|
|
36
36
|
pyqt6 = "^6.10.1"
|
|
37
|
-
toml-repo = "^0.1.
|
|
37
|
+
toml-repo = "^0.1.6"
|
|
38
38
|
|
|
39
39
|
[tool.poetry.group.dev.dependencies]
|
|
40
40
|
pytest = ">=8.4.2,<10.0.0"
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
2
4
|
|
|
3
5
|
from starbash import InputDef, Metadata, RequireDef
|
|
4
6
|
from starbash.aliases import get_aliases
|
|
@@ -62,6 +64,16 @@ def _apply_filter(requires: RequireDef, candidates: list[ImageRow]) -> list[Imag
|
|
|
62
64
|
repo_kind = metadata["repo"].kind() # I think repo is guaranteed to be present
|
|
63
65
|
return repo_kind != "processed" and repo_kind != "master"
|
|
64
66
|
|
|
67
|
+
def _filter_filename(metadata: Metadata) -> bool:
|
|
68
|
+
"""Keep images whose basename matches (or, with mode='exclude', does not match) a regex."""
|
|
69
|
+
pattern = get_safe(requires, "value")
|
|
70
|
+
mode = requires.get("mode", "include")
|
|
71
|
+
if mode not in ("include", "exclude"):
|
|
72
|
+
raise ValueError(f"Unknown filename filter mode: {mode}")
|
|
73
|
+
basename = os.path.basename(str(metadata.get("path") or metadata.get("abspath") or ""))
|
|
74
|
+
matched = re.search(pattern, basename) is not None
|
|
75
|
+
return matched if mode == "include" else not matched
|
|
76
|
+
|
|
65
77
|
def _filter_min_count(metadata: Metadata) -> bool:
|
|
66
78
|
"""Min_count is handled in stage 2, so always return True here."""
|
|
67
79
|
return True
|
|
@@ -71,6 +83,7 @@ def _apply_filter(requires: RequireDef, candidates: list[ImageRow]) -> list[Imag
|
|
|
71
83
|
"metadata": _filter_metadata,
|
|
72
84
|
"camera": _filter_camera,
|
|
73
85
|
"unprocessed": _filter_unprocessed,
|
|
86
|
+
"filename": _filter_filename,
|
|
74
87
|
"min_count": _filter_min_count,
|
|
75
88
|
}
|
|
76
89
|
|
|
@@ -14,7 +14,7 @@ from toml_repo import Repo
|
|
|
14
14
|
from tomlkit.items import AoT
|
|
15
15
|
|
|
16
16
|
import starbash
|
|
17
|
-
from starbash import InputDef, OutputDef, StageDict
|
|
17
|
+
from starbash import InputDef, OutputDef, RequireDef, StageDict
|
|
18
18
|
from starbash.aliases import get_aliases, normalize_target_name
|
|
19
19
|
from starbash.app import Starbash
|
|
20
20
|
from starbash.database import (
|
|
@@ -900,6 +900,18 @@ class Processing(ProcessingLike):
|
|
|
900
900
|
# Collect all image rows from prior stage outputs
|
|
901
901
|
child_exception: Exception | None = None
|
|
902
902
|
|
|
903
|
+
# `filename` requires match the actual files we consume (the prior stage's
|
|
904
|
+
# OUTPUT names), which differ from its inputs; every other requires kind
|
|
905
|
+
# (metadata/camera/unprocessed/min_count) gates on the prior stage's rich
|
|
906
|
+
# INPUT rows. So split them and apply each to the correct row set.
|
|
907
|
+
all_requires: list[RequireDef] = input.get("requires", [])
|
|
908
|
+
filename_requires = [r for r in all_requires if r.get("kind") == "filename"]
|
|
909
|
+
gating_input: InputDef = (
|
|
910
|
+
{**input, "requires": [r for r in all_requires if r.get("kind") != "filename"]}
|
|
911
|
+
if filename_requires
|
|
912
|
+
else input
|
|
913
|
+
)
|
|
914
|
+
|
|
903
915
|
base: str | None = None # We will set base from the first matching prior task output
|
|
904
916
|
for task in prior_tasks:
|
|
905
917
|
task_context: dict[str, Any] = task["meta"]["context"] # type: ignore
|
|
@@ -911,7 +923,7 @@ class Processing(ProcessingLike):
|
|
|
911
923
|
images = file_info.image_rows
|
|
912
924
|
images = [self._with_defaults(img) for img in images]
|
|
913
925
|
try:
|
|
914
|
-
task_filtered_input = filter_by_requires(
|
|
926
|
+
task_filtered_input = filter_by_requires(gating_input, images)
|
|
915
927
|
if (
|
|
916
928
|
task_filtered_input
|
|
917
929
|
): # This task had matching inputs for us, so therefore we want its outputs
|
|
@@ -922,7 +934,13 @@ class Processing(ProcessingLike):
|
|
|
922
934
|
and task_output.image_rows
|
|
923
935
|
):
|
|
924
936
|
base = task_output.base
|
|
925
|
-
|
|
937
|
+
# filename filters select which produced files we want.
|
|
938
|
+
output_rows = task_output.image_rows
|
|
939
|
+
if filename_requires:
|
|
940
|
+
output_rows = filter_by_requires(
|
|
941
|
+
{"requires": filename_requires}, output_rows
|
|
942
|
+
)
|
|
943
|
+
for img in output_rows:
|
|
926
944
|
image_rows[img["abspath"]] = img
|
|
927
945
|
except NotEnoughFilesError as e:
|
|
928
946
|
child_exception = e # In case we need to raise later
|
|
@@ -933,6 +951,12 @@ class Processing(ProcessingLike):
|
|
|
933
951
|
# we failed on every child, give up
|
|
934
952
|
raise child_exception
|
|
935
953
|
|
|
954
|
+
if filename_requires and len(image_rows) == 0:
|
|
955
|
+
# Prior outputs existed but none matched the filename filter; skip this stage.
|
|
956
|
+
raise NotEnoughFilesError(
|
|
957
|
+
"No prior stage outputs matched the input filename filter", []
|
|
958
|
+
)
|
|
959
|
+
|
|
936
960
|
return FileInfo(base=base, image_rows=list(image_rows.values()), definition=input)
|
|
937
961
|
|
|
938
962
|
def preflight_tasks(self, pt: ProcessedTarget, tasks: list[TaskDict]) -> list[TaskDict]:
|
|
@@ -8,12 +8,16 @@ import subprocess
|
|
|
8
8
|
import sys
|
|
9
9
|
import tempfile
|
|
10
10
|
import textwrap
|
|
11
|
+
from collections import deque
|
|
11
12
|
from collections.abc import Callable
|
|
12
13
|
from contextlib import nullcontext
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
16
|
+
from rich.console import Group, RenderableType
|
|
15
17
|
from rich.live import Live
|
|
18
|
+
from rich.padding import Padding
|
|
16
19
|
from rich.spinner import Spinner
|
|
20
|
+
from rich.text import Text
|
|
17
21
|
|
|
18
22
|
from starbash.commands import SPINNER_STYLE
|
|
19
23
|
from starbash.exception import UserHandledError
|
|
@@ -95,6 +99,56 @@ def color_lines(lines: list[str]) -> str:
|
|
|
95
99
|
return "\n".join(color_line(line) for line in lines)
|
|
96
100
|
|
|
97
101
|
|
|
102
|
+
class ToolLiveDisplay:
|
|
103
|
+
"""Live renderable: a running spinner plus the most recent tool output lines.
|
|
104
|
+
|
|
105
|
+
Fed incrementally by ``tool_run_streaming`` via :meth:`add_line`. stderr lines
|
|
106
|
+
render red, stdout lines yellow. Each line is truncated to a single row so the
|
|
107
|
+
display stays a fixed height while the tool runs.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
MAX_LINES = 3
|
|
111
|
+
|
|
112
|
+
def __init__(self, name: str) -> None:
|
|
113
|
+
self.name = name
|
|
114
|
+
self.spinner = Spinner(
|
|
115
|
+
"arc",
|
|
116
|
+
text=f"Tool running: [bold]{name}[/bold]...",
|
|
117
|
+
speed=2.0,
|
|
118
|
+
style=SPINNER_STYLE,
|
|
119
|
+
)
|
|
120
|
+
self._lines: deque[Text] = deque(maxlen=self.MAX_LINES)
|
|
121
|
+
self._done = False
|
|
122
|
+
|
|
123
|
+
def add_line(self, line: str, is_stderr: bool) -> None:
|
|
124
|
+
"""Append a tool output line, keeping only the most recent MAX_LINES."""
|
|
125
|
+
text = line.rstrip("\n")
|
|
126
|
+
if not text.strip():
|
|
127
|
+
return
|
|
128
|
+
self._lines.append(
|
|
129
|
+
Text(
|
|
130
|
+
text,
|
|
131
|
+
style="red" if is_stderr else "yellow",
|
|
132
|
+
no_wrap=True,
|
|
133
|
+
overflow="ellipsis",
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def finish(self) -> None:
|
|
138
|
+
"""Swap the spinner for a static header so the final frame persists."""
|
|
139
|
+
self._done = True
|
|
140
|
+
|
|
141
|
+
def __rich__(self) -> RenderableType:
|
|
142
|
+
header: RenderableType = (
|
|
143
|
+
Text(f"Tool completed: {self.name}", style="dim")
|
|
144
|
+
if self._done
|
|
145
|
+
else self.spinner
|
|
146
|
+
)
|
|
147
|
+
# Indent each output line by 4 spaces beneath the header.
|
|
148
|
+
lines = [Padding(line, (0, 0, 0, 4)) for line in self._lines]
|
|
149
|
+
return Group(header, *lines)
|
|
150
|
+
|
|
151
|
+
|
|
98
152
|
def tool_emit_logs(lines: str, log_level: int = logging.INFO) -> None:
|
|
99
153
|
"""Emit log lines from a tool to the logger at the specified log level.
|
|
100
154
|
|
|
@@ -172,10 +226,10 @@ def tool_run_streaming(
|
|
|
172
226
|
"""Execute an external tool, invoking on_line for each stdout line as it arrives.
|
|
173
227
|
|
|
174
228
|
Unlike a blocking communicate()-based runner, this streams stdout line-by-line so
|
|
175
|
-
callers can react to incremental output (e.g. JSON progress events).
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
raises ToolError.
|
|
229
|
+
callers can react to incremental output (e.g. JSON progress events). Separate
|
|
230
|
+
reader threads for stdout and stderr feed a shared queue so the timeout fires
|
|
231
|
+
correctly even for silent processes and both streams are written to log_out in
|
|
232
|
+
approximate arrival order. A non-zero exit code raises ToolError.
|
|
179
233
|
"""
|
|
180
234
|
import queue
|
|
181
235
|
import threading
|
|
@@ -196,6 +250,7 @@ def tool_run_streaming(
|
|
|
196
250
|
env=env,
|
|
197
251
|
)
|
|
198
252
|
assert process.stdout is not None
|
|
253
|
+
assert process.stderr is not None
|
|
199
254
|
|
|
200
255
|
if commands:
|
|
201
256
|
assert process.stdin is not None
|
|
@@ -209,57 +264,70 @@ def tool_run_streaming(
|
|
|
209
264
|
|
|
210
265
|
threading.Thread(target=_write_stdin, daemon=True).start()
|
|
211
266
|
|
|
212
|
-
|
|
267
|
+
# Both reader threads push (stream_name, line) tuples so we can interleave the
|
|
268
|
+
# two streams into log_out in approximate arrival order. A None line marks EOF
|
|
269
|
+
# for that reader; the main loop stops once both readers have finished.
|
|
270
|
+
line_queue: queue.Queue[tuple[str, str | None]] = queue.Queue()
|
|
213
271
|
|
|
214
|
-
def _reader() -> None:
|
|
272
|
+
def _reader(name: str, stream: io.TextIOBase) -> None:
|
|
215
273
|
try:
|
|
216
|
-
for line in
|
|
217
|
-
line_queue.put(line)
|
|
274
|
+
for line in stream:
|
|
275
|
+
line_queue.put((name, line))
|
|
218
276
|
except Exception:
|
|
219
277
|
pass
|
|
220
278
|
finally:
|
|
221
|
-
line_queue.put(None) # EOF sentinel
|
|
279
|
+
line_queue.put((name, None)) # EOF sentinel
|
|
222
280
|
|
|
223
|
-
threading.Thread(
|
|
281
|
+
threading.Thread(
|
|
282
|
+
target=_reader, args=("stdout", process.stdout), daemon=True
|
|
283
|
+
).start()
|
|
284
|
+
threading.Thread(
|
|
285
|
+
target=_reader, args=("stderr", process.stderr), daemon=True
|
|
286
|
+
).start()
|
|
224
287
|
|
|
225
288
|
deadline = (time.monotonic() + timeout) if timeout else None
|
|
226
289
|
stdout_captured: list[str] = []
|
|
290
|
+
stderr_captured: list[str] = []
|
|
291
|
+
readers_done = 0
|
|
227
292
|
|
|
228
293
|
try:
|
|
229
|
-
while
|
|
294
|
+
while readers_done < 2:
|
|
230
295
|
remaining = max(0.0, deadline - time.monotonic()) if deadline else None
|
|
231
296
|
try:
|
|
232
|
-
line = line_queue.get(timeout=remaining)
|
|
297
|
+
stream_name, line = line_queue.get(timeout=remaining)
|
|
233
298
|
except queue.Empty:
|
|
234
299
|
process.kill()
|
|
235
300
|
process.wait()
|
|
236
|
-
if process.stderr:
|
|
237
|
-
process.stderr.close()
|
|
238
301
|
raise RuntimeError(f"Tool timed out after {timeout} seconds")
|
|
239
|
-
if line is None: # EOF
|
|
240
|
-
|
|
241
|
-
|
|
302
|
+
if line is None: # EOF for one of the streams
|
|
303
|
+
readers_done += 1
|
|
304
|
+
continue
|
|
242
305
|
if log_out:
|
|
243
306
|
log_out.write(line)
|
|
244
307
|
log_out.flush() # Just in case the user is 'tailing' the file
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
308
|
+
active_display = Tool._active_display
|
|
309
|
+
if active_display is not None:
|
|
310
|
+
active_display.add_line(line, is_stderr=stream_name == "stderr")
|
|
311
|
+
if stream_name == "stdout":
|
|
312
|
+
stdout_captured.append(line)
|
|
313
|
+
if on_line:
|
|
314
|
+
try:
|
|
315
|
+
on_line(line)
|
|
316
|
+
except Exception:
|
|
317
|
+
logger.exception("Error in tool output line handler")
|
|
318
|
+
else:
|
|
319
|
+
stderr_captured.append(line)
|
|
250
320
|
finally:
|
|
251
321
|
process.stdout.close()
|
|
252
|
-
|
|
253
|
-
stderr_text = process.stderr.read() if process.stderr else ""
|
|
254
|
-
if process.stderr:
|
|
255
322
|
process.stderr.close()
|
|
323
|
+
|
|
256
324
|
returncode = process.wait()
|
|
257
325
|
|
|
258
326
|
stdout_str = "".join(stdout_captured)
|
|
259
327
|
if returncode != 0:
|
|
260
328
|
tool_emit_logs(stdout_str, log_level=logging.ERROR)
|
|
261
329
|
|
|
262
|
-
stderr_list =
|
|
330
|
+
stderr_list = [line.rstrip("\n") for line in stderr_captured]
|
|
263
331
|
if stderr_fixup:
|
|
264
332
|
stderr_list = stderr_fixup(stdout_captured, stderr_list)
|
|
265
333
|
if stderr_list:
|
|
@@ -285,6 +353,10 @@ class Tool:
|
|
|
285
353
|
# of our call tree, that variables get passed down to all sub-tools.
|
|
286
354
|
_default_log_out: io.TextIOWrapper | None = None
|
|
287
355
|
|
|
356
|
+
# The live output display owned by the outermost tool in a call chain. Nested
|
|
357
|
+
# tools (and tool_run_streaming) feed their output lines into this same display.
|
|
358
|
+
_active_display: "ToolLiveDisplay | None" = None
|
|
359
|
+
|
|
288
360
|
# If True, this tool renders its own progress display in _run() (e.g. a Rich
|
|
289
361
|
# Progress bar), so Tool.run() suppresses the default spinner to avoid nested
|
|
290
362
|
# Live displays.
|
|
@@ -320,22 +392,19 @@ class Tool:
|
|
|
320
392
|
from starbash import console # Lazy import to avoid circular dependency
|
|
321
393
|
|
|
322
394
|
temp_dir = None
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
"arc",
|
|
328
|
-
text=f"Tool running: [bold]{self.name}[/bold]...",
|
|
329
|
-
speed=2.0,
|
|
330
|
-
style=SPINNER_STYLE,
|
|
331
|
-
)
|
|
332
|
-
)
|
|
395
|
+
# Only the outermost tool in a call chain owns the live display; nested tools
|
|
396
|
+
# (and tool_run_streaming) feed their output into the already-active display.
|
|
397
|
+
owns_display = not self.manages_own_progress and Tool._active_display is None
|
|
398
|
+
display_obj = ToolLiveDisplay(self.name) if owns_display else None
|
|
333
399
|
display = (
|
|
334
|
-
|
|
335
|
-
if
|
|
336
|
-
else
|
|
400
|
+
Live(display_obj, console=console, refresh_per_second=8, transient=False)
|
|
401
|
+
if display_obj is not None
|
|
402
|
+
else nullcontext()
|
|
337
403
|
)
|
|
338
404
|
with display:
|
|
405
|
+
if display_obj is not None:
|
|
406
|
+
Tool._active_display = display_obj
|
|
407
|
+
|
|
339
408
|
did_set_default_log = (
|
|
340
409
|
False # Assume we are not the top entry into the chain of tool calls
|
|
341
410
|
)
|
|
@@ -359,8 +428,9 @@ class Tool:
|
|
|
359
428
|
|
|
360
429
|
self._run(cwd, commands, context=context, log_out=my_log, **kwargs)
|
|
361
430
|
finally:
|
|
362
|
-
if
|
|
363
|
-
|
|
431
|
+
if display_obj is not None:
|
|
432
|
+
display_obj.finish()
|
|
433
|
+
Tool._active_display = None
|
|
364
434
|
if temp_dir:
|
|
365
435
|
shutil.rmtree(temp_dir)
|
|
366
436
|
context.pop("temp_dir", None)
|
|
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
|