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

1"""The engine seam: the abstraction the rest of the codebase depends on. 

2 

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. 

9 

10Once docling is the only engine, this Protocol can be collapsed — a 

11one-implementation interface is just ceremony. 

12""" 

13 

14from __future__ import annotations 

15 

16from enum import StrEnum 

17from pathlib import Path 

18from typing import TYPE_CHECKING, Protocol, runtime_checkable 

19 

20if TYPE_CHECKING: 

21 from docling.datamodel.base_models import DocumentStream 

22 

23 from .config import ConversionConfig 

24 from .outcome import ConversionOutcome 

25 

26 

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 

30 

31 

32@runtime_checkable 

33class ConversionEngine(Protocol): 

34 """Source -> ConversionOutcome. The codebase depends on this, not on docling.""" 

35 

36 def convert(self, source: ConversionSource) -> ConversionOutcome: ... 

37 

38 

39class EngineKind(StrEnum): 

40 MARKITDOWN = "markitdown" 

41 DOCLING = "docling" 

42 

43 

44def build_engine(kind: EngineKind, config: ConversionConfig) -> ConversionEngine: 

45 """Composition root: construct the docling conversion engine. 

46 

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 

54 

55 return DoclingConverter(config) 

56 raise ValueError(f"build_engine only constructs the docling engine, got: {kind!r}")