Coverage for src/qdrant_loader/core/conversion/engine.py: 93%
15 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-20 10:15 +0000
1"""The engine seam: the abstraction the rest of the codebase depends on.
3A :class:`ConversionEngine` is anything that turns a source into a
4:class:`~.outcome.ConversionOutcome`. :func:`build_engine` constructs the docling
5engine behind this Protocol (the markitdown path is handled directly by
6:class:`~.service.ConversionService`, which routes to ``FileConverter`` rather than
7through an engine adapter). Keeping callers on the Protocol (not on docling) is what
8makes the swap reversible.
10Once docling is the only engine, this Protocol can be collapsed — a
11one-implementation interface is just ceremony.
12"""
14from __future__ import annotations
16from enum import StrEnum
17from pathlib import Path
18from typing import TYPE_CHECKING, Protocol, runtime_checkable
20if TYPE_CHECKING:
21 from docling.datamodel.base_models import DocumentStream
23 from .config import ConversionConfig
24 from .outcome import ConversionOutcome
27# Anything docling's DocumentConverter.convert accepts. PEP 695 alias: the body is
28# evaluated lazily, so the docling type need not exist at runtime.
29type ConversionSource = Path | str | DocumentStream
32@runtime_checkable
33class ConversionEngine(Protocol):
34 """Source -> ConversionOutcome. The codebase depends on this, not on docling."""
36 def convert(self, source: ConversionSource) -> ConversionOutcome: ...
39class EngineKind(StrEnum):
40 MARKITDOWN = "markitdown"
41 DOCLING = "docling"
44def build_engine(kind: EngineKind, config: ConversionConfig) -> ConversionEngine:
45 """Composition root: construct the docling conversion engine.
47 Only ``EngineKind.DOCLING`` is built through an engine adapter; the markitdown
48 path is routed directly to ``FileConverter`` by ``ConversionService`` and never
49 reaches here. docling is imported lazily so the markitdown path never requires it
50 (and the import also breaks the engine <-> docling_engine cycle).
51 """
52 if kind is EngineKind.DOCLING:
53 from .docling_engine import DoclingConverter
55 return DoclingConverter(config)
56 raise ValueError(f"build_engine only constructs the docling engine, got: {kind!r}")