218 lines
6.0 KiB
Python
218 lines
6.0 KiB
Python
"""Dynamic progress logger with formatting utilities"""
|
|
import sys
|
|
import logging
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
|
|
def format_size(bytes_size: int) -> str:
|
|
"""Format bytes to human-readable size string
|
|
|
|
Args:
|
|
bytes_size: Size in bytes
|
|
|
|
Returns:
|
|
Human-readable size string (e.g., "1.5 GB", "234.5 MB")
|
|
"""
|
|
for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB']:
|
|
if bytes_size < 1024.0:
|
|
return f"{bytes_size:.1f} {unit}"
|
|
bytes_size /= 1024.0
|
|
return f"{bytes_size:.1f} EB"
|
|
|
|
|
|
def format_rate(bytes_per_second: float) -> str:
|
|
"""Format transfer rate to human-readable string
|
|
|
|
Args:
|
|
bytes_per_second: Transfer rate in bytes per second
|
|
|
|
Returns:
|
|
Human-readable rate string (e.g., "125.3 MB/s")
|
|
"""
|
|
return f"{format_size(int(bytes_per_second))}/s"
|
|
|
|
|
|
def format_time(seconds: float) -> str:
|
|
"""Format seconds to human-readable time string
|
|
|
|
Args:
|
|
seconds: Time in seconds
|
|
|
|
Returns:
|
|
Human-readable time string (e.g., "2h 34m 12s", "45m 23s", "12s")
|
|
"""
|
|
if seconds < 60:
|
|
return f"{int(seconds)}s"
|
|
elif seconds < 3600:
|
|
minutes = int(seconds // 60)
|
|
secs = int(seconds % 60)
|
|
return f"{minutes}m {secs}s"
|
|
else:
|
|
hours = int(seconds // 3600)
|
|
minutes = int((seconds % 3600) // 60)
|
|
secs = int(seconds % 60)
|
|
return f"{hours}h {minutes}m {secs}s"
|
|
|
|
|
|
class ProgressLogger:
|
|
"""Dynamic progress logger with real-time statistics"""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str = "defrag",
|
|
level: int = logging.INFO,
|
|
log_file: Optional[Path] = None,
|
|
console_output: bool = True
|
|
):
|
|
"""Initialize progress logger
|
|
|
|
Args:
|
|
name: Logger name
|
|
level: Logging level
|
|
log_file: Optional log file path
|
|
console_output: Whether to output to console
|
|
"""
|
|
self.logger = logging.getLogger(name)
|
|
self.logger.setLevel(level)
|
|
self.logger.handlers.clear()
|
|
|
|
# Create formatter
|
|
formatter = logging.Formatter(
|
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
)
|
|
|
|
# Add console handler
|
|
if console_output:
|
|
console_handler = logging.StreamHandler(sys.stdout)
|
|
console_handler.setLevel(level)
|
|
console_handler.setFormatter(formatter)
|
|
self.logger.addHandler(console_handler)
|
|
|
|
# Add file handler
|
|
if log_file:
|
|
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
file_handler = logging.FileHandler(log_file)
|
|
file_handler.setLevel(level)
|
|
file_handler.setFormatter(formatter)
|
|
self.logger.addHandler(file_handler)
|
|
|
|
self._last_progress_line = ""
|
|
|
|
def info(self, message: str) -> None:
|
|
"""Log info message"""
|
|
self.logger.info(message)
|
|
|
|
def warning(self, message: str) -> None:
|
|
"""Log warning message"""
|
|
self.logger.warning(message)
|
|
|
|
def error(self, message: str) -> None:
|
|
"""Log error message"""
|
|
self.logger.error(message)
|
|
|
|
def debug(self, message: str) -> None:
|
|
"""Log debug message"""
|
|
self.logger.debug(message)
|
|
|
|
def critical(self, message: str) -> None:
|
|
"""Log critical message"""
|
|
self.logger.critical(message)
|
|
|
|
def progress(
|
|
self,
|
|
current: int,
|
|
total: int,
|
|
prefix: str = "",
|
|
suffix: str = "",
|
|
bytes_processed: Optional[int] = None,
|
|
elapsed_seconds: Optional[float] = None
|
|
) -> None:
|
|
"""Log progress with dynamic statistics
|
|
|
|
Args:
|
|
current: Current progress count
|
|
total: Total count
|
|
prefix: Prefix message
|
|
suffix: Suffix message
|
|
bytes_processed: Optional bytes processed for rate calculation
|
|
elapsed_seconds: Optional elapsed time for rate calculation
|
|
"""
|
|
if total == 0:
|
|
percent = 0.0
|
|
else:
|
|
percent = (current / total) * 100
|
|
|
|
progress_msg = f"{prefix} [{current}/{total}] {percent:.1f}%"
|
|
|
|
if bytes_processed is not None and elapsed_seconds is not None and elapsed_seconds > 0:
|
|
rate = bytes_per_second = bytes_processed / elapsed_seconds
|
|
progress_msg += f" | {format_size(bytes_processed)} @ {format_rate(rate)}"
|
|
|
|
# Estimate time remaining
|
|
if current > 0:
|
|
estimated_total_seconds = (elapsed_seconds / current) * total
|
|
remaining_seconds = estimated_total_seconds - elapsed_seconds
|
|
progress_msg += f" | ETA: {format_time(remaining_seconds)}"
|
|
|
|
if suffix:
|
|
progress_msg += f" | {suffix}"
|
|
|
|
self.info(progress_msg)
|
|
|
|
def section(self, title: str) -> None:
|
|
"""Log section header
|
|
|
|
Args:
|
|
title: Section title
|
|
"""
|
|
separator = "=" * 60
|
|
self.info(separator)
|
|
self.info(f" {title}")
|
|
self.info(separator)
|
|
|
|
def subsection(self, title: str) -> None:
|
|
"""Log subsection header
|
|
|
|
Args:
|
|
title: Subsection title
|
|
"""
|
|
self.info(f"\n--- {title} ---")
|
|
|
|
|
|
def create_logger(
|
|
name: str = "defrag",
|
|
level: str = "INFO",
|
|
log_file: Optional[Path] = None,
|
|
console_output: bool = True
|
|
) -> ProgressLogger:
|
|
"""Create and configure a progress logger
|
|
|
|
Args:
|
|
name: Logger name
|
|
level: Logging level as string
|
|
log_file: Optional log file path
|
|
console_output: Whether to output to console
|
|
|
|
Returns:
|
|
Configured ProgressLogger instance
|
|
"""
|
|
level_map = {
|
|
'DEBUG': logging.DEBUG,
|
|
'INFO': logging.INFO,
|
|
'WARNING': logging.WARNING,
|
|
'ERROR': logging.ERROR,
|
|
'CRITICAL': logging.CRITICAL
|
|
}
|
|
|
|
log_level = level_map.get(level.upper(), logging.INFO)
|
|
|
|
return ProgressLogger(
|
|
name=name,
|
|
level=log_level,
|
|
log_file=log_file,
|
|
console_output=console_output
|
|
)
|