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/utils.py CHANGED
@@ -1,21 +1,36 @@
1
1
  import asyncio
2
2
  import contextlib
3
+ import fcntl
3
4
  import inspect
4
5
  import json
6
+ import logging
5
7
  import os
6
8
  import shutil
7
- from collections.abc import Callable, Coroutine, Iterable, Sequence
9
+ import sys
10
+ import threading
11
+ import time
12
+ from collections.abc import (
13
+ AsyncIterable,
14
+ Awaitable,
15
+ Callable,
16
+ Coroutine,
17
+ Generator,
18
+ Iterable,
19
+ Sequence,
20
+ )
8
21
  from copy import deepcopy
9
22
  from dataclasses import dataclass
10
23
  from datetime import timedelta
11
- from functools import wraps
24
+ from functools import cache, wraps
12
25
  from hashlib import sha256
13
26
  from io import BytesIO
14
27
  from pathlib import Path
15
- from typing import Any, ParamSpec, TypeVar
28
+ from typing import Any, ParamSpec, Self, TypeVar
16
29
  from uuid import uuid4
17
30
 
18
31
  import temporalio
32
+ from aiofile import async_open
33
+ from lru import LRU
19
34
  from pydantic import ValidationError
20
35
  from temporalio import activity, workflow
21
36
  from temporalio.api.common.v1 import Payload
@@ -36,10 +51,19 @@ from datashare_python.types_ import (
36
51
  SyncProgressRateHandler,
37
52
  )
38
53
 
39
- from .constants import METADATA_JSON
40
- from .objects import DocArtifact, DocumentLocation, FilesystemDocument
54
+ from .constants import MANIFEST_JSON, METADATA_JSON
55
+ from .mimetypes_ import types_map
56
+ from .objects import (
57
+ BaseModel,
58
+ DocArtifact,
59
+ DocumentLocation,
60
+ ProcessedFile,
61
+ WorkerPaths,
62
+ )
41
63
  from .types_ import RawAsyncProgressHandler
42
64
 
65
+ logger = logging.getLogger(__name__)
66
+
43
67
  DependencyLabel = str | None
44
68
  DependencySetup = Callable[..., None]
45
69
  DependencyAsyncSetup = Callable[..., Coroutine[None, None, None]]
@@ -57,6 +81,11 @@ _NEVER_RETRIABLES = {
57
81
  }
58
82
 
59
83
 
84
+ _ARTIFACT_LOCK = threading.Lock()
85
+ # For test
86
+ _LOCKED = threading.Event()
87
+
88
+
60
89
  @dataclass(frozen=True)
61
90
  class Progress:
62
91
  max_progress: float
@@ -126,7 +155,7 @@ def _retry_policy_with_default(retry_policy: RetryPolicy | None) -> RetryPolicy:
126
155
 
127
156
 
