datashare-python 0.9.4__tar.gz → 0.9.7__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.
Files changed (28) hide show
  1. {datashare_python-0.9.4 → datashare_python-0.9.7}/PKG-INFO +1 -3
  2. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/conftest.py +26 -18
  3. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/discovery.py +3 -1
  4. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/interceptors.py +37 -8
  5. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/logging_.py +2 -4
  6. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/types_.py +20 -4
  7. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/utils.py +31 -11
  8. datashare_python-0.9.7/datashare_python/worker-template.tar.gz +0 -0
  9. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/worker.py +4 -1
  10. {datashare_python-0.9.4 → datashare_python-0.9.7}/pyproject.toml +2 -3
  11. datashare_python-0.9.4/datashare_python/worker-template.tar.gz +0 -0
  12. {datashare_python-0.9.4 → datashare_python-0.9.7}/.gitignore +0 -0
  13. {datashare_python-0.9.4 → datashare_python-0.9.7}/README.md +0 -0
  14. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/.gitignore +0 -0
  15. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/__init__.py +0 -0
  16. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/__main__.py +0 -0
  17. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/cli/__init__.py +0 -0
  18. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/cli/project.py +0 -0
  19. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/cli/task.py +0 -0
  20. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/cli/utils.py +0 -0
  21. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/cli/worker.py +0 -0
  22. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/config.py +0 -0
  23. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/constants.py +0 -0
  24. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/dependencies.py +0 -0
  25. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/exceptions.py +0 -0
  26. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/objects.py +0 -0
  27. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/task_client.py +0 -0
  28. {datashare_python-0.9.4 → datashare_python-0.9.7}/datashare_python/template.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datashare-python
3
- Version: 0.9.4
3
+ Version: 0.9.7
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/
@@ -14,8 +14,6 @@ Requires-Dist: hatchling~=1.27
14
14
  Requires-Dist: icij-common[elasticsearch]~=0.8.2
15
15
  Requires-Dist: langcodes~=3.5
16
16
  Requires-Dist: lru-dict~=1.4
17
- Requires-Dist: nest-asyncio~=1.6
18
- Requires-Dist: orjson~=3.11
19
17
  Requires-Dist: pydantic-extra-types[pycountry]>=2.11.1
20
18
  Requires-Dist: python-json-logger~=4.0
21
19
  Requires-Dist: pyyaml~=6.0
@@ -1,7 +1,5 @@
1
- import asyncio
2
1
  import shutil
3
- from asyncio import AbstractEventLoop
4
- from collections.abc import AsyncGenerator, Generator, Iterator, Sequence
2
+ from collections.abc import AsyncGenerator, Generator, Sequence
5
3
  from pathlib import Path
6
4
 
7
5
  import aiohttp
@@ -10,7 +8,9 @@ import pytest
10
8
  from elasticsearch._async.helpers import async_streaming_bulk
11
9
  from icij_common.es import DOC_ROOT_ID, ES_DOCUMENT_TYPE, ID, ESClient
12
10
  from icij_common.test_utils import reset_env # noqa: F401
11
+ from pytest_asyncio import is_async_test
13
12
  from temporalio import workflow
13
+ from temporalio.service import RPCError, RPCStatusCode
14
14
 
