Coverage for src / ezpl / handlers / wizard / progress.py: 95.33%
130 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-13 19:35 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-13 19:35 +0000
1# ///////////////////////////////////////////////////////////////
2# EZPL - Wizard Progress Mixin
3# Project: ezpl
4# ///////////////////////////////////////////////////////////////
6"""
7Progress methods mixin for Rich Wizard.
9This module provides all progress bar-related methods for the RichWizard class.
10"""
12from __future__ import annotations
14# ///////////////////////////////////////////////////////////////
15# IMPORTS
16# ///////////////////////////////////////////////////////////////
17# Standard library imports
18import time
19from collections.abc import Generator
20from contextlib import contextmanager
21from typing import Any
23# Third-party imports
24from rich.console import Console
25from rich.progress import (
26 BarColumn,
27 DownloadColumn,
28 Progress,
29 ProgressColumn,
30 SpinnerColumn,
31 TaskID,
32 TaskProgressColumn,
33 TextColumn,
34 TimeElapsedColumn,
35 TimeRemainingColumn,
36 TransferSpeedColumn,
37)
39# ///////////////////////////////////////////////////////////////
40# CLASSES
41# ///////////////////////////////////////////////////////////////
44class ProgressMixin:
45 """
46 Mixin providing progress bar methods for RichWizard.
48 This mixin adds all progress-related functionality including
49 generic progress bars, spinners, download progress, installation progress,
50 and layered progress bars.
51 """
53 # Type hints for attributes provided by RichWizard
54 _console: Console
55 _progress_prefix: str
57 # ///////////////////////////////////////////////////////////////
58 # PROGRESS METHODS
59 # ///////////////////////////////////////////////////////////////
61 @contextmanager
62 def progress(
63 self,
64 description: str = "Working...",
65 total: int | None = None,
66 transient: bool = False,
67 ) -> Generator[tuple[Progress, TaskID], None, None]:
68 """
69 Create a progress bar context manager.
71 Args:
72 description: Progress description
73 total: Total number of items (None for indeterminate)
74 transient: Whether to clear progress on exit
76 Yields:
77 tuple of (Progress, task_id)
79 Example:
80 >>> with printer.wizard.progress("Processing...", total=100) as (progress, task):
81 ... for i in range(100):
82 ... progress.update(task, advance=1)
83 """
84 progress = Progress(
85 TextColumn(self._progress_prefix),
86 SpinnerColumn(),
87 TextColumn("[progress.description]{task.description}"),
88 BarColumn(),
89 TaskProgressColumn(),
90 console=self._console,
91 transient=transient,
92 )
94 with progress:
95 task = progress.add_task(description, total=total)
96 yield progress, task
98 @contextmanager
99 def spinner(
100 self, description: str = "Working..."
101 ) -> Generator[tuple[Progress, TaskID], None, None]:
102 """
103 Create a simple spinner with description.
105 Args:
106 description: Spinner description
108 Yields:
109 tuple of (Progress, task_id)
111 Example:
112 >>> with printer.wizard.spinner("Loading...") as (progress, task):
113 ... # Do work
114 ... pass
115 """
116 progress = Progress(
117 TextColumn(self._progress_prefix),
118 SpinnerColumn(),
119 TextColumn("[progress.description]{task.description}"),
120 console=self._console,
121 )
123 with progress:
124 task = progress.add_task(description, total=None)
125 yield progress, task
127 @contextmanager
128 def spinner_with_status(
129 self, description: str = "Working..."
130 ) -> Generator[tuple[Progress, TaskID], None, None]:
131 """
132 Create a spinner that can update status messages.
134 Args:
135 description: Spinner description
137 Yields:
138 tuple of (Progress, task_id)
140 Example:
141 >>> with printer.wizard.spinner_with_status("Processing...") as (progress, task):
142 ... progress.update(task, status="Step 1/3")
143 """
144 progress = Progress(
145 TextColumn(self._progress_prefix),
146 SpinnerColumn(),
147 TextColumn("[progress.description]{task.description}"),
148 TextColumn("[dim]{task.fields[status]}"),
149 console=self._console,
150 )
152 with progress:
153 task = progress.add_task(description, total=None, status="")
154 yield progress, task
156 @contextmanager
157 def download_progress(
158 self, description: str = "Downloading..."
159 ) -> Generator[tuple[Progress, TaskID], None, None]:
160 """
161 Create a download progress bar with speed and size information.
163 Args:
164 description: Download description
166 Yields:
167 tuple of (Progress, task_id)
169 Example:
170 >>> with printer.wizard.download_progress() as (progress, task):
171 ... progress.update(task, completed=50, total=100)
172 """
173 progress = Progress(
174 TextColumn(self._progress_prefix),
175 TextColumn("[progress.description]{task.description}"),
176 BarColumn(),
177 TaskProgressColumn(),
178 TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
179 DownloadColumn(),
180 TransferSpeedColumn(),
181 console=self._console,
182 )
184 with progress:
185 task = progress.add_task(description, total=100)
186 yield progress, task
188 @contextmanager
189 def file_download_progress(
190 self, filename: str, total_size: int, description: str = "Downloading file..."
191 ) -> Generator[tuple[Progress, TaskID], None, None]:
192 """
193 Create a progress bar for downloading a specific file.
195 Args:
196 filename: Name of the file being downloaded
197 total_size: Total size in bytes
198 description: Main description
200 Yields:
201 tuple of (Progress, task_id)
203 Example:
204 >>> with printer.wizard.file_download_progress("file.zip", 1024000) as (progress, task):
205 ... progress.update(task, advance=512000)
206 """
207 progress = Progress(
208 TextColumn(self._progress_prefix),
209 TextColumn("[bold blue]{task.description}"),
210 BarColumn(),
211 TaskProgressColumn(),
212 DownloadColumn(),
213 TransferSpeedColumn(),
214 TextColumn("[dim]{task.fields[filename]}"),
215 console=self._console,
216 )
218 with progress:
219 task = progress.add_task(description, total=total_size, filename=filename)
220 yield progress, task
222 @contextmanager
223 def dependency_progress(
224 self, dependencies: list[str], description: str = "Installing dependencies..."
225 ) -> Generator[tuple[Progress, TaskID, str], None, None]:
226 """
227 Create a progress bar for dependency installation.
229 Args:
230 dependencies: list of dependency names
231 description: Main description
233 Yields:
234 tuple of (Progress, task_id, dependency_name) for each dependency
236 Example:
237 >>> deps = ["package1", "package2", "package3"]
238 >>> with printer.wizard.dependency_progress(deps) as (progress, task, dep):
239 ... # Install dependency
240 ... progress.advance(task)
241 """
242 progress = Progress(
243 TextColumn(self._progress_prefix),
244 SpinnerColumn(),
245 TextColumn("[bold green]{task.description}"),
246 TextColumn(
247 "[dim]Dependency {task.fields[current]}/{task.fields[total_deps]}"
248 ),
249 TextColumn("[dim]{task.fields[dependency]}"),
250 BarColumn(),
251 TaskProgressColumn(),
252 TimeElapsedColumn(),
253 console=self._console,
254 )
256 with progress:
257 task = progress.add_task(
258 description,
259 total=len(dependencies),
260 current=1,
261 total_deps=len(dependencies),
262 dependency="",
263 )
265 for i, dependency in enumerate(dependencies):
266 progress.update(
267 task,
268 description=f"[bold green]Installing {dependency}",
269 current=i + 1,
270 dependency=dependency,
271 )
272 yield progress, task, dependency
273 progress.advance(task)
274 time.sleep(0.1)
276 @contextmanager
277 def package_install_progress(
278 self,
279 packages: list[tuple[str, str]],
280 description: str = "Installing packages...",
281 ) -> Generator[tuple[Progress, TaskID, str, str], None, None]:
282 """
283 Create a progress bar for package installation with version info.
285 Args:
286 packages: list of tuples (package_name, version)
287 description: Main description
289 Yields:
290 tuple of (Progress, task_id, package_name, version) for each package
292 Example:
293 >>> packages = [("requests", "2.31.0"), ("click", "8.1.0")]
294 >>> with printer.wizard.package_install_progress(packages) as (progress, task, pkg, ver):
295 ... # Install package
296 ... progress.advance(task)
297 """
298 progress = Progress(
299 TextColumn(self._progress_prefix),
300 SpinnerColumn(),
301 TextColumn("[bold cyan]{task.description}"),
302 TextColumn(
303 "[dim]Package {task.fields[current]}/{task.fields[total_packages]}"
304 ),
305 TextColumn("[dim]{task.fields[package]}"),
306 TextColumn("[dim]{task.fields[version]}"),
307 BarColumn(),
308 TaskProgressColumn(),
309 TimeElapsedColumn(),
310 console=self._console,
311 )
313 with progress:
314 task = progress.add_task(
315 description,
316 total=len(packages),
317 current=1,
318 total_packages=len(packages),
319 package="",
320 version="",
321 )
323 for i, (package_name, version) in enumerate(packages):
324 progress.update(
325 task,
326 description=f"[bold cyan]Installing {package_name}",
327 current=i + 1,
328 package=package_name,
329 version=version,
330 )
331 yield progress, task, package_name, version
332 progress.advance(task)
333 time.sleep(0.1)
335 @contextmanager
336 def step_progress(
337 self,
338 steps: list[str],
339 description: str = "Processing...",
340 show_step_numbers: bool = True,
341 show_time: bool = True,
342 ) -> Generator[tuple[Progress, TaskID, list[str]], None, None]:
343 """
344 Create a step-based progress bar with detailed step information.
346 Args:
347 steps: list of step names
348 description: Main description
349 show_step_numbers: Show step numbers (e.g., "Step 1/5")
350 show_time: Show elapsed and remaining time
352 Yields:
353 tuple of (Progress, task_id, steps_list)
355 Example:
356 >>> steps = ["Step 1", "Step 2", "Step 3"]
357 >>> with printer.wizard.step_progress(steps) as (progress, task, steps_list):
358 ... for i, step in enumerate(steps_list):
359 ... progress.update(task, completed=i, current_step=step)
360 ... progress.advance(task)
361 """
362 # Build columns based on options
363 columns: list[ProgressColumn] = [
364 TextColumn(self._progress_prefix),
365 SpinnerColumn(),
366 TextColumn("[bold blue]{task.description}"),
367 ]
369 if show_step_numbers: 369 ↛ 374line 369 didn't jump to line 374 because the condition on line 369 was always true
370 columns.append(
371 TextColumn("[dim]Step {task.fields[step]}/{task.fields[total_steps]}")
372 )
374 columns.extend(
375 [
376 TextColumn("[dim]{task.fields[current_step]}"),
377 BarColumn(),
378 TaskProgressColumn(),
379 ]
380 )
382 if show_time: 382 ↛ 390line 382 didn't jump to line 390 because the condition on line 382 was always true
383 columns.extend(
384 [
385 TimeElapsedColumn(),
386 TimeRemainingColumn(),
387 ]
388 )
390 progress = Progress(*columns, console=self._console)
392 with progress:
393 task = progress.add_task(
394 description,
395 total=len(steps),
396 step=1,
397 total_steps=len(steps),
398 current_step="",
399 )
401 yield progress, task, steps
403 @contextmanager
404 def file_copy_progress(
405 self, files: list[str], description: str = "Copying files..."
406 ) -> Generator[tuple[Progress, TaskID, list[str]], None, None]:
407 """
408 Create a progress bar specifically for file copying operations.
410 Args:
411 files: list of file paths to copy
412 description: Main description
414 Yields:
415 tuple of (Progress, task_id, files_list)
417 Example:
418 >>> files = ["file1.txt", "file2.txt"]
419 >>> with printer.wizard.file_copy_progress(files) as (progress, task, files_list):
420 ... for i, file in enumerate(files_list):
421 ... progress.update(task, current_file=file)
422 ... progress.advance(task)
423 """
424 progress = Progress(
425 TextColumn(self._progress_prefix),
426 SpinnerColumn(),
427 TextColumn("[bold magenta]{task.description}"),
428 BarColumn(),
429 TaskProgressColumn(),
430 TextColumn("[dim]{task.fields[current_file]}"),
431 console=self._console,
432 )
434 with progress:
435 task = progress.add_task(description, total=len(files), current_file="")
436 yield progress, task, files
438 @contextmanager
439 def installation_progress(
440 self,
441 steps: list[tuple[str, str]],
442 description: str = "Installation in progress...",
443 ) -> Generator[tuple[Progress, TaskID, str, str], None, None]:
444 """
445 Create a progress bar for installation processes with step details.
447 Args:
448 steps: list of tuples (step_name, step_description)
449 description: Main description
451 Yields:
452 tuple of (Progress, task_id, step_name, step_description) for each step
454 Example:
455 >>> steps = [("Init", "Initializing..."), ("Install", "Installing...")]
456 >>> with printer.wizard.installation_progress(steps) as (progress, task, name, desc):
457 ... # Process step
458 ... progress.advance(task)
459 """
460 progress = Progress(
461 TextColumn(self._progress_prefix),
462 SpinnerColumn(),
463 TextColumn("[bold green]{task.description}"),
464 TextColumn("[dim]Step {task.fields[step]}/{task.fields[total_steps]}"),
465 TextColumn("[dim]{task.fields[step_detail]}"),
466 BarColumn(),
467 TaskProgressColumn(),
468 TimeElapsedColumn(),
469 console=self._console,
470 )
472 with progress:
473 task = progress.add_task(
474 description,
475 total=len(steps),
476 step=1,
477 total_steps=len(steps),
478 step_detail="",
479 )
481 for i, (step_name, step_detail) in enumerate(steps):
482 progress.update(
483 task,
484 description=f"[bold green]{step_name}",
485 step=i + 1,
486 step_detail=step_detail,
487 )
488 yield progress, task, step_name, step_detail
489 progress.advance(task)
490 time.sleep(0.1)
492 @contextmanager
493 def build_progress(
494 self, phases: list[tuple[str, int]], description: str = "Building project..."
495 ) -> Generator[tuple[Progress, TaskID, str, int], None, None]:
496 """
497 Create a progress bar for build processes with weighted phases.
499 Args:
500 phases: list of tuples (phase_name, weight_percentage)
501 description: Main description
503 Yields:
504 tuple of (Progress, task_id, phase_name, weight) for each phase
506 Example:
507 >>> phases = [("Compile", 40), ("Test", 30), ("Package", 30)]
508 >>> with printer.wizard.build_progress(phases) as (progress, task, phase, weight):
509 ... # Process phase
510 ... progress.update(task, advance=weight)
511 """
512 progress = Progress(
513 TextColumn(self._progress_prefix),
514 SpinnerColumn(),
515 TextColumn("[bold cyan]{task.description}"),
516 TextColumn("[dim]{task.fields[current_phase]}"),
517 BarColumn(),
518 TaskProgressColumn(),
519 TextColumn("[bold yellow]{task.percentage:>3.0f}%"),
520 TimeElapsedColumn(),
521 console=self._console,
522 )
524 with progress:
525 task = progress.add_task(description, total=100, current_phase="")
527 current_progress = 0
528 for phase_name, weight in phases:
529 progress.update(task, current_phase=phase_name)
530 yield progress, task, phase_name, weight
531 current_progress += weight
532 progress.update(task, completed=current_progress)
534 @contextmanager
535 def deployment_progress(
536 self, stages: list[str], description: str = "Deploying..."
537 ) -> Generator[tuple[Progress, TaskID, str], None, None]:
538 """
539 Create a progress bar for deployment processes.
541 Args:
542 stages: list of deployment stage names
543 description: Main description
545 Yields:
546 tuple of (Progress, task_id, stage_name) for each stage
548 Example:
549 >>> stages = ["Build", "Test", "Deploy"]
550 >>> with printer.wizard.deployment_progress(stages) as (progress, task, stage):
551 ... # Process stage
552 ... progress.advance(task)
553 """
554 progress = Progress(
555 TextColumn(self._progress_prefix),
556 SpinnerColumn(),
557 TextColumn("[bold magenta]{task.description}"),
558 TextColumn("[dim]Stage {task.fields[stage]}/{task.fields[total_stages]}"),
559 TextColumn("[dim]{task.fields[current_stage]}"),
560 BarColumn(),
561 TaskProgressColumn(),
562 TimeElapsedColumn(),
563 TimeRemainingColumn(),
564 console=self._console,
565 )
567 with progress:
568 task = progress.add_task(
569 description,
570 total=len(stages),
571 stage=1,
572 total_stages=len(stages),
573 current_stage="",
574 )
576 for i, stage in enumerate(stages):
577 progress.update(
578 task,
579 description=f"[bold magenta]{stage}",
580 stage=i + 1,
581 current_stage=stage,
582 )
583 yield progress, task, stage
584 progress.advance(task)
585 time.sleep(0.1)
587 @contextmanager
588 def layered_progress(
589 self,
590 layers: list[dict[str, Any]],
591 show_time: bool = True,
592 ) -> Generator[tuple[Progress, dict[str, TaskID]], None, None]:
593 """
594 Create a multi-level progress bar with dynamic layers.
596 Args:
597 layers: list of layer configurations, each containing:
598 - 'name': Layer name/description
599 - 'total': Total items for this layer (optional, None for indeterminate)
600 - 'description': Display description for the layer
601 - 'style': Rich style for this layer (optional)
602 - 'type': Layer type - 'progress' (default) or 'steps'
603 - 'steps': list of step names (required if type='steps')
604 show_time: Show elapsed and remaining time
606 Yields:
607 tuple of (Progress, task_ids_dict) where task_ids_dict maps layer names to task IDs
609 Example:
610 >>> layers = [
611 ... {"name": "layer1", "description": "Layer 1", "total": 10},
612 ... {"name": "layer2", "description": "Layer 2", "total": 5}
613 ... ]
614 >>> with printer.wizard.layered_progress(layers) as (progress, task_ids):
615 ... progress.update(task_ids["layer1"], advance=1)
616 """
617 # Build columns for the main progress bar
618 columns: list[ProgressColumn] = [
619 TextColumn(self._progress_prefix),
620 SpinnerColumn(),
621 TextColumn("[bold blue]{task.description}"),
622 BarColumn(),
623 TaskProgressColumn(),
624 TextColumn("[dim]{task.fields[details]}"),
625 ]
627 if show_time: 627 ↛ 635line 627 didn't jump to line 635 because the condition on line 627 was always true
628 columns.extend(
629 [
630 TimeElapsedColumn(),
631 TimeRemainingColumn(),
632 ]
633 )
635 progress = Progress(*columns, console=self._console)
637 with progress:
638 # Create tasks for each layer
639 task_ids: dict[str, TaskID] = {}
641 for i, layer in enumerate(layers):
642 layer_name = layer.get("name", f"Layer_{i}")
643 layer_type = layer.get("type", "progress")
644 layer_desc = layer.get("description", layer_name)
645 layer_style = layer.get("style", "default")
647 if layer_type == "steps": 647 ↛ 649line 647 didn't jump to line 649 because the condition on line 647 was never true
648 # Handle step-based layer
649 steps = layer.get("steps", [])
650 layer_total = len(steps)
651 task_id: TaskID = progress.add_task(
652 f"[{layer_style}]{layer_desc}",
653 total=layer_total,
654 details="",
655 steps=steps,
656 )
657 else:
658 # Handle regular progress layer
659 layer_total = layer.get("total", None)
660 task_id = progress.add_task(
661 f"[{layer_style}]{layer_desc}",
662 total=layer_total,
663 details="",
664 )
666 task_ids[layer_name] = task_id
668 yield progress, task_ids