datashare-passport-worker 0.1.2__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,21 @@
1
+ .idea/
2
+ # Python
3
+ *.log
4
+ venv
5
+ *.egg-info
6
+ .eggs
7
+ __pycache__
8
+ *.pytest_cache
9
+ *.pyc
10
+ build
11
+ dist
12
+ test/.env
13
+ .cache
14
+ tmp
15
+ ./*.csv
16
+ .DS_Store
17
+
18
+ # VS code
19
+ .vscode
20
+ # Doc
21
+ site
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: datashare-passport-worker
3
+ Version: 0.1.2
4
+ Summary: Datashare passport detection worker
5
+ Author-email: Clément Doumouro <cdoumouro@icij.org>, Clément Doumouro <clement.doumouro@gmail.com>
6
+ Requires-Python: <3.14,>=3.12
7
+ Requires-Dist: datashare-python~=0.10.0
8
+ Requires-Dist: icij-common[elasticsearch]~=0.8.3
9
+ Provides-Extra: inference-cpu
10
+ Requires-Dist: icij-passport-core[cpu,inference]==0.10.9; extra == 'inference-cpu'
11
+ Provides-Extra: inference-gpu
12
+ Requires-Dist: icij-passport-core[gpu,inference]==0.10.9; extra == 'inference-gpu'
13
+ Provides-Extra: preprocessing
14
+ Requires-Dist: icij-passport-core[preprocessing]==0.10.9; extra == 'preprocessing'
File without changes
@@ -0,0 +1,305 @@
1
+ import logging
2
+ from enum import StrEnum
3
+ from functools import partial
4
+ from pathlib import Path
5
+ from typing import Annotated, cast
6
+
7
+ from aiofile import async_open
8
+ from datashare_python.dependencies import lifespan_es_client, lifespan_worker_config
9
+ from datashare_python.types_ import (
10
+ AsyncProgressRateHandler,
11
+ SyncProgressRateHandler,
12
+ Weight,
13
+ )
14
+ from datashare_python.utils import (
15
+ ActivityWithProgress,
16
+ activity_defn,
17
+ activity_workdir,
18
+ async_enter_cm,
19
+ config_cache_key,
20
+ enter_cm,
21
+ )
22
+
23
+ from .aggregate import aggregate_results_act
24
+ from .config import PassportWorkerConfig
25
+ from .dependencies import (
26
+ lifespan_image_preprocessor_cache,
27
+ lifespan_passport_detector_cache,
28
+ lifespan_pdf_converter_cache,
29
+ )
30
+ from .inference import (
31
+ PassportDetector,
32
+ create_inference_batches_act,
33
+ detect_passports_act,
34
+ )
35
+ from .objects import (
36
+ DocId,
37
+ DocumentSearchQuery,
38
+ ImagePreprocessorConfig,
39
+ PassportDetectionArgs,
40
+ PassportDetectionResponse,
41
+ PDFConverterConfig,
42
+ PreprocessingBatches,
43
+ )
44
+ from .preprocessing import (
45
+ ImagePreprocessor,
46
+ PDFConverter,
47
+ convert_to_pdfs_act,
48
+ preprocess_images_act,
49
+ preprocess_pdfs_act,
50
+ )
51
+ from .search import create_preprocessing_batches_act
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+ _BASE_WEIGHT = 1.0
56
+ _CREATE_PREPROCESSING_BATCHES_WEIGHT = _BASE_WEIGHT * 1
57
+ _PREPROCESS_IMAGES_WEIGHT = _BASE_WEIGHT * 5
58
+ _CONVERT_TO_PDF_WEIGHT = _BASE_WEIGHT * 7
59
+ _PREPROCESS_PDF_WEIGHT = _BASE_WEIGHT * 3
60
+ _CREATE_INFERENCE_BATCH_WEIGHT = _CREATE_PREPROCESSING_BATCHES_WEIGHT * 1
61
+
62
+
63
+ class Activity(StrEnum):
64
+ CREATE_PREPROCESSING_BATCHES = "passport-detection.create-preprocessing-batches"
65
+ PREPROCESS_IMAGES = "passport-detection.preprocess.images"
66
+ CONVERT_TO_PDFS = "passport-detection.convert-to-pdf"
67
+ PREPROCESS_PDFS = "passport-detection.preprocess.pdfs"
68
+ CREATE_INFERENCE_BATCHES = "passport-detection.create-inference-batches"
69
+ DETECT_PASSPORTS = "passport-detection.detect-passports"
70
+ AGGREGATE_RESULTS = "passport-detection.aggregate-results"
71
+
72
+
73
+ class PassportDetectionActivities(ActivityWithProgress):
74
+ @activity_defn(name=Activity.CREATE_PREPROCESSING_BATCHES)
75
+ async def create_preprocessing_batches(
76
+ self,
77
+ docs: list[DocId] | DocumentSearchQuery | None,
78
+ project: str,
79
+ *,
80
+ progress: Annotated[ # noqa: ARG002
81
+ AsyncProgressRateHandler | None,
82
+ Weight(value=_CREATE_PREPROCESSING_BATCHES_WEIGHT),
83
+ ] = None,
84
+ ) -> PreprocessingBatches:
85
+ es_client = lifespan_es_client()
86
+ worker_config = cast(PassportWorkerConfig, lifespan_worker_config())
87
+ workdir = worker_config.paths.workdir
88
+ output_root = activity_workdir(workdir, project, act_context=True)
89
+ output_root.mkdir(parents=True, exist_ok=True)
90
+ target_n_pages_per_batch = worker_config.preprocessing.target_n_pages_per_batch
91
+ return await create_preprocessing_batches_act(
92
+ docs,
93
+ project,
94
+ es_client,
95
+ worker_config.paths,
96
+ target_n_pages_per_batch,
97
+ output_root=output_root,
98
+ )
99
+
100
+ @activity_defn(name=Activity.PREPROCESS_IMAGES)
101
+ def preprocess_images(
102
+ self,
103
+ batch: Path,
104
+ project: str,
105
+ config: ImagePreprocessorConfig,
106
+ *,
107
+ progress: Annotated[
108
+ SyncProgressRateHandler | None, Weight(value=_PREPROCESS_IMAGES_WEIGHT)
109
+ ] = None,
110
+ ) -> tuple[Path, Path]:
111
+ worker_config = cast(PassportWorkerConfig, lifespan_worker_config())
112
+ workdir = worker_config.paths.workdir
113
+ logger.info("loading image preprocessor...")
114
+ cache = lifespan_image_preprocessor_cache()
115
+ image_preprocessor_cache_key = config_cache_key(config)
116
+ image_preprocessor_factory = enter_cm(
117
+ partial(ImagePreprocessor.from_config, config)
118
+ )
119
+ image_preprocessor = cache.get_or_cache_resource(
120
+ image_preprocessor_cache_key, image_preprocessor_factory
121
+ )
122
+ logger.info("loaded image preprocessor !")
123
+ pages_root = activity_workdir(workdir, project, act_context=False)
124
+ pages_root.mkdir(parents=True, exist_ok=True)
125
+ executor = worker_config.to_image_preprocessing_executor()
126
+ chunk_size = worker_config.preprocessing.images.chunk_size
127
+ success, errors = preprocess_images_act(
128
+ batch,
129
+ worker_config.paths,
130
+ output_root=pages_root,
131
+ image_preprocessor=image_preprocessor,
132
+ executor=executor,
133
+ chunk_size=chunk_size,
134
+ event_loop=self._event_loop,
135
+ progress=progress,
136
+ )
137
+ res_root = activity_workdir(workdir, project, act_context=True)
138
+ res_root.mkdir(parents=True, exist_ok=True)
139
+ successes_path = res_root / "pages.jsonl"
140
+ successes_path.write_text("\n".join(p.model_dump_json() for p in success))
141
+ errors_path = res_root / "errors.jsonl"
142
+ errors_path.write_text("\n".join(p.model_dump_json() for p in errors))
143
+ return successes_path, errors_path
144
+
145
+ @activity_defn(name=Activity.CONVERT_TO_PDFS)
146
+ async def convert_to_pdfs(
147
+ self,
148
+ batch: Path,
149
+ project: str,
150
+ config: PDFConverterConfig,
151
+ *,
152
+ progress: Annotated[
153
+ AsyncProgressRateHandler | None, Weight(value=_CONVERT_TO_PDF_WEIGHT)
154
+ ] = None,
155
+ ) -> tuple[Path, Path]:
156
+ worker_config = cast(PassportWorkerConfig, lifespan_worker_config())
157
+ cache = lifespan_pdf_converter_cache()
158
+ pdf_converter_cache_key = config_cache_key(config)
159
+ pdf_converter_factory = async_enter_cm(
160
+ partial(PDFConverter.from_config, config)
161
+ )
162
+ pdf_converter = await cache.async_get_or_cache_resource(
163
+ pdf_converter_cache_key, pdf_converter_factory
164
+ )
165
+ workdir = worker_config.paths.workdir
166
+ pdfs_root = activity_workdir(workdir, project, act_context=False)
167
+ pdfs_root.mkdir(parents=True, exist_ok=True)
168
+ max_concurrency = worker_config.preprocessing.pdfs.max_concurrency
169
+ successes, errors = await convert_to_pdfs_act(
170
+ batch,
171
+ pdf_converter,
172
+ worker_config.paths,
173
+ max_concurrency,
174
+ output_root=pdfs_root,
175
+ progress=progress,
176
+ )
177
+ res_root = activity_workdir(workdir, project, act_context=True)
178
+ res_root.mkdir(parents=True, exist_ok=True)
179
+ pdf_paths = res_root / "pdfs.jsonl"
180
+ async with async_open(pdf_paths, "w") as f:
181
+ await f.write("\n".join(d.model_dump_json() for d in successes))
182
+ errors_path = res_root / "errors.jsonl"
183
+ async with async_open(errors_path, "w") as f:
184
+ await f.write("\n".join(e.model_dump_json() for e in errors))
185
+ return pdf_paths, errors_path
186
+
187
+ @activity_defn(name=Activity.PREPROCESS_PDFS)
188
+ async def preprocess_pdfs(
189
+ self,
190
+ batch: Path,
191
+ project: str,
192
+ *,
193
+ progress: Annotated[
194
+ AsyncProgressRateHandler | None, Weight(value=_PREPROCESS_PDF_WEIGHT)
195
+ ] = None,
196
+ ) -> tuple[Path, Path]:
197
+ worker_config = cast(PassportWorkerConfig, lifespan_worker_config())
198
+ workdir = worker_config.paths.workdir
199
+ output_root = activity_workdir(workdir, project, act_context=False)
200
+ output_root.mkdir(parents=True, exist_ok=True)
201
+ successes, errors = await preprocess_pdfs_act(
202
+ batch, worker_config.paths, output_root=output_root, progress=progress
203
+ )
204
+ res_root = activity_workdir(workdir, project, act_context=True)
205
+ res_root.mkdir(parents=True, exist_ok=True)
206
+ pdf_paths = res_root / "pdfs.jsonl"
207
+ async with async_open(pdf_paths, "w") as f:
208
+ await f.write("\n".join(d.model_dump_json() for d in successes))
209
+ errors_path = res_root / "errors.jsonl"
210
+ async with async_open(errors_path, "w") as f:
211
+ await f.write("\n".join(e.model_dump_json() for e in errors))
212
+ return pdf_paths, errors_path
213
+
214
+ @activity_defn(name=Activity.CREATE_INFERENCE_BATCHES)
215
+ async def create_inference_batches(
216
+ self,
217
+ batches: list[Path],
218
+ project: str,
219
+ *,
220
+ progress: Annotated[ # noqa:ARG002
221
+ AsyncProgressRateHandler | None,
222
+ Weight(value=_CREATE_INFERENCE_BATCH_WEIGHT),
223
+ ] = None,
224
+ ) -> list[Path]:
225
+ worker_config = cast(PassportWorkerConfig, lifespan_worker_config())
226
+ batch_size = worker_config.inference.batch_size
227
+ batches_per_task = worker_config.inference.batches_per_task
228
+ worker_paths = worker_config.paths
229
+ output_root = activity_workdir(worker_paths.workdir, project)
230
+ output_root.mkdir(parents=True, exist_ok=True)
231
+ return await create_inference_batches_act(
232
+ batches,
233
+ worker_paths,
234
+ output_root,
235
+ target_batches_per_task=batches_per_task,
236
+ inference_batch_size=batch_size,
237
+ )
238
+
239
+ @activity_defn(name=Activity.DETECT_PASSPORTS)
240
+ async def detect_passports(
241
+ self,
242
+ batch: Path,
243
+ args: PassportDetectionArgs,
244
+ *,
245
+ progress: Annotated[
246
+ AsyncProgressRateHandler | None,
247
+ Weight(value=_CREATE_INFERENCE_BATCH_WEIGHT),
248
+ ] = None,
249
+ ) -> Path:
250
+ logger.info("loading passport detector...")
251
+ worker_config = cast(PassportWorkerConfig, lifespan_worker_config())
252
+ batch_size = worker_config.inference.batch_size
253
+ cache = lifespan_passport_detector_cache()
254
+ passport_detector_config = args.config.inference.passport_detector
255
+ passport_detector_key = config_cache_key(passport_detector_config)
256
+ passport_detector_factory = enter_cm(
257
+ partial(PassportDetector.from_config, passport_detector_config)
258
+ )
259
+ passport_detector = cache.get_or_cache_resource(
260
+ passport_detector_key, passport_detector_factory
261
+ )
262
+ logger.info("passport detector loaded !")
263
+ workdir = worker_config.paths.workdir
264
+ res_root = activity_workdir(workdir, args.project, act_context=True)
265
+ res_root.mkdir(parents=True, exist_ok=True)
266
+ res = await detect_passports_act(
267
+ batch,
268
+ passport_detector,
269
+ worker_config.paths,
270
+ args,
271
+ batch_size=batch_size,
272
+ progress=progress,
273
+ )
274
+ result_path = res_root / "inference_results.json"
275
+ async with async_open(result_path, "w") as f:
276
+ await f.write(res.model_dump_json())
277
+ return result_path
278
+
279
+ @activity_defn(name=Activity.AGGREGATE_RESULTS)
280
+ async def aggregate_results(
281
+ self,
282
+ error_paths: list[Path],
283
+ result_paths: list[Path],
284
+ *,
285
+ progress: Annotated[ # noqa:ARG002
286
+ AsyncProgressRateHandler | None,
287
+ Weight(value=_CREATE_INFERENCE_BATCH_WEIGHT),
288
+ ] = None,
289
+ ) -> PassportDetectionResponse:
290
+ worker_config = cast(PassportWorkerConfig, lifespan_worker_config())
291
+ res = await aggregate_results_act(
292
+ error_paths, result_paths=result_paths, paths=worker_config.paths
293
+ )
294
+ return res
295
+
296
+
297
+ ACTIVITIES = [
298
+ PassportDetectionActivities.create_preprocessing_batches,
299
+ PassportDetectionActivities.preprocess_images,
300
+ PassportDetectionActivities.convert_to_pdfs,
301
+ PassportDetectionActivities.preprocess_pdfs,
302
+ PassportDetectionActivities.create_inference_batches,
303
+ PassportDetectionActivities.detect_passports,
304
+ PassportDetectionActivities.aggregate_results,
305
+ ]
@@ -0,0 +1,44 @@
1
+ import asyncio
2
+ from collections.abc import AsyncIterable
3
+ from pathlib import Path
4
+ from typing import TypeVar
5
+
6
+ from datashare_python.objects import WorkerPaths
7
+ from datashare_python.utils import async_read_jsonl_as
8
+
9
+ from passport_worker.objects import (
10
+ FileProcessingError,
11
+ PartialDetectionResult,
12
+ PassportDetectionResponse,
13
+ )
14
+
15
+
16
+ async def aggregate_results_act(
17
+ error_paths: list[Path], *, result_paths: list[Path], paths: WorkerPaths
18
+ ) -> PassportDetectionResponse:
19
+ workdir = paths.workdir
20
+ preprocessing_errors = await asyncio.gather(
21
+ *(
22
+ _as_list(async_read_jsonl_as(workdir / p, FileProcessingError))
23
+ for p in error_paths
24
+ )
25
+ )
26
+ preprocessing_errors = sum(preprocessing_errors, [])
27
+ inference_results = await asyncio.gather(
28
+ *(
29
+ _as_list(async_read_jsonl_as(workdir / p, PartialDetectionResult))
30
+ for p in result_paths
31
+ )
32
+ )
33
+ inference_results = sum(inference_results, [])
34
+ res = PassportDetectionResponse.aggregate(
35
+ preprocessing_errors, inference_results=inference_results
36
+ )
37
+ return res
38
+
39
+
40
+ T = TypeVar("T")
41
+
42
+
43
+ async def _as_list[T](iterable: AsyncIterable[T]) -> list[T]:
44
+ return [i async for i in iterable]
@@ -0,0 +1,82 @@
1
+ from concurrent.futures import ProcessPoolExecutor
2
+
3
+ import datashare_python
4
+ from datashare_python.config import (
5
+ LogFormat,
6
+ LoggingConfig,
7
+ ResourceCacheConfig,
8
+ WorkerConfig,
9
+ )
10
+ from datashare_python.objects import DatashareModel, WorkerPaths
11
+ from pydantic import Field
12
+
13
+ _ALL_LOGGERS = [datashare_python.__name__, __name__, "__main__"]
14
+
15
+ _DEFAULT_LOGGERS = {
16
+ datashare_python.__name__: "INFO",
17
+ __name__: "INFO",
18
+ "__main__": "INFO",
19
+ "passport_service": "INFO",
20
+ }
21
+ _DEFAULT_LOGGING_CONFIG = LoggingConfig(
22
+ format=LogFormat.DEFAULT, loggers=_DEFAULT_LOGGERS
23
+ )
24
+
25
+
26
+ class ImagePreprocessingWorkerConfig(DatashareModel):
27
+ n_processes: int | None = None
28
+ chunk_size: int = 5
29
+
30
+ def to_image_preprocessing_executor(self) -> ProcessPoolExecutor:
31
+ return ProcessPoolExecutor(max_workers=self.n_processes)
32
+
33
+
34
+ class PDFConversionWorkerConfig(DatashareModel):
35
+ max_concurrency: int = 10
36
+
37
+
38
+ class PreprocessingWorkerConfig(DatashareModel):
39
+ target_n_pages_per_batch: int = 200
40
+
41
+ images: ImagePreprocessingWorkerConfig = Field(
42
+ default_factory=ImagePreprocessingWorkerConfig
43
+ )
44
+ pdfs: PDFConversionWorkerConfig = Field(default_factory=PDFConversionWorkerConfig)
45
+
46
+ def to_image_preprocessing_executor(self) -> ProcessPoolExecutor:
47
+ return self.images.to_image_preprocessing_executor()
48
+
49
+
50
+ class InferenceWorkerConfig(DatashareModel):
51
+ batch_size: int = 32
52
+ batches_per_task: int = 5
53
+
54
+
55
+ class PreprocessingCacheConfig(DatashareModel):
56
+ pdf: ResourceCacheConfig = Field(default_factory=ResourceCacheConfig)
57
+ images: ResourceCacheConfig = Field(default_factory=ResourceCacheConfig)
58
+
59
+
60
+ class PassportWorkerCacheConfig(DatashareModel):
61
+ preprocessing: PreprocessingCacheConfig = Field(
62
+ default_factory=PreprocessingCacheConfig
63
+ )
64
+ inference: ResourceCacheConfig = Field(default_factory=ResourceCacheConfig)
65
+
66
+
67
+ class PassportWorkerConfig(WorkerConfig):
68
+ logging: LoggingConfig = _DEFAULT_LOGGING_CONFIG
69
+ paths: WorkerPaths
70
+
71
+ cache: PassportWorkerCacheConfig = Field(default_factory=PassportWorkerCacheConfig)
72
+
73
+ preprocessing: PreprocessingWorkerConfig = Field(
74
+ default_factory=PreprocessingWorkerConfig
75
+ )
76
+ inference: InferenceWorkerConfig = Field(default_factory=InferenceWorkerConfig)
77
+
78
+ def to_image_preprocessing_executor(self) -> ProcessPoolExecutor:
79
+ return self.preprocessing.to_image_preprocessing_executor()
80
+
81
+
82
+ WORKER_CONFIG_CLS = PassportWorkerConfig
@@ -0,0 +1,6 @@
1
+ def pil_supported_extensions() -> set[str]:
2
+ from passport_service.core.preprocessing import ( # noqa:PLC0415
3
+ PIL_SUPPORTED_EXTENSIONS,
4
+ )
5
+
6
+ return set(PIL_SUPPORTED_EXTENSIONS)
@@ -0,0 +1,67 @@
1
+ from contextvars import ContextVar
2
+
3
+ from datashare_python.dependencies import set_es_client, set_loggers, set_worker_config
4
+ from datashare_python.exceptions import DependencyInjectionError
5
+ from datashare_python.utils import SharedResources
6
+
7
+ from passport_worker.config import PassportWorkerConfig
8
+
9
+ _IMAGE_PREPROCESSOR_CACHE: ContextVar[SharedResources] = ContextVar(
10
+ "image_preprocessor_cache"
11
+ )
12
+ _PDF_CONVERTER_CACHE: ContextVar[SharedResources] = ContextVar("pdf_converter_cache")
13
+ _PASSPORT_DETECTOR_CACHE: ContextVar[SharedResources] = ContextVar("preprocessor_cache")
14
+
15
+
16
+ def set_image_preprocessor_cache(
17
+ worker_config: PassportWorkerConfig,
18
+ ) -> SharedResources:
19
+ cache = worker_config.cache.preprocessing.images.to_resource_cache()
20
+ _IMAGE_PREPROCESSOR_CACHE.set(cache)
21
+ return cache
22
+
23
+
24
+ def lifespan_image_preprocessor_cache() -> SharedResources:
25
+ try:
26
+ return _IMAGE_PREPROCESSOR_CACHE.get()
27
+ except LookupError as e:
28
+ raise DependencyInjectionError("image preprocessor") from e
29
+
30
+
31
+ def set_pdf_converter_cache(
32
+ worker_config: PassportWorkerConfig,
33
+ ) -> SharedResources:
34
+ cache = worker_config.cache.preprocessing.pdf.to_resource_cache()
35
+ _PDF_CONVERTER_CACHE.set(cache)
36
+ return cache
37
+
38
+
39
+ def lifespan_pdf_converter_cache() -> SharedResources:
40
+ try:
41
+ return _PDF_CONVERTER_CACHE.get()
42
+ except LookupError as e:
43
+ raise DependencyInjectionError("pdf converter cache") from e
44
+
45
+
46
+ def set_passport_detector_cache(worker_config: PassportWorkerConfig) -> SharedResources:
47
+ cache = worker_config.cache.inference.to_resource_cache()
48
+ _PASSPORT_DETECTOR_CACHE.set(cache)
49
+ return cache
50
+
51
+
52
+ def lifespan_passport_detector_cache() -> SharedResources:
53
+ try:
54
+ return _PASSPORT_DETECTOR_CACHE.get()
55
+ except LookupError as e:
56
+ raise DependencyInjectionError("passport detector cache") from e
57
+
58
+
59
+ IO = [set_worker_config, set_loggers, set_es_client, set_pdf_converter_cache]
60
+ PREPROCESSING = [set_worker_config, set_loggers, set_image_preprocessor_cache]
61
+ INFERENCE = [set_worker_config, set_loggers, set_passport_detector_cache]
62
+
63
+ DEPENDENCIES = {
64
+ "passport-detection.io": IO,
65
+ "passport-detection.preprocessing": PREPROCESSING,
66
+ "passport-detection.inference": INFERENCE,
67
+ }