datashare-python 0.10.0rc1__tar.gz → 0.10.4__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.10.0rc1 → datashare_python-0.10.4}/PKG-INFO +2 -2
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/config.py +1 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/conftest.py +3 -2
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/interceptors.py +108 -33
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/objects.py +13 -3
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/types_.py +13 -7
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/utils.py +16 -16
- datashare_python-0.10.4/datashare_python/worker-template.tar.gz +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/worker.py +3 -1
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/pyproject.toml +1 -1
- datashare_python-0.10.0rc1/datashare_python/worker-template.tar.gz +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/.gitignore +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/README.md +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/.gitignore +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/__init__.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/__main__.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/cli/__init__.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/cli/project.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/cli/task.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/cli/utils.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/cli/worker.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/constants.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/dependencies.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/discovery.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/exceptions.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/logging_.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/mimetypes_.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/task_client.py +0 -0
- {datashare_python-0.10.0rc1 → datashare_python-0.10.4}/datashare_python/template.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: datashare-python
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.4
|
|
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/
|
|
@@ -10,7 +10,6 @@ from multiprocessing import Event
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
import aiohttp
|
|
13
|
-
import nest_asyncio
|
|
14
13
|
import pytest
|
|
15
14
|
from elasticsearch._async.helpers import async_streaming_bulk
|
|
16
15
|
from icij_common.es import DOC_ROOT_ID, ES_DOCUMENT_TYPE, ID, ESClient
|
|
@@ -171,7 +170,7 @@ async def test_temporal_client(
|
|
|
171
170
|
|
|
172
171
|
|
|
173
172
|
@pytest.fixture
|
|
174
|
-
async def indexed_docs(
|
|
173
|
+
async def indexed_docs( # noqa: PLR0917
|
|
175
174
|
doc_0: Document,
|
|
176
175
|
doc_1: Document,
|
|
177
176
|
doc_2: Document,
|
|
@@ -299,6 +298,8 @@ async def all_done(task_client: DatashareTaskClient, not_done: list[str]) -> boo
|
|
|
299
298
|
|
|
300
299
|
@pytest.fixture # noqa: F405
|
|
301
300
|
def typer_asyncio_patch() -> None:
|
|
301
|
+
import nest_asyncio # noqa: PLC0415
|
|
302
|
+
|
|
302
303
|
nest_asyncio.apply()
|
|
303
304
|
|
|
304
305
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import contextlib
|
|
3
2
|
import dataclasses
|
|
3
|
+
import datetime
|
|
4
|
+
import logging
|
|
4
5
|
import secrets
|
|
5
6
|
from collections.abc import Callable, Generator, Mapping
|
|
6
7
|
from contextlib import contextmanager
|
|
7
8
|
from contextvars import ContextVar
|
|
8
9
|
from copy import deepcopy
|
|
9
|
-
from functools import
|
|
10
|
+
from functools import wraps
|
|
10
11
|
from inspect import signature
|
|
11
12
|
from types import UnionType
|
|
12
13
|
from typing import (
|
|
@@ -29,6 +30,7 @@ from temporalio.client import WorkflowHandle
|
|
|
29
30
|
from temporalio.converter import DataConverter
|
|
30
31
|
from temporalio.worker import (
|
|
31
32
|
ActivityInboundInterceptor,
|
|
33
|
+
ActivityOutboundInterceptor,
|
|
32
34
|
ContinueAsNewInput,
|
|
33
35
|
ExecuteActivityInput,
|
|
34
36
|
ExecuteWorkflowInput,
|
|
@@ -65,6 +67,8 @@ from .utils import (
|
|
|
65
67
|
ProgressSignal,
|
|
66
68
|
)
|
|
67
69
|
|
|
70
|
+
logger = logging.getLogger(__name__)
|
|
71
|
+
|
|
68
72
|
_TRACEPARENT = "traceparent"
|
|
69
73
|
_DEFAULT_PAYLOAD_CONVERTER = DataConverter.default.payload_converter
|
|
70
74
|
_PROGRESS_TYPES = {
|
|
@@ -189,6 +193,8 @@ class _TraceContextWorkflowOutboundInterceptor(WorkflowOutboundInterceptor):
|
|
|
189
193
|
|
|
190
194
|
class _TraceContextActivityInboundInterceptor(ActivityInboundInterceptor):
|
|
191
195
|
async def execute_activity(self, input: ExecuteActivityInput) -> Any: # noqa: A002
|
|
196
|
+
info = activity.info()
|
|
197
|
+
logger.info("start executing activity: %s", info.activity_id)
|
|
192
198
|
with _trace_context(input.headers):
|
|
193
199
|
return await super().execute_activity(input)
|
|
194
200
|
|
|
@@ -219,7 +225,7 @@ def _trace_context(headers: Mapping[str, Payload]) -> Generator[None, None, None
|
|
|
219
225
|
InputWithHeaders = TypeVar("InputWithHeaders")
|
|
220
226
|
|
|
221
227
|
|
|
222
|
-
def _with_trace_context_header(
|
|
228
|
+
def _with_trace_context_header[InputWithHeaders](
|
|
223
229
|
input_with_headers: InputWithHeaders,
|
|
224
230
|
) -> InputWithHeaders:
|
|
225
231
|
ctx = get_trace_context()
|
|
@@ -234,11 +240,14 @@ def _with_trace_context_header(
|
|
|
234
240
|
|
|
235
241
|
|
|
236
242
|
class ProgressInterceptor(Interceptor):
|
|
243
|
+
def __init__(self, min_progress_interval_s: float = 30.0):
|
|
244
|
+
self._min_progress_interval_s: float = min_progress_interval_s
|
|
245
|
+
|
|
237
246
|
def intercept_activity(
|
|
238
247
|
self,
|
|
239
248
|
next: ActivityInboundInterceptor, # noqa: A002
|
|
240
249
|
) -> ActivityInboundInterceptor:
|
|
241
|
-
return _ProgressInboundInterceptor(next)
|
|
250
|
+
return _ProgressInboundInterceptor(next, self._min_progress_interval_s)
|
|
242
251
|
|
|
243
252
|
|
|
244
253
|
def _parse_progress_weight(act_fn: Callable) -> float:
|
|
@@ -254,18 +263,41 @@ def _parse_progress_weight(act_fn: Callable) -> float:
|
|
|
254
263
|
return 1.0
|
|
255
264
|
|
|
256
265
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
class TemporalProgressHandler:
|
|
267
|
+
def __init__(
|
|
268
|
+
self,
|
|
269
|
+
handle: WorkflowHandle,
|
|
270
|
+
activity_id: str,
|
|
271
|
+
*,
|
|
272
|
+
run_id: str,
|
|
273
|
+
min_progress_interval_s: float = 30.0,
|
|
274
|
+
weight: float = 1.0,
|
|
275
|
+
) -> None:
|
|
276
|
+
self._handle = handle
|
|
277
|
+
self._activity_id = activity_id
|
|
278
|
+
self._run_id = run_id
|
|
279
|
+
self._weight = weight
|
|
280
|
+
self._min_progress_interval_s = min_progress_interval_s
|
|
281
|
+
self._last: datetime.datetime | None = None
|
|
282
|
+
|
|
283
|
+
async def progress(self, progress: float, *, force: bool = False) -> None:
|
|
284
|
+
# TODO: we could lock here to avoid race conditions, it's not critical though
|
|
285
|
+
now = datetime.datetime.now(datetime.UTC)
|
|
286
|
+
report_progress = (
|
|
287
|
+
force
|
|
288
|
+
or self._last is None
|
|
289
|
+
or (now - self._last).total_seconds() >= self._min_progress_interval_s
|
|
290
|
+
)
|
|
291
|
+
if not report_progress:
|
|
292
|
+
return
|
|
293
|
+
self._last = now
|
|
294
|
+
signal = ProgressSignal(
|
|
295
|
+
activity_id=self._activity_id,
|
|
296
|
+
run_id=self._run_id,
|
|
297
|
+
progress=progress,
|
|
298
|
+
weight=self._weight,
|
|
299
|
+
)
|
|
300
|
+
await self._handle.signal("update_progress", signal)
|
|
269
301
|
|
|
270
302
|
|
|
271
303
|
def supports_progress(task_fn: Callable) -> bool:
|
|
@@ -275,7 +307,9 @@ def supports_progress(task_fn: Callable) -> bool:
|
|
|
275
307
|
)
|
|
276
308
|
|
|
277
309
|
|
|
278
|
-
def _get_progress_handler(
|
|
310
|
+
def _get_progress_handler(
|
|
311
|
+
act_fn: Callable, min_progress_interval_s: float
|
|
312
|
+
) -> ProgressRateHandler:
|
|
279
313
|
act = getattr(act_fn, "__self__", None)
|
|
280
314
|
# Weirdly isinstance doesn't work here
|
|
281
315
|
if act is None or not isinstance(act, ActivityWithProgress):
|
|
@@ -291,14 +325,14 @@ def _get_progress_handler(act_fn: Callable) -> ProgressRateHandler:
|
|
|
291
325
|
activity_id = activity.info().activity_id
|
|
292
326
|
client = act._temporal_client
|
|
293
327
|
workflow_handle = client.get_workflow_handle(workflow_id, run_id=run_id)
|
|
294
|
-
handler =
|
|
295
|
-
|
|
296
|
-
|
|
328
|
+
handler = TemporalProgressHandler(
|
|
329
|
+
workflow_handle,
|
|
330
|
+
activity_id,
|
|
297
331
|
run_id=run_id,
|
|
298
|
-
activity_id=activity_id,
|
|
299
332
|
weight=weight,
|
|
333
|
+
min_progress_interval_s=min_progress_interval_s,
|
|
300
334
|
)
|
|
301
|
-
return handler
|
|
335
|
+
return handler.progress
|
|
302
336
|
|
|
303
337
|
|
|
304
338
|
def _is_progress(t: type) -> bool:
|
|
@@ -320,13 +354,23 @@ def _without_progress(arg_types: list[type] | None) -> list[type] | None:
|
|
|
320
354
|
|
|
321
355
|
|
|
322
356
|
class _ProgressInboundInterceptor(ActivityInboundInterceptor):
|
|
357
|
+
def __init__(
|
|
358
|
+
self,
|
|
359
|
+
next: ActivityInboundInterceptor, # noqa: A002
|
|
360
|
+
min_progress_interval_s: float,
|
|
361
|
+
) -> None:
|
|
362
|
+
super().__init__(next)
|
|
363
|
+
self._min_progress_interval_s = min_progress_interval_s
|
|
364
|
+
|
|
323
365
|
async def execute_activity(self, input: ExecuteActivityInput) -> Any: # noqa: A002
|
|
324
366
|
if not supports_progress(input.fn):
|
|
325
367
|
return await super().execute_activity(input)
|
|
326
368
|
# The progress args breaks trigger a bypass of the dataloader:
|
|
327
369
|
# https://github.com/temporalio/sdk-python/blob/631ebaf0e20fb214b16589b45627b358048a5d77/temporalio/worker/_activity.py#L600
|
|
328
370
|
# we have to force it here again
|
|
329
|
-
progress_handler = _get_progress_handler(
|
|
371
|
+
progress_handler = _get_progress_handler(
|
|
372
|
+
input.fn, self._min_progress_interval_s
|
|
373
|
+
)
|
|
330
374
|
new_args = []
|
|
331
375
|
act_definition = _Definition.must_from_callable(input.fn)
|
|
332
376
|
if input.args:
|
|
@@ -343,9 +387,9 @@ class _ProgressInboundInterceptor(ActivityInboundInterceptor):
|
|
|
343
387
|
)
|
|
344
388
|
new_args.append(injected_progress)
|
|
345
389
|
new_input = dataclasses.replace(input, args=new_args)
|
|
346
|
-
await progress_handler(0.0)
|
|
390
|
+
await progress_handler(0.0, force=True)
|
|
347
391
|
res = await super().execute_activity(new_input)
|
|
348
|
-
await progress_handler(1.0)
|
|
392
|
+
await progress_handler(1.0, force=True)
|
|
349
393
|
return res
|
|
350
394
|
|
|
351
395
|
|
|
@@ -360,13 +404,13 @@ class HeartbeatInterceptor(Interceptor):
|
|
|
360
404
|
return _HeartbeatInboundInterceptor(next, self._n_missed_before_timeout)
|
|
361
405
|
|
|
362
406
|
|
|
363
|
-
async def _heartbeat_every(
|
|
364
|
-
|
|
365
|
-
|
|
407
|
+
async def _heartbeat_every(
|
|
408
|
+
heartbeat_fn: Callable[[Any], None], period: float, *details: Any
|
|
409
|
+
) -> None:
|
|
410
|
+
heartbeat_fn(*details)
|
|
366
411
|
while True:
|
|
367
412
|
await asyncio.sleep(period)
|
|
368
|
-
|
|
369
|
-
activity.heartbeat(*details)
|
|
413
|
+
heartbeat_fn(*details)
|
|
370
414
|
|
|
371
415
|
|
|
372
416
|
class _HeartbeatInboundInterceptor(ActivityInboundInterceptor):
|
|
@@ -377,13 +421,25 @@ class _HeartbeatInboundInterceptor(ActivityInboundInterceptor):
|
|
|
377
421
|
) -> None:
|
|
378
422
|
super().__init__(next)
|
|
379
423
|
self._n_missed_before_timeout = n_missed_before_timeout
|
|
424
|
+
self._outbound: ActivityOutboundInterceptor | None = None
|
|
425
|
+
|
|
426
|
+
def init(self, outbound: ActivityOutboundInterceptor) -> None:
|
|
427
|
+
super().init(outbound)
|
|
428
|
+
self._outbound = outbound
|
|
380
429
|
|
|
381
430
|
async def execute_activity(self, input: ExecuteActivityInput) -> Any: # noqa: A002
|
|
431
|
+
# We need to capture the output interceptor to access the outbound hearbeat
|
|
432
|
+
# function. This inbound interceptor runs on the temporal event loop side.
|
|
433
|
+
# We want the heartbeat to run on the worker thread, on on the temporal event
|
|
434
|
+
# loop !
|
|
382
435
|
heartbeat_timeout = activity.info().heartbeat_timeout
|
|
383
436
|
heartbeat_task = None
|
|
384
437
|
if heartbeat_timeout:
|
|
385
438
|
period = heartbeat_timeout.total_seconds() / self._n_missed_before_timeout
|
|
386
|
-
|
|
439
|
+
# We don't want a failing hearbeat to fail the worker task so we just
|
|
440
|
+
# ignore any exception
|
|
441
|
+
heartbeat_fn = _fail_safe(self._outbound.heartbeat)
|
|
442
|
+
heartbeat_task = asyncio.create_task(_heartbeat_every(heartbeat_fn, period))
|
|
387
443
|
try:
|
|
388
444
|
activity.heartbeat()
|
|
389
445
|
return await super().execute_activity(input)
|
|
@@ -393,13 +449,32 @@ class _HeartbeatInboundInterceptor(ActivityInboundInterceptor):
|
|
|
393
449
|
await asyncio.wait([heartbeat_task])
|
|
394
450
|
|
|
395
451
|
|
|
452
|
+
def _fail_safe(
|
|
453
|
+
fn: Callable[[Any], None], excs: tuple[type[Exception]] | None = None
|
|
454
|
+
) -> Callable[[Any], None]:
|
|
455
|
+
if excs is None:
|
|
456
|
+
excs = (Exception,)
|
|
457
|
+
|
|
458
|
+
@wraps(fn)
|
|
459
|
+
def wrapper(*args, **kwargs) -> None:
|
|
460
|
+
try:
|
|
461
|
+
fn(*args, **kwargs)
|
|
462
|
+
except excs as exc:
|
|
463
|
+
msg = f"failed to heartbeat due to {exc}"
|
|
464
|
+
logger.exception(msg)
|
|
465
|
+
|
|
466
|
+
return wrapper
|
|
467
|
+
|
|
468
|
+
|
|
396
469
|
def _sync_progress(
|
|
397
470
|
progress_handler: AsyncProgressRateHandler,
|
|
398
471
|
) -> SyncProgressRateHandler:
|
|
399
472
|
@wraps(progress_handler)
|
|
400
|
-
def p(
|
|
473
|
+
def p(
|
|
474
|
+
progress: float, event_loop: asyncio.AbstractEventLoop, *, force: bool = False
|
|
475
|
+
) -> None:
|
|
401
476
|
asyncio.run_coroutine_threadsafe(
|
|
402
|
-
progress_handler(progress), event_loop
|
|
477
|
+
progress_handler(progress, force=force), event_loop
|
|
403
478
|
).result()
|
|
404
479
|
|
|
405
480
|
return p
|
|
@@ -7,14 +7,14 @@ 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,
|
|
10
|
+
from typing import Annotated, Any, ClassVar, Literal, Self, TypeVar, cast
|
|
11
11
|
|
|
12
12
|
import langcodes
|
|
13
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
|
-
from temporalio import workflow
|
|
17
|
+
from temporalio import activity, workflow
|
|
18
18
|
|
|
19
19
|
from .constants import TIKA_METADATA_RESOURCENAME
|
|
20
20
|
|
|
@@ -323,8 +323,10 @@ class TaskArgs(DatashareModel, ABC):
|
|
|
323
323
|
A = TypeVar("A", bound=TaskArgs)
|
|
324
324
|
|
|
325
325
|
|
|
326
|
-
class ManifestEntry
|
|
326
|
+
class ManifestEntry[A](DatashareModel, ABC):
|
|
327
327
|
status: ManifestEntryStatus
|
|
328
|
+
# TODO: make this one non optional in the next major !
|
|
329
|
+
task_id: str | None
|
|
328
330
|
label: str | None = None
|
|
329
331
|
input: Annotated[
|
|
330
332
|
dict[str, Any] | None,
|
|
@@ -336,7 +338,11 @@ class ManifestEntry(DatashareModel, Generic[A], ABC):
|
|
|
336
338
|
|
|
337
339
|
@classmethod
|
|
338
340
|
def complete(cls, args: A, label: str | None = None, **kwargs) -> Self:
|
|
341
|
+
task_id = None
|
|
342
|
+
if activity.in_activity():
|
|
343
|
+
task_id = activity.info().workflow_id
|
|
339
344
|
return cls(
|
|
345
|
+
task_id=task_id,
|
|
340
346
|
input=args.as_manifest_task_input(),
|
|
341
347
|
label=label,
|
|
342
348
|
status=ManifestEntryStatus.COMPLETE,
|
|
@@ -345,7 +351,11 @@ class ManifestEntry(DatashareModel, Generic[A], ABC):
|
|
|
345
351
|
|
|
346
352
|
@classmethod
|
|
347
353
|
def partial(cls, args: A, label: str | None = None, **kwargs) -> Self:
|
|
354
|
+
task_id = None
|
|
355
|
+
if activity.in_activity():
|
|
356
|
+
task_id = activity.info().workflow_id
|
|
348
357
|
return cls(
|
|
358
|
+
task_id=task_id,
|
|
349
359
|
input=args.as_manifest_task_input(),
|
|
350
360
|
label=label,
|
|
351
361
|
status=ManifestEntryStatus.PARTIAL,
|
|
@@ -10,15 +10,17 @@ TemporalClient = Client
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class AsyncProgressRateHandler(Protocol):
|
|
13
|
-
async def __call__(self, progress_rate: float) -> None:
|
|
14
|
-
pass
|
|
13
|
+
async def __call__(self, progress_rate: float, *, force: bool = False) -> None: ...
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class SyncProgressRateHandler(Protocol):
|
|
18
17
|
def __call__(
|
|
19
|
-
self,
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
self,
|
|
19
|
+
progress_rate: float,
|
|
20
|
+
event_loop: asyncio.AbstractEventLoop,
|
|
21
|
+
*,
|
|
22
|
+
force: bool = False,
|
|
23
|
+
) -> None: ...
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
ProgressRateHandler = SyncProgressRateHandler | AsyncProgressRateHandler
|
|
@@ -30,12 +32,16 @@ class Weight:
|
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class RawAsyncProgressHandler(Protocol):
|
|
33
|
-
async def __call__(self, iteration: int) -> None: ...
|
|
35
|
+
async def __call__(self, iteration: int, *, force: bool = False) -> None: ...
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
class RawSyncProgressHandler(Protocol):
|
|
37
39
|
async def __call__(
|
|
38
|
-
self,
|
|
40
|
+
self,
|
|
41
|
+
iteration: int,
|
|
42
|
+
event_loop: asyncio.AbstractEventLoop,
|
|
43
|
+
*,
|
|
44
|
+
force: bool = False,
|
|
39
45
|
) -> None: ...
|
|
40
46
|
|
|
41
47
|
|
|
@@ -25,7 +25,7 @@ from functools import cache, wraps
|
|
|
25
25
|
from hashlib import sha256
|
|
26
26
|
from io import BytesIO
|
|
27
27
|
from pathlib import Path
|
|
28
|
-
from typing import Any,
|
|
28
|
+
from typing import Any, Self, TypeVar
|
|
29
29
|
from uuid import uuid4
|
|
30
30
|
|
|
31
31
|
import temporalio
|
|
@@ -70,8 +70,6 @@ DependencyAsyncSetup = Callable[..., Coroutine[None, None, None]]
|
|
|
70
70
|
|
|
71
71
|
PROGRESS_HANDLER_ARG = "progress"
|
|
72
72
|
|
|
73
|
-
P = ParamSpec("P")
|
|
74
|
-
T = TypeVar("T")
|
|
75
73
|
|
|
76
74
|
_NEVER_RETRIABLES = {
|
|
77
75
|
"ValidationError",
|
|
@@ -178,7 +176,7 @@ async def execute_activity(
|
|
|
178
176
|
)
|
|
179
177
|
|
|
180
178
|
|
|
181
|
-
def positional_args_only(activity_fn: Callable[P, T]) -> Callable[P, T]:
|
|
179
|
+
def positional_args_only[**P, T](activity_fn: Callable[P, T]) -> Callable[P, T]:
|
|
182
180
|
sig = inspect.signature(activity_fn)
|
|
183
181
|
|
|
184
182
|
# Keep track of kwargs-only
|
|
@@ -226,7 +224,7 @@ def _unpack_positional_args(
|
|
|
226
224
|
return new_args, new_kwargs
|
|
227
225
|
|
|
228
226
|
|
|
229
|
-
def with_retriables(
|
|
227
|
+
def with_retriables[**P, T](
|
|
230
228
|
retriables: set[type[Exception]] = None,
|
|
231
229
|
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
232
230
|
if retriables is None:
|
|
@@ -281,7 +279,7 @@ def with_retriables(
|
|
|
281
279
|
return decorator
|
|
282
280
|
|
|
283
281
|
|
|
284
|
-
def activity_defn(
|
|
282
|
+
def activity_defn[**P, T](
|
|
285
283
|
name: str, retriables: set[type[Exception]] = None
|
|
286
284
|
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
287
285
|
|
|
@@ -319,8 +317,8 @@ def to_raw_async_progress(
|
|
|
319
317
|
if not max_progress > 0:
|
|
320
318
|
raise ValueError("max_progress must be > 0")
|
|
321
319
|
|
|
322
|
-
async def raw(p: int) -> None:
|
|
323
|
-
await progress(p / max_progress)
|
|
320
|
+
async def raw(p: int, *, force: bool = False) -> None:
|
|
321
|
+
await progress(p / max_progress, force=force)
|
|
324
322
|
|
|
325
323
|
return raw
|
|
326
324
|
|
|
@@ -331,10 +329,10 @@ def to_incremental_async_progress(
|
|
|
331
329
|
|
|
332
330
|
offset = 0
|
|
333
331
|
|
|
334
|
-
async def incremental(p: int) -> None:
|
|
332
|
+
async def incremental(p: int, *, force: bool = False) -> None:
|
|
335
333
|
nonlocal offset
|
|
336
334
|
offset += p
|
|
337
|
-
await progress(offset)
|
|
335
|
+
await progress(offset, force=force)
|
|
338
336
|
|
|
339
337
|
return incremental
|
|
340
338
|
|
|
@@ -345,8 +343,10 @@ def to_raw_sync_progress(
|
|
|
345
343
|
if not max_progress > 0:
|
|
346
344
|
raise ValueError("max_progress must be > 0")
|
|
347
345
|
|
|
348
|
-
def raw(
|
|
349
|
-
|
|
346
|
+
def raw(
|
|
347
|
+
iteration: int, event_loop: asyncio.AbstractEventLoop, *, force: bool = False
|
|
348
|
+
) -> None:
|
|
349
|
+
progress(iteration / max_progress, event_loop, force=force)
|
|
350
350
|
|
|
351
351
|
return raw
|
|
352
352
|
|
|
@@ -359,8 +359,8 @@ def to_scaled_async_progress(
|
|
|
359
359
|
if not start < end <= 1.0:
|
|
360
360
|
raise ValueError("end must be ]start, 1.0]")
|
|
361
361
|
|
|
362
|
-
async def _scaled(p: float) -> None:
|
|
363
|
-
await progress(start + p * (end - start))
|
|
362
|
+
async def _scaled(p: float, *, force: bool = False) -> None:
|
|
363
|
+
await progress(start + p * (end - start), force=force)
|
|
364
364
|
|
|
365
365
|
return _scaled
|
|
366
366
|
|
|
@@ -615,11 +615,11 @@ def _read_jsonl(path: Path) -> Iterable[dict]:
|
|
|
615
615
|
M = TypeVar("M", bound=BaseModel)
|
|
616
616
|
|
|
617
617
|
|
|
618
|
-
def read_jsonl_as(path: Path, cls: type[M]) -> Iterable[M]:
|
|
618
|
+
def read_jsonl_as[M](path: Path, cls: type[M]) -> Iterable[M]:
|
|
619
619
|
return (cls.model_validate(d) for d in _read_jsonl(path))
|
|
620
620
|
|
|
621
621
|
|
|
622
|
-
async def async_read_jsonl_as(
|
|
622
|
+
async def async_read_jsonl_as[M](
|
|
623
623
|
path: Path, processed_file_cls: type[M]
|
|
624
624
|
) -> AsyncIterable[M]:
|
|
625
625
|
async with async_open(path, "r") as f:
|
|
Binary file
|
|
@@ -83,6 +83,7 @@ def datashare_worker(
|
|
|
83
83
|
# at a time
|
|
84
84
|
max_concurrent_activities: int = 1,
|
|
85
85
|
max_activities_per_second: float = 20.0,
|
|
86
|
+
min_progress_interval_s: float = 30.0,
|
|
86
87
|
sandboxed: bool = True,
|
|
87
88
|
) -> DatashareWorker:
|
|
88
89
|
if workflows is None:
|
|
@@ -108,7 +109,7 @@ def datashare_worker(
|
|
|
108
109
|
logger.warning(_SEPARATE_IO_AND_CPU_WORKERS)
|
|
109
110
|
interceptors = [
|
|
110
111
|
TraceContextInterceptor(),
|
|
111
|
-
ProgressInterceptor(),
|
|
112
|
+
ProgressInterceptor(min_progress_interval_s=min_progress_interval_s),
|
|
112
113
|
HeartbeatInterceptor(),
|
|
113
114
|
]
|
|
114
115
|
wf_runner = SandboxedWorkflowRunner() if sandboxed else UnsandboxedWorkflowRunner()
|
|
@@ -216,6 +217,7 @@ async def worker_context(
|
|
|
216
217
|
activities=acts,
|
|
217
218
|
task_queue=task_queue,
|
|
218
219
|
max_concurrent_activities=worker_config.max_concurrent_activities,
|
|
220
|
+
min_progress_interval_s=worker_config.min_progress_interval_s,
|
|
219
221
|
sandboxed=sandboxed,
|
|
220
222
|
)
|
|
221
223
|
async with worker:
|
|
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
|