davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
6.87 kB
"""
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="<div>rendered content</div>",
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