durable-workflow 2.1.2__tar.gz → 2.2.0__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.
- {durable_workflow-2.1.2/src/durable_workflow.egg-info → durable_workflow-2.2.0}/PKG-INFO +1 -1
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/pyproject.toml +3 -3
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/activity.py +10 -1
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/client.py +31 -10
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/errors.py +5 -3
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/external_storage.py +12 -2
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/worker.py +358 -26
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/workflow.py +175 -8
- {durable_workflow-2.1.2 → durable_workflow-2.2.0/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_client.py +24 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_external_storage.py +219 -1
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_replay.py +81 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_replay_regression_corpus.py +7 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_worker.py +255 -3
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/LICENSE +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/README.md +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/setup.cfg +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_errors.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_invocable.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_metrics.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_queries.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_schedules.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_serializer.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_signals.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_sleep.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_sync.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_updates.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.2.0}/tests/test_workflow_updates_conformance.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "durable-workflow"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.2.0"
|
|
8
8
|
description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -71,8 +71,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
|
|
|
71
71
|
durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
|
|
72
72
|
|
|
73
73
|
[tool.durable-workflow]
|
|
74
|
-
product-train = "2.
|
|
75
|
-
registry-version = "2.
|
|
74
|
+
product-train = "2.2.0"
|
|
75
|
+
registry-version = "2.2.0"
|
|
76
76
|
supported-server-versions = "2.4.0"
|
|
77
77
|
worker-protocol-version = "1.19"
|
|
78
78
|
control-plane-version = "2"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import contextvars
|
|
5
|
-
from collections.abc import Callable
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
@@ -38,9 +38,11 @@ class ActivityContext:
|
|
|
38
38
|
*,
|
|
39
39
|
info: ActivityInfo,
|
|
40
40
|
client: Client,
|
|
41
|
+
heartbeat_callback: Callable[[dict[str, Any] | None], Awaitable[None]] | None = None,
|
|
41
42
|
) -> None:
|
|
42
43
|
self._info = info
|
|
43
44
|
self._client = client
|
|
45
|
+
self._heartbeat_callback = heartbeat_callback
|
|
44
46
|
self._cancel_requested = False
|
|
45
47
|
|
|
46
48
|
@property
|
|
@@ -65,6 +67,13 @@ class ActivityContext:
|
|
|
65
67
|
owning workflow has requested cancellation, so the activity can exit
|
|
66
68
|
cleanly at its next natural break point.
|
|
67
69
|
"""
|
|
70
|
+
if self._heartbeat_callback is not None:
|
|
71
|
+
try:
|
|
72
|
+
await self._heartbeat_callback(details)
|
|
73
|
+
except ActivityCancelled:
|
|
74
|
+
self._cancel_requested = True
|
|
75
|
+
raise
|
|
76
|
+
return
|
|
68
77
|
resp = await self._client.heartbeat_activity_task(
|
|
69
78
|
task_id=self._info.task_id,
|
|
70
79
|
activity_attempt_id=self._info.activity_attempt_id,
|
|
@@ -66,9 +66,9 @@ PROTOCOL_VERSION = "1.19"
|
|
|
66
66
|
CONTROL_PLANE_VERSION = "2"
|
|
67
67
|
PORTABLE_WORKER_AFFINITY_CAPABILITY_MANIFEST: dict[str, dict[str, str | bool]] = {
|
|
68
68
|
"local_activities": {
|
|
69
|
-
"supported":
|
|
69
|
+
"supported": True,
|
|
70
70
|
"minimum_protocol_version": "1.18",
|
|
71
|
-
"
|
|
71
|
+
"implementation": "record_local_activity",
|
|
72
72
|
},
|
|
73
73
|
"worker_sessions": {
|
|
74
74
|
"supported": False,
|
|
@@ -1316,7 +1316,7 @@ class WorkflowHandle:
|
|
|
1316
1316
|
return await self._client.query_workflow(self.workflow_id, query_name, args=args)
|
|
1317
1317
|
|
|
1318
1318
|
async def cancel(self, *, reason: str | None = None) -> None:
|
|
1319
|
-
"""
|
|
1319
|
+
"""Close this workflow's current run as cancelled. See :meth:`Client.cancel_workflow`."""
|
|
1320
1320
|
await self._client.cancel_workflow(self.workflow_id, reason=reason)
|
|
1321
1321
|
|
|
1322
1322
|
async def terminate(self, *, reason: str | None = None) -> None:
|
|
@@ -4114,11 +4114,13 @@ class Client:
|
|
|
4114
4114
|
)
|
|
4115
4115
|
|
|
4116
4116
|
async def cancel_workflow(self, workflow_id: str, *, reason: str | None = None) -> None:
|
|
4117
|
-
"""
|
|
4117
|
+
"""Close the current run as cancelled immediately.
|
|
4118
4118
|
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4119
|
+
Server cancels open tasks and timers; it does not resume workflow code
|
|
4120
|
+
to run saga or ``finally`` cleanup. :meth:`terminate_workflow` also
|
|
4121
|
+
closes immediately, with a distinct terminal outcome. Embedded
|
|
4122
|
+
Laravel's cooperative ``requestCancellation()`` is not yet available
|
|
4123
|
+
through this service-mode API.
|
|
4122
4124
|
"""
|
|
4123
4125
|
body: dict[str, Any] = {}
|
|
4124
4126
|
if reason is not None:
|
|
@@ -4126,10 +4128,11 @@ class Client:
|
|
|
4126
4128
|
await self._request("POST", f"/workflows/{workflow_id}/cancel", json=body, context=workflow_id)
|
|
4127
4129
|
|
|
4128
4130
|
async def terminate_workflow(self, workflow_id: str, *, reason: str | None = None) -> None:
|
|
4129
|
-
"""
|
|
4131
|
+
"""Close the current run as terminated immediately.
|
|
4130
4132
|
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
+
Like :meth:`cancel_workflow`, this does not resume workflow code for
|
|
4134
|
+
cleanup. Use the distinct terminal outcome when termination is the
|
|
4135
|
+
appropriate operator action.
|
|
4133
4136
|
"""
|
|
4134
4137
|
body: dict[str, Any] = {}
|
|
4135
4138
|
if reason is not None:
|
|
@@ -4964,6 +4967,24 @@ class Client:
|
|
|
4964
4967
|
"POST", f"/worker/workflow-tasks/{task_id}/fail", worker=True, json=body
|
|
4965
4968
|
)
|
|
4966
4969
|
|
|
4970
|
+
async def heartbeat_workflow_task(
|
|
4971
|
+
self,
|
|
4972
|
+
*,
|
|
4973
|
+
task_id: str,
|
|
4974
|
+
lease_owner: str,
|
|
4975
|
+
workflow_task_attempt: int,
|
|
4976
|
+
) -> Any:
|
|
4977
|
+
"""Renew a leased workflow task while in-process work is running."""
|
|
4978
|
+
return await self._request(
|
|
4979
|
+
"POST",
|
|
4980
|
+
f"/worker/workflow-tasks/{task_id}/heartbeat",
|
|
4981
|
+
worker=True,
|
|
4982
|
+
json={
|
|
4983
|
+
"lease_owner": lease_owner,
|
|
4984
|
+
"workflow_task_attempt": workflow_task_attempt,
|
|
4985
|
+
},
|
|
4986
|
+
)
|
|
4987
|
+
|
|
4967
4988
|
async def workflow_task_history(
|
|
4968
4989
|
self,
|
|
4969
4990
|
*,
|
|
@@ -581,7 +581,8 @@ class ActivityFailed(DurableWorkflowError):
|
|
|
581
581
|
class WorkflowTerminated(DurableWorkflowError):
|
|
582
582
|
"""A workflow was terminated by operator action.
|
|
583
583
|
|
|
584
|
-
|
|
584
|
+
Like terminal cancellation, termination does not resume workflow cleanup.
|
|
585
|
+
It records a distinct terminal outcome.
|
|
585
586
|
"""
|
|
586
587
|
|
|
587
588
|
def __init__(self, message: str = "workflow was terminated") -> None:
|
|
@@ -643,8 +644,9 @@ class ActivityCancelled(BaseException):
|
|
|
643
644
|
"""An in-flight activity was cancelled.
|
|
644
645
|
|
|
645
646
|
Raised inside :meth:`durable_workflow.ActivityContext.heartbeat` when the
|
|
646
|
-
server reports that the
|
|
647
|
-
activity can exit
|
|
647
|
+
server reports that the task was revoked or its run was cancelled, so the
|
|
648
|
+
activity can exit on its next heartbeat. This permits local activity cleanup,
|
|
649
|
+
not durable workflow compensation after the run closes.
|
|
648
650
|
|
|
649
651
|
Inherits from :class:`BaseException` — not :class:`Exception` — so that a
|
|
650
652
|
user ``except Exception:`` block inside the activity function cannot
|
|
@@ -12,7 +12,12 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Any, Protocol
|
|
13
13
|
from urllib.parse import quote, unquote, urlparse
|
|
14
14
|
|
|
15
|
-
from .errors import
|
|
15
|
+
from .errors import (
|
|
16
|
+
ExternalPayloadError,
|
|
17
|
+
ExternalPayloadIntegrityMismatch,
|
|
18
|
+
ExternalPayloadUnavailable,
|
|
19
|
+
ExternalPayloadUnsupported,
|
|
20
|
+
)
|
|
16
21
|
|
|
17
22
|
EXTERNAL_PAYLOAD_REFERENCE_SCHEMA = "durable-workflow.v2.external-payload-reference.v1"
|
|
18
23
|
RUNTIME_EXTERNAL_PAYLOAD_REFERENCE_SCHEMA = (
|
|
@@ -556,7 +561,12 @@ def store_external_payload(
|
|
|
556
561
|
if expires_at is not None:
|
|
557
562
|
_validate_rfc3339(expires_at)
|
|
558
563
|
sha256 = hashlib.sha256(data).hexdigest()
|
|
559
|
-
|
|
564
|
+
try:
|
|
565
|
+
uri = driver.put(data, sha256=sha256, codec=codec)
|
|
566
|
+
except ExternalPayloadError:
|
|
567
|
+
raise
|
|
568
|
+
except Exception as exc:
|
|
569
|
+
raise ExternalPayloadUnavailable("external payload storage upload failed") from exc
|
|
560
570
|
return ExternalPayloadReference(
|
|
561
571
|
uri=uri,
|
|
562
572
|
sha256=sha256,
|