datashare-python 0.11.4__tar.gz → 0.12.1__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.
- {datashare_python-0.11.4 → datashare_python-0.12.1}/PKG-INFO +1 -1
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/config.py +16 -3
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/mimetypes_.py +4 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/objects.py +368 -109
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/utils.py +136 -39
- datashare_python-0.12.1/datashare_python/worker-template.tar.gz +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/pyproject.toml +1 -1
- datashare_python-0.11.4/datashare_python/worker-template.tar.gz +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/.gitignore +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/README.md +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/.gitignore +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/__init__.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/__main__.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/cli/__init__.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/cli/project.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/cli/task.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/cli/utils.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/cli/worker.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/conftest.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/constants.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/dependencies.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/discovery.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/exceptions.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/interceptors.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/logging_.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/task_client.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/template.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/types_.py +0 -0
- {datashare_python-0.11.4 → datashare_python-0.12.1}/datashare_python/worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: datashare-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.1
|
|
4
4
|
Summary: Manage Python tasks and local resources in Datashare
|
|
5
5
|
Project-URL: Homepage, https://icij.github.io/datashare-python/
|
|
6
6
|
Project-URL: Documentation, https://icij.github.io/datashare-python/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
1
2
|
from enum import StrEnum
|
|
2
|
-
from typing import Literal
|
|
3
|
+
from typing import TYPE_CHECKING, Literal
|
|
3
4
|
|
|
4
5
|
from icij_common.es import ESClient
|
|
5
6
|
from icij_common.pydantic_utils import ICIJSettings
|
|
@@ -12,7 +13,9 @@ import datashare_python
|
|
|
12
13
|
from .objects import BaseModel, WorkerPaths
|
|
13
14
|
from .task_client import DatashareTaskClient
|
|
14
15
|
from .types_ import TemporalClient
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .utils import SharedResources
|
|
16
19
|
|
|
17
20
|
_ALL_LOGGERS = [datashare_python.__name__]
|
|
18
21
|
|
|
@@ -63,6 +66,8 @@ class TemporalClientConfig(BaseModel):
|
|
|
63
66
|
_client: TemporalClient | None = PrivateAttr(default=None)
|
|
64
67
|
|
|
65
68
|
async def to_client(self) -> TemporalClient:
|
|
69
|
+
from .utils import PYDANTIC_DATA_CONVERTER # noqa: PLC0415
|
|
70
|
+
|
|
66
71
|
if self._client is None:
|
|
67
72
|
runtime = None
|
|
68
73
|
if self.prometheus_host is not None:
|
|
@@ -70,6 +75,7 @@ class TemporalClientConfig(BaseModel):
|
|
|
70
75
|
metrics=PrometheusConfig(bind_address="0.0.0.0:9000")
|
|
71
76
|
)
|
|
72
77
|
runtime = Runtime(telemetry=telemetry_config)
|
|
78
|
+
|
|
73
79
|
self._client = await TemporalClient.connect(
|
|
74
80
|
target_host=self.host,
|
|
75
81
|
namespace=self.namespace,
|
|
@@ -97,7 +103,9 @@ class ResourceCacheConfig(BaseModel):
|
|
|
97
103
|
size: int = 1
|
|
98
104
|
exit_context_managers: bool = True
|
|
99
105
|
|
|
100
|
-
def to_resource_cache(self) -> SharedResources:
|
|
106
|
+
def to_resource_cache(self) -> "SharedResources":
|
|
107
|
+
from .utils import SharedResources, close_cm_callback # noqa: PLC0415
|
|
108
|
+
|
|
101
109
|
eviction_callback = None
|
|
102
110
|
if self.exit_context_managers:
|
|
103
111
|
eviction_callback = close_cm_callback
|
|
@@ -134,3 +142,8 @@ class WorkerConfig(ICIJSettings, BaseModel):
|
|
|
134
142
|
|
|
135
143
|
async def to_temporal_client(self) -> TemporalClient:
|
|
136
144
|
return await self.temporal.to_client()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ActivityTimeouts(BaseModel):
|
|
148
|
+
start_to_close: timedelta | None = None
|
|
149
|
+
heartbeat: timedelta = timedelta(minutes=1)
|
|
@@ -75,6 +75,7 @@ def types_map() -> dict[str, set[str]]:
|
|
|
75
75
|
".bmp": {"image/bmp"},
|
|
76
76
|
".book": {"application/vnd.framemaker"},
|
|
77
77
|
".box": {"application/vnd.previewsystems.box"},
|
|
78
|
+
".boxnote": {"application/octet-stream"},
|
|
78
79
|
".boz": {"application/x-bzip2"},
|
|
79
80
|
".bpk": {"application/octet-stream"},
|
|
80
81
|
".bufr": {"application/x-bufr"},
|
|
@@ -172,6 +173,7 @@ def types_map() -> dict[str, set[str]]:
|
|
|
172
173
|
".davmount": {"application/davmount+xml"},
|
|
173
174
|
".dbf": {"application/dbf", "application/dbase"},
|
|
174
175
|
".dbk": {"application/docbook+xml"},
|
|
176
|
+
".dclx": {"application/octet-stream"},
|
|
175
177
|
".dcr": {"application/x-director"},
|
|
176
178
|
".dcurl": {"text/vnd.curl.dcurl"},
|
|
177
179
|
".dcx": {"image/vnd.zbrush.dcx", "image/x-dcx"},
|
|
@@ -226,6 +228,8 @@ def types_map() -> dict[str, set[str]]:
|
|
|
226
228
|
".dxf": {"image/vnd.dxf"},
|
|
227
229
|
".dxp": {"application/vnd.spotfire.dxp"},
|
|
228
230
|
".dxr": {"application/x-director"},
|
|
231
|
+
".ebc": {"application/octet-stream"},
|
|
232
|
+
".ebcdic": {"application/octet-stream"},
|
|
229
233
|
".ecelp4800": {"audio/vnd.nuera.ecelp4800"},
|
|
230
234
|
".ecelp7470": {"audio/vnd.nuera.ecelp7470"},
|
|
231
235
|
".ecelp9600": {"audio/vnd.nuera.ecelp9600"},
|
|
@@ -2,14 +2,15 @@ import hashlib
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
-
|
|
6
|
-
from
|
|
5
|
+
import traceback
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from datetime import UTC, datetime
|
|
9
10
|
from enum import StrEnum, unique
|
|
10
11
|
from io import BytesIO
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar, cast
|
|
13
|
+
from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar, cast, final
|
|
13
14
|
|
|
14
15
|
import langcodes
|
|
15
16
|
from icij_common.registrable import Registrable
|
|
@@ -50,6 +51,7 @@ from pydantic import (
|
|
|
50
51
|
Discriminator,
|
|
51
52
|
Field,
|
|
52
53
|
GetCoreSchemaHandler,
|
|
54
|
+
Tag,
|
|
53
55
|
TypeAdapter,
|
|
54
56
|
model_validator,
|
|
55
57
|
)
|
|
@@ -59,8 +61,9 @@ from pydantic.main import IncEx
|
|
|
59
61
|
logger = logging.getLogger(__name__)
|
|
60
62
|
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
Routing = str
|
|
65
|
+
DocID = str
|
|
66
|
+
DocRoute = tuple[DocID, Routing]
|
|
64
67
|
|
|
65
68
|
|
|
66
69
|
class BaseModel(_BaseModel):
|
|
@@ -118,68 +121,34 @@ class DatashareLanguage(str):
|
|
|
118
121
|
return self.as_language_name.alpha3
|
|
119
122
|
|
|
120
123
|
|
|
121
|
-
class WorkerPaths(BaseModel):
|
|
122
|
-
filesystem: Path
|
|
123
|
-
artifacts: Path
|
|
124
|
-
workdir: Path
|
|
125
|
-
|
|
126
|
-
|
|
127
124
|
@unique
|
|
128
|
-
class
|
|
125
|
+
class FileLocation(StrEnum):
|
|
129
126
|
FILESYSTEM = "filesystem"
|
|
130
127
|
ARTIFACTS = "artifacts"
|
|
131
128
|
WORKDIR = "workdir"
|
|
132
129
|
|
|
133
130
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
)
|
|
139
|
-
return value
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
class ProcessedFile(BaseModel):
|
|
143
|
-
id: str
|
|
144
|
-
path: Annotated[Path, AfterValidator(_is_relative)]
|
|
145
|
-
project: str
|
|
146
|
-
location: DocumentLocation
|
|
147
|
-
resource_name: str
|
|
148
|
-
n_pages: int
|
|
149
|
-
parent: "ProcessedFile | None" = None
|
|
150
|
-
|
|
151
|
-
@classmethod
|
|
152
|
-
def from_doc(cls, doc: "Document") -> Self:
|
|
153
|
-
return doc.to_processed_file()
|
|
154
|
-
|
|
155
|
-
def child(self, path: Path, paths: WorkerPaths) -> Self:
|
|
156
|
-
return ProcessedFile(
|
|
157
|
-
id=self.id,
|
|
158
|
-
path=path.relative_to(paths.workdir),
|
|
159
|
-
project=self.project,
|
|
160
|
-
location=DocumentLocation.WORKDIR,
|
|
161
|
-
resource_name=path.name,
|
|
162
|
-
n_pages=self.n_pages,
|
|
163
|
-
parent=self,
|
|
164
|
-
)
|
|
131
|
+
class WorkerPaths(BaseModel):
|
|
132
|
+
filesystem: Path
|
|
133
|
+
artifacts: Path
|
|
134
|
+
workdir: Path
|
|
165
135
|
|
|
166
|
-
def locate(self,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
case
|
|
171
|
-
return
|
|
172
|
-
case
|
|
173
|
-
|
|
174
|
-
return paths.artifacts / artifacts_dir(self.id, project=project) / "raw"
|
|
175
|
-
case DocumentLocation.WORKDIR:
|
|
176
|
-
return paths.workdir / self.path
|
|
136
|
+
def locate(self, path: Path, location: FileLocation) -> Path:
|
|
137
|
+
match location:
|
|
138
|
+
case FileLocation.FILESYSTEM:
|
|
139
|
+
return self.filesystem / path
|
|
140
|
+
case FileLocation.ARTIFACTS:
|
|
141
|
+
return self.artifacts / path
|
|
142
|
+
case FileLocation.WORKDIR:
|
|
143
|
+
return self.workdir / path
|
|
177
144
|
case _:
|
|
178
|
-
raise ValueError(f"invalid location: {
|
|
145
|
+
raise ValueError(f"invalid location: {path}")
|
|
179
146
|
|
|
180
147
|
|
|
181
|
-
class
|
|
182
|
-
|
|
148
|
+
class FromParent[P](ABC):
|
|
149
|
+
@classmethod
|
|
150
|
+
@abstractmethod
|
|
151
|
+
def from_parent(cls, parent: P, *args, **kwargs) -> Self: ...
|
|
183
152
|
|
|
184
153
|
|
|
185
154
|
class IETFLanguage(str):
|
|
@@ -213,12 +182,9 @@ class Translation(BaseModel): # No camelcase here we don't know why
|
|
|
213
182
|
content: Annotated[str, BeforeValidator(_from_sentences)]
|
|
214
183
|
|
|
215
184
|
|
|
216
|
-
Routing = str
|
|
217
|
-
DocID = str
|
|
218
|
-
DocRoute = tuple[DocID, Routing]
|
|
219
|
-
|
|
220
|
-
|
|
221
185
|
class Document(DatashareModel):
|
|
186
|
+
"""ES datashare document"""
|
|
187
|
+
|
|
222
188
|
id: str
|
|
223
189
|
language: DatashareLanguage
|
|
224
190
|
index: str | None = None
|
|
@@ -285,7 +251,7 @@ class Document(DatashareModel):
|
|
|
285
251
|
id=es_doc[ID_],
|
|
286
252
|
index=es_doc.get(INDEX_),
|
|
287
253
|
content=sources.get(DOC_CONTENT),
|
|
288
|
-
content_translated=sources.get(DOC_CONTENT_TRANSLATED
|
|
254
|
+
content_translated=sources.get(DOC_CONTENT_TRANSLATED),
|
|
289
255
|
content_text_length=sources.get("content_text_length"),
|
|
290
256
|
language=DatashareLanguage(sources[DOC_LANGUAGE]),
|
|
291
257
|
root_document=sources.get(DOC_ROOT_ID),
|
|
@@ -295,58 +261,57 @@ class Document(DatashareModel):
|
|
|
295
261
|
metadata=sources.get(DOC_METADATA),
|
|
296
262
|
)
|
|
297
263
|
|
|
298
|
-
|
|
299
|
-
|
|
264
|
+
@property
|
|
265
|
+
def project(self) -> str:
|
|
266
|
+
if self.index is None:
|
|
267
|
+
raise ValueError("missing index")
|
|
268
|
+
return self.index
|
|
300
269
|
|
|
270
|
+
@property
|
|
271
|
+
def doc_id(self) -> str:
|
|
272
|
+
return self.id
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def route(self) -> DocRoute:
|
|
276
|
+
if self.root_document:
|
|
277
|
+
return self.id, self.id
|
|
278
|
+
return self.id, self.root_document
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def resource_name(self) -> str:
|
|
301
282
|
if self.metadata is None:
|
|
302
|
-
raise ValueError(
|
|
303
|
-
"can't compute filesystem path for document withtout metadata"
|
|
304
|
-
)
|
|
283
|
+
raise ValueError("missing metadata")
|
|
305
284
|
resource_name = self.metadata.get(TIKA_METADATA_RESOURCENAME)
|
|
306
285
|
if resource_name is None:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
resource_name = cast(str, resource_name)
|
|
318
|
-
if self.is_root_document:
|
|
319
|
-
path = self.path
|
|
320
|
-
location = DocumentLocation.FILESYSTEM
|
|
321
|
-
else:
|
|
322
|
-
if self.index is None:
|
|
323
|
-
msg = (
|
|
324
|
-
f"can't compute filesystem path for embedded doc {self.id} without"
|
|
325
|
-
f" index"
|
|
326
|
-
)
|
|
327
|
-
raise ValueError(msg)
|
|
328
|
-
path = artifacts_dir(doc_id=self.id, project=self.index) / "raw"
|
|
329
|
-
location = DocumentLocation.ARTIFACTS
|
|
330
|
-
# The filesystem dod is alway relative to the base location, let's make sure
|
|
331
|
-
# we store a relative path otherwise joining with the location will fail
|
|
332
|
-
if path.parts and path.parts[0] == os.path.sep:
|
|
333
|
-
path = Path(*path.parts[1:])
|
|
334
|
-
n_pages = 1
|
|
335
|
-
if self.metadata:
|
|
336
|
-
n_pages = self.metadata.get("tika_metadata_xmptpg_npages", n_pages)
|
|
337
|
-
return ProcessedFile(
|
|
338
|
-
id=self.id,
|
|
339
|
-
path=path,
|
|
340
|
-
project=self.index,
|
|
341
|
-
location=location,
|
|
342
|
-
resource_name=resource_name,
|
|
343
|
-
n_pages=n_pages,
|
|
286
|
+
msg = f"missing {TIKA_METADATA_RESOURCENAME} in metadata"
|
|
287
|
+
raise KeyError(msg)
|
|
288
|
+
cast(str, resource_name)
|
|
289
|
+
return resource_name
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _is_relative(value: Path) -> Path:
|
|
293
|
+
if value.is_absolute():
|
|
294
|
+
raise ValueError(
|
|
295
|
+
f"WorkerFilePath path should always be relative, found {value}"
|
|
344
296
|
)
|
|
297
|
+
return value
|
|
345
298
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
299
|
+
|
|
300
|
+
class WorkerFilePath(BaseModel):
|
|
301
|
+
"""Path of a file produced or processed by a worker, located somewhere on the DS
|
|
302
|
+
filesystem, artifactsdir or workdir
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
path: Annotated[Path, AfterValidator(_is_relative)]
|
|
306
|
+
location: FileLocation
|
|
307
|
+
|
|
308
|
+
def locate(self, paths: WorkerPaths) -> Path:
|
|
309
|
+
return paths.locate(self.path, self.location)
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def relative_to_workdir(cls, path: Path, paths: WorkerPaths) -> Self:
|
|
313
|
+
path = path.relative_to(paths.workdir)
|
|
314
|
+
return cls(path=path, location=FileLocation.WORKDIR)
|
|
350
315
|
|
|
351
316
|
|
|
352
317
|
def _is_absolute_path(v: bytes | BytesIO | Path) -> Any:
|
|
@@ -617,3 +582,297 @@ class TaskGroup:
|
|
|
617
582
|
@classmethod
|
|
618
583
|
def python(cls) -> Self:
|
|
619
584
|
return cls(name="PYTHON")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
class Error(BaseModel):
|
|
588
|
+
title: str
|
|
589
|
+
detail: str | None
|
|
590
|
+
|
|
591
|
+
@classmethod
|
|
592
|
+
def from_exception(cls, exception: BaseException) -> "Error":
|
|
593
|
+
title = exception.__class__.__name__
|
|
594
|
+
trace_lines = traceback.format_exception(
|
|
595
|
+
None, value=exception, tb=exception.__traceback__
|
|
596
|
+
)
|
|
597
|
+
detail = f"{exception}\n{''.join(trace_lines)}"
|
|
598
|
+
error = Error(title=title, detail=detail)
|
|
599
|
+
return error
|
|
600
|
+
|
|
601
|
+
def without_detail(self) -> "Error":
|
|
602
|
+
return Error(title=self.title, detail=None)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class ErrorSource(BaseModel, ABC):
|
|
606
|
+
"""Source of the processing error."""
|
|
607
|
+
|
|
608
|
+
doc_id: str
|
|
609
|
+
project: str
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
class ErrorSourceWithPages(ErrorSource, ABC):
|
|
613
|
+
"""Source of the processing error and number of pages processed at that time"""
|
|
614
|
+
|
|
615
|
+
n_pages: int
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
class ProcessingError[S: ErrorSource](BaseModel):
|
|
619
|
+
"""Error occurring while processing a document or intermediate result"""
|
|
620
|
+
|
|
621
|
+
source: S
|
|
622
|
+
error: Error
|
|
623
|
+
|
|
624
|
+
@final
|
|
625
|
+
@classmethod
|
|
626
|
+
def from_exception(cls, source: S, exception: BaseException) -> Self:
|
|
627
|
+
return cls(source=source, error=Error.from_exception(exception))
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
class ProcessingReport(DatashareModel):
|
|
631
|
+
"""Basic processing report indicating how many docs where processed"""
|
|
632
|
+
|
|
633
|
+
n_docs: int = 0
|
|
634
|
+
|
|
635
|
+
def __add__(self, other: Self) -> Self:
|
|
636
|
+
return ProcessingReport(n_docs=other.n_docs + self.n_docs)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
class ProcessingReportWithPages(ProcessingReport):
|
|
640
|
+
"""Processing report indicating how many pages where processed"""
|
|
641
|
+
|
|
642
|
+
n_pages: int = 0
|
|
643
|
+
|
|
644
|
+
def __add__(self, other: Self) -> Self:
|
|
645
|
+
return ProcessingReportWithPages(
|
|
646
|
+
n_docs=other.n_docs + self.n_docs, n_pages=other.n_pages + self.n_pages
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _without_detail(errors: list[Error]) -> list[Error]:
|
|
651
|
+
if any(e.detail is not None for e in errors):
|
|
652
|
+
msg = "expected errors without details"
|
|
653
|
+
raise ValueError(msg)
|
|
654
|
+
return errors
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
class DocProcessingErrors(BaseModel):
|
|
658
|
+
"""All errors related to several processing happening on a single document.
|
|
659
|
+
If a source doc has many children (typically a document split into pages
|
|
660
|
+
processed separately), all children errors are merged under the older parent.
|
|
661
|
+
"""
|
|
662
|
+
|
|
663
|
+
doc_id: str
|
|
664
|
+
root_document: str
|
|
665
|
+
project: str
|
|
666
|
+
errors: Annotated[
|
|
667
|
+
list[Error], AfterValidator(_without_detail), Field(default_factory=list)
|
|
668
|
+
]
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class ErrorReport(ProcessingReport):
|
|
672
|
+
"""Processing report indicated how many docs failed and listing errors per doc."""
|
|
673
|
+
|
|
674
|
+
errors: list[DocProcessingErrors] = Field(default_factory=list)
|
|
675
|
+
|
|
676
|
+
def __add__(self, other: Self) -> Self:
|
|
677
|
+
return ErrorReportWithPages(
|
|
678
|
+
n_docs=other.n_docs + self.n_docs, errors=self.errors + other.errors
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
@classmethod
|
|
682
|
+
def from_errors(cls, *errors: ProcessingError) -> Self:
|
|
683
|
+
"""Aggregate errors by root source documents"""
|
|
684
|
+
roots = dict()
|
|
685
|
+
for error in errors:
|
|
686
|
+
root = error.source
|
|
687
|
+
while (parent := getattr(root, "parent", None)) is not None:
|
|
688
|
+
root = parent
|
|
689
|
+
root_errors = roots.get(root.doc_id)
|
|
690
|
+
if root_errors is not None:
|
|
691
|
+
_, root_errors = root_errors
|
|
692
|
+
else:
|
|
693
|
+
root_errors = []
|
|
694
|
+
root_errors.append(error)
|
|
695
|
+
roots[root.doc_id] = (root, root_errors)
|
|
696
|
+
errors = []
|
|
697
|
+
for _, (root, root_errors) in sorted(roots.items()):
|
|
698
|
+
# We keep details for errors stored as intermediate steps to allow
|
|
699
|
+
# debugging, we don't output them in the response to keep everything
|
|
700
|
+
# lightweight and more secure
|
|
701
|
+
root_errors = [err.error.without_detail() for err in root_errors] # noqa: PLW2901
|
|
702
|
+
doc_errors = DocProcessingErrors(
|
|
703
|
+
doc_id=root.doc_id,
|
|
704
|
+
project=root.project,
|
|
705
|
+
root_document=root.root_document,
|
|
706
|
+
errors=root_errors,
|
|
707
|
+
)
|
|
708
|
+
errors.append(doc_errors)
|
|
709
|
+
return cls(n_docs=len(errors), errors=errors)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
class ErrorReportWithPages(ProcessingReportWithPages, ABC):
|
|
713
|
+
"""Same as ErrorReport but providing details about how many pages failed"""
|
|
714
|
+
|
|
715
|
+
errors: list[DocProcessingErrors] = Field(default_factory=list)
|
|
716
|
+
|
|
717
|
+
def __add__(self, other: Self) -> Self:
|
|
718
|
+
return ErrorReportWithPages(
|
|
719
|
+
n_docs=other.n_docs + self.n_docs,
|
|
720
|
+
n_pages=other.n_pages + self.n_pages,
|
|
721
|
+
errors=self.errors + other.errors,
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
@classmethod
|
|
725
|
+
def from_errors(cls, *errors: ProcessingError) -> Self:
|
|
726
|
+
"""Aggregate errors by root source documents"""
|
|
727
|
+
roots = dict()
|
|
728
|
+
n_pages = 0
|
|
729
|
+
for error in errors:
|
|
730
|
+
root = error.source
|
|
731
|
+
while (parent := getattr(root, "parent", None)) is not None:
|
|
732
|
+
root = parent
|
|
733
|
+
root_errors = roots.get(root.doc_id)
|
|
734
|
+
if root_errors is not None:
|
|
735
|
+
_, root_errors = root_errors
|
|
736
|
+
else:
|
|
737
|
+
root_errors = []
|
|
738
|
+
n_pages += error.source.n_pages
|
|
739
|
+
root_errors.append(error)
|
|
740
|
+
roots[root.doc_id] = (root, root_errors)
|
|
741
|
+
errors = []
|
|
742
|
+
for _, (root, root_errors) in sorted(roots.items()):
|
|
743
|
+
# We keep details for errors stored as intermediate steps to allow
|
|
744
|
+
# debugging, we don't output them in the response to keep everything
|
|
745
|
+
# lightweight and more secure
|
|
746
|
+
root_errors = [err.error.without_detail() for err in root_errors] # noqa: PLW2901
|
|
747
|
+
doc_errors = DocProcessingErrors(
|
|
748
|
+
doc_id=root.doc_id,
|
|
749
|
+
project=root.project,
|
|
750
|
+
root_document=root.root_document,
|
|
751
|
+
errors=root_errors,
|
|
752
|
+
)
|
|
753
|
+
errors.append(doc_errors)
|
|
754
|
+
return cls(n_docs=len(errors), n_pages=n_pages, errors=errors)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
class ProcessingResult[V](BaseModel):
|
|
758
|
+
value: V
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
class WithParent[P](FromParent[P]):
|
|
762
|
+
"""Result produced from another processing result
|
|
763
|
+
|
|
764
|
+
Tracking parent allows track errors from one intermediate results to another back
|
|
765
|
+
to the initial DS Document from the index. It also allows error aggregation.
|
|
766
|
+
"""
|
|
767
|
+
|
|
768
|
+
parent: P
|
|
769
|
+
|
|
770
|
+
@property
|
|
771
|
+
def doc_id(self) -> str:
|
|
772
|
+
return self.parent.doc_id
|
|
773
|
+
|
|
774
|
+
@property
|
|
775
|
+
def root_document(self) -> str:
|
|
776
|
+
return self.parent.root_document
|
|
777
|
+
|
|
778
|
+
@property
|
|
779
|
+
def project(self) -> str:
|
|
780
|
+
return self.parent.project
|
|
781
|
+
|
|
782
|
+
@property
|
|
783
|
+
def route(self) -> DocRoute:
|
|
784
|
+
return self.parent.route
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
class DatashareFile(
|
|
788
|
+
WithParent[Document], ProcessingResult[WorkerFilePath], ErrorSourceWithPages
|
|
789
|
+
):
|
|
790
|
+
"""A datashare document file ready to be processed and located somewhere on the
|
|
791
|
+
filesystem or artifact directory.
|
|
792
|
+
"""
|
|
793
|
+
|
|
794
|
+
parent: Document
|
|
795
|
+
|
|
796
|
+
def locate(self, paths: WorkerPaths) -> Path:
|
|
797
|
+
return self.value.locate(paths)
|
|
798
|
+
|
|
799
|
+
@classmethod
|
|
800
|
+
def from_parent(cls, parent: Document) -> Self:
|
|
801
|
+
from .utils import artifacts_dir # noqa: PLC0415
|
|
802
|
+
|
|
803
|
+
if parent.metadata is None:
|
|
804
|
+
raise ValueError(
|
|
805
|
+
"can't compute filesystem path for document withtout metadata"
|
|
806
|
+
)
|
|
807
|
+
if parent.is_root_document:
|
|
808
|
+
path = parent.path
|
|
809
|
+
if path is None:
|
|
810
|
+
msg = (
|
|
811
|
+
"can't create a Datashare file from ES document without path, "
|
|
812
|
+
"path is needed to locate the file, retrieve it from ES"
|
|
813
|
+
)
|
|
814
|
+
raise ValueError(msg)
|
|
815
|
+
location = FileLocation.FILESYSTEM
|
|
816
|
+
else:
|
|
817
|
+
path = artifacts_dir(doc_id=parent.id, project=parent.project) / "raw"
|
|
818
|
+
location = FileLocation.ARTIFACTS
|
|
819
|
+
# The filesystem dod is alway relative to the base location, let's make sure
|
|
820
|
+
# we store a relative path otherwise joining with the location will fail
|
|
821
|
+
if path.parts and path.parts[0] == os.path.sep:
|
|
822
|
+
path = Path(*path.parts[1:])
|
|
823
|
+
n_pages = 1
|
|
824
|
+
if parent.metadata:
|
|
825
|
+
n_pages = parent.metadata.get("tika_metadata_xmptpg_npages", n_pages)
|
|
826
|
+
path = WorkerFilePath(path=path, location=location)
|
|
827
|
+
return cls(
|
|
828
|
+
doc_id=parent.id,
|
|
829
|
+
parent=parent,
|
|
830
|
+
value=path,
|
|
831
|
+
n_pages=n_pages,
|
|
832
|
+
project=parent.project,
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
class WorkerFile(
|
|
837
|
+
WithParent["ProcessedFile"], ProcessingResult[WorkerFilePath], ErrorSourceWithPages
|
|
838
|
+
):
|
|
839
|
+
"""An intermediate results produce by the worker either from a Datashare doc on the
|
|
840
|
+
filesystem or from another intermediate result
|
|
841
|
+
"""
|
|
842
|
+
|
|
843
|
+
parent: "ProcessedFile"
|
|
844
|
+
|
|
845
|
+
def locate(self, paths: WorkerPaths) -> Path:
|
|
846
|
+
return self.value.locate(paths)
|
|
847
|
+
|
|
848
|
+
@classmethod
|
|
849
|
+
def from_parent(
|
|
850
|
+
cls, parent: "ProcessedFile", path: Path, paths: WorkerPaths
|
|
851
|
+
) -> Self:
|
|
852
|
+
path = WorkerFilePath.relative_to_workdir(path, paths)
|
|
853
|
+
return cls(
|
|
854
|
+
doc_id=parent.doc_id,
|
|
855
|
+
project=parent.project,
|
|
856
|
+
parent=parent,
|
|
857
|
+
value=path,
|
|
858
|
+
n_pages=parent.n_pages,
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _file_discriminator(v: Any) -> str:
|
|
863
|
+
parent = v.get("parent") if isinstance(v, dict) else getattr(v, "parent", None)
|
|
864
|
+
if parent is None:
|
|
865
|
+
raise ValueError(f"{v} has no parent")
|
|
866
|
+
if isinstance(parent, dict):
|
|
867
|
+
is_document = parent.get("type") == "Document"
|
|
868
|
+
else:
|
|
869
|
+
is_document = isinstance(parent, Document)
|
|
870
|
+
return "datashare" if is_document else "worker"
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
ProcessedFile = Annotated[
|
|
874
|
+
Annotated[DatashareFile, Tag("datashare")] | Annotated[WorkerFile, Tag("worker")],
|
|
875
|
+
Discriminator(_file_discriminator),
|
|
876
|
+
]
|
|
877
|
+
WorkerFile.model_rebuild()
|
|
878
|
+
PROCESSED_FILE_TA = TypeAdapter(ProcessedFile, config=BaseModel.model_config)
|
|
@@ -21,18 +21,17 @@ from collections.abc import (
|
|
|
21
21
|
)
|
|
22
22
|
from copy import deepcopy
|
|
23
23
|
from dataclasses import dataclass
|
|
24
|
-
from datetime import timedelta
|
|
25
24
|
from functools import cache, wraps
|
|
26
25
|
from hashlib import sha256
|
|
27
26
|
from io import BytesIO
|
|
28
27
|
from pathlib import Path
|
|
29
|
-
from typing import Any, Self, TypeVar
|
|
28
|
+
from typing import Any, Protocol, Self, TypeVar
|
|
30
29
|
from uuid import uuid4
|
|
31
30
|
|
|
32
31
|
import temporalio
|
|
33
32
|
from aiofile import async_open
|
|
34
33
|
from lru import LRU
|
|
35
|
-
from pydantic import ValidationError
|
|
34
|
+
from pydantic import TypeAdapter, ValidationError
|
|
36
35
|
from pydantic.alias_generators import to_snake
|
|
37
36
|
from temporalio import activity, workflow
|
|
38
37
|
from temporalio.api.common.v1 import Payload
|
|
@@ -47,6 +46,7 @@ from temporalio.converter import (
|
|
|
47
46
|
)
|
|
48
47
|
from temporalio.exceptions import ApplicationError
|
|
49
48
|
|
|
49
|
+
from datashare_python.config import ActivityTimeouts
|
|
50
50
|
from datashare_python.types_ import (
|
|
51
51
|
AsyncProgressRateHandler,
|
|
52
52
|
RawSyncProgressHandler,
|
|
@@ -57,9 +57,13 @@ from .constants import MANIFEST_JSON, METADATA_JSON
|
|
|
57
57
|
from .mimetypes_ import types_map
|
|
58
58
|
from .objects import (
|
|
59
59
|
BaseModel,
|
|
60
|
+
DatashareFile,
|
|
60
61
|
DocArtifact,
|
|
61
|
-
|
|
62
|
+
Document,
|
|
63
|
+
ErrorSource,
|
|
62
64
|
ProcessedFile,
|
|
65
|
+
ProcessingError,
|
|
66
|
+
WorkerFile,
|
|
63
67
|
WorkerPaths,
|
|
64
68
|
)
|
|
65
69
|
from .types_ import RawAsyncProgressHandler
|
|
@@ -162,10 +166,11 @@ async def execute_activity(
|
|
|
162
166
|
arg: Any = temporalio.common._arg_unset,
|
|
163
167
|
*,
|
|
164
168
|
args: list | None = None,
|
|
165
|
-
|
|
166
|
-
heartbeat_timeout: timedelta = timedelta(minutes=1),
|
|
169
|
+
timeouts: ActivityTimeouts | None = None,
|
|
167
170
|
retry_policy: temporalio.common.RetryPolicy | None = None,
|
|
168
171
|
) -> Any:
|
|
172
|
+
if timeouts is None:
|
|
173
|
+
timeouts = ActivityTimeouts()
|
|
169
174
|
if args is None:
|
|
170
175
|
args = []
|
|
171
176
|
retry_policy = _retry_policy_with_default(retry_policy)
|
|
@@ -173,10 +178,10 @@ async def execute_activity(
|
|
|
173
178
|
activity,
|
|
174
179
|
arg=arg,
|
|
175
180
|
args=args,
|
|
176
|
-
start_to_close_timeout=start_to_close_timeout,
|
|
177
181
|
task_queue=task_queue,
|
|
178
182
|
retry_policy=retry_policy,
|
|
179
|
-
|
|
183
|
+
start_to_close_timeout=timeouts.start_to_close,
|
|
184
|
+
heartbeat_timeout=timeouts.heartbeat,
|
|
180
185
|
)
|
|
181
186
|
|
|
182
187
|
|
|
@@ -389,8 +394,7 @@ def to_scaled_async_progress(
|
|
|
389
394
|
def safe_dir(doc_id: str) -> Path:
|
|
390
395
|
if len(doc_id) < 4:
|
|
391
396
|
raise ValueError(f"expected doc_id to be at least 4, found {doc_id}")
|
|
392
|
-
|
|
393
|
-
return Path(*parts)
|
|
397
|
+
return Path(doc_id[:2], doc_id[2:4])
|
|
394
398
|
|
|
395
399
|
|
|
396
400
|
def artifacts_dir(doc_id: str, *, project: str) -> Path:
|
|
@@ -608,31 +612,18 @@ def activity_workdir(
|
|
|
608
612
|
|
|
609
613
|
|
|
610
614
|
def symlink_embedded_document_to_workdir(
|
|
611
|
-
doc:
|
|
615
|
+
doc: Document, paths: WorkerPaths
|
|
612
616
|
) -> ProcessedFile:
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
os.symlink(artifact_path, symlink_path)
|
|
624
|
-
return ProcessedFile(
|
|
625
|
-
path=symlink_path.relative_to(paths.workdir),
|
|
626
|
-
id=doc.id,
|
|
627
|
-
location=DocumentLocation.WORKDIR,
|
|
628
|
-
project=doc.project,
|
|
629
|
-
resource_name=doc.resource_name,
|
|
630
|
-
n_pages=doc.n_pages,
|
|
631
|
-
)
|
|
632
|
-
case DocumentLocation.FILESYSTEM:
|
|
633
|
-
return doc
|
|
634
|
-
case _:
|
|
635
|
-
raise ValueError(f"unsupported location {doc.location}")
|
|
617
|
+
ds_file = DatashareFile.from_parent(doc)
|
|
618
|
+
if doc.is_root_document:
|
|
619
|
+
return ds_file
|
|
620
|
+
symlinks_dir = paths.workdir / doc.project / "symlinks"
|
|
621
|
+
symlink_path = symlinks_dir / safe_dir(doc.id) / doc.id / doc.resource_name
|
|
622
|
+
symlink_path.parent.mkdir(parents=True, exist_ok=True)
|
|
623
|
+
raw_path = ds_file.locate(paths)
|
|
624
|
+
with contextlib.suppress(FileExistsError):
|
|
625
|
+
os.symlink(raw_path, symlink_path)
|
|
626
|
+
return WorkerFile.from_parent(ds_file, symlink_path, paths)
|
|
636
627
|
|
|
637
628
|
|
|
638
629
|
def artifact_path(
|
|
@@ -645,7 +636,7 @@ def artifact_path(
|
|
|
645
636
|
return root / artifacts_dir(doc_id, project=project) / artifact_type.filename
|
|
646
637
|
|
|
647
638
|
|
|
648
|
-
def
|
|
639
|
+
def read_jsonl(path: Path) -> Iterable[dict]:
|
|
649
640
|
with path.open() as f:
|
|
650
641
|
for line in f:
|
|
651
642
|
line = line.strip() # noqa: PLW2901
|
|
@@ -656,18 +647,82 @@ def _read_jsonl(path: Path) -> Iterable[dict]:
|
|
|
656
647
|
M = TypeVar("M", bound=BaseModel)
|
|
657
648
|
|
|
658
649
|
|
|
659
|
-
def read_jsonl_as[M](
|
|
660
|
-
|
|
650
|
+
def read_jsonl_as[M](
|
|
651
|
+
path: Path, processed_file_cls: type[M] | TypeAdapter[M]
|
|
652
|
+
) -> Iterable[M]:
|
|
653
|
+
if isinstance(processed_file_cls, TypeAdapter):
|
|
654
|
+
deser_fn = processed_file_cls.validate_python
|
|
655
|
+
else:
|
|
656
|
+
deser_fn = processed_file_cls.model_validate
|
|
657
|
+
return (deser_fn(d) for d in read_jsonl(path))
|
|
661
658
|
|
|
662
659
|
|
|
663
660
|
async def async_read_jsonl_as[M](
|
|
664
|
-
path: Path, processed_file_cls: type[M]
|
|
661
|
+
path: Path, processed_file_cls: type[M] | TypeAdapter[M]
|
|
665
662
|
) -> AsyncIterable[M]:
|
|
663
|
+
if isinstance(processed_file_cls, TypeAdapter):
|
|
664
|
+
deser_fn = processed_file_cls.validate_json
|
|
665
|
+
else:
|
|
666
|
+
deser_fn = processed_file_cls.model_validate_json
|
|
666
667
|
async with async_open(path, "r") as f:
|
|
667
668
|
async for line in f:
|
|
668
669
|
line = line.strip() # noqa: PLW2901
|
|
669
670
|
if line:
|
|
670
|
-
yield
|
|
671
|
+
yield deser_fn(line)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
async def write_batches(
|
|
675
|
+
batches: AsyncIterable[list[ProcessedFile]] | Iterable[list[ProcessedFile]],
|
|
676
|
+
root: Path,
|
|
677
|
+
batch_offset: int = 0,
|
|
678
|
+
prefix: str = "batch_",
|
|
679
|
+
) -> AsyncIterable[Path]:
|
|
680
|
+
if hasattr(batches, "__aiter__"):
|
|
681
|
+
async for b in _async_write_batches(batches, root, batch_offset, prefix):
|
|
682
|
+
yield b
|
|
683
|
+
return
|
|
684
|
+
async for b in _write_batches(batches, root, batch_offset, prefix):
|
|
685
|
+
yield b
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
async def _async_write_batches(
|
|
689
|
+
batches: AsyncIterable[list[ProcessedFile]],
|
|
690
|
+
root: Path,
|
|
691
|
+
batch_offset: int,
|
|
692
|
+
prefix: str,
|
|
693
|
+
) -> AsyncIterable[Path]:
|
|
694
|
+
batch_id = batch_offset
|
|
695
|
+
async for batch in batches:
|
|
696
|
+
batch_path = root / f"{batch_id // 1000}" / f"{prefix}{batch_id}.jsonl"
|
|
697
|
+
batch_path.parent.mkdir(parents=True, exist_ok=True)
|
|
698
|
+
async with async_open(batch_path, "w") as f:
|
|
699
|
+
for fs_doc in batch:
|
|
700
|
+
serialized = fs_doc.model_dump_json(
|
|
701
|
+
exclude_none=True, polymorphic_serialization=True
|
|
702
|
+
)
|
|
703
|
+
await f.write(f"{serialized}\n")
|
|
704
|
+
yield batch_path
|
|
705
|
+
batch_id += 1
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
async def _write_batches(
|
|
709
|
+
batches: Iterable[list[ProcessedFile]],
|
|
710
|
+
root: Path,
|
|
711
|
+
batch_offset: int,
|
|
712
|
+
prefix: str,
|
|
713
|
+
) -> AsyncIterable[Path]:
|
|
714
|
+
batch_id = batch_offset
|
|
715
|
+
for batch in batches:
|
|
716
|
+
batch_path = root / f"{batch_id // 1000}" / f"{prefix}{batch_id}.jsonl"
|
|
717
|
+
batch_path.parent.mkdir(parents=True, exist_ok=True)
|
|
718
|
+
async with async_open(batch_path, "w") as f:
|
|
719
|
+
for fs_doc in batch:
|
|
720
|
+
serialized = fs_doc.model_dump_json(
|
|
721
|
+
exclude_none=True, polymorphic_serialization=True
|
|
722
|
+
)
|
|
723
|
+
await f.write(f"{serialized}\n")
|
|
724
|
+
yield batch_path
|
|
725
|
+
batch_id += 1
|
|
671
726
|
|
|
672
727
|
|
|
673
728
|
@cache
|
|
@@ -690,6 +745,8 @@ def ext_to_mime_types(ext: str) -> set[str]:
|
|
|
690
745
|
return ext_to_mime_types(".xml")
|
|
691
746
|
case ".dclg.xml":
|
|
692
747
|
return ext_to_mime_types(".xml")
|
|
748
|
+
case ".msg":
|
|
749
|
+
return ext_to_mime_types(".eml")
|
|
693
750
|
try:
|
|
694
751
|
return types_map()[ext]
|
|
695
752
|
except KeyError as e:
|
|
@@ -907,3 +964,43 @@ def config_cache_key(config: BaseModel) -> str:
|
|
|
907
964
|
.replace("_", "-")
|
|
908
965
|
)
|
|
909
966
|
return f"{config_label}-{str(hash(config))}"
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
class _PreprocessingFunction[S: ErrorSource, R](Protocol):
|
|
970
|
+
def __call__(self, doc: S, *args, **kwargs) -> R: ...
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def reports_errors[S: ErrorSource, R](
|
|
974
|
+
errors: tuple[type[Exception]], exc_cls: type[ProcessingError[S]]
|
|
975
|
+
) -> Callable[
|
|
976
|
+
[_PreprocessingFunction[S, R]], _PreprocessingFunction[S, R | ProcessingError[S]]
|
|
977
|
+
]:
|
|
978
|
+
|
|
979
|
+
def parent_wrapper(
|
|
980
|
+
f: _PreprocessingFunction[S, R],
|
|
981
|
+
) -> _PreprocessingFunction[S, R | ProcessingError[S]]:
|
|
982
|
+
if inspect.iscoroutinefunction(f):
|
|
983
|
+
|
|
984
|
+
@wraps(f) # noqa: F821
|
|
985
|
+
async def wrapper(doc: S, *args, **kwargs) -> R | ProcessingError[S]:
|
|
986
|
+
try:
|
|
987
|
+
return await f(doc, *args, **kwargs)
|
|
988
|
+
except errors as e:
|
|
989
|
+
logger.exception("error while processing doc %s", doc)
|
|
990
|
+
report = exc_cls.from_exception(doc, e)
|
|
991
|
+
return report
|
|
992
|
+
else:
|
|
993
|
+
|
|
994
|
+
@wraps(f)
|
|
995
|
+
def wrapper(doc: ProcessedFile, *args, **kwargs) -> R | ProcessingError[S]:
|
|
996
|
+
|
|
997
|
+
try:
|
|
998
|
+
return f(doc, *args, **kwargs)
|
|
999
|
+
except errors as e:
|
|
1000
|
+
logger.exception("error while processing doc %s", doc)
|
|
1001
|
+
report = exc_cls.from_exception(doc, e)
|
|
1002
|
+
return report
|
|
1003
|
+
|
|
1004
|
+
return wrapper
|
|
1005
|
+
|
|
1006
|
+
return parent_wrapper
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|