""" Gallery logging wrapper using Python's built-in logging module with tree-like output. """ import logging import sys class Colors: """ANSI color codes for terminal output""" RESET = '\033[0m' BOLD = '\033[1m' # Standard colors RED = '\033[31m' GREEN = '\033[32m' YELLOW = '\033[33m' BLUE = '\033[34m' MAGENTA = '\033[35m' CYAN = '\033[36m' WHITE = '\033[37m' # Bright colors BRIGHT_GREEN = '\033[92m' BRIGHT_YELLOW = '\033[93m' BRIGHT_BLUE = '\033[94m' BRIGHT_CYAN = '\033[96m' class TreeFormatter(logging.Formatter): """Custom formatter that creates clean output with colors""" def __init__(self, use_colors: bool = True): super().__init__() self.use_colors = use_colors and sys.stdout.isatty() def _colorize(self, text: str, color: str) -> str: """Apply color to text if colors are enabled""" if not self.use_colors: return text return f"{color}{text}{Colors.RESET}" def format(self, record): # Extract custom attributes from the record indent = getattr(record, 'indent', 0) # Create simple indentation prefix = " " * indent # Apply colors based on level and content message = record.getMessage() if record.levelname == 'INFO': if 'Processing:' in message: # Main source headers message = self._colorize(message, Colors.BOLD + Colors.MAGENTA) elif 'Generated' in message: message = self._colorize(message, Colors.GREEN) elif 'PDF files' in message: message = self._colorize(message, Colors.BLUE) elif 'Converting' in message: message = self._colorize(message, Colors.CYAN) elif 'Completed' in message and 'sources' in message: message = self._colorize(message, Colors.BOLD + Colors.GREEN) else: message = self._colorize(message, Colors.WHITE) elif record.levelname == 'WARNING': message = self._colorize(f"WARNING: {message}", Colors.YELLOW) elif record.levelname == 'ERROR': message = self._colorize(f"ERROR: {message}", Colors.RED) elif record.levelname == 'DEBUG': message = self._colorize(message, Colors.CYAN) else: message = message return f"{prefix}{message}" class GalleryLogger: """ Simple logger for gallery generation using Python's logging module. Provides convenient methods for common logging patterns with clean output. """ def __init__(self, name: str = "gallery", level: int = logging.INFO, verbose: bool = False, use_colors: bool = True): """ Initialize the gallery logger. Args: name: Logger name level: Logging level (default: INFO) verbose: If True, enables DEBUG level logging use_colors: If True, enables colored output """ self.logger = logging.getLogger(name) self.use_colors = use_colors self.current_source = None self.current_depth = 0 # Set level based on verbose flag if verbose: self.logger.setLevel(logging.DEBUG) else: self.logger.setLevel(level) # Only add handler if logger doesn't have one already if not self.logger.handlers: handler = logging.StreamHandler(sys.stdout) formatter = TreeFormatter(use_colors=use_colors) handler.setFormatter(formatter) self.logger.addHandler(handler) # Prevent propagation to avoid duplicate messages self.logger.propagate = False def _log_with_tree(self, level: int, message: str, indent: int = 0): """Log a message with simple indentation""" # Store indentation info in a way the formatter can access original_makeRecord = self.logger.makeRecord def makeRecord_with_indent(*args, **kwargs): record = original_makeRecord(*args, **kwargs) record.indent = indent return record # Temporarily replace makeRecord to add our custom attributes self.logger.makeRecord = makeRecord_with_indent try: self.logger.log(level, message) finally: # Restore original makeRecord self.logger.makeRecord = original_makeRecord def start_source(self, source_name: str): """Begin processing a new source""" self.current_source = source_name self.current_depth = 0 if self.use_colors: colored_name = f"{Colors.BOLD}{Colors.MAGENTA}{source_name}{Colors.RESET}" self.logger.info(f"\nProcessing: {colored_name}") else: self.logger.info(f"\nProcessing: {source_name}") def info(self, message: str, indent: int = 0): """Log an info message""" self._log_with_tree(logging.INFO, message, indent) def debug(self, message: str, indent: int = 0): """Log a debug message""" self._log_with_tree(logging.DEBUG, message, indent) def warning(self, message: str, indent: int = 0): """Log a warning message""" self._log_with_tree(logging.WARNING, message, indent) def error(self, message: str, indent: int = 0): """Log an error message""" self._log_with_tree(logging.ERROR, message, indent) def found_directory(self, dir_name: str, pdf_count: int, indent: int = 1): """Log discovery of a directory with PDFs""" if pdf_count > 0: self._log_with_tree( logging.INFO, f"Found {dir_name}/ ({pdf_count} PDFs)", indent ) def found_pdf(self, pdf_name: str, indent: int = 2): """Log discovery of a PDF file (debug only)""" self._log_with_tree(logging.DEBUG, f"Found PDF: {pdf_name}", indent) def found_metadata(self, metadata_file: str, field_count: int, indent: int = 1): """Log discovery of a metadata file""" self._log_with_tree( logging.INFO, f"Found metadata: {metadata_file} ({field_count} fields)", indent ) def converted_pdf(self, pdf_name: str, duration: float = None, indent: int = 2): """Log successful PDF conversion""" if duration is not None: self._log_with_tree( logging.INFO, f"Converting {pdf_name} ({duration:.2f}s)", indent ) else: self._log_with_tree(logging.INFO, f"Converting {pdf_name}", indent) def skipped_pdf(self, pdf_name: str, reason: str = "up-to-date", indent: int = 2): """Log skipped PDF conversion""" self._log_with_tree(logging.DEBUG, f"Skipping {pdf_name} ({reason})", indent) def generated_html(self, html_path: str, plot_count: int, indent: int = 1): """Log HTML page generation""" # Extract just the meaningful part of the path if '/gallery/' in html_path: short_path = html_path.split('/gallery/')[-1] else: short_path = html_path if plot_count > 0: self._log_with_tree( logging.INFO, f"Generated {short_path} ({plot_count} plots)", indent ) else: self._log_with_tree( logging.DEBUG, f"Generated {short_path} (index only)", indent ) def assets_updated(self): """Log assets update""" self.logger.info("Assets updated") def summary(self, source_count: int): """Print a summary""" if self.use_colors: message = f"{Colors.BOLD}{Colors.GREEN}Completed processing {source_count} sources{Colors.RESET}" else: message = f"Completed processing {source_count} sources" self.logger.info(f"\n{message}") def create_logger(verbose: bool = False, quiet: bool = False, use_colors: bool = True) -> GalleryLogger: """ Create a configured logger for the gallery application. Args: verbose: Enable debug-level logging quiet: Suppress most output (only errors and warnings) use_colors: Enable colored output Returns: Configured GalleryLogger instance """ if quiet: level = logging.WARNING elif verbose: level = logging.DEBUG else: level = logging.INFO return GalleryLogger(level=level, verbose=verbose, use_colors=use_colors)