durable-workflow 2.4.2__tar.gz → 2.4.3__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.4.2/src/durable_workflow.egg-info → durable_workflow-2.4.3}/PKG-INFO +1 -1
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/pyproject.toml +3 -3
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/__init__.py +2 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/client.py +3 -1
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/errors.py +30 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_client.py +29 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_errors.py +53 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_sync.py +29 -1
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/LICENSE +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/README.md +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/setup.cfg +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/worker.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/workflow.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_invocable.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_metrics.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_queries.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_replay.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_replay_regression_corpus.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_schedules.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_serializer.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_signals.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_sleep.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_updates.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_worker.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.4.2 → durable_workflow-2.4.3}/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.4.
|
|
7
|
+
version = "2.4.3"
|
|
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.4.
|
|
75
|
-
registry-version = "2.4.
|
|
74
|
+
product-train = "2.4.3"
|
|
75
|
+
registry-version = "2.4.3"
|
|
76
76
|
supported-server-versions = "2.5.0"
|
|
77
77
|
worker-protocol-version = "1.19"
|
|
78
78
|
control-plane-version = "2"
|
|
@@ -95,6 +95,7 @@ from .errors import (
|
|
|
95
95
|
ServerError,
|
|
96
96
|
SignalFailed,
|
|
97
97
|
Unauthorized,
|
|
98
|
+
UpdateFailed,
|
|
98
99
|
UpdateRejected,
|
|
99
100
|
UpdateValidationFailed,
|
|
100
101
|
WorkflowAlreadyStarted,
|
|
@@ -385,6 +386,7 @@ __all__ = [
|
|
|
385
386
|
"SignalFailed",
|
|
386
387
|
"TransportRetryPolicy",
|
|
387
388
|
"Unauthorized",
|
|
389
|
+
"UpdateFailed",
|
|
388
390
|
"UpdateRejected",
|
|
389
391
|
"UpdateValidationFailed",
|
|
390
392
|
"WorkflowAlreadyStarted",
|
|
@@ -4349,7 +4349,9 @@ class Client:
|
|
|
4349
4349
|
:class:`~durable_workflow.errors.UpdateRejected` when the workflow's
|
|
4350
4350
|
validator rejects the update, or
|
|
4351
4351
|
:class:`~durable_workflow.errors.UpdateValidationFailed` when the
|
|
4352
|
-
declared validation boundary cannot be enforced.
|
|
4352
|
+
declared validation boundary cannot be enforced. An accepted update
|
|
4353
|
+
whose handler fails raises :class:`~durable_workflow.errors.UpdateFailed`
|
|
4354
|
+
with the failure message, Server response and durable identities.
|
|
4353
4355
|
"""
|
|
4354
4356
|
await self._require_update_wait_stage(wait_for or "accepted")
|
|
4355
4357
|
body: dict[str, Any] = {}
|
|
@@ -453,6 +453,29 @@ class NonDeterministicReplayError(DurableWorkflowError):
|
|
|
453
453
|
self.recorded_event_types = list(recorded_event_types)
|
|
454
454
|
|
|
455
455
|
|
|
456
|
+
class UpdateFailed(InvalidArgument):
|
|
457
|
+
"""An accepted workflow update failed during handler execution.
|
|
458
|
+
|
|
459
|
+
``body`` preserves the Server response, including its durable failure
|
|
460
|
+
identity. This inherits from :class:`InvalidArgument` to preserve catches
|
|
461
|
+
written for earlier SDK versions that mapped all HTTP 422 responses there.
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
def __init__(self, message: str, *, status: int, body: dict[str, Any]) -> None:
|
|
465
|
+
super().__init__(message)
|
|
466
|
+
self.status = status
|
|
467
|
+
self.body = body
|
|
468
|
+
self.workflow_id = self._identity(body, "workflow_id")
|
|
469
|
+
self.run_id = self._identity(body, "run_id")
|
|
470
|
+
self.update_id = self._identity(body, "update_id")
|
|
471
|
+
self.failure_id = self._identity(body, "failure_id")
|
|
472
|
+
|
|
473
|
+
@staticmethod
|
|
474
|
+
def _identity(body: dict[str, Any], name: str) -> str | None:
|
|
475
|
+
value = body.get(name)
|
|
476
|
+
return value if isinstance(value, str) and value.strip() else None
|
|
477
|
+
|
|
478
|
+
|
|
456
479
|
class UpdateRejected(DurableWorkflowError):
|
|
457
480
|
"""A workflow update was rejected by the workflow's validator."""
|
|
458
481
|
|
|
@@ -859,6 +882,13 @@ def _raise_for_status(status: int, body: object, *, context: str = "") -> None:
|
|
|
859
882
|
raise signal_failed("signal argument validation failed")
|
|
860
883
|
if reason == "invalid_query_arguments":
|
|
861
884
|
raise query_failed("query argument validation failed")
|
|
885
|
+
if isinstance(body, dict) and body.get("update_status") == "failed":
|
|
886
|
+
failure_message = body.get("failure_message")
|
|
887
|
+
if not isinstance(failure_message, str) or not failure_message.strip():
|
|
888
|
+
failure_message = message
|
|
889
|
+
if not isinstance(failure_message, str) or not failure_message.strip():
|
|
890
|
+
failure_message = "workflow update failed"
|
|
891
|
+
raise UpdateFailed(failure_message, status=status, body=body)
|
|
862
892
|
errors = None
|
|
863
893
|
if isinstance(body, dict):
|
|
864
894
|
errors = body.get("errors") or body.get("validation_errors")
|
|
@@ -30,6 +30,7 @@ from durable_workflow.errors import (
|
|
|
30
30
|
ServerError,
|
|
31
31
|
SignalFailed,
|
|
32
32
|
Unauthorized,
|
|
33
|
+
UpdateFailed,
|
|
33
34
|
UpdateRejected,
|
|
34
35
|
WorkflowAlreadyStarted,
|
|
35
36
|
WorkflowNotFound,
|
|
@@ -3090,6 +3091,34 @@ class TestHeartbeatActivityTask:
|
|
|
3090
3091
|
|
|
3091
3092
|
|
|
3092
3093
|
class TestUpdateWorkflow:
|
|
3094
|
+
@pytest.mark.asyncio
|
|
3095
|
+
async def test_failed_update_retries_preserve_failure_identity(self, client: Client) -> None:
|
|
3096
|
+
body = {
|
|
3097
|
+
"update_status": "failed", "accepted": True,
|
|
3098
|
+
"failure_message": "inventory unavailable", "workflow_id": "wf-1",
|
|
3099
|
+
"run_id": "run-1", "update_id": "update-1", "failure_id": "failure-1",
|
|
3100
|
+
}
|
|
3101
|
+
with patch.object(
|
|
3102
|
+
client._http, "request", new_callable=AsyncMock, return_value=_mock_response(422, body),
|
|
3103
|
+
) as request:
|
|
3104
|
+
failures = []
|
|
3105
|
+
for _ in range(2):
|
|
3106
|
+
with pytest.raises(UpdateFailed) as exc_info:
|
|
3107
|
+
await client.update_workflow(
|
|
3108
|
+
"wf-1", "reserve", wait_for="completed", request_id="request-1",
|
|
3109
|
+
)
|
|
3110
|
+
failures.append(exc_info.value)
|
|
3111
|
+
assert request.await_count == 2
|
|
3112
|
+
assert all(call.kwargs["json"]["request_id"] == "request-1"
|
|
3113
|
+
for call in request.await_args_list)
|
|
3114
|
+
for error in failures:
|
|
3115
|
+
assert str(error) == "inventory unavailable"
|
|
3116
|
+
assert error.status == 422
|
|
3117
|
+
assert error.body == body
|
|
3118
|
+
assert (error.workflow_id, error.run_id, error.update_id, error.failure_id) == (
|
|
3119
|
+
"wf-1", "run-1", "update-1", "failure-1",
|
|
3120
|
+
)
|
|
3121
|
+
|
|
3093
3122
|
@pytest.mark.asyncio
|
|
3094
3123
|
async def test_unsupported_wait_stage_raises_typed_error_before_update(self) -> None:
|
|
3095
3124
|
client = Client("http://localhost:8080", token="test-token")
|
|
@@ -13,6 +13,7 @@ from durable_workflow.errors import (
|
|
|
13
13
|
ServerError,
|
|
14
14
|
SignalFailed,
|
|
15
15
|
Unauthorized,
|
|
16
|
+
UpdateFailed,
|
|
16
17
|
UpdateRejected,
|
|
17
18
|
UpdateValidationFailed,
|
|
18
19
|
WorkflowAlreadyStarted,
|
|
@@ -102,6 +103,56 @@ class TestRaiseForStatus:
|
|
|
102
103
|
with pytest.raises(InvalidArgument) as exc_info:
|
|
103
104
|
_raise_for_status(422, {"message": "bad", "errors": {"f": ["req"]}})
|
|
104
105
|
assert exc_info.value.errors == {"f": ["req"]}
|
|
106
|
+
assert type(exc_info.value) is InvalidArgument
|
|
107
|
+
|
|
108
|
+
def test_422_accepted_update_failure_preserves_diagnostics_and_legacy_catch(self) -> None:
|
|
109
|
+
body = {
|
|
110
|
+
"update_status": "failed", "accepted": True,
|
|
111
|
+
"failure_message": "inventory unavailable", "message": "generic failure",
|
|
112
|
+
"workflow_id": "wf-1", "run_id": "run-1", "update_id": "update-1",
|
|
113
|
+
"failure_id": "failure-1",
|
|
114
|
+
}
|
|
115
|
+
with pytest.raises(InvalidArgument) as exc_info:
|
|
116
|
+
_raise_for_status(422, body)
|
|
117
|
+
error = exc_info.value
|
|
118
|
+
assert isinstance(error, UpdateFailed)
|
|
119
|
+
assert str(error) == "inventory unavailable"
|
|
120
|
+
assert error.status == 422
|
|
121
|
+
assert error.body is body
|
|
122
|
+
assert error.errors is None
|
|
123
|
+
assert (error.workflow_id, error.run_id, error.update_id, error.failure_id) == (
|
|
124
|
+
"wf-1", "run-1", "update-1", "failure-1",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@pytest.mark.parametrize("failure_message", [None, "", " ", 42])
|
|
128
|
+
@pytest.mark.parametrize("message", ["handler failed", None, "", " ", 42])
|
|
129
|
+
def test_update_failure_message_fallback(self, failure_message: object, message: object) -> None:
|
|
130
|
+
with pytest.raises(UpdateFailed) as exc_info:
|
|
131
|
+
_raise_for_status(422, {
|
|
132
|
+
"update_status": "failed", "failure_message": failure_message, "message": message,
|
|
133
|
+
})
|
|
134
|
+
assert str(exc_info.value) == (
|
|
135
|
+
message if message == "handler failed" else "workflow update failed"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def test_update_failure_missing_or_malformed_identities_remain_absent(self) -> None:
|
|
139
|
+
with pytest.raises(UpdateFailed) as exc_info:
|
|
140
|
+
_raise_for_status(422, {
|
|
141
|
+
"update_status": "failed", "workflow_id": 42, "run_id": " ", "update_id": None,
|
|
142
|
+
})
|
|
143
|
+
error = exc_info.value
|
|
144
|
+
assert (error.workflow_id, error.run_id, error.update_id, error.failure_id) == (
|
|
145
|
+
None, None, None, None,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def test_rejected_update_arguments_are_not_handler_failure(self) -> None:
|
|
149
|
+
with pytest.raises(InvalidArgument) as exc_info:
|
|
150
|
+
_raise_for_status(422, {
|
|
151
|
+
"update_status": "rejected", "message": "invalid arguments",
|
|
152
|
+
"validation_errors": {"quantity": ["must be positive"]},
|
|
153
|
+
})
|
|
154
|
+
assert type(exc_info.value) is InvalidArgument
|
|
155
|
+
assert exc_info.value.errors == {"quantity": ["must be positive"]}
|
|
105
156
|
|
|
106
157
|
def test_422_update_validator_rejected_is_typed(self) -> None:
|
|
107
158
|
with pytest.raises(UpdateRejected) as exc_info:
|
|
@@ -109,6 +160,7 @@ class TestRaiseForStatus:
|
|
|
109
160
|
422,
|
|
110
161
|
{
|
|
111
162
|
"reason": "update_validator_rejected",
|
|
163
|
+
"update_status": "failed",
|
|
112
164
|
"message": "approval required",
|
|
113
165
|
"validation_errors": {"approved": ["must be true"]},
|
|
114
166
|
},
|
|
@@ -132,6 +184,7 @@ class TestRaiseForStatus:
|
|
|
132
184
|
status,
|
|
133
185
|
{
|
|
134
186
|
"reason": reason,
|
|
187
|
+
"update_status": "failed",
|
|
135
188
|
"message": "validation could not complete",
|
|
136
189
|
"retryable": retryable,
|
|
137
190
|
},
|
|
@@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, patch
|
|
|
9
9
|
import httpx
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
|
-
from durable_workflow import serializer
|
|
12
|
+
from durable_workflow import InvalidArgument, UpdateFailed, serializer
|
|
13
13
|
from durable_workflow.client import WorkflowHandle
|
|
14
14
|
from durable_workflow.external_storage import ExternalPayloadCache, LocalFilesystemExternalStorage
|
|
15
15
|
from durable_workflow.sync import Client, SyncStandaloneActivityHandle, SyncWorkflowHandle
|
|
@@ -581,6 +581,34 @@ class TestSyncClientMaintenance:
|
|
|
581
581
|
|
|
582
582
|
|
|
583
583
|
class TestSyncClientUpdate:
|
|
584
|
+
def test_failed_update_preserves_diagnostics(self) -> None:
|
|
585
|
+
body = {
|
|
586
|
+
"update_status": "failed", "failure_message": "inventory unavailable",
|
|
587
|
+
"workflow_id": "wf-1", "run_id": "run-1", "update_id": "update-1",
|
|
588
|
+
"failure_id": "failure-1",
|
|
589
|
+
}
|
|
590
|
+
with Client("http://localhost:8080") as client:
|
|
591
|
+
client._async._cluster_info = {
|
|
592
|
+
"control_plane": {"request_contract": {"operations": {"update": {
|
|
593
|
+
"fields": {"wait_for": {"canonical_values": ["accepted", "completed"]}},
|
|
594
|
+
}}}},
|
|
595
|
+
}
|
|
596
|
+
with (
|
|
597
|
+
patch.object(client._async._http, "request", new_callable=AsyncMock,
|
|
598
|
+
return_value=_mock_response(422, body)) as request,
|
|
599
|
+
pytest.raises(InvalidArgument) as exc_info,
|
|
600
|
+
):
|
|
601
|
+
client.update_workflow("wf-1", "reserve", request_id="request-1")
|
|
602
|
+
error = exc_info.value
|
|
603
|
+
assert isinstance(error, UpdateFailed)
|
|
604
|
+
assert str(error) == "inventory unavailable"
|
|
605
|
+
assert error.status == 422
|
|
606
|
+
assert error.body == body
|
|
607
|
+
assert (error.workflow_id, error.run_id, error.update_id, error.failure_id) == (
|
|
608
|
+
"wf-1", "run-1", "update-1", "failure-1",
|
|
609
|
+
)
|
|
610
|
+
assert request.call_args.kwargs["json"]["request_id"] == "request-1"
|
|
611
|
+
|
|
584
612
|
def test_update(self) -> None:
|
|
585
613
|
client = Client("http://localhost:8080")
|
|
586
614
|
client._async._cluster_info = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/_cancellation_scope.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/_cooperative_cancellation.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/_prepared_local_activity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/history_bundle_verify.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow/replay_conformance.py
RENAMED
|
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
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_activity_cancellation_policies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cancellation_scope_admission.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cancellation_scope_authoring.py
RENAMED
|
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
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_committed_cancellation_scope_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_committed_cancellation_scope_history.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_committed_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cooperative_cancellation_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_cooperative_cancellation_worker.py
RENAMED
|
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
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_parallel_completion_order_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_prepared_local_activity_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_prepared_local_activity_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_prepared_local_activity_policies.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_prepared_local_activity_worker.py
RENAMED
|
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
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_run_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_runtime_external_payload_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_scoped_run_cancellation_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_update_signal_condition_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.4.2 → durable_workflow-2.4.3}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|