durable-workflow 2.1.2__tar.gz → 2.3.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.3.0}/PKG-INFO +1 -1
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/pyproject.toml +4 -4
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/__init__.py +3 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/activity.py +10 -1
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/client.py +66 -13
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/errors.py +5 -3
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/external_storage.py +12 -2
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/worker.py +412 -26
- durable_workflow-2.3.0/src/durable_workflow/worker_session.py +115 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/workflow.py +183 -8
- {durable_workflow-2.1.2 → durable_workflow-2.3.0/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow.egg-info/SOURCES.txt +2 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_client.py +24 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_external_storage.py +219 -1
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_release_metadata.py +1 -1
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_replay.py +81 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_replay_regression_corpus.py +7 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_worker.py +263 -5
- durable_workflow-2.3.0/tests/test_worker_session.py +118 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/LICENSE +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/README.md +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/setup.cfg +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_errors.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_invocable.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_metrics.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_queries.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_schedules.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_serializer.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_signals.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_sleep.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_sync.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_updates.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.0}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.1.2 → durable_workflow-2.3.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.3.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,9 +71,9 @@ 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.
|
|
76
|
-
supported-server-versions = "2.4.
|
|
74
|
+
product-train = "2.3.0"
|
|
75
|
+
registry-version = "2.3.0"
|
|
76
|
+
supported-server-versions = "2.4.2"
|
|
77
77
|
worker-protocol-version = "1.19"
|
|
78
78
|
control-plane-version = "2"
|
|
79
79
|
durable-selection = true
|
|
@@ -210,6 +210,7 @@ from .serializer import (
|
|
|
210
210
|
to_avro_payload_values,
|
|
211
211
|
)
|
|
212
212
|
from .worker import Worker
|
|
213
|
+
from .worker_session import WorkerSession, WorkerSessionOptions
|
|
213
214
|
from .workflow import (
|
|
214
215
|
ActivityRetryPolicy,
|
|
215
216
|
ChildWorkflowRetryPolicy,
|
|
@@ -290,6 +291,8 @@ __all__ = [
|
|
|
290
291
|
"TaskQueueQueryAdmission",
|
|
291
292
|
"TaskQueueTaskAdmission",
|
|
292
293
|
"Worker",
|
|
294
|
+
"WorkerSession",
|
|
295
|
+
"WorkerSessionOptions",
|
|
293
296
|
"WorkerInterceptor",
|
|
294
297
|
"WorkflowCommandResult",
|
|
295
298
|
"WorkflowExecution",
|
|
@@ -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,
|
|
@@ -31,7 +31,7 @@ from collections.abc import AsyncIterator, Callable, Mapping, Sequence
|
|
|
31
31
|
from dataclasses import dataclass
|
|
32
32
|
from importlib.metadata import PackageNotFoundError
|
|
33
33
|
from importlib.metadata import version as _pkg_version
|
|
34
|
-
from typing import Any
|
|
34
|
+
from typing import Any, cast
|
|
35
35
|
from urllib.parse import quote, unquote, urlencode, urlsplit
|
|
36
36
|
|
|
37
37
|
import httpx
|
|
@@ -61,19 +61,20 @@ from .external_storage import (
|
|
|
61
61
|
from .metrics import CLIENT_REQUEST_DURATION_SECONDS, CLIENT_REQUESTS, NOOP_METRICS, MetricsRecorder
|
|
62
62
|
from .nexus import NexusOperationResult, nexus_request_payload
|
|
63
63
|
from .retry_policy import TransportRetryPolicy
|
|
64
|
+
from .worker_session import WorkerSessionOptions
|
|
64
65
|
|
|
65
66
|
PROTOCOL_VERSION = "1.19"
|
|
66
67
|
CONTROL_PLANE_VERSION = "2"
|
|
67
68
|
PORTABLE_WORKER_AFFINITY_CAPABILITY_MANIFEST: dict[str, dict[str, str | bool]] = {
|
|
68
69
|
"local_activities": {
|
|
69
|
-
"supported":
|
|
70
|
+
"supported": True,
|
|
70
71
|
"minimum_protocol_version": "1.18",
|
|
71
|
-
"
|
|
72
|
+
"implementation": "record_local_activity",
|
|
72
73
|
},
|
|
73
74
|
"worker_sessions": {
|
|
74
|
-
"supported":
|
|
75
|
+
"supported": True,
|
|
75
76
|
"minimum_protocol_version": "1.18",
|
|
76
|
-
"
|
|
77
|
+
"implementation": "typed_worker_session",
|
|
77
78
|
},
|
|
78
79
|
"sticky_execution": {
|
|
79
80
|
"supported": False,
|
|
@@ -1316,7 +1317,7 @@ class WorkflowHandle:
|
|
|
1316
1317
|
return await self._client.query_workflow(self.workflow_id, query_name, args=args)
|
|
1317
1318
|
|
|
1318
1319
|
async def cancel(self, *, reason: str | None = None) -> None:
|
|
1319
|
-
"""
|
|
1320
|
+
"""Close this workflow's current run as cancelled. See :meth:`Client.cancel_workflow`."""
|
|
1320
1321
|
await self._client.cancel_workflow(self.workflow_id, reason=reason)
|
|
1321
1322
|
|
|
1322
1323
|
async def terminate(self, *, reason: str | None = None) -> None:
|
|
@@ -4114,11 +4115,13 @@ class Client:
|
|
|
4114
4115
|
)
|
|
4115
4116
|
|
|
4116
4117
|
async def cancel_workflow(self, workflow_id: str, *, reason: str | None = None) -> None:
|
|
4117
|
-
"""
|
|
4118
|
+
"""Close the current run as cancelled immediately.
|
|
4118
4119
|
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4120
|
+
Server cancels open tasks and timers; it does not resume workflow code
|
|
4121
|
+
to run saga or ``finally`` cleanup. :meth:`terminate_workflow` also
|
|
4122
|
+
closes immediately, with a distinct terminal outcome. Embedded
|
|
4123
|
+
Laravel's cooperative ``requestCancellation()`` is not yet available
|
|
4124
|
+
through this service-mode API.
|
|
4122
4125
|
"""
|
|
4123
4126
|
body: dict[str, Any] = {}
|
|
4124
4127
|
if reason is not None:
|
|
@@ -4126,10 +4129,11 @@ class Client:
|
|
|
4126
4129
|
await self._request("POST", f"/workflows/{workflow_id}/cancel", json=body, context=workflow_id)
|
|
4127
4130
|
|
|
4128
4131
|
async def terminate_workflow(self, workflow_id: str, *, reason: str | None = None) -> None:
|
|
4129
|
-
"""
|
|
4132
|
+
"""Close the current run as terminated immediately.
|
|
4130
4133
|
|
|
4131
|
-
|
|
4132
|
-
|
|
4134
|
+
Like :meth:`cancel_workflow`, this does not resume workflow code for
|
|
4135
|
+
cleanup. Use the distinct terminal outcome when termination is the
|
|
4136
|
+
appropriate operator action.
|
|
4133
4137
|
"""
|
|
4134
4138
|
body: dict[str, Any] = {}
|
|
4135
4139
|
if reason is not None:
|
|
@@ -4667,6 +4671,7 @@ class Client:
|
|
|
4667
4671
|
supported_activity_types: list[str] | None = None,
|
|
4668
4672
|
max_concurrent_workflow_tasks: int | None = None,
|
|
4669
4673
|
max_concurrent_activity_tasks: int | None = None,
|
|
4674
|
+
max_concurrent_worker_sessions: int | None = None,
|
|
4670
4675
|
runtime: str = "python",
|
|
4671
4676
|
sdk_version: str | None = None,
|
|
4672
4677
|
build_id: str | None = None,
|
|
@@ -4690,6 +4695,8 @@ class Client:
|
|
|
4690
4695
|
raise ValueError("max_concurrent_workflow_tasks must be at least 1")
|
|
4691
4696
|
if max_concurrent_activity_tasks is not None and max_concurrent_activity_tasks < 1:
|
|
4692
4697
|
raise ValueError("max_concurrent_activity_tasks must be at least 1")
|
|
4698
|
+
if max_concurrent_worker_sessions is not None and max_concurrent_worker_sessions < 1:
|
|
4699
|
+
raise ValueError("max_concurrent_worker_sessions must be at least 1")
|
|
4693
4700
|
if (
|
|
4694
4701
|
capabilities
|
|
4695
4702
|
and _MESSAGE_STREAMS_CAPABILITY in capabilities
|
|
@@ -4718,6 +4725,8 @@ class Client:
|
|
|
4718
4725
|
body["max_concurrent_workflow_tasks"] = max_concurrent_workflow_tasks
|
|
4719
4726
|
if max_concurrent_activity_tasks is not None:
|
|
4720
4727
|
body["max_concurrent_activity_tasks"] = max_concurrent_activity_tasks
|
|
4728
|
+
if max_concurrent_worker_sessions is not None:
|
|
4729
|
+
body["max_concurrent_worker_sessions"] = max_concurrent_worker_sessions
|
|
4721
4730
|
if task_slots is not None:
|
|
4722
4731
|
body["task_slots"] = task_slots
|
|
4723
4732
|
if process_metrics is not None:
|
|
@@ -4726,6 +4735,32 @@ class Client:
|
|
|
4726
4735
|
body["heartbeat_interval_seconds"] = heartbeat_interval_seconds
|
|
4727
4736
|
return await self._request("POST", "/worker/register", worker=True, json=body)
|
|
4728
4737
|
|
|
4738
|
+
async def create_worker_session(
|
|
4739
|
+
self, worker_id: str, options: WorkerSessionOptions
|
|
4740
|
+
) -> dict[str, Any]:
|
|
4741
|
+
return cast(dict[str, Any], await self._request(
|
|
4742
|
+
"POST", "/worker/sessions", worker=True,
|
|
4743
|
+
json={"worker_id": worker_id, **options.to_wire()},
|
|
4744
|
+
))
|
|
4745
|
+
|
|
4746
|
+
async def renew_worker_session(
|
|
4747
|
+
self, worker_id: str, session_id: str, lease_seconds: int
|
|
4748
|
+
) -> dict[str, Any]:
|
|
4749
|
+
if lease_seconds < 1:
|
|
4750
|
+
raise ValueError("worker session lease_seconds must be positive")
|
|
4751
|
+
return cast(dict[str, Any], await self._request(
|
|
4752
|
+
"POST", f"/worker/sessions/{quote(session_id, safe='')}/heartbeat", worker=True,
|
|
4753
|
+
json={"worker_id": worker_id, "lease_seconds": lease_seconds},
|
|
4754
|
+
))
|
|
4755
|
+
|
|
4756
|
+
async def close_worker_session(
|
|
4757
|
+
self, worker_id: str, session_id: str, reason: str = "worker_shutdown"
|
|
4758
|
+
) -> dict[str, Any]:
|
|
4759
|
+
return cast(dict[str, Any], await self._request(
|
|
4760
|
+
"DELETE", f"/worker/sessions/{quote(session_id, safe='')}", worker=True,
|
|
4761
|
+
json={"worker_id": worker_id, "reason": reason},
|
|
4762
|
+
))
|
|
4763
|
+
|
|
4729
4764
|
async def deregister_worker_registration(self, worker_id: str) -> dict[str, Any]:
|
|
4730
4765
|
"""Remove this runtime's successful worker-plane registration.
|
|
4731
4766
|
|
|
@@ -4964,6 +4999,24 @@ class Client:
|
|
|
4964
4999
|
"POST", f"/worker/workflow-tasks/{task_id}/fail", worker=True, json=body
|
|
4965
5000
|
)
|
|
4966
5001
|
|
|
5002
|
+
async def heartbeat_workflow_task(
|
|
5003
|
+
self,
|
|
5004
|
+
*,
|
|
5005
|
+
task_id: str,
|
|
5006
|
+
lease_owner: str,
|
|
5007
|
+
workflow_task_attempt: int,
|
|
5008
|
+
) -> Any:
|
|
5009
|
+
"""Renew a leased workflow task while in-process work is running."""
|
|
5010
|
+
return await self._request(
|
|
5011
|
+
"POST",
|
|
5012
|
+
f"/worker/workflow-tasks/{task_id}/heartbeat",
|
|
5013
|
+
worker=True,
|
|
5014
|
+
json={
|
|
5015
|
+
"lease_owner": lease_owner,
|
|
5016
|
+
"workflow_task_attempt": workflow_task_attempt,
|
|
5017
|
+
},
|
|
5018
|
+
)
|
|
5019
|
+
|
|
4967
5020
|
async def workflow_task_history(
|
|
4968
5021
|
self,
|
|
4969
5022
|
*,
|
|
@@ -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,
|