datashare-python 0.9.8__tar.gz → 0.9.9__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.9.8 → datashare_python-0.9.9}/PKG-INFO +2 -2
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/cli/task.py +2 -2
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/constants.py +1 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/logging_.py +1 -1
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/objects.py +125 -6
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/utils.py +110 -20
- datashare_python-0.9.9/datashare_python/worker-template.tar.gz +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/pyproject.toml +2 -2
- datashare_python-0.9.8/datashare_python/worker-template.tar.gz +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/.gitignore +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/README.md +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/.gitignore +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/__init__.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/__main__.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/cli/__init__.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/cli/project.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/cli/utils.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/cli/worker.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/config.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/conftest.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/dependencies.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/discovery.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/exceptions.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/interceptors.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/task_client.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/template.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/types_.py +0 -0
- {datashare_python-0.9.8 → datashare_python-0.9.9}/datashare_python/worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datashare-python
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.9
|
|
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/
|
|
@@ -11,7 +11,7 @@ Requires-Python: <4,>=3.11
|
|
|
11
11
|
Requires-Dist: aiohttp~=3.11
|
|
12
12
|
Requires-Dist: alive-progress~=3.2
|
|
13
13
|
Requires-Dist: hatchling~=1.27
|
|
14
|
-
Requires-Dist: icij-common[elasticsearch]~=0.8.
|
|
14
|
+
Requires-Dist: icij-common[elasticsearch]~=0.8.3
|
|
15
15
|
Requires-Dist: langcodes~=3.5
|
|
16
16
|
Requires-Dist: lru-dict~=1.4
|
|
17
17
|
Requires-Dist: pydantic-extra-types[pycountry]>=2.11.1
|
|
@@ -28,7 +28,7 @@ _START_HELP = "creates a new task and start it"
|
|
|
28
28
|
_TASK_ID_HELP = "task ID"
|
|
29
29
|
_WATCH_HELP = "watch a task until it's complete"
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
StrTaskArgs = str
|
|
32
32
|
|
|
33
33
|
task_app = AsyncTyper(name="task")
|
|
34
34
|
|
|
@@ -36,7 +36,7 @@ task_app = AsyncTyper(name="task")
|
|
|
36
36
|
@task_app.async_command(help=_START_HELP)
|
|
37
37
|
async def start(
|
|
38
38
|
name: Annotated[str, typer.Argument(help=_NAME_HELP)],
|
|
39
|
-
args: Annotated[
|
|
39
|
+
args: Annotated[StrTaskArgs, typer.Argument(help=_ARGS_HELP)] = None,
|
|
40
40
|
group: Annotated[
|
|
41
41
|
str | None,
|
|
42
42
|
typer.Option("--group", "-g", help=_GROUP_HELP),
|
|
@@ -132,7 +132,7 @@ def _encode_value(value: Any) -> str:
|
|
|
132
132
|
return "true" if value else "false"
|
|
133
133
|
if isinstance(value, numbers.Number):
|
|
134
134
|
return str(value)
|
|
135
|
-
return json.dumps(value)
|
|
135
|
+
return json.dumps(value)
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
def _json_formatter(datefmt: str) -> BaseJsonFormatter:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
from abc import ABC
|
|
3
4
|
from asyncio import Lock
|
|
4
5
|
from collections.abc import Awaitable, Callable
|
|
5
6
|
from dataclasses import InitVar, dataclass, field
|
|
@@ -7,12 +8,15 @@ from datetime import UTC, datetime
|
|
|
7
8
|
from enum import StrEnum, unique
|
|
8
9
|
from io import BytesIO
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar, cast
|
|
11
|
+
from typing import Annotated, Any, ClassVar, Generic, Literal, Self, TypeVar, cast
|
|
11
12
|
|
|
12
13
|
import langcodes
|
|
14
|
+
from icij_common.registrable import Registrable
|
|
13
15
|
from lru import LRU
|
|
14
16
|
from pydantic_core import PydanticCustomError, ValidationError, core_schema
|
|
15
|
-
from pydantic_core.core_schema import
|
|
17
|
+
from pydantic_core.core_schema import (
|
|
18
|
+
PlainValidatorFunctionSchema,
|
|
19
|
+
)
|
|
16
20
|
from pydantic_extra_types.language_code import LanguageName
|
|
17
21
|
from temporalio import workflow
|
|
18
22
|
|
|
@@ -34,12 +38,17 @@ with workflow.unsafe.imports_passed_through():
|
|
|
34
38
|
from icij_common.pydantic_utils import (
|
|
35
39
|
icij_config,
|
|
36
40
|
lowercamel_case_config,
|
|
41
|
+
make_enum_discriminator,
|
|
37
42
|
merge_configs,
|
|
38
43
|
no_enum_values_config,
|
|
44
|
+
tagged_union,
|
|
39
45
|
)
|
|
40
46
|
from pydantic import (
|
|
41
47
|
AfterValidator,
|
|
48
|
+
AliasChoices,
|
|
42
49
|
BeforeValidator,
|
|
50
|
+
ConfigDict,
|
|
51
|
+
Discriminator,
|
|
43
52
|
Field,
|
|
44
53
|
GetCoreSchemaHandler,
|
|
45
54
|
TypeAdapter,
|
|
@@ -256,13 +265,123 @@ def _is_absolute_path(v: bytes | BytesIO | Path) -> Any:
|
|
|
256
265
|
return v
|
|
257
266
|
|
|
258
267
|
|
|
259
|
-
|
|
260
|
-
|
|
268
|
+
class ArtifactType(StrEnum):
|
|
269
|
+
STRUCTURE = "structure"
|
|
270
|
+
ASR_TRANSCRIPTION = "transcription"
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class ManifestEntryStatus(StrEnum):
|
|
274
|
+
COMPLETE = "complete"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class TaskArgs(DatashareModel, ABC):
|
|
278
|
+
def as_manifest_task_input(self) -> dict[str, Any]:
|
|
279
|
+
# This is a base implementation, if the input is too large to be dumped,
|
|
280
|
+
# override this and pop large keys
|
|
281
|
+
as_manifest = self.model_dump(by_alias=True)
|
|
282
|
+
return as_manifest
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
A = TypeVar("A", bound=TaskArgs)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class ManifestEntry(DatashareModel, Generic[A], ABC):
|
|
289
|
+
status: ManifestEntryStatus
|
|
290
|
+
label: str | None = None
|
|
291
|
+
input: Annotated[
|
|
292
|
+
dict[str, Any] | None,
|
|
293
|
+
Field(
|
|
294
|
+
validation_alias=AliasChoices("taskInput", "input"),
|
|
295
|
+
serialization_alias="taskInput",
|
|
296
|
+
),
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
@classmethod
|
|
300
|
+
def complete(cls, args: A, label: str | None = None, **kwargs) -> Self:
|
|
301
|
+
return cls(
|
|
302
|
+
input=args.as_manifest_task_input(),
|
|
303
|
+
label=label,
|
|
304
|
+
status=ManifestEntryStatus.COMPLETE,
|
|
305
|
+
**kwargs,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class PaginationType(StrEnum):
|
|
310
|
+
FILESYSTEM = "filesystem"
|
|
311
|
+
BYTE_RANGES = "byteRanges"
|
|
312
|
+
|
|
313
|
+
|
|
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
|
+
def _validate_pages_range(v: Any) -> None:
|
|
322
|
+
if not isinstance(v, list):
|
|
323
|
+
msg = f"expected a list, got {type(v)}"
|
|
324
|
+
raise TypeError(msg)
|
|
325
|
+
previous_end = None
|
|
326
|
+
for page_i, (start, end) in enumerate(v):
|
|
327
|
+
if not start <= end:
|
|
328
|
+
msg = "end of page must be >= start"
|
|
329
|
+
raise ValueError(msg)
|
|
330
|
+
if previous_end is not None and previous_end != start:
|
|
331
|
+
msg = (
|
|
332
|
+
f"start of page {page_i} doesn't match end of previous "
|
|
333
|
+
f"page {previous_end}"
|
|
334
|
+
)
|
|
335
|
+
raise ValueError(msg)
|
|
336
|
+
return v
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
PagesRange = Annotated[list[tuple[int, int]], AfterValidator(_validate_pages_range)]
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@BasePagination.register(PaginationType.FILESYSTEM)
|
|
343
|
+
class FilesystemPagination(BasePagination):
|
|
344
|
+
type: ClassVar[PaginationType] = Field(
|
|
345
|
+
default=PaginationType.FILESYSTEM, frozen=True
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@BasePagination.register(PaginationType.BYTE_RANGES)
|
|
350
|
+
class ByteRangesPagination(BasePagination):
|
|
351
|
+
type: ClassVar[PaginationType] = Field(
|
|
352
|
+
default=PaginationType.BYTE_RANGES, frozen=True
|
|
353
|
+
)
|
|
354
|
+
byte_ranges: PagesRange
|
|
355
|
+
|
|
356
|
+
@model_validator(mode="after")
|
|
357
|
+
def byte_ranges_length_should_match_total(self) -> Self:
|
|
358
|
+
if len(self.byte_ranges) != self.total:
|
|
359
|
+
msg = (
|
|
360
|
+
f"byte_ranges must match total. Found {len(self.byte_ranges)} for"
|
|
361
|
+
f" byte_ranges and {self.total} for total."
|
|
362
|
+
)
|
|
363
|
+
raise ValueError(msg)
|
|
364
|
+
return self
|
|
365
|
+
|
|
366
|
+
|
|
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):
|
|
375
|
+
# This object is not used for serde, just as a container, it's OK to allow
|
|
376
|
+
# arbitrary types (to allow storing BytesIO)
|
|
377
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
378
|
+
|
|
261
379
|
project: str
|
|
262
380
|
doc_id: str
|
|
263
381
|
artifact: Annotated[bytes | BytesIO | Path, AfterValidator(_is_absolute_path)]
|
|
264
|
-
filename: str
|
|
265
|
-
|
|
382
|
+
filename: ClassVar[str] # Override this
|
|
383
|
+
type: ClassVar[ArtifactType] # Override this
|
|
384
|
+
manifest_entry: ManifestEntry
|
|
266
385
|
|
|
267
386
|
|
|
268
387
|
@unique
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import contextlib
|
|
3
|
+
import fcntl
|
|
3
4
|
import inspect
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
import shutil
|
|
7
|
-
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable, Coroutine, Generator, Iterable, Sequence
|
|
8
11
|
from copy import deepcopy
|
|
9
12
|
from dataclasses import dataclass
|
|
10
13
|
from datetime import timedelta
|
|
@@ -36,7 +39,7 @@ from datashare_python.types_ import (
|
|
|
36
39
|
SyncProgressRateHandler,
|
|
37
40
|
)
|
|
38
41
|
|
|
39
|
-
from .constants import METADATA_JSON
|
|
42
|
+
from .constants import MANIFEST_JSON, METADATA_JSON
|
|
40
43
|
from .objects import DocArtifact, DocumentLocation, FilesystemDocument
|
|
41
44
|
from .types_ import RawAsyncProgressHandler
|
|
42
45
|
|
|
@@ -57,6 +60,11 @@ _NEVER_RETRIABLES = {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
|
|
63
|
+
_ARTIFACT_LOCK = threading.Lock()
|
|
64
|
+
# For test
|
|
65
|
+
_LOCKED = threading.Event()
|
|
66
|
+
|
|
67
|
+
|
|
60
68
|
@dataclass(frozen=True)
|
|
61
69
|
class Progress:
|
|
62
70
|
max_progress: float
|
|
@@ -338,34 +346,116 @@ def _metadata_path(doc_id: str, *, project: str) -> Path:
|
|
|
338
346
|
return metadata_path
|
|
339
347
|
|
|
340
348
|
|
|
341
|
-
def
|
|
342
|
-
|
|
349
|
+
def _manifest_path(doc_id: str, *, project: str) -> Path:
|
|
350
|
+
manifest_path = artifacts_dir(doc_id, project=project) / MANIFEST_JSON
|
|
351
|
+
return manifest_path
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _read_artifact_manifest(root: Path, artifact: DocArtifact) -> dict:
|
|
355
|
+
m_path = root / _manifest_path(artifact.doc_id, project=artifact.project)
|
|
356
|
+
if not m_path.exists():
|
|
357
|
+
m_path = root / _metadata_path(artifact.doc_id, project=artifact.project)
|
|
358
|
+
if not m_path.exists():
|
|
359
|
+
msg = f"couldn't find manifest nor metadata for {artifact.doc_id}"
|
|
360
|
+
raise FileNotFoundError(msg)
|
|
343
361
|
return json.loads(m_path.read_text())
|
|
344
362
|
|
|
345
363
|
|
|
346
|
-
def write_artifact(
|
|
364
|
+
def write_artifact(
|
|
365
|
+
root: Path, artifact: DocArtifact, lock_timeout_ms: int = 30_000
|
|
366
|
+
) -> Path:
|
|
367
|
+
# TODO: WARNING many writers could write at the time, to avoid inconsistent
|
|
368
|
+
# states we should handle this somehow
|
|
347
369
|
artif_dir = root / artifacts_dir(artifact.doc_id, project=artifact.project)
|
|
348
370
|
artif_dir.mkdir(exist_ok=True, parents=True)
|
|
349
|
-
|
|
350
|
-
#
|
|
351
|
-
|
|
352
|
-
|
|
371
|
+
artifact_path = artif_dir / artifact.filename
|
|
372
|
+
# We're using POSIX locks which are not exclusive to filedescriptors
|
|
373
|
+
# but to processes, we hence have to ensure several threads from the same
|
|
374
|
+
# Python process acquire the lock at the same time. We're using a python mutex lock
|
|
375
|
+
with artifact_lock(artif_dir, lock_timeout_ms):
|
|
376
|
+
# Read the metadata first (things could go wrong here in case someone is reading
|
|
377
|
+
# at the same time). We read in a backward compatible wat and write to that same
|
|
378
|
+
# location. We don't take responsibility for migrating the data, the DS back
|
|
379
|
+
# will do it
|
|
380
|
+
manifest_path, manifest = _read_manifest_backward_compatible(root, artifact)
|
|
381
|
+
is_legacy = manifest_path.name == "metadata.json"
|
|
382
|
+
# Pop the status key from the manifest before writing
|
|
383
|
+
manifest_entry = manifest.get(artifact.type)
|
|
384
|
+
if manifest_entry is not None and not is_legacy:
|
|
385
|
+
manifest[artifact.type].pop("status", None)
|
|
386
|
+
manifest_path.write_text(json.dumps(manifest))
|
|
387
|
+
# Write the artifact
|
|
388
|
+
_write_artifact_bytes(artifact_path, artifact.artifact)
|
|
389
|
+
# Update the manifest entry with details and new states
|
|
390
|
+
if is_legacy:
|
|
391
|
+
manifest_entry = str(artifact_path.relative_to(artif_dir))
|
|
392
|
+
else:
|
|
393
|
+
manifest_entry = artifact.manifest_entry.model_dump(
|
|
394
|
+
mode="json", by_alias=True
|
|
395
|
+
)
|
|
396
|
+
manifest[artifact.type] = manifest_entry
|
|
397
|
+
manifest_path.write_text(json.dumps(manifest))
|
|
398
|
+
return artifact_path.relative_to(artif_dir)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@contextlib.contextmanager
|
|
402
|
+
def _set_event() -> Generator[None, None, None]:
|
|
403
|
+
try:
|
|
404
|
+
_LOCKED.set()
|
|
405
|
+
yield
|
|
406
|
+
finally:
|
|
407
|
+
_LOCKED.clear()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@contextlib.contextmanager
|
|
411
|
+
def artifact_lock(
|
|
412
|
+
artifacts_dir: Path, lock_timeout_ms: int
|
|
413
|
+
) -> Generator[None, None, None]:
|
|
414
|
+
lock_path = artifacts_dir / f"{MANIFEST_JSON}.lock"
|
|
415
|
+
fd = os.open(lock_path, os.O_CREAT | os.O_WRONLY)
|
|
416
|
+
with _ARTIFACT_LOCK, _set_event():
|
|
417
|
+
start = time.time()
|
|
418
|
+
timeout_s = lock_timeout_ms / 1000
|
|
419
|
+
while time.time() - start < timeout_s:
|
|
420
|
+
try:
|
|
421
|
+
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
422
|
+
except OSError:
|
|
423
|
+
# TODO: we shouldn't block here, use asyncio instead
|
|
424
|
+
time.sleep(0.02)
|
|
425
|
+
continue
|
|
426
|
+
yield
|
|
427
|
+
return
|
|
428
|
+
msg = (
|
|
429
|
+
f"failed to acquire lock on {lock_path} in less than {lock_timeout_ms} ms."
|
|
430
|
+
)
|
|
431
|
+
raise TimeoutError(msg)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _read_manifest_backward_compatible(
|
|
435
|
+
root: Path, artifact: DocArtifact
|
|
436
|
+
) -> tuple[Path, dict[str, Any]]:
|
|
437
|
+
manifest_path = root / _manifest_path(artifact.doc_id, project=artifact.project)
|
|
438
|
+
if manifest_path.exists():
|
|
439
|
+
return manifest_path, _read_artifact_manifest(root, artifact)
|
|
440
|
+
meta_path = root / _metadata_path(artifact.doc_id, project=artifact.project)
|
|
441
|
+
if meta_path.exists():
|
|
442
|
+
return meta_path, _read_artifact_manifest(root, artifact)
|
|
443
|
+
return manifest_path, dict()
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _write_artifact_bytes(path: Path, artifact: bytes | BytesIO | Path) -> None:
|
|
447
|
+
match artifact:
|
|
353
448
|
case bytes():
|
|
354
|
-
|
|
449
|
+
path.write_bytes(artifact)
|
|
355
450
|
case BytesIO():
|
|
356
|
-
with
|
|
357
|
-
f.write(artifact.
|
|
451
|
+
with path.open("wb") as f:
|
|
452
|
+
f.write(artifact.read())
|
|
358
453
|
case Path():
|
|
359
|
-
|
|
360
|
-
shutil.move(artifact
|
|
454
|
+
path.unlink(missing_ok=True)
|
|
455
|
+
shutil.move(artifact, path)
|
|
361
456
|
case _:
|
|
362
|
-
msg = f"unsupported artifact type: {artifact.
|
|
457
|
+
msg = f"unsupported artifact type: {artifact.__class__.__name__}"
|
|
363
458
|
raise ValueError(msg)
|
|
364
|
-
meta_path = root / _metadata_path(artifact.doc_id, project=artifact.project)
|
|
365
|
-
meta = _read_artifact_metadata(root, artifact) if meta_path.exists() else dict()
|
|
366
|
-
meta[artifact.metadata_key] = artifact.filename
|
|
367
|
-
meta_path.write_text(json.dumps(meta))
|
|
368
|
-
return artifact_path.relative_to(artif_dir)
|
|
369
459
|
|
|
370
460
|
|
|
371
461
|
def debuggable_name(
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "datashare-python"
|
|
3
|
-
version = "0.9.
|
|
3
|
+
version = "0.9.9"
|
|
4
4
|
description = "Manage Python tasks and local resources in Datashare"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Clément Doumouro", email = "cdoumouro@icij.org" },
|
|
@@ -13,7 +13,7 @@ dependencies = [
|
|
|
13
13
|
"alive-progress~=3.2",
|
|
14
14
|
"aiohttp~=3.11",
|
|
15
15
|
"hatchling~=1.27",
|
|
16
|
-
"icij-common[elasticsearch]~=0.8.
|
|
16
|
+
"icij-common[elasticsearch]~=0.8.3",
|
|
17
17
|
"langcodes~=3.5",
|
|
18
18
|
"python-json-logger~=4.0",
|
|
19
19
|
"pyyaml~=6.0",
|
|
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
|