datashare-python 0.9.9__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.
@@ -1,9 +1,8 @@
1
1
  import logging
2
2
  import os
3
3
  from abc import ABC
4
- from asyncio import Lock
5
4
  from collections.abc import Awaitable, Callable
6
- from dataclasses import InitVar, dataclass, field
5
+ from dataclasses import dataclass
7
6
  from datetime import UTC, datetime
8
7
  from enum import StrEnum, unique
9
8
  from io import BytesIO
@@ -12,15 +11,12 @@ from typing import Annotated, Any, ClassVar, Generic, Literal, Self, TypeVar, ca
12
11
 
13
12
  import langcodes
14
13
  from icij_common.registrable import Registrable
15
- from lru import LRU
16
14
  from pydantic_core import PydanticCustomError, ValidationError, core_schema
17
- from pydantic_core.core_schema import (
18
- PlainValidatorFunctionSchema,
19
- )
15
+ from pydantic_core.core_schema import PlainValidatorFunctionSchema
20
16
  from pydantic_extra_types.language_code import LanguageName
21
17
  from temporalio import workflow
22
18
 
23
- from .constants import DEFAULT_SHARED_RESOURCES_SIZE, TIKA_METADATA_RESOURCENAME
19
+ from .constants import TIKA_METADATA_RESOURCENAME
24
20
 
25
21
  with workflow.unsafe.imports_passed_through():
26
22
  from icij_common.es import (
@@ -113,9 +109,15 @@ class DatashareLanguage(str):
113
109
  return self.as_language_name.alpha3
114
110
 
115
111
 
112
+ class WorkerPaths(BaseModel):
113
+ filesystem: Path
114
+ artifacts: Path
115
+ workdir: Path
116
+
117
+
116
118
  @unique
117
119
  class DocumentLocation(StrEnum):
118
- ORIGINAL = "original"
120
+ FILESYSTEM = "filesystem"
119
121
  ARTIFACTS = "artifacts"
120
122
  WORKDIR = "workdir"
121
123
 
@@ -128,30 +130,49 @@ def _is_relative(value: Path) -> Path:
128
130
  return value
129
131
 
130
132
 
131
- class FilesystemDocument(DatashareModel):
133
+ class ProcessedFile(BaseModel):
132
134
  id: str
133
135
  path: Annotated[Path, AfterValidator(_is_relative)]
134
- index: str
136
+ project: str
135
137
  location: DocumentLocation
136
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
+ )
137
156
 
138
- def locate(
139
- self, original_root: Path, *, artifacts_root: Path, workdir: Path
140
- ) -> Path:
157
+ def locate(self, paths: WorkerPaths) -> Path:
141
158
  from datashare_python.utils import artifacts_dir # noqa: PLC0415
142
159
 
143
160
  match self.location:
144
- case DocumentLocation.ORIGINAL:
145
- return original_root / self.path
161
+ case DocumentLocation.FILESYSTEM:
162
+ return paths.filesystem / self.path
146
163
  case DocumentLocation.ARTIFACTS:
147
- project = self.index
148
- return artifacts_root / artifacts_dir(self.id, project=project) / "raw"
164
+ project = self.project
165
+ return paths.artifacts / artifacts_dir(self.id, project=project) / "raw"
149
166
  case DocumentLocation.WORKDIR:
150
- return workdir / self.path
167
+ return paths.workdir / self.path
151
168
  case _:
152
169
  raise ValueError(f"invalid location: {self.path}")
153
170
 
154
171
 
172
+ class ProcessedPage(ProcessedFile):
173
+ page_number: int
174
+
175
+
155
176
  class IETFLanguage(str):
156
177
  @classmethod
157
178
  def __get_pydantic_core_schema__(
@@ -183,6 +204,11 @@ class Translation(BaseModel): # No camelcase here we don't know why
183
204
  content: Annotated[str, BeforeValidator(_from_sentences)]
184
205
 
185
206
 
207
+ Routing = str
208
+ DocID = str
209
+ DocRoute = tuple[DocID, Routing]
210
+
211
+
186
212
  class Document(DatashareModel):
187
213
  id: str
188
214
  language: DatashareLanguage
@@ -226,17 +252,17 @@ class Document(DatashareModel):
226
252
  metadata=sources.get(DOC_METADATA),
227
253
  )
228
254
 
229
- def to_filesystem(self) -> FilesystemDocument:
255
+ def to_processed_file(self) -> ProcessedFile:
230
256
  from .utils import artifacts_dir # noqa: PLC0415
231
257
 
232
258
  if self.metadata is None:
233
259
  raise ValueError(
234
- "can't compute filesyste path for document withtout metadata"
260
+ "can't compute filesystem path for document withtout metadata"
235
261
  )
236
262
  resource_name = cast(str, self.metadata[TIKA_METADATA_RESOURCENAME])
237
263
  if self.root_document is None:
238
264
  path = self.path
239
- location = DocumentLocation.ORIGINAL
265
+ location = DocumentLocation.FILESYSTEM
240
266
  else:
241
267
  if self.index is None:
