sdfsdf
This commit is contained in:
287
models.py
287
models.py
@@ -681,6 +681,7 @@ class PipelineLiveProgress:
|
||||
self._overall: Optional[Progress] = None
|
||||
self._pipe_progress: Optional[Progress] = None
|
||||
self._subtasks: Optional[Progress] = None
|
||||
self._status: Optional[Progress] = None
|
||||
self._transfers: Optional[Progress] = None
|
||||
|
||||
self._overall_task: Optional[TaskID] = None
|
||||
@@ -688,6 +689,17 @@ class PipelineLiveProgress:
|
||||
|
||||
self._transfer_tasks: Dict[str, TaskID] = {}
|
||||
|
||||
# Per-pipe status line shown below the pipe bars.
|
||||
self._status_tasks: Dict[int, TaskID] = {}
|
||||
|
||||
# When a pipe is operating on a single item, allow percent-based progress
|
||||
# updates on the pipe bar (0..100) so it doesn't sit at 0% until emit().
|
||||
self._pipe_percent_mode: Dict[int, bool] = {}
|
||||
|
||||
# Per-pipe step counters used for status lines and percent mapping.
|
||||
self._pipe_step_total: Dict[int, int] = {}
|
||||
self._pipe_step_done: Dict[int, int] = {}
|
||||
|
||||
# Per-pipe state
|
||||
self._pipe_totals: List[int] = [0 for _ in self._pipe_labels]
|
||||
self._pipe_done: List[int] = [0 for _ in self._pipe_labels]
|
||||
@@ -700,26 +712,11 @@ class PipelineLiveProgress:
|
||||
def _title_text(self) -> str:
|
||||
"""Compute the Pipeline panel title.
|
||||
|
||||
We keep per-pipe elapsed time on the pipe rows. The panel title is used
|
||||
to show the currently active item (cmd + url/path) with a lightweight
|
||||
spinner so the UI reads as "working on X".
|
||||
The title remains stable ("Pipeline"). Per-item step detail is rendered
|
||||
using a dedicated progress bar within the panel.
|
||||
"""
|
||||
|
||||
active = str(self._active_subtask_text or "").strip()
|
||||
if not active:
|
||||
return "Pipeline"
|
||||
|
||||
# Lightweight spinner frames (similar intent to Rich's simpleDots).
|
||||
try:
|
||||
import time
|
||||
|
||||
frames = [".", "..", "..."]
|
||||
idx = int(time.monotonic() * 4) % len(frames)
|
||||
prefix = frames[idx]
|
||||
except Exception:
|
||||
prefix = "..."
|
||||
|
||||
return f"{prefix} {active}"
|
||||
return "Pipeline"
|
||||
|
||||
def set_active_subtask_text(self, text: Optional[str]) -> None:
|
||||
"""Update the Pipeline panel title to reflect the current in-item step.
|
||||
@@ -744,6 +741,7 @@ class PipelineLiveProgress:
|
||||
"""
|
||||
|
||||
pipe_progress = self._pipe_progress
|
||||
status = self._status
|
||||
transfers = self._transfers
|
||||
overall = self._overall
|
||||
if pipe_progress is None or transfers is None or overall is None:
|
||||
@@ -751,23 +749,27 @@ class PipelineLiveProgress:
|
||||
yield Panel("", title="Pipeline", expand=False)
|
||||
return
|
||||
|
||||
yield Group(
|
||||
Panel(Group(pipe_progress, transfers), title=self._title_text(), expand=False),
|
||||
overall,
|
||||
)
|
||||
body_parts: List[Any] = [pipe_progress]
|
||||
if status is not None and self._status_tasks:
|
||||
body_parts.append(status)
|
||||
body_parts.append(transfers)
|
||||
|
||||
yield Group(Panel(Group(*body_parts), title=self._title_text(), expand=False), overall)
|
||||
|
||||
def _render_group(self) -> Group:
|
||||
# Backward-compatible helper (some callers may still expect a Group).
|
||||
pipe_progress = self._pipe_progress
|
||||
status = self._status
|
||||
transfers = self._transfers
|
||||
overall = self._overall
|
||||
assert pipe_progress is not None
|
||||
assert transfers is not None
|
||||
assert overall is not None
|
||||
return Group(
|
||||
Panel(Group(pipe_progress, transfers), title=self._title_text(), expand=False),
|
||||
overall,
|
||||
)
|
||||
body_parts: List[Any] = [pipe_progress]
|
||||
if status is not None and self._status_tasks:
|
||||
body_parts.append(status)
|
||||
body_parts.append(transfers)
|
||||
return Group(Panel(Group(*body_parts), title=self._title_text(), expand=False), overall)
|
||||
|
||||
def start(self) -> None:
|
||||
if not self._enabled:
|
||||
@@ -803,6 +805,14 @@ class PipelineLiveProgress:
|
||||
transient=False,
|
||||
)
|
||||
|
||||
# Status line below the pipe bars. Kept simple (no extra bar) so it
|
||||
# doesn't visually offset the main pipe bar columns.
|
||||
self._status = Progress(
|
||||
TextColumn(" [bold]└─ {task.description}[/bold]"),
|
||||
console=self._console,
|
||||
transient=False,
|
||||
)
|
||||
|
||||
# Byte-based transfer bars (download/upload) integrated into the Live view.
|
||||
self._transfers = Progress(
|
||||
TextColumn(" {task.description}"),
|
||||
@@ -878,12 +888,178 @@ class PipelineLiveProgress:
|
||||
self._overall = None
|
||||
self._pipe_progress = None
|
||||
self._subtasks = None
|
||||
self._status = None
|
||||
self._transfers = None
|
||||
self._overall_task = None
|
||||
self._pipe_tasks = []
|
||||
self._transfer_tasks = {}
|
||||
self._status_tasks = {}
|
||||
self._pipe_percent_mode = {}
|
||||
self._pipe_step_total = {}
|
||||
self._pipe_step_done = {}
|
||||
self._active_subtask_text = None
|
||||
|
||||
def _hide_pipe_subtasks(self, pipe_index: int) -> None:
|
||||
"""Hide any visible per-item spinner rows for a pipe."""
|
||||
subtasks = self._subtasks
|
||||
if subtasks is None:
|
||||
return
|
||||
try:
|
||||
for sub_id in self._subtask_ids[int(pipe_index)]:
|
||||
try:
|
||||
subtasks.stop_task(sub_id)
|
||||
subtasks.update(sub_id, visible=False)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def set_pipe_status_text(self, pipe_index: int, text: str) -> None:
|
||||
"""Set a status line under the pipe bars for the given pipe."""
|
||||
if not self._enabled:
|
||||
return
|
||||
if not self._ensure_pipe(int(pipe_index)):
|
||||
return
|
||||
prog = self._status
|
||||
if prog is None:
|
||||
return
|
||||
|
||||
try:
|
||||
pidx = int(pipe_index)
|
||||
msg = str(text or "").strip()
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# For long single-item work, hide the per-item spinner line and use this
|
||||
# dedicated status line instead.
|
||||
if self._pipe_percent_mode.get(pidx, False):
|
||||
try:
|
||||
self._hide_pipe_subtasks(pidx)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
task_id = self._status_tasks.get(pidx)
|
||||
if task_id is None:
|
||||
try:
|
||||
task_id = prog.add_task(msg)
|
||||
except Exception:
|
||||
return
|
||||
self._status_tasks[pidx] = task_id
|
||||
|
||||
try:
|
||||
prog.update(task_id, description=msg, refresh=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def clear_pipe_status_text(self, pipe_index: int) -> None:
|
||||
prog = self._status
|
||||
if prog is None:
|
||||
return
|
||||
try:
|
||||
pidx = int(pipe_index)
|
||||
except Exception:
|
||||
return
|
||||
task_id = self._status_tasks.pop(pidx, None)
|
||||
if task_id is None:
|
||||
return
|
||||
try:
|
||||
prog.remove_task(task_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def set_pipe_percent(self, pipe_index: int, percent: int) -> None:
|
||||
"""Update the pipe bar as a percent (only when single-item mode is enabled)."""
|
||||
if not self._enabled:
|
||||
return
|
||||
if not self._ensure_pipe(int(pipe_index)):
|
||||
return
|
||||
pipe_progress = self._pipe_progress
|
||||
if pipe_progress is None:
|
||||
return
|
||||
try:
|
||||
pidx = int(pipe_index)
|
||||
except Exception:
|
||||
return
|
||||
if not self._pipe_percent_mode.get(pidx, False):
|
||||
return
|
||||
try:
|
||||
pct = max(0, min(100, int(percent)))
|
||||
pipe_task = self._pipe_tasks[pidx]
|
||||
pipe_progress.update(pipe_task, completed=pct, total=100, refresh=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def begin_pipe_steps(self, pipe_index: int, *, total_steps: int) -> None:
|
||||
"""Initialize step tracking for a pipe.
|
||||
|
||||
The cmdlet must call this once up-front so we can map steps to percent.
|
||||
"""
|
||||
if not self._enabled:
|
||||
return
|
||||
if not self._ensure_pipe(int(pipe_index)):
|
||||
return
|
||||
|
||||
try:
|
||||
pidx = int(pipe_index)
|
||||
tot = max(1, int(total_steps))
|
||||
except Exception:
|
||||
return
|
||||
|
||||
self._pipe_step_total[pidx] = tot
|
||||
self._pipe_step_done[pidx] = 0
|
||||
|
||||
# Reset status line and percent.
|
||||
try:
|
||||
self.clear_pipe_status_text(pidx)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.set_pipe_percent(pidx, 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def advance_pipe_step(self, pipe_index: int, text: str) -> None:
|
||||
"""Advance the pipe's step counter by one.
|
||||
|
||||
Each call is treated as a new step (no in-place text rewrites).
|
||||
Updates:
|
||||
- status line: "i/N step: {text}"
|
||||
- pipe percent (single-item pipes only): round(i/N*100)
|
||||
"""
|
||||
if not self._enabled:
|
||||
return
|
||||
if not self._ensure_pipe(int(pipe_index)):
|
||||
return
|
||||
|
||||
try:
|
||||
pidx = int(pipe_index)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
total = int(self._pipe_step_total.get(pidx, 0) or 0)
|
||||
if total <= 0:
|
||||
# If steps weren't declared, treat as a single-step operation.
|
||||
total = 1
|
||||
self._pipe_step_total[pidx] = total
|
||||
|
||||
done = int(self._pipe_step_done.get(pidx, 0) or 0) + 1
|
||||
done = min(done, total)
|
||||
self._pipe_step_done[pidx] = done
|
||||
|
||||
msg = str(text or "").strip()
|
||||
line = f"{done}/{total} step: {msg}" if msg else f"{done}/{total} step"
|
||||
try:
|
||||
self.set_pipe_status_text(pidx, line)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Percent mapping only applies when the pipe is in percent mode (single-item).
|
||||
try:
|
||||
pct = 100 if done >= total else int(round((done / max(1, total)) * 100.0))
|
||||
self.set_pipe_percent(pidx, pct)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def begin_transfer(self, *, label: str, total: Optional[int] = None) -> None:
|
||||
if not self._enabled:
|
||||
return
|
||||
@@ -962,8 +1138,23 @@ class PipelineLiveProgress:
|
||||
self._subtask_active_index[pipe_index] = 0
|
||||
self._subtask_ids[pipe_index] = []
|
||||
|
||||
# Reset per-item step progress for this pipe.
|
||||
try:
|
||||
self.clear_pipe_status_text(pipe_index)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._pipe_step_total.pop(pipe_index, None)
|
||||
self._pipe_step_done.pop(pipe_index, None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If this pipe will process exactly one item, allow percent-based updates.
|
||||
percent_mode = bool(int(total_items) == 1)
|
||||
self._pipe_percent_mode[pipe_index] = percent_mode
|
||||
|
||||
pipe_task = self._pipe_tasks[pipe_index]
|
||||
pipe_progress.update(pipe_task, completed=0, total=total_items)
|
||||
pipe_progress.update(pipe_task, completed=0, total=(100 if percent_mode else total_items))
|
||||
# Start the per-pipe timer now that the pipe is actually running.
|
||||
try:
|
||||
pipe_progress.start_task(pipe_task)
|
||||
@@ -974,6 +1165,12 @@ class PipelineLiveProgress:
|
||||
if isinstance(items_preview, list) and items_preview:
|
||||
labels = [_pipeline_progress_item_label(x) for x in items_preview]
|
||||
|
||||
# For single-item pipes, keep the UI clean: don't show a spinner row.
|
||||
if percent_mode:
|
||||
self._subtask_ids[pipe_index] = []
|
||||
self._active_subtask_text = None
|
||||
return
|
||||
|
||||
for i in range(total_items):
|
||||
suffix = labels[i] if i < len(labels) else f"item {i + 1}/{total_items}"
|
||||
# Use start=False so elapsed time starts when we explicitly start_task().
|
||||
@@ -1038,7 +1235,21 @@ class PipelineLiveProgress:
|
||||
self._pipe_done[pipe_index] = done
|
||||
|
||||
pipe_task = self._pipe_tasks[pipe_index]
|
||||
pipe_progress.update(pipe_task, completed=done)
|
||||
if self._pipe_percent_mode.get(pipe_index, False):
|
||||
pipe_progress.update(pipe_task, completed=100, total=100)
|
||||
else:
|
||||
pipe_progress.update(pipe_task, completed=done)
|
||||
|
||||
# Clear any status line now that it emitted.
|
||||
try:
|
||||
self.clear_pipe_status_text(pipe_index)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._pipe_step_total.pop(pipe_index, None)
|
||||
self._pipe_step_done.pop(pipe_index, None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Start next subtask spinner.
|
||||
next_index = active + 1
|
||||
@@ -1072,7 +1283,10 @@ class PipelineLiveProgress:
|
||||
# Ensure the pipe bar finishes even if cmdlet didn’t emit per item.
|
||||
if force_complete and done < total:
|
||||
pipe_task = self._pipe_tasks[pipe_index]
|
||||
pipe_progress.update(pipe_task, completed=total)
|
||||
if self._pipe_percent_mode.get(pipe_index, False):
|
||||
pipe_progress.update(pipe_task, completed=100, total=100)
|
||||
else:
|
||||
pipe_progress.update(pipe_task, completed=total)
|
||||
self._pipe_done[pipe_index] = total
|
||||
|
||||
# Hide any remaining subtask spinners.
|
||||
@@ -1086,6 +1300,17 @@ class PipelineLiveProgress:
|
||||
# If we just finished the active pipe, clear the title context.
|
||||
self._active_subtask_text = None
|
||||
|
||||
# Ensure status line is cleared when a pipe finishes.
|
||||
try:
|
||||
self.clear_pipe_status_text(pipe_index)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self._pipe_step_total.pop(pipe_index, None)
|
||||
self._pipe_step_done.pop(pipe_index, None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Stop the per-pipe timer once the pipe is finished.
|
||||
try:
|
||||
pipe_task = self._pipe_tasks[pipe_index]
|
||||
@@ -1112,12 +1337,14 @@ class PipelineStageContext:
|
||||
self,
|
||||
stage_index: int,
|
||||
total_stages: int,
|
||||
pipe_index: Optional[int] = None,
|
||||
worker_id: Optional[str] = None,
|
||||
on_emit: Optional[Callable[[Any], None]] = None,
|
||||
):
|
||||
self.stage_index = stage_index
|
||||
self.total_stages = total_stages
|
||||
self.is_last_stage = (stage_index == total_stages - 1)
|
||||
self.pipe_index = int(pipe_index) if pipe_index is not None else None
|
||||
self.worker_id = worker_id
|
||||
self._on_emit = on_emit
|
||||
self.emits: List[Any] = []
|
||||
@@ -1141,7 +1368,7 @@ class PipelineStageContext:
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"PipelineStageContext(stage={self.stage_index}/{self.total_stages}, "
|
||||
f"is_last={self.is_last_stage}, worker_id={self.worker_id})"
|
||||
f"pipe_index={self.pipe_index}, is_last={self.is_last_stage}, worker_id={self.worker_id})"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user