datashare-python 0.9.8__py3-none-any.whl → 0.10.0.dev0__py3-none-any.whl
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/cli/task.py +2 -2
- datashare_python/cli/worker.py +1 -1
- datashare_python/config.py +17 -7
- datashare_python/conftest.py +176 -44
- datashare_python/constants.py +1 -2
- datashare_python/dependencies.py +0 -16
- datashare_python/logging_.py +1 -1
- datashare_python/mimetypes_.py +1105 -0
- datashare_python/objects.py +198 -69
- datashare_python/utils.py +347 -42
- datashare_python/worker-template.tar.gz +0 -0
- datashare_python/worker.py +14 -3
- {datashare_python-0.9.8.dist-info → datashare_python-0.10.0.dev0.dist-info}/METADATA +4 -3
- datashare_python-0.10.0.dev0.dist-info/RECORD +28 -0
- {datashare_python-0.9.8.dist-info → datashare_python-0.10.0.dev0.dist-info}/WHEEL +1 -1
- datashare_python-0.9.8.dist-info/RECORD +0 -27
- {datashare_python-0.9.8.dist-info → datashare_python-0.10.0.dev0.dist-info}/entry_points.txt +0 -0
datashare_python/objects.py
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
-
from
|
|
3
|
+
from abc import ABC
|
|
4
4
|
from collections.abc import Awaitable, Callable
|
|
5
|
-
from dataclasses import
|
|
5
|
+
from dataclasses import dataclass
|
|
6
6
|
from datetime import UTC, datetime
|
|
7
7
|
from enum import StrEnum, unique
|
|
8
8
|
from io import BytesIO
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar, cast
|
|
10
|
+
from typing import Annotated, Any, ClassVar, Generic, Literal, Self, TypeVar, cast
|
|
11
11
|
|
|
12
12
|
import langcodes
|
|
13
|
-
from
|
|
13
|
+
from icij_common.registrable import Registrable
|
|
14
14
|
from pydantic_core import PydanticCustomError, ValidationError, core_schema
|
|
15
15
|
from pydantic_core.core_schema import PlainValidatorFunctionSchema
|
|
16
16
|
from pydantic_extra_types.language_code import LanguageName
|
|
17
17
|
from temporalio import workflow
|
|
18
18
|
|
|
19
|
-
from .constants import
|
|
19
|
+
from .constants import TIKA_METADATA_RESOURCENAME
|
|
20
20
|
|
|
21
21
|
with workflow.unsafe.imports_passed_through():
|
|
22
22
|
from icij_common.es import (
|
|
@@ -34,12 +34,17 @@ with workflow.unsafe.imports_passed_through():
|
|
|
34
34
|
from icij_common.pydantic_utils import (
|
|
35
35
|
icij_config,
|
|
36
36
|
lowercamel_case_config,
|
|
37
|
+
make_enum_discriminator,
|
|
37
38
|
merge_configs,
|
|
38
39
|
no_enum_values_config,
|
|
40
|
+
tagged_union,
|
|
39
41
|
)
|
|
40
42
|
from pydantic import (
|
|
41
43
|
AfterValidator,
|
|
44
|
+
AliasChoices,
|
|
42
45
|
BeforeValidator,
|
|
46
|
+
ConfigDict,
|
|
47
|
+
Discriminator,
|
|
43
48
|
Field,
|
|
44
49
|
GetCoreSchemaHandler,
|
|
45
50
|
TypeAdapter,
|
|
@@ -104,9 +109,15 @@ class DatashareLanguage(str):
|
|
|
104
109
|
return self.as_language_name.alpha3
|
|
105
110
|
|
|
106
111
|
|
|
112
|
+
class WorkerPaths(BaseModel):
|
|
113
|
+
filesystem: Path
|
|
114
|
+
artifacts: Path
|
|
115
|
+
workdir: Path
|
|
116
|
+
|
|
117
|
+
|
|
107
118
|
@unique
|
|
108
119
|
class DocumentLocation(StrEnum):
|
|
109
|
-
|
|
120
|
+
FILESYSTEM = "filesystem"
|
|
110
121
|
ARTIFACTS = "artifacts"
|
|
111
122
|
WORKDIR = "workdir"
|
|
112
123
|
|
|
@@ -119,30 +130,49 @@ def _is_relative(value: Path) -> Path:
|
|
|
119
130
|
return value
|
|
120
131
|
|
|
121
132
|
|
|
122
|
-
class
|
|
133
|
+
class ProcessedFile(BaseModel):
|
|
123
134
|
id: str
|
|
124
135
|
path: Annotated[Path, AfterValidator(_is_relative)]
|
|
125
|
-
|
|
136
|
+
project: str
|
|
126
137
|
location: DocumentLocation
|
|
127
138
|
resource_name: str
|
|
139
|
+
n_pages: int
|
|
140
|
+
parent: "ProcessedFile | None" = None
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def from_doc(cls, doc: "Document") -> Self:
|
|
144
|
+
return doc.to_processed_file()
|
|
145
|
+
|
|
146
|
+
def child(self, path: Path, paths: WorkerPaths) -> Self:
|
|
147
|
+
return ProcessedFile(
|
|
148
|
+
id=self.id,
|
|
149
|
+
path=path.relative_to(paths.workdir),
|
|
150
|
+
project=self.project,
|
|
151
|
+
location=DocumentLocation.WORKDIR,
|
|
152
|
+
resource_name=path.name,
|
|
153
|
+
n_pages=self.n_pages,
|
|
154
|
+
parent=self,
|
|
155
|
+
)
|
|
128
156
|
|
|
129
|
-
def locate(
|
|
130
|
-
self, original_root: Path, *, artifacts_root: Path, workdir: Path
|
|
131
|
-
) -> Path:
|
|
157
|
+
def locate(self, paths: WorkerPaths) -> Path:
|
|
132
158
|
from datashare_python.utils import artifacts_dir # noqa: PLC0415
|
|
133
159
|
|
|
134
160
|
match self.location:
|
|
135
|
-
case DocumentLocation.
|
|
136
|
-
return
|
|
161
|
+
case DocumentLocation.FILESYSTEM:
|
|
162
|
+
return paths.filesystem / self.path
|
|
137
163
|
case DocumentLocation.ARTIFACTS:
|
|
138
|
-
project = self.
|
|
139
|
-
return
|
|
164
|
+
project = self.project
|
|
165
|
+
return paths.artifacts / artifacts_dir(self.id, project=project) / "raw"
|
|
140
166
|
case DocumentLocation.WORKDIR:
|
|
141
|
-
return workdir / self.path
|
|
167
|
+
return paths.workdir / self.path
|
|
142
168
|
case _:
|
|
143
169
|
raise ValueError(f"invalid location: {self.path}")
|
|
144
170
|
|
|
145
171
|
|
|
172
|
+
class ProcessedPage(ProcessedFile):
|
|
173
|
+
page_number: int
|
|
174
|
+
|
|
175
|
+
|
|
146
176
|
class IETFLanguage(str):
|
|
147
177
|
@classmethod
|
|
148
178
|
def __get_pydantic_core_schema__(
|
|
@@ -174,6 +204,11 @@ class Translation(BaseModel): # No camelcase here we don't know why
|
|
|
174
204
|
content: Annotated[str, BeforeValidator(_from_sentences)]
|
|
175
205
|
|
|
176
206
|
|
|
207
|
+
Routing = str
|
|
208
|
+
DocID = str
|
|
209
|
+
DocRoute = tuple[DocID, Routing]
|
|
210
|
+
|
|
211
|
+
|
|
177
212
|
class Document(DatashareModel):
|
|
178
213
|
id: str
|
|
179
214
|
language: DatashareLanguage
|
|
@@ -217,17 +252,17 @@ class Document(DatashareModel):
|
|
|
217
252
|
metadata=sources.get(DOC_METADATA),
|
|
218
253
|
)
|
|
219
254
|
|
|
220
|
-
def
|
|
255
|
+
def to_processed_file(self) -> ProcessedFile:
|
|
221
256
|
from .utils import artifacts_dir # noqa: PLC0415
|
|
222
257
|
|
|
223
258
|
if self.metadata is None:
|
|
224
259
|
raise ValueError(
|
|
225
|
-
"can't compute
|
|
260
|
+
"can't compute filesystem path for document withtout metadata"
|
|
226
261
|
)
|
|
227
262
|
resource_name = cast(str, self.metadata[TIKA_METADATA_RESOURCENAME])
|
|
228
263
|
if self.root_document is None:
|
|
229
264
|
path = self.path
|
|
230
|
-
location = DocumentLocation.
|
|
265
|
+
location = DocumentLocation.FILESYSTEM
|
|
231
266
|
else:
|
|
232
267
|
if self.index is None:
|
|
233
268
|
msg = (
|
|
@@ -241,14 +276,23 @@ class Document(DatashareModel):
|
|
|
241
276
|
# we store a relative path otherwise joining with the location will fail
|
|
242
277
|
if path.parts and path.parts[0] == os.path.sep:
|
|
243
278
|
path = Path(*path.parts[1:])
|
|
244
|
-
|
|
279
|
+
n_pages = 1
|
|
280
|
+
if self.metadata:
|
|
281
|
+
n_pages = self.metadata.get("tika_metadata_xmptpg_npages", n_pages)
|
|
282
|
+
return ProcessedFile(
|
|
245
283
|
id=self.id,
|
|
246
284
|
path=path,
|
|
247
|
-
|
|
285
|
+
project=self.index,
|
|
248
286
|
location=location,
|
|
249
287
|
resource_name=resource_name,
|
|
288
|
+
n_pages=n_pages,
|
|
250
289
|
)
|
|
251
290
|
|
|
291
|
+
def to_route(self) -> DocRoute:
|
|
292
|
+
if self.root_document is not None:
|
|
293
|
+
return self.root_document, self.id
|
|
294
|
+
return self.id, self.id
|
|
295
|
+
|
|
252
296
|
|
|
253
297
|
def _is_absolute_path(v: bytes | BytesIO | Path) -> Any:
|
|
254
298
|
if isinstance(v, Path) and not v.is_absolute():
|
|
@@ -256,13 +300,142 @@ def _is_absolute_path(v: bytes | BytesIO | Path) -> Any:
|
|
|
256
300
|
return v
|
|
257
301
|
|
|
258
302
|
|
|
259
|
-
|
|
260
|
-
|
|
303
|
+
class ArtifactType(StrEnum):
|
|
304
|
+
STRUCTURE = "structure"
|
|
305
|
+
ASR_TRANSCRIPTION = "transcription"
|
|
306
|
+
PASSPORTS = "passports"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class ManifestEntryStatus(StrEnum):
|
|
310
|
+
COMPLETE = "complete"
|
|
311
|
+
PARTIAL = "partial"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class TaskArgs(DatashareModel, ABC):
|
|
315
|
+
def as_manifest_task_input(self) -> dict[str, Any]:
|
|
316
|
+
# This is a base implementation, if the input is too large to be dumped,
|
|
317
|
+
# override this and pop large keys
|
|
318
|
+
# Dump in json mode to make testing easier
|
|
319
|
+
as_manifest = self.model_dump(by_alias=True, mode="json")
|
|
320
|
+
return as_manifest
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
A = TypeVar("A", bound=TaskArgs)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class ManifestEntry(DatashareModel, Generic[A], ABC):
|
|
327
|
+
status: ManifestEntryStatus
|
|
328
|
+
label: str | None = None
|
|
329
|
+
input: Annotated[
|
|
330
|
+
dict[str, Any] | None,
|
|
331
|
+
Field(
|
|
332
|
+
validation_alias=AliasChoices("taskInput", "input"),
|
|
333
|
+
serialization_alias="taskInput",
|
|
334
|
+
),
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
@classmethod
|
|
338
|
+
def complete(cls, args: A, label: str | None = None, **kwargs) -> Self:
|
|
339
|
+
return cls(
|
|
340
|
+
input=args.as_manifest_task_input(),
|
|
341
|
+
label=label,
|
|
342
|
+
status=ManifestEntryStatus.COMPLETE,
|
|
343
|
+
**kwargs,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
@classmethod
|
|
347
|
+
def partial(cls, args: A, label: str | None = None, **kwargs) -> Self:
|
|
348
|
+
return cls(
|
|
349
|
+
input=args.as_manifest_task_input(),
|
|
350
|
+
label=label,
|
|
351
|
+
status=ManifestEntryStatus.PARTIAL,
|
|
352
|
+
**kwargs,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class PaginationType(StrEnum):
|
|
357
|
+
FILESYSTEM = "filesystem"
|
|
358
|
+
BYTE_RANGES = "byteRanges"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _validate_pages_range(v: Any) -> None:
|
|
362
|
+
if not isinstance(v, list):
|
|
363
|
+
msg = f"expected a list, got {type(v)}"
|
|
364
|
+
raise TypeError(msg)
|
|
365
|
+
previous_end = None
|
|
366
|
+
for page_i, (start, end) in enumerate(v):
|
|
367
|
+
if not start <= end:
|
|
368
|
+
msg = "end of page must be >= start"
|
|
369
|
+
raise ValueError(msg)
|
|
370
|
+
if previous_end is not None and previous_end != start:
|
|
371
|
+
msg = (
|
|
372
|
+
f"start of page {page_i} doesn't match end of previous "
|
|
373
|
+
f"page {previous_end}"
|
|
374
|
+
)
|
|
375
|
+
raise ValueError(msg)
|
|
376
|
+
return v
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
PagesRange = Annotated[list[tuple[int, int]], AfterValidator(_validate_pages_range)]
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class BasePagination(Registrable, DatashareModel):
|
|
383
|
+
registry_key: ClassVar[str] = Field(frozen=True, default="type")
|
|
384
|
+
type: ClassVar[PaginationType] = Field(frozen=True)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@BasePagination.register(PaginationType.FILESYSTEM)
|
|
388
|
+
class FilesystemPagination(BasePagination):
|
|
389
|
+
type: ClassVar[PaginationType] = Field(
|
|
390
|
+
default=PaginationType.FILESYSTEM, frozen=True
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
@BasePagination.register(PaginationType.BYTE_RANGES)
|
|
395
|
+
class ByteRangesPagination(BasePagination):
|
|
396
|
+
type: ClassVar[PaginationType] = Field(
|
|
397
|
+
default=PaginationType.BYTE_RANGES, frozen=True
|
|
398
|
+
)
|
|
399
|
+
byte_ranges: PagesRange
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
pagination_discriminator = make_enum_discriminator("type", PaginationType)
|
|
403
|
+
Pagination = Annotated[
|
|
404
|
+
tagged_union(BasePagination.__subclasses__(), lambda x: x.type.default),
|
|
405
|
+
Discriminator(pagination_discriminator),
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class Pages(DatashareModel):
|
|
410
|
+
total: int
|
|
411
|
+
pagination: Pagination
|
|
412
|
+
|
|
413
|
+
@model_validator(mode="after")
|
|
414
|
+
def byte_ranges_length_should_match_total(self) -> Self:
|
|
415
|
+
if (
|
|
416
|
+
isinstance(self.pagination, ByteRangesPagination)
|
|
417
|
+
and len(self.pagination.byte_ranges) != self.total
|
|
418
|
+
):
|
|
419
|
+
n_pages = len(self.pagination.byte_ranges)
|
|
420
|
+
msg = (
|
|
421
|
+
f"byte_ranges must match total. Found {n_pages} for"
|
|
422
|
+
f" byte_ranges and {self.total} for total."
|
|
423
|
+
)
|
|
424
|
+
raise ValueError(msg)
|
|
425
|
+
return self
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class DocArtifact(DatashareModel, ABC):
|
|
429
|
+
# This object is not used for serde, just as a container, it's OK to allow
|
|
430
|
+
# arbitrary types (to allow storing BytesIO)
|
|
431
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
432
|
+
|
|
261
433
|
project: str
|
|
262
434
|
doc_id: str
|
|
263
435
|
artifact: Annotated[bytes | BytesIO | Path, AfterValidator(_is_absolute_path)]
|
|
264
|
-
filename: str
|
|
265
|
-
|
|
436
|
+
filename: ClassVar[str] # Override this
|
|
437
|
+
type: ClassVar[ArtifactType] # Override this
|
|
438
|
+
manifest_entry: ManifestEntry
|
|
266
439
|
|
|
267
440
|
|
|
268
441
|
@unique
|
|
@@ -369,47 +542,3 @@ class TaskGroup:
|
|
|
369
542
|
@classmethod
|
|
370
543
|
def python(cls) -> Self:
|
|
371
544
|
return cls(name="PYTHON")
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
@dataclass(frozen=True)
|
|
375
|
-
class Shared:
|
|
376
|
-
cache_size: InitVar[int] = DEFAULT_SHARED_RESOURCES_SIZE
|
|
377
|
-
eviction_callback: InitVar[Callable] = None
|
|
378
|
-
_resources: LRU = field(init=False, repr=False)
|
|
379
|
-
_lock: Lock = field(init=False, repr=False)
|
|
380
|
-
|
|
381
|
-
def __post_init__(self, cache_size: int, eviction_callback: Callable) -> None:
|
|
382
|
-
object.__setattr__(
|
|
383
|
-
self, "_resources", LRU(cache_size, callback=eviction_callback)
|
|
384
|
-
)
|
|
385
|
-
object.__setattr__(self, "_lock", Lock())
|
|
386
|
-
|
|
387
|
-
def get_resource(
|
|
388
|
-
self, key: str, default: Any = None, *, set_if_unavailable: bool = True
|
|
389
|
-
) -> Any:
|
|
390
|
-
if key not in self._resources and set_if_unavailable:
|
|
391
|
-
self.set_resource(key, default)
|
|
392
|
-
|
|
393
|
-
return self._resources.get(key, default)
|
|
394
|
-
|
|
395
|
-
def set_resource(self, key: str, value: Any) -> None:
|
|
396
|
-
self._resources[key] = value
|
|
397
|
-
|
|
398
|
-
def pop_resource(self, key: str, default: Any = None) -> Any:
|
|
399
|
-
return self._resources.pop(key, default)
|
|
400
|
-
|
|
401
|
-
async def async_get_resource(
|
|
402
|
-
self, key: str, default: Any = None, *, set_if_unavailable: bool = True
|
|
403
|
-
) -> Any:
|
|
404
|
-
if key not in self._resources and set_if_unavailable:
|
|
405
|
-
await self.async_set_resource(key, default)
|
|
406
|
-
|
|
407
|
-
return self._resources.get(key, default)
|
|
408
|
-
|
|
409
|
-
async def async_set_resource(self, key: str, value: Any) -> None:
|
|
410
|
-
async with self._lock:
|
|
411
|
-
self._resources[key] = value
|
|
412
|
-
|
|
413
|
-
async def async_pop_resource(self, key: str, default: Any = None) -> Any:
|
|
414
|
-
async with self._lock:
|
|
415
|
-
return self._resources.pop(key, default)
|