242
268
  msg = (
@@ -250,14 +276,23 @@ class Document(DatashareModel):
250
276
  # we store a relative path otherwise joining with the location will fail
251
277
  if path.parts and path.parts[0] == os.path.sep:
252
278
  path = Path(*path.parts[1:])
253
- return FilesystemDocument(
279
+ n_pages = 1
280
+ if self.metadata:
281
+ n_pages = self.metadata.get("tika_metadata_xmptpg_npages", n_pages)
282
+ return ProcessedFile(
254
283
  id=self.id,
255
284
  path=path,
256
- index=self.index,
285
+ project=self.index,
257
286
  location=location,
258
287
  resource_name=resource_name,
288
+ n_pages=n_pages,
259
289
  )
260
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
+
261
296
 
262
297
  def _is_absolute_path(v: bytes | BytesIO | Path) -> Any:
263
298
  if isinstance(v, Path) and not v.is_absolute():
@@ -268,17 +303,20 @@ def _is_absolute_path(v: bytes | BytesIO | Path) -> Any:
268
303
  class ArtifactType(StrEnum):
269
304
  STRUCTURE = "structure"
270
305
  ASR_TRANSCRIPTION = "transcription"
306
+ PASSPORTS = "passports"
271
307
 
272
308
 
273
309
  class ManifestEntryStatus(StrEnum):
274
310
  COMPLETE = "complete"
311
+ PARTIAL = "partial"
275
312
 
276
313
 
277
314
  class TaskArgs(DatashareModel, ABC):
278
315
  def as_manifest_task_input(self) -> dict[str, Any]:
279
316
  # This is a base implementation, if the input is too large to be dumped,
280
317
  # override this and pop large keys
281
- as_manifest = self.model_dump(by_alias=True)
318
+ # Dump in json mode to make testing easier
319
+ as_manifest = self.model_dump(by_alias=True, mode="json")
282
320
  return as_manifest
283
321
 
284
322
 
@@ -305,19 +343,21 @@ class ManifestEntry(DatashareModel, Generic[A], ABC):
305
343
  **kwargs,
306
344
  )
307
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
+
308
355
 
309
356
  class PaginationType(StrEnum):
310
357
  FILESYSTEM = "filesystem"
311
358
  BYTE_RANGES = "byteRanges"
312
359
 
313
360
 
314
- class BasePagination(DatashareModel, Registrable, ABC):
315
- registry_key: ClassVar[str] = Field(frozen=True, default="type")
316
-
317
- total: int
318
- type: ClassVar[PaginationType] = Field(frozen=True)
319
-
320
-
321
361
  def _validate_pages_range(v: Any) -> None:
322
362
  if not isinstance(v, list):
323
363
  msg = f"expected a list, got {type(v)}"
@@ -339,6 +379,11 @@ def _validate_pages_range(v: Any) -> None:
339
379
  PagesRange = Annotated[list[tuple[int, int]], AfterValidator(_validate_pages_range)]
340
380
 
341
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
+
342
387
  @BasePagination.register(PaginationType.FILESYSTEM)
343
388
  class FilesystemPagination(BasePagination):
344
389
  type: ClassVar[PaginationType] = Field(
@@ -353,25 +398,34 @@ class ByteRangesPagination(BasePagination):
353
398
  )
354
399
  byte_ranges: PagesRange
355
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
+
356
413
  @model_validator(mode="after")
357
414
  def byte_ranges_length_should_match_total(self) -> Self:
358
- if len(self.byte_ranges) != self.total:
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)
359
420
  msg = (
360
- f"byte_ranges must match total. Found {len(self.byte_ranges)} for"
421
+ f"byte_ranges must match total. Found {n_pages} for"
361
422
  f" byte_ranges and {self.total} for total."
362
423
  )
363
424
  raise ValueError(msg)
364
425
  return self
365
426
 
366
427
 
367
- pagination_discriminator = make_enum_discriminator("type", PaginationType)
368
- Pagination = Annotated[
369
- tagged_union(BasePagination.__subclasses__(), lambda x: x.type.default),
370
- Discriminator(pagination_discriminator),
371
- ]
372
-
373
-
374
- class DocArtifact(BaseModel, ABC):
428
+ class DocArtifact(DatashareModel, ABC):
375
429
  # This object is not used for serde, just as a container, it's OK to allow
376
430
  # arbitrary types (to allow storing BytesIO)
377
431
  model_config = ConfigDict(arbitrary_types_allowed=True)
@@ -488,47 +542,3 @@ class TaskGroup:
488
542
  @classmethod
489
543
  def python(cls) -> Self:
490
544
  return cls(name="PYTHON")
491
-
492
-
493
- @dataclass(frozen=True)
494
- class Shared:
495
- cache_size: InitVar[int] = DEFAULT_SHARED_RESOURCES_SIZE
496
- eviction_callback: InitVar[Callable] = None
497
- _resources: LRU = field(init=False, repr=False)
498
- _lock: Lock = field(init=False, repr=False)
499
-
500
- def __post_init__(self, cache_size: int, eviction_callback: Callable) -> None:
501
- object.__setattr__(
502
- self, "_resources", LRU(cache_size, callback=eviction_callback)
503
- )
504
- object.__setattr__(self, "_lock", Lock())
505
-
506
- def get_resource(
507
- self, key: str, default: Any = None, *, set_if_unavailable: bool = True
508
- ) -> Any:
509
- if key not in self._resources and set_if_unavailable:
510
- self.set_resource(key, default)
511
-
512
- return self._resources.get(key, default)
513
-
514
- def set_resource(self, key: str, value: Any) -> None:
515
- self._resources[key] = value
516
-
517
- def pop_resource(self, key: str, default: Any = None) -> Any:
518
- return self._resources.pop(key, default)
519
-
520
- async def async_get_resource(
521
- self, key: str, default: Any = None, *, set_if_unavailable: bool = True
522
- ) -> Any:
523
- if key not in self._resources and set_if_unavailable:
524
- await self.async_set_resource(key, default)
525
-
526
- return self._resources.get(key, default)
527
-
528
- async def async_set_resource(self, key: str, value: Any) -> None:
529
- async with self._lock:
530
- self._resources[key] = value
531
-
532
- async def async_pop_resource(self, key: str, default: Any = None) -> Any:
533
- async with self._lock:
534
- return self._resources.pop(key, default)