extract-core 0.5.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ .idea
2
+
3
+ # Python
4
+ *.log
5
+ venv
6
+ *.egg-info
7
+ .eggs
8
+ __pycache__
9
+ *.pytest_cache
10
+ *.pyc
11
+ build
12
+ dist
13
+ test/.env
14
+ .cache
15
+ tmp
16
+ ./*.csv
17
+
18
+ # VS code
19
+ .vscode
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: extract-core
3
+ Version: 0.5.4
4
+ Summary: Core of extract-python
5
+ Project-URL: Homepage, https://github.com/ICIJ/extract-python
6
+ Project-URL: Repository, https://github.com/ICIJ/extract-python
7
+ Project-URL: Issues, https://github.com/ICIJ/extract-python/issues
8
+ Author-email: Clément Doumouro <cdoumouro@icij.org>
9
+ Requires-Python: <3.14,>=3.11
10
+ Requires-Dist: docling-slim~=2.96
11
+ Requires-Dist: icij-common~=0.8.2
12
+ Requires-Dist: marker-pdf~=1.10
13
+ Requires-Dist: mineru~=3.2
14
+ Requires-Dist: pydantic-extra-types[pycountry]>=2.11.1
15
+ Requires-Dist: pydantic~=2.13
File without changes
@@ -0,0 +1,63 @@
1
+ from typing import Annotated
2
+
3
+ from icij_common.pydantic_utils import make_enum_discriminator, tagged_union
4
+ from pydantic import Discriminator
5
+
6
+ from .configs import BasePipelineConfig, PipelineType
7
+ from .objects import (
8
+ BaseModel,
9
+ ConversionOutput,
10
+ Error,
11
+ InputDoc,
12
+ MarkdownDoc,
13
+ OutputFormat,
14
+ PageIndexes,
15
+ Result,
16
+ Status,
17
+ )
18
+ from .pipeline import Pipeline
19
+
20
+ try:
21
+ from .docling_ import DoclingFormatOption, DoclingPipelineConfig
22
+ except ImportError:
23
+ DoclingPipelineConfig, DoclingFormatOption = None, None
24
+ try:
25
+ from .marker_ import MarkerPipelineConfig
26
+ except ImportError:
27
+ MarkerPipelineConfig = None
28
+
29
+
30
+ try:
31
+ from .miner_u import MinerUBackend, MinerUConfig, MinerUPipelineConfig
32
+ except ImportError:
33
+ MinerUBackend, MinerUPipelineConfig, MinerUConfig = None, None, None
34
+
35
+
36
+ pipeline_type_discriminator = make_enum_discriminator("pipeline", PipelineType)
37
+ PipelineConfig = Annotated[
38
+ tagged_union(
39
+ BasePipelineConfig.__subclasses__(), lambda t: t.pipeline.default.value
40
+ ),
41
+ Discriminator(pipeline_type_discriminator),
42
+ ]
43
+
44
+
45
+ __all__ = [
46
+ "BaseModel",
47
+ "BasePipelineConfig",
48
+ "ConversionOutput",
49
+ "DoclingPipelineConfig",
50
+ "Error",
51
+ "InputDoc",
52
+ "MarkdownDoc",
53
+ "MarkerPipelineConfig",
54
+ "MinerUBackend",
55
+ "MinerUConfig",
56
+ "MinerUPipelineConfig",
57
+ "OutputFormat",
58
+ "PageIndexes",
59
+ "Pipeline",
60
+ "PipelineType",
61
+ "Result",
62
+ "Status",
63
+ ]
@@ -0,0 +1,27 @@
1
+ from abc import ABC, abstractmethod
2
+ from enum import StrEnum
3
+ from typing import ClassVar
4
+
5
+ from icij_common.pydantic_utils import icij_config, merge_configs, no_enum_values_config
6
+ from icij_common.registrable import RegistrableConfig
7
+ from pydantic import Field
8
+
9
+ from .objects import SupportedExt
10
+
11
+
12
+ class PipelineType(StrEnum):
13
+ DOCLING = "docling"
14
+ MARKER = "marker"
15
+ MINER_U = "miner_u"
16
+
17
+
18
+ class BasePipelineConfig(RegistrableConfig, ABC):
19
+ # TODO: move this icij_config() to RegistrableConfig
20
+ model_config = merge_configs(icij_config(), no_enum_values_config())
21
+
22
+ registry_key: ClassVar[str] = Field(frozen=True, default="pipeline")
23
+ pipeline: ClassVar[PipelineType]
24
+
25
+ @classmethod
26
+ @abstractmethod
27
+ def supported_exts(cls) -> set[SupportedExt]: ...
@@ -0,0 +1,268 @@
1
+ import importlib
2
+ from functools import cache
3
+ from typing import Annotated, Any, ClassVar, TypeVar, get_type_hints
4
+
5
+ from docling.datamodel.backend_options import BackendOptions, BaseBackendOptions
6
+ from docling.datamodel.base_models import (
7
+ BaseFormatOption,
8
+ FormatToExtensions,
9
+ InputFormat,
10
+ )
11
+ from docling.datamodel.pipeline_options import (
12
+ BaseLayoutOptions,
13
+ BaseTableStructureOptions,
14
+ EasyOcrOptions,
15
+ LayoutOptions,
16
+ OcrOptions,
17
+ PictureDescriptionBaseOptions,
18
+ PictureDescriptionVlmEngineOptions,
19
+ PipelineOptions,
20
+ TableStructureOptions,
21
+ ThreadedPdfPipelineOptions,
22
+ )
23
+ from icij_common.pydantic_utils import (
24
+ merge_configs,
25
+ tagged_union,
26
+ to_lower_snake_case,
27
+ )
28
+ from pydantic import (
29
+ ConfigDict,
30
+ Discriminator,
31
+ Field,
32
+ TypeAdapter,
33
+ WrapSerializer,
34
+ )
35
+ from pydantic_core.core_schema import SerializerFunctionWrapHandler
36
+
37
+ from .configs import BasePipelineConfig, PipelineType
38
+ from .objects import BaseModel, SupportedExt
39
+ from .utils import all_subclasses
40
+
41
+
42
+ @cache
43
+ def _ext_to_docling_input_format() -> dict:
44
+
45
+ mapping = dict()
46
+ supported = DoclingPipelineConfig.supported_exts()
47
+ for input_f, exts in FormatToExtensions.items():
48
+ for ext in exts:
49
+ try:
50
+ ext = SupportedExt(f".{ext.lower()}") # noqa: PLW2901
51
+ except ValueError:
52
+ continue
53
+ if ext in supported:
54
+ mapping[ext] = input_f
55
+ return mapping
56
+
57
+
58
+ def _validate_pipeline_opts(v: PipelineOptions) -> PipelineOptions:
59
+ generate_picture_images = getattr(v, "generate_picture_images", None)
60
+ if generate_picture_images is False:
61
+ msg = "generate_picture_images should be set to True"
62
+ raise ValueError(msg)
63
+ return v
64
+
65
+
66
+ T = TypeVar("T")
67
+
68
+
69
+ def _find_subcls(cls: type[T], name: str) -> type[T]:
70
+ # Check if the class available
71
+ for c in all_subclasses(cls):
72
+ if c.__name__ == name:
73
+ return c
74
+ # Then apply ad-hoc search
75
+ if "pipeline" in cls.__name__.lower():
76
+ module_name = f"docling.pipeline.{to_lower_snake_case(name)}"
77
+ try:
78
+ module = importlib.import_module(module_name)
79
+ return getattr(module, name)
80
+ except (ModuleNotFoundError, AttributeError):
81
+ pass
82
+ raise ValueError(f"unknown {cls.__name__} subclass {name}")
83
+
84
+
85
+ def _find_init_arg_type(cls: type[Any], arg: str) -> type[BaseModel]:
86
+ hints = get_type_hints(cls.__init__)
87
+ return hints[arg]
88
+
89
+
90
+ def _resolve_pipeline_cls(v: str) -> Any:
91
+ if isinstance(v, str):
92
+ from docling.pipeline.base_pipeline import BasePipeline # noqa: PLC0415
93
+
94
+ return _find_subcls(BasePipeline, v)
95
+ return v
96
+
97
+
98
+ def _ser_as_str(v: type) -> str:
99
+ return v.__name__
100
+
101
+
102
+ def _ser_with_backend_option_kind(
103
+ v: Any, handler: SerializerFunctionWrapHandler
104
+ ) -> Any:
105
+ serialized = handler(v)
106
+ if isinstance(v, BaseBackendOptions):
107
+ kind = getattr(v, "kind", None)
108
+ if kind is not None:
109
+ serialized["kind"] = kind
110
+ return serialized
111
+
112
+
113
+ def _resolve_backend(v: Any) -> Any:
114
+ from docling.backend.abstract_backend import ( # noqa: PLC0415
115
+ AbstractDocumentBackend,
116
+ )
117
+
118
+ if isinstance(v, str):
119
+ return _find_subcls(AbstractDocumentBackend, v)
120
+ return v
121
+
122
+
123
+ @cache
124
+ def _picture_descr_opts_type_adapter() -> TypeAdapter:
125
+ _PictureDescriptionModel = Annotated[ # noqa: N806
126
+ tagged_union(
127
+ PictureDescriptionBaseOptions.__subclasses__(), tag_getter=lambda x: x.kind
128
+ ),
129
+ Discriminator(lambda x: x["kind"]),
130
+ ]
131
+ return TypeAdapter(_PictureDescriptionModel)
132
+
133
+
134
+ @cache
135
+ def _ocr_opts_type_adapter() -> TypeAdapter:
136
+ _OcrOptions = Annotated[ # noqa: N806
137
+ tagged_union(OcrOptions.__subclasses__(), tag_getter=lambda x: x.kind),
138
+ Discriminator(lambda x: x.pop("kind")),
139
+ ]
140
+ return TypeAdapter(_OcrOptions)
141
+
142
+
143
+ @cache
144
+ def _layout_opts_type_adapter() -> TypeAdapter:
145
+ _LayoutOptions = Annotated[ # noqa: N806
146
+ tagged_union(BaseLayoutOptions.__subclasses__(), tag_getter=lambda x: x.kind),
147
+ Discriminator(lambda x: x["kind"]),
148
+ ]
149
+ return TypeAdapter(_LayoutOptions)
150
+
151
+
152
+ @cache
153
+ def _table_structure_opts_type_adapter() -> TypeAdapter:
154
+ _TableStructureOptions = Annotated[ # noqa: N806
155
+ tagged_union(
156
+ BaseTableStructureOptions.__subclasses__(), tag_getter=lambda x: x.kind
157
+ ),
158
+ Discriminator(lambda x: x["kind"]),
159
+ ]
160
+ return TypeAdapter(_TableStructureOptions)
161
+
162
+
163
+ def _resolve_pipeline_options(
164
+ pipeline_options: dict[str, Any] | None | PipelineOptions, pipeline_cls: type
165
+ ) -> PipelineOptions:
166
+ option_cls = _find_init_arg_type(pipeline_cls, "pipeline_options")
167
+ picture_descr_opts = pipeline_options.get("picture_description_options")
168
+ if picture_descr_opts is not None:
169
+ if "kind" not in picture_descr_opts:
170
+ msg = f"missing picture description options kind: {picture_descr_opts}"
171
+ raise ValueError(msg)
172
+
173
+ picture_descr_opts = _picture_descr_opts_type_adapter().validate_python(
174
+ picture_descr_opts
175
+ )
176
+ pipeline_options["picture_description_options"] = picture_descr_opts
177
+ ocr_opts = pipeline_options.get("ocr_options")
178
+ if ocr_opts is not None:
179
+ if "kind" not in ocr_opts:
180
+ msg = f"missing ocr options kind: {ocr_opts}"
181
+ raise ValueError(msg)
182
+ ocr_opts = _ocr_opts_type_adapter().validate_python(ocr_opts)
183
+ pipeline_options["ocr_options"] = ocr_opts
184
+ layout_opts = pipeline_options.get("layout_options")
185
+ if layout_opts is not None:
186
+ if "kind" not in layout_opts:
187
+ msg = f"missing layout options kind: {layout_opts}"
188
+ raise ValueError(msg)
189
+ layout_opts = _layout_opts_type_adapter().validate_python(layout_opts)
190
+ pipeline_options["layout_options"] = layout_opts
191
+ table_structure_opts = pipeline_options.get("table_structure_options")
192
+ if table_structure_opts is not None:
193
+ if "kind" not in table_structure_opts:
194
+ msg = f"missing table structure options kind: {table_structure_opts}"
195
+ raise ValueError(msg)
196
+ table_structure_opts = _table_structure_opts_type_adapter().validate_python(
197
+ table_structure_opts
198
+ )
199
+ pipeline_options["table_structure_options"] = table_structure_opts
200
+ pipeline_options = option_cls.model_validate(pipeline_options)
201
+ return pipeline_options
202
+
203
+
204
+ # Mimics the docling FormatOption but only with lightweight types,
205
+ # the heavy convertion is done at runtime
206
+ class DoclingFormatOption(BaseFormatOption):
207
+ model_config = merge_configs(
208
+ BaseModel.model_config, ConfigDict(polymorphic_serialization=True)
209
+ )
210
+ backend: str
211
+ backend_options: Annotated[
212
+ BackendOptions | None, WrapSerializer(_ser_with_backend_option_kind)
213
+ ] = None
214
+ pipeline_cls: str
215
+ pipeline_options: dict[str, Any] | None = None
216
+
217
+ def to_docling(self) -> BaseFormatOption: # noqa: ANN201
218
+ from docling.document_converter import FormatOption # noqa: PLC0415
219
+
220
+ pipeline_cls = _resolve_pipeline_cls(self.pipeline_cls)
221
+ pipeline_opts = _resolve_pipeline_options(self.pipeline_options, pipeline_cls)
222
+ pipeline_opts = _validate_pipeline_opts(pipeline_opts)
223
+ return FormatOption(
224
+ pipeline_cls=pipeline_cls,
225
+ pipeline_options=pipeline_opts,
226
+ backend=_resolve_backend(self.backend),
227
+ backend_options=self.backend_options,
228
+ )
229
+
230
+
231
+ @cache
232
+ def _default_format_opts() -> dict[InputFormat, DoclingFormatOption]:
233
+ pipeline_opts = ThreadedPdfPipelineOptions(
234
+ ocr_options=EasyOcrOptions(), generate_picture_images=True
235
+ ).model_dump(polymorphic_serialization=True)
236
+ pipeline_opts["picture_description_options"]["kind"] = (
237
+ PictureDescriptionVlmEngineOptions.kind
238
+ )
239
+ pipeline_opts["ocr_options"]["kind"] = EasyOcrOptions.kind
240
+ pipeline_opts["layout_options"]["kind"] = LayoutOptions.kind
241
+ pipeline_opts["table_structure_options"]["kind"] = TableStructureOptions.kind
242
+ return {
243
+ InputFormat.PDF: DoclingFormatOption(
244
+ pipeline_cls="StandardPdfPipeline",
245
+ backend="DoclingParseDocumentBackend",
246
+ pipeline_options=pipeline_opts,
247
+ ),
248
+ }
249
+
250
+
251
+ class DoclingPipelineConfig(BasePipelineConfig):
252
+ pipeline: ClassVar[PipelineType] = Field(frozen=True, default=PipelineType.DOCLING)
253
+
254
+ format_options: dict[InputFormat, DoclingFormatOption] = Field(
255
+ default_factory=_default_format_opts
256
+ )
257
+
258
+ @classmethod
259
+ @cache
260
+ def supported_exts(cls) -> set[SupportedExt]:
261
+ unsupported = {InputFormat.AUDIO, InputFormat.METS_GBS, InputFormat.VTT}
262
+ supported = set()
263
+ for f in InputFormat:
264
+ if f in unsupported:
265
+ continue
266
+ for ext in FormatToExtensions[f]:
267
+ supported.add(SupportedExt(f".{ext.lower()}"))
268
+ return supported
@@ -0,0 +1,40 @@
1
+ from functools import cache
2
+ from typing import Any, ClassVar
3
+
4
+ from pydantic import Field
5
+
6
+ from .configs import BasePipelineConfig, PipelineType
7
+ from .objects import SupportedExt
8
+
9
+
10
+ class MarkerPipelineConfig(BasePipelineConfig):
11
+ pipeline: ClassVar[PipelineType] = Field(frozen=True, default=PipelineType.MARKER)
12
+
13
+ config: dict[str, Any] = dict()
14
+
15
+ @classmethod
16
+ @cache
17
+ def supported_exts(cls) -> set[SupportedExt]:
18
+ # Subset of https://documentation.datalab.to/docs/common/supportedfiletypes
19
+ return {
20
+ SupportedExt.PDF,
21
+ SupportedExt.XLS,
22
+ SupportedExt.XLSX,
23
+ SupportedExt.XLSM,
24
+ SupportedExt.CSV,
25
+ SupportedExt.ODS,
26
+ SupportedExt.DOC,
27
+ SupportedExt.DOCX,
28
+ SupportedExt.ODT,
29
+ SupportedExt.PPT,
30
+ SupportedExt.PPTX,
31
+ SupportedExt.ODP,
32
+ SupportedExt.HTLM,
33
+ SupportedExt.EPUB,
34
+ SupportedExt.PNG,
35
+ SupportedExt.JPG,
36
+ SupportedExt.JPEG,
37
+ SupportedExt.WEBP,
38
+ SupportedExt.GIF,
39
+ SupportedExt.TIFF,
40
+ }
@@ -0,0 +1,76 @@
1
+ from collections.abc import Callable
2
+ from copy import copy
3
+ from enum import StrEnum
4
+ from functools import cache
5
+ from typing import Any, ClassVar
6
+
7
+ from pydantic import Field
8
+ from pydantic_extra_types.language_code import LanguageAlpha2
9
+
10
+ from .configs import BasePipelineConfig, PipelineType
11
+ from .objects import BaseModel, SupportedExt
12
+
13
+ _MINER_U_CONVERSION_ERRORS = tuple()
14
+ MDMakeFunction = Callable[[list, str, str], str | None]
15
+
16
+
17
+ class MinerUBackend(StrEnum):
18
+ PIPELINE = "pipeline"
19
+ VLM = "vlm"
20
+
21
+
22
+ class MinerUConfig(BaseModel):
23
+ backend: MinerUBackend = MinerUBackend.PIPELINE
24
+ enable_formula_extraction: bool = True
25
+ enable_table_extraction: bool = True
26
+ # TODO: use enum or literal here
27
+ parse_method: str = "auto"
28
+
29
+ def as_parse_kwargs(self) -> dict[str, Any]:
30
+ kwargs = copy(self._get_default_kwargs())
31
+ kwargs["backend"] = self.backend
32
+ kwargs["parse_method"] = self.parse_method
33
+ kwargs["formula_enable"] = self.enable_formula_extraction
34
+ kwargs["table_enable"] = self.enable_table_extraction
35
+ return kwargs
36
+
37
+ @classmethod
38
+ @cache
39
+ def _get_default_kwargs(cls) -> dict[str, Any]:
40
+ from mineru.utils.enum_class import MakeMode # noqa: PLC0415
41
+
42
+ return {
43
+ "server_url": None,
44
+ # We don't dump md directly we process, we dump the middle json in order
45
+ # to be able to get page indexes
46
+ "parse_method": "auto",
47
+ "dump_md": False,
48
+ "dump_middle_json": True,
49
+ "f_draw_layout_bbox": False,
50
+ "f_draw_span_bbox": False,
51
+ "f_dump_model_output": False, # might be useful for debug though
52
+ "f_dump_orig_pdf": False,
53
+ "f_dump_content_list": False, # might be useful for debug though
54
+ "start_page_id": 0,
55
+ "f_make_md_mode": MakeMode.MM_MD,
56
+ "image_analysis": True,
57
+ "end_page_id": None,
58
+ "client_side_output_generation": False,
59
+ }
60
+
61
+
62
+ class MinerUPipelineConfig(BasePipelineConfig): # noqa: F821
63
+ pipeline: ClassVar[PipelineType] = Field(frozen=True, default=PipelineType.MINER_U)
64
+
65
+ config: MinerUConfig = Field(frozen=True, default=MinerUConfig())
66
+ language: LanguageAlpha2 = Field(frozen=True, default="en")
67
+
68
+ @classmethod
69
+ @cache
70
+ def supported_exts(cls) -> set[SupportedExt]:
71
+ return {
72
+ SupportedExt.PDF,
73
+ SupportedExt.DOCX,
74
+ SupportedExt.PPTX,
75
+ SupportedExt.XLSX,
76
+ }