""" Base Format Handler Provides the abstract base class for format handlers and the FormatOutput dataclass that represents extracted content from documents. Usage: from potato.format_handlers.base import BaseFormatHandler, FormatOutput class MyFormatHandler(BaseFormatHandler): format_name = "my_format" supported_extensions = [".myf"] def extract(self, file_path, options=None): # Parse file and return FormatOutput return FormatOutput( text="extracted text", rendered_html="
rendered content
", coordinate_map={...}, metadata={...} ) """ from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Dict, List, Any, Optional from pathlib import Path @dataclass class FormatOutput: """ Represents the extracted content from a document. Attributes: text: Plain text extracted from the document (for annotation) rendered_html: HTML representation for display in the annotation UI coordinate_map: Mapping from character offsets to format-specific coordinates metadata: Additional document metadata (pages, structure, etc.) format_name: Name of the format that produced this output source_path: Path to the original source file """ text: str rendered_html: str coordinate_map: Dict[str, Any] = field(default_factory=dict) metadata: Dict[str, Any] = field(default_factory=dict) format_name: str = "" source_path: str = "" def get_format_coords(self, start: int, end: int) -> Optional[Dict[str, Any]]: """ Get format-specific coordinates for a character range. Args: start: Start character offset (inclusive) end: End character offset (exclusive) Returns: Dictionary with format-specific coordinates, or None if not available """ if not self.coordinate_map: return None # Look up coordinates using the mapping # Implementation varies by format type if "get_coords_for_range" in self.coordinate_map: return self.coordinate_map["get_coords_for_range"](start, end) return None def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for serialization.""" return { "text": self.text, "rendered_html": self.rendered_html, "metadata": self.metadata, "format_name": self.format_name, "source_path": self.source_path, } class BaseFormatHandler(ABC): """ Abstract base class for document format handlers. Subclasses must implement the `extract` method and define class attributes for format identification. Class Attributes: format_name: Unique identifier for this format (e.g., "pdf", "docx") supported_extensions: List of file extensions this handler supports description: Human-readable description of this format handler requires_dependencies: List of optional dependencies needed """ format_name: str = "" supported_extensions: List[str] = [] description: str = "" requires_dependencies: List[str] = [] @abstractmethod def extract(self, file_path: str, options: Optional[Dict[str, Any]] = None) -> FormatOutput: """ Extract annotatable content from a document. Args: file_path: Path to the document file options: Optional configuration for extraction: - extraction_mode: How to extract text (e.g., 'text', 'ocr', 'hybrid') - preserve_layout: Whether to preserve document layout - max_pages: Maximum pages to process (for paged documents) - encoding: Text encoding to use Returns: FormatOutput with extracted text, rendered HTML, and coordinate mappings Raises: FileNotFoundError: If the file doesn't exist ValueError: If the file format is not supported ImportError: If required dependencies are not installed """ pass def can_handle(self, file_path: str) -> bool: """ Check if this handler can process the given file. Args: file_path: Path to the file Returns: True if this handler supports the file's extension """ ext = Path(file_path).suffix.lower() return ext in self.supported_extensions def check_dependencies(self) -> List[str]: """ Check if required dependencies are installed. Returns: List of missing dependency names (empty if all installed) """ missing = [] for dep in self.requires_dependencies: try: __import__(dep.replace("-", "_")) except ImportError: missing.append(dep) return missing def validate_file(self, file_path: str) -> List[str]: """ Validate that a file can be processed. Args: file_path: Path to the file Returns: List of error messages (empty if valid) """ errors = [] path = Path(file_path) if not path.exists(): errors.append(f"File not found: {file_path}") return errors if not path.is_file(): errors.append(f"Not a file: {file_path}") return errors if not self.can_handle(file_path): errors.append( f"Unsupported extension '{path.suffix}'. " f"Supported: {', '.join(self.supported_extensions)}" ) missing_deps = self.check_dependencies() if missing_deps: errors.append( f"Missing dependencies for {self.format_name}: " f"{', '.join(missing_deps)}. " f"Install with: pip install {' '.join(missing_deps)}" ) return errors def get_default_options(self) -> Dict[str, Any]: """ Get default extraction options for this handler. Override in subclasses to provide format-specific defaults. Returns: Dictionary of default option values """ return { "preserve_layout": False, "max_pages": None, "encoding": "utf-8", } def merge_options(self, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """ Merge user options with defaults. Args: options: User-provided options Returns: Merged options dictionary """ merged = self.get_default_options() if options: merged.update(options) return merged