Research-Stack/4-Infrastructure/NoDupeLabs/nodupe/tools/scanner_engine/progress.py

228 lines
7.4 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025 Allaun
"""Progress tracker for file processing operations.
This module provides progress tracking functionality for file processing operations,
including time estimation, rate calculation, and progress reporting.
Key Features:
- Progress tracking with time estimation
- Rate calculation (files/second, bytes/second)
- Console and callback-based reporting
- Error handling
- Thread-safe operations
Dependencies:
- time (standard library)
- typing (standard library)
- threading (standard library)
"""
import time
from typing import Dict, Any, Optional, Callable
import threading
class ProgressTracker:
"""Progress tracker for file processing operations.
Responsibilities:
- Track progress of file processing operations
- Calculate time estimates and rates
- Report progress via callbacks
- Handle progress tracking errors
- Support thread-safe operations
"""
def __init__(self):
"""Initialize progress tracker."""
self._lock = threading.Lock()
self._start_time: float = 0.0
self._last_update_time: float = 0.0
self._total_items = 0
self._completed_items = 0
self._total_bytes = 0
self._processed_bytes = 0
self._status = "not_started"
self._error_count = 0
def start(self, total_items: int = 0, total_bytes: int = 0) -> None:
"""Start progress tracking.
Args:
total_items: Total number of items to process
total_bytes: Total number of bytes to process
"""
with self._lock:
self._start_time = time.monotonic()
self._last_update_time = self._start_time
self._total_items = total_items
self._completed_items = 0
self._total_bytes = total_bytes
self._processed_bytes = 0
self._status = "in_progress"
self._error_count = 0
def update(self, items_completed: int = 1, bytes_processed: int = 0) -> None:
"""Update progress with completed items and processed bytes.
Args:
items_completed: Number of items completed since last update
bytes_processed: Number of bytes processed since last update
"""
with self._lock:
self._completed_items += items_completed
self._processed_bytes += bytes_processed
self._last_update_time = time.monotonic()
def complete(self) -> None:
"""Mark progress as complete."""
with self._lock:
self._status = "completed"
self._last_update_time = time.monotonic()
def error(self) -> None:
"""Record an error."""
with self._lock:
self._error_count += 1
self._last_update_time = time.monotonic()
def get_progress(self) -> Dict[str, Any]:
"""Get current progress information.
Returns:
Dictionary containing progress information
"""
with self._lock:
elapsed = time.monotonic() - self._start_time if self._start_time > 0 else 0
# Calculate rates
items_per_second: float = self._completed_items / elapsed if elapsed > 0 else 0.0
bytes_per_second: float = self._processed_bytes / elapsed if elapsed > 0 else 0.0
# Calculate estimates
remaining_items = max(0, self._total_items - self._completed_items)
remaining_bytes = max(0, self._total_bytes - self._processed_bytes)
time_remaining: float = 0.0
if items_per_second > 0:
time_remaining = remaining_items / items_per_second
elif bytes_per_second > 0:
time_remaining = remaining_bytes / bytes_per_second
percent_complete: float = 0.0
if self._total_items > 0:
percent_complete = (self._completed_items / self._total_items) * 100
elif self._total_bytes > 0:
percent_complete = (self._processed_bytes / self._total_bytes) * 100
return {
'status': self._status,
'start_time': self._start_time,
'elapsed_time': elapsed,
'total_items': self._total_items,
'completed_items': self._completed_items,
'remaining_items': remaining_items,
'total_bytes': self._total_bytes,
'processed_bytes': self._processed_bytes,
'remaining_bytes': remaining_bytes,
'percent_complete': percent_complete,
'items_per_second': items_per_second,
'bytes_per_second': bytes_per_second,
'time_remaining': time_remaining,
'error_count': self._error_count
}
def report_progress(self, on_progress: Optional[Callable[[Dict[str, Any]], None]] = None) -> None:
"""Report progress via callback.
Args:
on_progress: Optional progress callback
"""
if on_progress:
progress = self.get_progress()
on_progress(progress)
def reset(self) -> None:
"""Reset progress tracker."""
with self._lock:
self._start_time = 0
self._last_update_time = 0
self._total_items = 0
self._completed_items = 0
self._total_bytes = 0
self._processed_bytes = 0
self._status = "not_started"
self._error_count = 0
def get_elapsed_time(self) -> float:
"""Get elapsed time since start.
Returns:
Elapsed time in seconds
"""
with self._lock:
return time.monotonic() - self._start_time if self._start_time > 0 else 0
def get_status(self) -> str:
"""Get current status.
Returns:
Current status string
"""
with self._lock:
return self._status
def is_complete(self) -> bool:
"""Check if progress is complete.
Returns:
True if complete, False otherwise
"""
with self._lock:
return self._status == "completed"
def get_error_count(self) -> int:
"""Get error count.
Returns:
Number of errors encountered
"""
with self._lock:
return self._error_count
def format_progress(self, progress: Optional[Dict[str, Any]] = None) -> str:
"""Format progress information as string.
Args:
progress: Optional progress dictionary
Returns:
Formatted progress string
"""
if progress is None:
progress = self.get_progress()
status = progress.get('status', 'unknown')
percent = progress.get('percent_complete', 0)
elapsed = progress.get('elapsed_time', 0)
remaining = progress.get('time_remaining', 0)
items = progress.get('completed_items', 0)
total = progress.get('total_items', 0)
errors = progress.get('error_count', 0)
return (f"Status: {status} | "
f"Progress: {percent:.1f}% | "
f"Items: {items}/{total} | "
f"Time: {elapsed:.1f}s (remaining: {remaining:.1f}s) | "
f"Errors: {errors}")
def create_progress_tracker() -> ProgressTracker:
"""Create and return a ProgressTracker instance.
Returns:
ProgressTracker instance
"""
return ProgressTracker()