15
15
  from datashare_python.config import (
16
16
  DatashareClientConfig,
@@ -59,6 +59,13 @@ _INDEX_BODY = {
59
59
  }
60
60
 
61
61
 
62
+ def pytest_collection_modifyitems(items: list) -> None:
63
+ pytest_asyncio_tests = (item for item in items if is_async_test(item))
64
+ session_scope_marker = pytest.mark.asyncio(loop_scope="session")
65
+ for async_test in pytest_asyncio_tests:
66
+ async_test.add_marker(session_scope_marker, append=False)
67
+
68
+
62
69
  @activity_defn(name="mocked-act")
63
70
  def mocked_act() -> None:
64
71
  pass
@@ -81,16 +88,6 @@ def test_deps() -> list[ContextManagerFactory]:
81
88
  return [set_es_client, set_task_client]
82
89
 
83
90
 
84
- @pytest.fixture(scope="session")
85
- def event_loop(
86
- request: pytest.FixtureRequest, # noqa: ARG001
87
- ) -> Iterator[asyncio.AbstractEventLoop]:
88
- """Create an instance of the default event loop for each test case."""
89
- loop = asyncio.get_event_loop_policy().new_event_loop()
90
- yield loop
91
- loop.close()
92
-
93
-
94
91
  @pytest.fixture(scope="session")
95
92
  def test_worker_config() -> WorkerConfig:
96
93
  logging_config = LoggingConfig(
@@ -117,9 +114,7 @@ def test_worker_config_path(test_worker_config: WorkerConfig, tmpdir: Path) -> P
117
114
 
118
115
  @pytest.fixture(scope="session")
119
116
  async def worker_lifetime_deps(
120
- event_loop: AbstractEventLoop,
121
- test_deps: list[ContextManagerFactory],
122
- test_worker_config: WorkerConfig,
117
+ test_deps: list[ContextManagerFactory], test_worker_config: WorkerConfig
123
118
  ) -> AsyncGenerator[None, None]:
124
119
  worker_id = "test-worker-id"
125
120
  ctx = "test application"
@@ -128,7 +123,6 @@ async def worker_lifetime_deps(
128
123
  ctx=ctx,
129
124
  worker_id=worker_id,
130
125
  worker_config=test_worker_config,
131
- event_loop=event_loop,
132
126
  ):
133
127
  yield
134
128
 
@@ -174,11 +168,25 @@ async def test_task_client(
174
168
  @pytest.fixture(scope="session")
175
169
  async def test_temporal_client_session(
176
170
  test_worker_config: WorkerConfig,
177
- event_loop: AbstractEventLoop, # noqa: ARG001
178
171
  ) -> TemporalClient: # noqa: ANN001
179
172
  return await test_worker_config.to_temporal_client()
180
173
 
181
174
 
175
+ @pytest.fixture
176
+ async def test_temporal_client(
177
+ test_temporal_client_session: TemporalClient,
178
+ ) -> TemporalClient: # noqa: ANN001
179
+ client = test_temporal_client_session
180
+ async for wf in client.list_workflows():
181
+ try:
182
+ await client.get_workflow_handle(wf.id).terminate()
183
+ except RPCError as e:
184
+ if e.status != RPCStatusCode.NOT_FOUND:
185
+ raise
186
+
187
+ return client
188
+
189
+
182
190
  @pytest.fixture
183
191
  async def populate_es(
184
192
  test_es_client: ESClient,
@@ -88,7 +88,9 @@ def discover(
88
88
  worker_config_cls = discover_worker_config_cls()
89
89
  else:
90
90
  worker_config_cls = WorkerConfig
91
- discovered += f"- worker config class: {worker_config_cls}"
91
+ if discovered:
92
+ discovered += "\n"
93
+ discovered += f"- worker config class: {worker_config_cls.__name__}"
92
94
  logger.info("discovered:\n%s", discovered)
93
95
  return wfs, acts, deps, worker_config_cls
94
96
 
@@ -6,7 +6,7 @@ from collections.abc import Callable, Generator, Mapping
6
6
  from contextlib import contextmanager
7
7
  from contextvars import ContextVar
8
8
  from copy import deepcopy
9
- from functools import partial
9
+ from functools import partial, wraps
10
10
  from inspect import signature
11
11
  from types import UnionType
12
12
  from typing import (
@@ -52,7 +52,12 @@ from temporalio.workflow import (
52
52
  )
53
53
 
54
54
  from .objects import BaseModel
55
- from .types_ import ProgressRateHandler, Weight
55
+ from .types_ import (
56
+ AsyncProgressRateHandler,
57
+ ProgressRateHandler,
58
+ SyncProgressRateHandler,
59
+ Weight,
60
+ )
56
61
  from .utils import (
57
62
  PROGRESS_HANDLER_ARG,
58
63
  PYDANTIC_DATA_CONVERTER,
@@ -62,6 +67,11 @@ from .utils import (
62
67
 
63
68
  _TRACEPARENT = "traceparent"
64
69
  _DEFAULT_PAYLOAD_CONVERTER = DataConverter.default.payload_converter
70
+ _PROGRESS_TYPES = {
71
+ ProgressRateHandler,
72
+ AsyncProgressRateHandler,
73
+ SyncProgressRateHandler,
74
+ }
65
75
 
66
76
 
67
77
  class TraceContext(BaseModel):
@@ -292,11 +302,13 @@ def _get_progress_handler(act_fn: Callable) -> ProgressRateHandler:
292
302
 
293
303
 
294
304
  def _is_progress(t: type) -> bool:
295
- if t is ProgressRateHandler:
305
+ if any(t is p_cls for p_cls in _PROGRESS_TYPES):
296
306
  return True
297
307
  return bool(
298
308
  isinstance(t, UnionType)
299
- and any(sub_t is ProgressRateHandler for sub_t in get_args(t))
309
+ and any(
310
+ any(sub_t is p_cls for p_cls in _PROGRESS_TYPES) for sub_t in get_args(t)
311
+ )
300
312
  )
301
313
 
302
314
 
@@ -315,16 +327,21 @@ class _ProgressInboundInterceptor(ActivityInboundInterceptor):
315
327
  # https://github.com/temporalio/sdk-python/blob/631ebaf0e20fb214b16589b45627b358048a5d77/temporalio/worker/_activity.py#L600
316
328
  # we have to force it here again
317
329
  progress_handler = _get_progress_handler(input.fn)
330
+ new_args = []
331
+ act_definition = _Definition.must_from_callable(input.fn)
318
332
  if input.args:
319
333
  data_converter = PYDANTIC_DATA_CONVERTER
320
- arg_types = _Definition.must_from_callable(input.fn).arg_types
334
+ arg_types = act_definition.arg_types
321
335
  arg_types = _without_progress(arg_types)
322
336
  arg_types = arg_types[: len(input.args)]
323
337
  encoded = await data_converter.encode(input.args)
324
338
  new_args = await data_converter.decode(encoded, type_hints=arg_types)
325
- new_args.append(progress_handler)
326
- else:
327
- new_args = [progress_handler]
339
+ injected_progress = (
340
+ progress_handler
341
+ if act_definition.is_async
342
+ else _sync_progress(progress_handler)
343
+ )
344
+ new_args.append(injected_progress)
328
345
  new_input = dataclasses.replace(input, args=new_args)
329
346
  await progress_handler(0.0)
330
347
  res = await super().execute_activity(new_input)
@@ -374,3 +391,15 @@ class _HeartbeatInboundInterceptor(ActivityInboundInterceptor):
374
391
  if heartbeat_task:
375
392
  heartbeat_task.cancel()
376
393
  await asyncio.wait([heartbeat_task])
394
+
395
+
396
+ def _sync_progress(
397
+ progress_handler: AsyncProgressRateHandler,
398
+ ) -> SyncProgressRateHandler:
399
+ @wraps(progress_handler)
400
+ def p(progress: float, event_loop: asyncio.AbstractEventLoop) -> None:
401
+ asyncio.run_coroutine_threadsafe(
402
+ progress_handler(progress), event_loop
403
+ ).result()
404
+
405
+ return p
@@ -1,10 +1,10 @@
1
+ import json
1
2
  import logging
2
3
  import numbers
3
4
  import sys
4
5
  from copy import copy
5
6
  from typing import Any
6
7
 
7
- import orjson
8
8
  from icij_common.logging_utils import DATE_FMT, STREAM_HANDLER_FMT
9
9
  from pythonjsonlogger.core import BaseJsonFormatter
10
10
  from pythonjsonlogger.orjson import OrjsonFormatter
@@ -67,8 +67,6 @@ class WorkerFilter(logging.Filter):
67
67
  self.worker_id = worker_id
68
68
 
69
69
  def filter(self, record: logging.LogRecord) -> bool:
70
- if not hasattr(record, "message") and record.msg is not None:
71
- setattr(record, "msg", record.getMessage()) # noqa: B010
72
70
  if self.worker_id is not None:
73
71
  record.worker_id = self.worker_id
74
72
  if workflow.in_workflow():
@@ -134,7 +132,7 @@ def _encode_value(value: Any) -> str:
134
132
  return "true" if value else "false"
135
133
  if isinstance(value, numbers.Number):
136
134
  return str(value)
137
- return orjson.dumps(value).decode()
135
+ return json.dumps(value).decode()
138
136
 
139
137
 
140
138
  def _json_formatter(datefmt: str) -> BaseJsonFormatter:
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  from collections.abc import Coroutine
2
3
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
3
4
  from dataclasses import dataclass
@@ -8,19 +9,34 @@ from temporalio.client import Client
8
9
  TemporalClient = Client
9
10
 
10
11
 
11
- class ProgressRateHandler(Protocol):
12
+ class AsyncProgressRateHandler(Protocol):
12
13
  async def __call__(self, progress_rate: float) -> None:
13
14
  pass
14
15
 
15
16
 
17
+ class SyncProgressRateHandler(Protocol):
18
+ def __call__(
19
+ self, progress_rate: float, event_loop: asyncio.AbstractEventLoop
20
+ ) -> None:
21
+ pass
22
+
23
+
24
+ ProgressRateHandler = SyncProgressRateHandler | AsyncProgressRateHandler
25
+
26
+
16
27
  @dataclass
17
28
  class Weight:
18
29
  value: float
19
30
 
20
31
 
21
- class RawProgressHandler(Protocol):
22
- async def __call__(self, iteration: int) -> None:
23
- pass
32
+ class RawAsyncProgressHandler(Protocol):
33
+ async def __call__(self, iteration: int) -> None: ...
34
+
35
+
36
+ class RawSyncProgressHandler(Protocol):
37
+ async def __call__(
38
+ self, iteration: int, event_loop: asyncio.AbstractEventLoop
39
+ ) -> None: ...
24
40
 
25
41
 
26
42
  FactoryReturnType = (
@@ -15,7 +15,6 @@ from pathlib import Path
15
15
  from typing import Any, ParamSpec, TypeVar
16
16
  from uuid import uuid4
17
17
 
18
- import nest_asyncio
19
18
  import temporalio
20
19
  from pydantic import ValidationError
21
20
  from temporalio import activity, workflow
@@ -31,9 +30,15 @@ from temporalio.converter import (
31
30
  )
32
31
  from temporalio.exceptions import ApplicationError
33
32
 
33
+ from datashare_python.types_ import (
34
+ AsyncProgressRateHandler,
35
+ RawSyncProgressHandler,
36
+ SyncProgressRateHandler,
37
+ )
38
+
34
39
  from .constants import METADATA_JSON
35
40
  from .objects import DocArtifact, DocumentLocation, FilesystemDocument
36
- from .types_ import ProgressRateHandler, RawProgressHandler
41
+ from .types_ import RawAsyncProgressHandler
37
42
 
38
43
  DependencyLabel = str | None
39
44
  DependencySetup = Callable[..., None]
@@ -76,10 +81,13 @@ class ProgressSignal:
76
81
 
77
82
 
78
83
  class ActivityWithProgress:
79
- def __init__(self, temporal_client: Client, event_loop: asyncio.AbstractEventLoop):
84
+ def __init__(
85
+ self,
86
+ temporal_client: Client,
87
+ event_loop: asyncio.AbstractEventLoop | None = None,
88
+ ):
80
89
  self._temporal_client = temporal_client
81
- nest_asyncio.apply()
82
- self._event_loop = event_loop
90
+ self._event_loop = event_loop or asyncio.get_event_loop()
83
91
 
84
92
 
85
93
  class WorkflowWithProgress:
@@ -276,9 +284,9 @@ def fatal_error_from_exception(exc: Exception) -> ApplicationError:
276
284
  return ApplicationError(str(exc), details, type=exc_type, non_retryable=True)
277
285
 
278
286
 
279
- def to_raw_progress(
280
- progress: ProgressRateHandler, max_progress: int
281
- ) -> RawProgressHandler:
287
+ def to_raw_async_progress(
288
+ progress: AsyncProgressRateHandler, max_progress: int
289
+ ) -> RawAsyncProgressHandler:
282
290
  if not max_progress > 0:
283
291
  raise ValueError("max_progress must be > 0")
284
292
 
@@ -288,9 +296,21 @@ def to_raw_progress(
288
296
  return raw
289
297
 
290
298
 
291
- def to_scaled_progress(
292
- progress: ProgressRateHandler, *, start: float = 0.0, end: float = 1.0
293
- ) -> ProgressRateHandler:
299
+ def to_raw_sync_progress(
300
+ progress: SyncProgressRateHandler, max_progress: int
301
+ ) -> RawSyncProgressHandler:
302
+ if not max_progress > 0:
303
+ raise ValueError("max_progress must be > 0")
304
+
305
+ def raw(iteration: int, event_loop: asyncio.AbstractEventLoop) -> None:
306
+ progress(iteration / max_progress, event_loop)
307
+
308
+ return raw
309
+
310
+
311
+ def to_scaled_async_progress(
312
+ progress: AsyncProgressRateHandler, *, start: float = 0.0, end: float = 1.0
313
+ ) -> AsyncProgressRateHandler:
294
314
  if not 0 <= start < end:
295
315
  raise ValueError("start must be [0, end[")
296
316
  if not start < end <= 1.0:
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import inspect
2
3
  import logging
3
4
  import os
@@ -157,7 +158,7 @@ async def worker_context(
157
158
  workflows: list[type] | None = None,
158
159
  worker_config: WorkerConfig,
159
160
  client: TemporalClient,
160
- event_loop: AbstractEventLoop,
161
+ event_loop: AbstractEventLoop | None = None,
161
162
  task_queue: str,
162
163
  dependencies: list[ContextManagerFactory] | None = None,
163
164
  sandboxed: bool = True,
@@ -169,6 +170,8 @@ async def worker_context(
169
170
  discovered.extend(workflows)
170
171
  if dependencies is not None:
171
172
  discovered.extend(dependencies)
173
+ if event_loop is None:
174
+ event_loop = asyncio.get_event_loop()
172
175
  discovered.append(worker_config)
173
176
  loggers = copy(worker_config.logging.loggers)
174
177
  discovered_loggers = {_get_object_package(o).__name__ for o in discovered}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "datashare-python"
3
- version = "0.9.4"
3
+ version = "0.9.7"
4
4
  description = "Manage Python tasks and local resources in Datashare"
5
5
  authors = [
6
6
  { name = "Clément Doumouro", email = "cdoumouro@icij.org" },
@@ -15,8 +15,6 @@ dependencies = [
15
15
  "hatchling~=1.27",
16
16
  "icij-common[elasticsearch]~=0.8.2",
17
17
  "langcodes~=3.5",
18
- "nest-asyncio~=1.6",
19
- "orjson~=3.11",
20
18
  "python-json-logger~=4.0",
21
19
  "pyyaml~=6.0",
22
20
  "pydantic-extra-types[pycountry]>=2.11.1",
@@ -76,6 +74,7 @@ dev = [
76
74
 
77
75
  [tool.pytest.ini_options]
78
76
  asyncio_mode = "auto"
77
+ asyncio_debug = true
79
78
  asyncio_default_fixture_loop_scope = "session"
80
79
  markers = [
81
80
  "integration",