128
157
  async def execute_activity(
129
- activity: Callable,
158
+ activity: Callable | str,
130
159
  task_queue: str,
131
160
  arg: Any = temporalio.common._arg_unset,
132
161
  *,
@@ -156,7 +185,7 @@ def positional_args_only(activity_fn: Callable[P, T]) -> Callable[P, T]:
156
185
  params = list(sig.parameters.values())
157
186
  keyword_only = {p.name for p in params if p.kind == inspect.Parameter.KEYWORD_ONLY}
158
187
 
159
- if asyncio.iscoroutinefunction(activity_fn):
188
+ if inspect.iscoroutinefunction(activity_fn):
160
189
 
161
190
  @wraps(activity_fn)
162
191
  async def wrapper(*args, **kwargs) -> T:
@@ -203,7 +232,7 @@ def with_retriables(
203
232
  if retriables is None:
204
233
 
205
234
  def decorator(activity_fn: Callable[P, T]) -> Callable[P, T]:
206
- if asyncio.iscoroutinefunction(activity_fn):
235
+ if inspect.iscoroutinefunction(activity_fn):
207
236
 
208
237
  @wraps(activity_fn)
209
238
  async def wrapper(*args, **kwargs) -> T:
@@ -225,7 +254,7 @@ def with_retriables(
225
254
  return decorator
226
255
 
227
256
  def decorator(activity_fn: Callable[P, T]) -> Callable[P, T]:
228
- if asyncio.iscoroutinefunction(activity_fn):
257
+ if inspect.iscoroutinefunction(activity_fn):
229
258
 
230
259
  @wraps(activity_fn)
231
260
  async def wrapper(*args, **kwargs) -> T:
@@ -261,7 +290,7 @@ def activity_defn(
261
290
  activity_fn = with_retriables(retriables)(activity_fn)
262
291
  activity_fn = activity.defn(activity_fn, name=name)
263
292
 
264
- is_async = asyncio.iscoroutinefunction(activity_fn)
293
+ is_async = inspect.iscoroutinefunction(activity_fn)
265
294
  if is_async:
266
295
 
267
296
  @wraps(activity_fn)
@@ -296,6 +325,20 @@ def to_raw_async_progress(
296
325
  return raw
297
326
 
298
327
 
328
+ def to_incremental_async_progress(
329
+ progress: RawAsyncProgressHandler,
330
+ ) -> RawAsyncProgressHandler:
331
+
332
+ offset = 0
333
+
334
+ async def incremental(p: int) -> None:
335
+ nonlocal offset
336
+ offset += p
337
+ await progress(offset)
338
+
339
+ return incremental
340
+
341
+
299
342
  def to_raw_sync_progress(
300
343
  progress: SyncProgressRateHandler, max_progress: int
301
344
  ) -> RawSyncProgressHandler:
@@ -338,34 +381,122 @@ def _metadata_path(doc_id: str, *, project: str) -> Path:
338
381
  return metadata_path
339
382
 
340
383
 
341
- def _read_artifact_metadata(root: Path, artifact: DocArtifact) -> dict:
342
- m_path = root / _metadata_path(artifact.doc_id, project=artifact.project)
384
+ def _manifest_path(doc_id: str, *, project: str) -> Path:
385
+ manifest_path = artifacts_dir(doc_id, project=project) / MANIFEST_JSON
386
+ return manifest_path
387
+
388
+
389
+ def _read_artifact_manifest(root: Path, artifact: DocArtifact) -> dict:
390
+ m_path = root / _manifest_path(artifact.doc_id, project=artifact.project)
391
+ if not m_path.exists():
392
+ m_path = root / _metadata_path(artifact.doc_id, project=artifact.project)
393
+ if not m_path.exists():
394
+ msg = f"couldn't find manifest nor metadata for {artifact.doc_id}"
395
+ raise FileNotFoundError(msg)
343
396
  return json.loads(m_path.read_text())
344
397
 
345
398
 
346
- def write_artifact(root: Path, artifact: DocArtifact) -> Path:
399
+ def write_artifact(
400
+ root: Path, artifact: DocArtifact, lock_timeout_ms: int = 30_000
401
+ ) -> Path:
402
+ # TODO: WARNING many writers could write at the time, to avoid inconsistent
403
+ # states we should handle this somehow
347
404
  artif_dir = root / artifacts_dir(artifact.doc_id, project=artifact.project)
348
405
  artif_dir.mkdir(exist_ok=True, parents=True)
349
- # TODO: if transcriptions are too large we could also serialize them
350
- # as jsonl
351
- artifact_path: Path = artif_dir / artifact.filename
352
- match artifact.artifact:
406
+ artifact_path = artif_dir / artifact.filename
407
+ # We're using POSIX locks which are not exclusive to filedescriptors
408
+ # but to processes, we hence have to ensure several threads from the same
409
+ # Python process acquire the lock at the same time. We're using a python mutex lock
410
+ with artifact_lock(artif_dir, lock_timeout_ms):
411
+ # Read the metadata first (things could go wrong here in case someone is reading
412
+ # at the same time). We read in a backward compatible wat and write to that same
413
+ # location. We don't take responsibility for migrating the data, the DS back
414
+ # will do it
415
+ manifest_path, manifest = _read_manifest_backward_compatible(root, artifact)
416
+ is_legacy = manifest_path.name == "metadata.json"
417
+ # Pop the status key from the manifest before writing
418
+ manifest_entry = manifest.get(artifact.type)
419
+ if manifest_entry is not None and not is_legacy:
420
+ manifest[artifact.type].pop("status", None)
421
+ manifest_path.write_text(json.dumps(manifest))
422
+ # Write the artifact
423
+ _write_artifact_bytes(artifact_path, artifact.artifact)
424
+ # Update the manifest entry with details and new states
425
+ if is_legacy:
426
+ manifest_entry = str(artifact_path.relative_to(artif_dir))
427
+ else:
428
+ manifest_entry = artifact.manifest_entry.model_dump(
429
+ mode="json", by_alias=True
430
+ )
431
+ manifest[artifact.type] = manifest_entry
432
+ manifest_path.write_text(json.dumps(manifest))
433
+ return artifact_path.relative_to(root)
434
+
435
+
436
+ @contextlib.contextmanager
437
+ def _set_event() -> Generator[None, None, None]:
438
+ try:
439
+ _LOCKED.set()
440
+ yield
441
+ finally:
442
+ _LOCKED.clear()
443
+
444
+
445
+ @contextlib.contextmanager
446
+ def artifact_lock(
447
+ artifacts_dir: Path, lock_timeout_ms: int
448
+ ) -> Generator[None, None, None]:
449
+ lock_path = artifacts_dir / f"{MANIFEST_JSON}.lock"
450
+ acquired = False
451
+ fd = os.open(lock_path, os.O_CREAT | os.O_WRONLY)
452
+ try:
453
+ with _ARTIFACT_LOCK, _set_event():
454
+ start = time.time()
455
+ timeout_s = lock_timeout_ms / 1000
456
+ while time.time() - start < timeout_s:
457
+ try:
458
+ _, acquired = fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB), True
459
+ except OSError:
460
+ # TODO: we shouldn't block here, use asyncio instead
461
+ time.sleep(0.02)
462
+ continue
463
+ yield
464
+ return
465
+ msg = (
466
+ f"failed to acquire lock on {lock_path} in less than "
467
+ f"{lock_timeout_ms} ms."
468
+ )
469
+ raise TimeoutError(msg)
470
+ finally:
471
+ if acquired:
472
+ lock_path.unlink(missing_ok=True)
473
+
474
+
475
+ def _read_manifest_backward_compatible(
476
+ root: Path, artifact: DocArtifact
477
+ ) -> tuple[Path, dict[str, Any]]:
478
+ manifest_path = root / _manifest_path(artifact.doc_id, project=artifact.project)
479
+ if manifest_path.exists():
480
+ return manifest_path, _read_artifact_manifest(root, artifact)
481
+ meta_path = root / _metadata_path(artifact.doc_id, project=artifact.project)
482
+ if meta_path.exists():
483
+ return meta_path, _read_artifact_manifest(root, artifact)
484
+ return manifest_path, dict()
485
+
486
+
487
+ def _write_artifact_bytes(path: Path, artifact: bytes | BytesIO | Path) -> None:
488
+ match artifact:
353
489
  case bytes():
354
- artifact_path.write_bytes(artifact.artifact)
490
+ path.write_bytes(artifact)
355
491
  case BytesIO():
356
- with artifact_path.open("wb") as f:
357
- f.write(artifact.artifact.read())
492
+ with path.open("wb") as f:
493
+ f.write(artifact.read())
358
494
  case Path():
359
- artifact_path.unlink(missing_ok=True)
360
- shutil.move(artifact.artifact, artifact_path)
495
+ path.unlink(missing_ok=True)
496
+ shutil.move(artifact, path)
361
497
  case _:
362
- msg = f"unsupported artifact type: {artifact.artifact.__class__.__name__}"
498
+ msg = f"unsupported artifact type: {artifact.__class__.__name__}"
363
499
  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
500
 
370
501
 
371
502
  def debuggable_name(
@@ -436,35 +567,44 @@ def activity_workdir(
436
567
 
437
568
 
438
569
  def symlink_embedded_document_to_workdir(
439
- doc: FilesystemDocument, artifacts_root: Path, *, workdir: Path
440
- ) -> FilesystemDocument:
570
+ doc: ProcessedFile, paths: WorkerPaths
571
+ ) -> ProcessedFile:
441
572
  match doc.location:
442
573
  case DocumentLocation.ARTIFACTS:
443
- symlinks_dir = workdir / doc.index / "symlinks"
574
+ symlinks_dir = paths.workdir / doc.project / "symlinks"
444
575
  symlinks_dir.mkdir(parents=True, exist_ok=True)
445
- symlink_path = Path(*doc.path.parts[:-1], doc.id)
446
- # Replace the "raw" with the doc id
447
- doc_ext = Path(doc.resource_name).suffix
448
- symlink_path = symlink_path.relative_to(Path(doc.index))
449
- symlink_path = symlinks_dir / f"{symlink_path}{doc_ext}"
576
+ symlink_path = Path(*doc.path.parts[1:-1], doc.resource_name)
577
+ symlink_path = symlinks_dir / symlink_path
578
+ # Replace the "raw" with the resource name
450
579
  symlink_path.parent.mkdir(parents=True, exist_ok=True)
451
- artifact_path = artifacts_root / doc.path
580
+ artifact_path = paths.artifacts / doc.path
452
581
  with contextlib.suppress(FileExistsError):
453
582
  os.symlink(artifact_path, symlink_path)
454
- return FilesystemDocument(
455
- path=symlink_path.relative_to(workdir),
583
+ return ProcessedFile(
584
+ path=symlink_path.relative_to(paths.workdir),
456
585
  id=doc.id,
457
586
  location=DocumentLocation.WORKDIR,
458
- index=doc.index,
587
+ project=doc.project,
459
588
  resource_name=doc.resource_name,
589
+ n_pages=doc.n_pages,
460
590
  )
461
- case DocumentLocation.ORIGINAL:
591
+ case DocumentLocation.FILESYSTEM:
462
592
  return doc
463
593
  case _:
464
594
  raise ValueError(f"unsupported location {doc.location}")
465
595
 
466
596
 
467
- def read_jsonl(path: Path) -> Iterable[dict]:
597
+ def artifact_path(
598
+ doc_id: str,
599
+ artifact_type: type[DocArtifact],
600
+ *,
601
+ project: str,
602
+ root: Path,
603
+ ) -> Path:
604
+ return root / artifacts_dir(doc_id, project=project) / artifact_type.filename
605
+
606
+
607
+ def _read_jsonl(path: Path) -> Iterable[dict]:
468
608
  with path.open() as f:
469
609
  for line in f:
470
610
  line = line.strip() # noqa: PLW2901
@@ -472,6 +612,77 @@ def read_jsonl(path: Path) -> Iterable[dict]:
472
612
  yield json.loads(line)
473
613
 
474
614
 
615
+ M = TypeVar("M", bound=BaseModel)
616
+
617
+
618
+ def read_jsonl_as(path: Path, cls: type[M]) -> Iterable[M]:
619
+ return (cls.model_validate(d) for d in _read_jsonl(path))
620
+
621
+
622
+ async def async_read_jsonl_as(
623
+ path: Path, processed_file_cls: type[M]
624
+ ) -> AsyncIterable[M]:
625
+ async with async_open(path, "r") as f:
626
+ async for line in f:
627
+ line = line.strip() # noqa: PLW2901
628
+ if line:
629
+ yield processed_file_cls.model_validate_json(line)
630
+
631
+
632
+ @cache
633
+ def ext_to_mime_types(ext: str) -> set[str]:
634
+ # All particular cases
635
+ match ext:
636
+ case ".nxml" | ".dclg" | ".dclg_xml":
637
+ return ext_to_mime_types(".xml")
638
+ case ".qmd" | ".rmd":
639
+ return ext_to_mime_types(".md")
640
+ case ".xbrl":
641
+ return ext_to_mime_types(".html")
642
+ case ".j2c" | ".j2k" | ".jpc" | ".jpf" | ".jpx":
643
+ return ext_to_mime_types(".jp2")
644
+ case ".rgba":
645
+ return ext_to_mime_types(".png")
646
+ case ".svm":
647
+ return ext_to_mime_types(".wmf")
648
+ case ".uof" | ".uop" | ".uos" | ".uot":
649
+ return ext_to_mime_types(".xml")
650
+ case ".dclg.xml":
651
+ return ext_to_mime_types(".xml")
652
+ try:
653
+ return types_map()[ext]
654
+ except KeyError as e:
655
+ raise ValueError(f"unsupported mimetype {ext}") from e
656
+
657
+
658
+ async def publish_and_consume(
659
+ publisher: asyncio.Task,
660
+ publisher_completion_callback: Callable[[], None],
661
+ *,
662
+ consumer: asyncio.Task,
663
+ ) -> tuple[Any, Any]:
664
+ # Publish and consume concurrently
665
+ logger.debug("starting publish and subscribe")
666
+ done, pending = await asyncio.wait(
667
+ [publisher, consumer], return_when=asyncio.FIRST_COMPLETED
668
+ )
669
+ for d in done:
670
+ # Stop everything case of exception
671
+ exc = d.exception()
672
+ if exc:
673
+ for p in pending:
674
+ p.cancel()
675
+ raise exc
676
+ # Wait for publish to be done and push the poison pill to stop consuming
677
+ p_res = await publisher
678
+ publisher_completion_callback()
679
+ logger.debug("done publishing, waiting for consumer to complete...")
680
+ # Wait for consumption to be done
681
+ c_res = await consumer
682
+ logger.debug("done consuming !")
683
+ return p_res, c_res
684
+
685
+
475
686
  class _PydanticPayloadConverter(CompositePayloadConverter):
476
687
  def __init__(self) -> None:
477
688
  json_payload_converter = PydanticJSONPlainPayloadConverter(
@@ -504,3 +715,97 @@ class _PydanticPayloadConverter(CompositePayloadConverter):
504
715
  PYDANTIC_DATA_CONVERTER = DataConverter(
505
716
  payload_converter_class=_PydanticPayloadConverter
506
717
  )
718
+
719
+
720
+ class SharedResources:
721
+ def __init__(
722
+ self,
723
+ cache_size: int = 1,
724
+ eviction_callback: Callable[[Any, Any], None] | None = None,
725
+ ) -> None:
726
+ self._eviction_callback = eviction_callback
727
+ self._cache = LRU(cache_size, self._eviction_callback)
728
+ self._sentinel = object()
729
+
730
+ def __enter__(self) -> Self:
731
+ return self
732
+
733
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None: # noqa: ANN001
734
+ if self._eviction_callback is not None:
735
+ for k, v in self._cache.items():
736
+ self._eviction_callback(k, v)
737
+
738
+ def get_or_cache_resource(
739
+ self, key: str, default_factory: Callable[[], Any]
740
+ ) -> Any:
741
+ value = self._cache.get(key, self._sentinel)
742
+ if value is not self._sentinel:
743
+ return value
744
+ # Get the value first to be sure to return the right one in case of concurrent
745
+ # access.
746
+ #
747
+ # Additionally, the factory can be long to complete (that's one of the reason
748
+ # we want to cache its results). Because we don't lock the factory call,
749
+ # together with the cache update, in case of concurrent access,
750
+ # quicker factories will complete first and will stay longer in the cache.
751
+ # This is fine, the alternative is to lock factory + cache update to avoid
752
+ # concurrent access but this will mean waiting for the first factory call to
753
+ # complete, this can potentially imply longer waits than no getting the value
754
+ # from the cache
755
+ value = default_factory()
756
+ self._cache[key] = value
757
+ return value
758
+
759
+ async def async_get_or_cache_resource(
760
+ self, key: str, default_factory: Callable[[], Awaitable[Any]]
761
+ ) -> Any:
762
+ value = self._cache.get(key, self._sentinel)
763
+ if value is not self._sentinel:
764
+ return value
765
+ # Get the value first to be sure to return the right one in case of concurrent
766
+ # access.
767
+ #
768
+ # Additionally, the factory can be long to complete (that's one of the reason
769
+ # we want to cache its results). Because we don't lock the factory call,
770
+ # together with the cache update, in case of concurrent access,
771
+ # quicker factories will complete first and will stay longer in the cache.
772
+ # This is fine, the alternative is to lock factory + cache update to avoid
773
+ # concurrent access but this will mean waiting for the first factory call to
774
+ # complete, this can potentially imply longer waits than no getting the value
775
+ # from the cache
776
+ value = await default_factory()
777
+ self._cache[key] = value
778
+ return value
779
+
780
+
781
+ def close_cm_callback(key: str, value: Any) -> None: # noqa: ARG001
782
+ if hasattr(value, "__exit__"):
783
+ value.__exit__(*sys.exc_info())
784
+
785
+
786
+ def enter_cm(
787
+ cm_factory: Callable[[], contextlib.AbstractContextManager],
788
+ ) -> Callable[[], contextlib.AbstractContextManager]: # noqa: ARG001
789
+ @wraps(cm_factory)
790
+ def factory() -> contextlib.AbstractContextManager:
791
+ cm = cm_factory()
792
+ cm.__enter__()
793
+ return cm
794
+
795
+ return factory
796
+
797
+
798
+ def async_enter_cm(
799
+ cm_factory: Callable[[], contextlib.AbstractAsyncContextManager],
800
+ ) -> Callable[[], Awaitable[contextlib.AbstractAsyncContextManager]]: # noqa: ARG001
801
+ @wraps(cm_factory)
802
+ async def wrapper() -> contextlib.AbstractAsyncContextManager:
803
+ cm = cm_factory()
804
+ await cm.__aenter__()
805
+ return cm
806
+
807
+ return wrapper
808
+
809
+
810
+ def config_cache_key(config: BaseModel) -> str:
811
+ return str(hash(config))
Binary file
@@ -61,6 +61,16 @@ class DatashareWorker(Worker):
61
61
  raise ValueError("worker is not running")
62
62
  await self._async_context_run_task
63
63
 
64
+ async def __aexit__(self, exc_type: type[BaseException] | None, *args: Any) -> None:
65
+ await super().__aexit__(exc_type, *args)
66
+ logger.info("worker %s exited", self._config["identity"])
67
+
68
+ async def __aenter__(self) -> "DatashareWorker":
69
+ await super().__aenter__()
70
+ # TODO: remove me
71
+ logger.info("worker %s started working", self._config["identity"])
72
+ return self
73
+
64
74
 
65
75
  def datashare_worker(
66
76
  client: TemporalClient,
@@ -71,7 +81,8 @@ def datashare_worker(
71
81
  task_queue: str,
72
82
  # Scale horizontally be default for activities, each worker processes one activity
73
83
  # at a time
74
- max_concurrent_io_activities: int = 10,
84
+ max_concurrent_activities: int = 1,
85
+ max_activities_per_second: float = 20.0,
75
86
  sandboxed: bool = True,
76
87
  ) -> DatashareWorker:
77
88
  if workflows is None:
@@ -91,7 +102,6 @@ def datashare_worker(
91
102
  )
92
103
  logger.warning(_SEPARATE_IO_AND_CPU_ACTIVITIES)
93
104
 
94
- max_concurrent_activities = max_concurrent_io_activities
95
105
  if isinstance(activity_executor, ThreadPoolExecutor):
96
106
  max_concurrent_activities = 1
97
107
  if workflows:
@@ -118,6 +128,7 @@ def datashare_worker(
118
128
  # several of them
119
129
  workflow_task_poller_behavior=PollerBehaviorSimpleMaximum(5),
120
130
  workflow_runner=wf_runner,
131
+ max_activities_per_second=max_activities_per_second,
121
132
  )
122
133
 
123
134
 
@@ -204,7 +215,7 @@ async def worker_context(
204
215
  workflows=workflows,
205
216
  activities=acts,
206
217
  task_queue=task_queue,
207
- max_concurrent_io_activities=worker_config.max_concurrent_io_activities,
218
+ max_concurrent_activities=worker_config.max_concurrent_activities,
208
219
  sandboxed=sandboxed,
209
220
  )
210
221
  async with worker:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datashare-python
3
- Version: 0.9.8
3
+ Version: 0.10.0.dev0
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/
@@ -8,15 +8,16 @@ Project-URL: Repository, https://github.com/ICIJ/datashare-python
8
8
  Project-URL: Issues, https://github.com/ICIJ/datashare-python/issues
9
9
  Author-email: Clément Doumouro <cdoumouro@icij.org>, Clément Doumouro <clement.doumouro@gmail.com>, Lion Summerbell <lsummerbell@icij.org>
10
10
  Requires-Python: <4,>=3.11
11
+ Requires-Dist: aiofile~=3.11
11
12
  Requires-Dist: aiohttp~=3.11
12
13
  Requires-Dist: alive-progress~=3.2
13
14
  Requires-Dist: hatchling~=1.27
14
- Requires-Dist: icij-common[elasticsearch]~=0.8.2
15
+ Requires-Dist: icij-common[elasticsearch]~=0.8.3
15
16
  Requires-Dist: langcodes~=3.5
16
17
  Requires-Dist: lru-dict~=1.4
17
18
  Requires-Dist: pydantic-extra-types[pycountry]>=2.11.1
18
19
  Requires-Dist: python-json-logger~=4.0
19
20
  Requires-Dist: pyyaml~=6.0
20
- Requires-Dist: temporalio~=1.23
21
+ Requires-Dist: temporalio~=1.31
21
22
  Requires-Dist: tomlkit~=0.14.0
22
23
  Requires-Dist: typer<0.25.1,>=0.15.4
@@ -0,0 +1,28 @@
1
+ datashare_python/.gitignore,sha256=e-SRgnvGGdsjRrqgKsTzALz6Obx8IYiOjr0yaAxT6v8,22
2
+ datashare_python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ datashare_python/__main__.py,sha256=g-fvS46zl9umKmGrSpl-OG-8PSuZgjqvTCqjpsZtSps,101
4
+ datashare_python/config.py,sha256=V3LBXdxzAt8_UcHiPw6I4HN7RTo-Fns6xd50eZ8nHtQ,4116
5
+ datashare_python/conftest.py,sha256=-cEtlPEl6EagrxB5GTvjD92QQats3ItyFuIKkOKTf1s,13623
6
+ datashare_python/constants.py,sha256=-Irwd2bbnFGIkLVJi_SiQ7PM2eNW3Ia_co4SE6z4j-A,352
7
+ datashare_python/dependencies.py,sha256=Q-BIhCwWu-GGp9omYNhENZ88cF0QJ8spfaMZX40mmNU,4386
8
+ datashare_python/discovery.py,sha256=3mUck2GUSKu2DSP9-MM1OdVM4FqFKqfBeSQDQZxgh2g,7177
9
+ datashare_python/exceptions.py,sha256=bVHEAXxDPKfxeeMC0hJXEsrJkgsKO2ESAhxWU96GA4M,496
10
+ datashare_python/interceptors.py,sha256=6ZJG7VJN5EHZkO6XD3nJyRCzCDb1Nh3x24VL1ip2Iyw,13314
11
+ datashare_python/logging_.py,sha256=96KNXhP-JR5ybu9dld4VLtF7v7PfiphOCR1Nio-mmUs,4483
12
+ datashare_python/mimetypes_.py,sha256=mjQgaqnSFZEcl3LoLDGBaBBjTTzwYfSNZND9Fmkk2zs,49682
13
+ datashare_python/objects.py,sha256=sSmXlsnj3PH_LZJwliUZcyXlVHbRHM7q19k_bLscAgs,16085
14
+ datashare_python/task_client.py,sha256=oTmP8bvZW0UyhLNMi1AV3XIAx7hrdbxNRss2Mw2azEc,8435
15
+ datashare_python/template.py,sha256=N1uM8TRXh5tQPV7BC1oJj-Pdm2Gs_SkjaXv50RnNYso,4682
16
+ datashare_python/types_.py,sha256=80XTD481HxjbkS330pqHHmlsM-cxUyGGgcJnG8_vYR8,1134
17
+ datashare_python/utils.py,sha256=9RhgubRztlCdDPaAgmnbnMzwFkiCL_azt74yKFUhqjc,26627
18
+ datashare_python/worker-template.tar.gz,sha256=EdE9rOAFuy01ghQAvLf8M0pvxh0nCuIs40Z-3rnr0QQ,278668
19
+ datashare_python/worker.py,sha256=9BqtVTell9lhyHwdvpuYS_SNvxHGkS75HGhu1E_OhMM,8224
20
+ datashare_python/cli/__init__.py,sha256=9BPWtssDgsVfWMsZ1TtZCla0EC_kai4RHttr8oNLYOE,1401
21
+ datashare_python/cli/project.py,sha256=w32Gy9AOL5B00uDT4in7YUCt2g68FnNbvwg2M3a8G6o,946
22
+ datashare_python/cli/task.py,sha256=GsyR5o0fN4Pd8tJKAUgivAvCBKT3L4Ay5j_BCW1XBHk,5828
23
+ datashare_python/cli/utils.py,sha256=p69CQb0zfixuyBkiZprhdMCc_NuYwXyAn6vC9H1UzAw,911
24
+ datashare_python/cli/worker.py,sha256=x0p3a1_NsZPbmV7-WSVMRyW3_iadYcgyZKbDvWgymQc,5790
25
+ datashare_python-0.10.0.dev0.dist-info/METADATA,sha256=3g8Tbq3QGjfHXMEp0YUop3xOCDSv7ptglIDrOmBHXW8,1017
26
+ datashare_python-0.10.0.dev0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
27
+ datashare_python-0.10.0.dev0.dist-info/entry_points.txt,sha256=ILE7auxabHWiu3GC-AunWnzjhOI_SbZp7D4GqZHlLw4,68
28
+ datashare_python-0.10.0.dev0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,27 +0,0 @@
1
- datashare_python/.gitignore,sha256=e-SRgnvGGdsjRrqgKsTzALz6Obx8IYiOjr0yaAxT6v8,22
2
- datashare_python/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- datashare_python/__main__.py,sha256=g-fvS46zl9umKmGrSpl-OG-8PSuZgjqvTCqjpsZtSps,101
4
- datashare_python/config.py,sha256=1-uksky9GwmPh85E3JYt6X4hgjP68gUmZjZM7qf3VUI,3779
5
- datashare_python/conftest.py,sha256=28mDi35cpGlH3gOofNWPzjPRp5FvZawE2JZTDVFnY6w,9184
6
- datashare_python/constants.py,sha256=uX90ZdWGXq7TjjKgylBKHPZ3Hdp01k4diN-xZaZ7VHA,355
7
- datashare_python/dependencies.py,sha256=JdKZX3TlbYQk1YLvscGEdXQNJ9TFjtbxmYwlxNyqGOg,4793
8
- datashare_python/discovery.py,sha256=3mUck2GUSKu2DSP9-MM1OdVM4FqFKqfBeSQDQZxgh2g,7177
9
- datashare_python/exceptions.py,sha256=bVHEAXxDPKfxeeMC0hJXEsrJkgsKO2ESAhxWU96GA4M,496
10
- datashare_python/interceptors.py,sha256=6ZJG7VJN5EHZkO6XD3nJyRCzCDb1Nh3x24VL1ip2Iyw,13314
11
- datashare_python/logging_.py,sha256=6ZZtS_msT-V7jDZo6n9MLxKS-Pie9j22qy7BaVFRk2I,4492
12
- datashare_python/objects.py,sha256=4vZXLHhrOEorghq4GY_E6SgJc1eVtqHi6ae-eP_H-X8,12649
13
- datashare_python/task_client.py,sha256=oTmP8bvZW0UyhLNMi1AV3XIAx7hrdbxNRss2Mw2azEc,8435
14
- datashare_python/template.py,sha256=N1uM8TRXh5tQPV7BC1oJj-Pdm2Gs_SkjaXv50RnNYso,4682
15
- datashare_python/types_.py,sha256=80XTD481HxjbkS330pqHHmlsM-cxUyGGgcJnG8_vYR8,1134
16
- datashare_python/utils.py,sha256=rfdcHlKnBhtn9ukDZLL5KYmk6MxZTiMog0csbzHsPBs,16609
17
- datashare_python/worker-template.tar.gz,sha256=NuInLEuBY_NagDLmsyx7vLwcCsEKYrZ7TFLpmaxIpkE,289250
18
- datashare_python/worker.py,sha256=cmqLRJ0_b1xm0qjjAZrRMOVXfhdrErimA_QY0xMtxRo,7774
19
- datashare_python/cli/__init__.py,sha256=9BPWtssDgsVfWMsZ1TtZCla0EC_kai4RHttr8oNLYOE,1401
20
- datashare_python/cli/project.py,sha256=w32Gy9AOL5B00uDT4in7YUCt2g68FnNbvwg2M3a8G6o,946
21
- datashare_python/cli/task.py,sha256=8mvKGS21bZ14BgZ0Uo-dfameljkaI2ZBha80ywCy-E8,5822
22
- datashare_python/cli/utils.py,sha256=p69CQb0zfixuyBkiZprhdMCc_NuYwXyAn6vC9H1UzAw,911
23
- datashare_python/cli/worker.py,sha256=eIMgUatSmpQpeu0MSMMKBrmx3GWm_eQ-aLfVrgl513o,5773
24
- datashare_python-0.9.8.dist-info/METADATA,sha256=vAUnuH6dRFVYQFfmfPHRdoYgzOyqHHviiBwYw49C21k,982
25
- datashare_python-0.9.8.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
26
- datashare_python-0.9.8.dist-info/entry_points.txt,sha256=ILE7auxabHWiu3GC-AunWnzjhOI_SbZp7D4GqZHlLw4,68
27
- datashare_python-0.9.8.dist-info/RECORD,,