""" Extension → extractor dispatch. Maps a file extension to an extractor "family" module under doocus/extractors/. Families are imported lazily so an app that only handles text files doesn't need the office/pdf/image dependencies installed. Unknown extensions (and families whose optional deps are missing) are reported as "unhandled" — never fatal. """ import importlib import logging from pathlib import Path from typing import Dict, Optional from .extractors.base import ExtractionResult logger = logging.getLogger(__name__) # extension (no dot, lowercase) → family module name under doocus.extractors EXT_FAMILY: Dict[str, str] = { "md": "text", "markdown": "text", "txt": "text", "yaml": "text", "yml": "text", "json": "text", "csv": "tabular", "docx": "office", "pptx": "office", "xlsx": "office", "pdf": "pdf", "html": "web", "htm": "web", "jpg": "image", "jpeg": "image", "png": "image", "mp4": "media", } def ext_of(source: Path) -> str: return source.suffix.lower().lstrip(".") def family_for(source: Path) -> Optional[str]: return EXT_FAMILY.get(ext_of(source)) def extract(source: Path, options: dict) -> ExtractionResult: """Dispatch to the right family extractor. Returns an ExtractionResult in all cases — unhandled types and import/extract failures come back as an empty result carrying a warning, so a batch keeps going. """ family = family_for(source) if family is None: return ExtractionResult( extractor="unhandled", warnings=[f"no extractor for extension '.{ext_of(source)}'"], ) try: module = importlib.import_module(f".extractors.{family}", package=__package__) except ImportError as e: return ExtractionResult( extractor=f"{family}/unavailable", warnings=[f"extractor '{family}' unavailable (missing dependency): {e}"], ) try: return module.extract(source, options) except Exception as e: # isolate: one bad file must not abort the batch logger.warning("extractor '%s' failed on %s: %s", family, source.name, e) return ExtractionResult( extractor=f"{family}/error", warnings=[f"extraction failed: {e}"], )