durable-workflow 2.4.1__tar.gz → 2.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. {durable_workflow-2.4.1/src/durable_workflow.egg-info → durable_workflow-2.4.2}/PKG-INFO +1 -1
  2. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/pyproject.toml +3 -3
  3. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/client.py +13 -1
  4. {durable_workflow-2.4.1 → durable_workflow-2.4.2/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  5. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_client.py +19 -0
  6. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_pending_operation_replay.py +40 -1
  7. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/LICENSE +0 -0
  8. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/README.md +0 -0
  9. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/setup.cfg +0 -0
  10. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/__init__.py +0 -0
  11. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/_activity_process.py +0 -0
  12. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/_avro.py +0 -0
  13. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/_cancellation_scope.py +0 -0
  14. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  15. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  16. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/_prepared_local_activity.py +0 -0
  17. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/activity.py +0 -0
  18. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/auth_composition.py +0 -0
  19. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/cancellation.py +0 -0
  20. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/errors.py +0 -0
  21. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/external_storage.py +0 -0
  22. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/external_task_input.py +0 -0
  23. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/external_task_result.py +0 -0
  24. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/history_bundle_verify.py +0 -0
  25. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/interceptors.py +0 -0
  26. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/invocable.py +0 -0
  27. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/metrics.py +0 -0
  28. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/nexus.py +0 -0
  29. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/py.typed +0 -0
  30. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/python_conformance.py +0 -0
  31. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/replay_conformance.py +0 -0
  32. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/replay_verify.py +0 -0
  33. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/retry_policy.py +0 -0
  34. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/serializer.py +0 -0
  35. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/sync.py +0 -0
  36. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/testing.py +0 -0
  37. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/worker.py +0 -0
  38. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/worker_session.py +0 -0
  39. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/workflow.py +0 -0
  40. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  41. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow.egg-info/SOURCES.txt +0 -0
  42. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  43. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  44. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow.egg-info/requires.txt +0 -0
  45. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/src/durable_workflow.egg-info/top_level.txt +0 -0
  46. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_activity_cancellation_policies.py +0 -0
  47. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_activity_context.py +0 -0
  48. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_activity_process.py +0 -0
  49. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_auth_composition.py +0 -0
  50. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_avro_value_benchmark.py +0 -0
  51. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_avro_value_protocol.py +0 -0
  52. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_cancellation_context.py +0 -0
  53. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_cancellation_remaining_time.py +0 -0
  54. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_cancellation_scope_admission.py +0 -0
  55. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_cancellation_scope_authoring.py +0 -0
  56. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_cancellation_scope_boundary.py +0 -0
  57. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_cancellation_scope_opening.py +0 -0
  58. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_child_workflow_policies.py +0 -0
  59. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_ci_checkout.py +0 -0
  60. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_ci_integration_endpoint.py +0 -0
  61. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_client_timeouts.py +0 -0
  62. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_codec_regression_corpus.py +0 -0
  63. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  64. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_committed_cancellation_scope_groups.py +0 -0
  65. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_committed_cancellation_scope_history.py +0 -0
  66. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_committed_cancellation_scope_replay.py +0 -0
  67. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_control_plane_parity_fixtures.py +0 -0
  68. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_cooperative_cancellation.py +0 -0
  69. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_cooperative_cancellation_client.py +0 -0
  70. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_cooperative_cancellation_worker.py +0 -0
  71. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_cooperative_remote_worker.py +0 -0
  72. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_docstring_cross_references.py +0 -0
  73. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_durable_selection.py +0 -0
  74. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_errors.py +0 -0
  75. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_external_storage.py +0 -0
  76. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_external_task_input.py +0 -0
  77. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_external_task_result.py +0 -0
  78. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_golden_history_replay.py +0 -0
  79. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_history_bundle_verify.py +0 -0
  80. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_history_event_contract.py +0 -0
  81. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_invocable.py +0 -0
  82. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_message_streams.py +0 -0
  83. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_metrics.py +0 -0
  84. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_order_processing_example.py +0 -0
  85. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_parallel_completion_order_replay.py +0 -0
  86. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_parallel_saga_authoring.py +0 -0
  87. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_prepared_local_activity_client.py +0 -0
  88. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_prepared_local_activity_groups.py +0 -0
  89. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_prepared_local_activity_policies.py +0 -0
  90. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_prepared_local_activity_worker.py +0 -0
  91. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_public_boundary_scanner.py +0 -0
  92. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_pypi_project_surface.py +0 -0
  93. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_python_conformance.py +0 -0
  94. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_queries.py +0 -0
  95. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_readme_quickstart.py +0 -0
  96. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_redrive_replay.py +0 -0
  97. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_release_metadata.py +0 -0
  98. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_replay.py +0 -0
  99. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_replay_conformance.py +0 -0
  100. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_replay_regression_corpus.py +0 -0
  101. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_replay_verify.py +0 -0
  102. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_retry_policy.py +0 -0
  103. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_run_cancellation_scope_replay.py +0 -0
  104. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_runtime_external_payload_transport.py +0 -0
  105. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_schedules.py +0 -0
  106. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_scoped_run_cancellation_context.py +0 -0
  107. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_serializer.py +0 -0
  108. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_signals.py +0 -0
  109. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_sleep.py +0 -0
  110. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_standalone_activity_client.py +0 -0
  111. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_storage_admission.py +0 -0
  112. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_sync.py +0 -0
  113. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_testing_harness.py +0 -0
  114. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_update_signal_condition_replay.py +0 -0
  115. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_updates.py +0 -0
  116. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_wait_condition.py +0 -0
  117. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_worker.py +0 -0
  118. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_worker_session.py +0 -0
  119. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_workflow_result_timeout.py +0 -0
  120. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_workflow_streams.py +0 -0
  121. {durable_workflow-2.4.1 → durable_workflow-2.4.2}/tests/test_workflow_updates_conformance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.4.1
3
+ Version: 2.4.2
4
4
  Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "2.4.1"
7
+ version = "2.4.2"
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.1"
75
- registry-version = "2.4.1"
74
+ product-train = "2.4.2"
75
+ registry-version = "2.4.2"
76
76
  supported-server-versions = "2.5.0"
77
77
  worker-protocol-version = "1.19"
78
78
  control-plane-version = "2"
@@ -5115,11 +5115,23 @@ class Client:
5115
5115
 
5116
5116
  Worker-plane endpoint, called by :class:`~durable_workflow.Worker`.
5117
5117
  ``commands`` is the list of serialized commands the workflow yielded
5118
- for this task.
5118
+ for this task. An empty list acknowledges waiting for already scheduled
5119
+ history without reporting a workflow failure or scheduling another operation.
5119
5120
  """
5120
5121
  if (message_stream_cursors or message_stream_waits) and not _worker_protocol_supports_message_streams():
5121
5122
  raise ValueError("message stream completion metadata requires worker protocol 1.15 or newer")
5122
5123
 
5124
+ if not commands:
5125
+ if message_stream_cursors or message_stream_waits:
5126
+ raise ValueError("Message stream completion metadata requires nonempty commands.")
5127
+ return await self.fail_workflow_task(
5128
+ task_id=task_id,
5129
+ lease_owner=lease_owner,
5130
+ workflow_task_attempt=workflow_task_attempt,
5131
+ message="Workflow task waiting for scheduled history.",
5132
+ failure_type="WorkflowTaskWaitingForHistory",
5133
+ )
5134
+
5123
5135
  body: dict[str, Any] = {
5124
5136
  "lease_owner": lease_owner,
5125
5137
  "workflow_task_attempt": workflow_task_attempt,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.4.1
3
+ Version: 2.4.2
4
4
  Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -2903,6 +2903,25 @@ class TestFailWorkflowTask:
2903
2903
  assert mock.call_args.args[:2] == (fixture["request"]["method"], f"/api{fixture['request']['path']}")
2904
2904
  assert mock.call_args.kwargs["json"] == fixture["request"]["body"]
2905
2905
 
2906
+ @pytest.mark.asyncio
2907
+ @pytest.mark.parametrize("metadata", [
2908
+ {"message_stream_cursors": [{"stream_name": "input", "sequence": 7}]},
2909
+ {"message_stream_waits": [{"stream_name": "input"}]},
2910
+ ])
2911
+ async def test_empty_completion_does_not_discard_message_stream_metadata(
2912
+ self, client: Client, metadata: dict
2913
+ ) -> None:
2914
+ with (
2915
+ patch.object(client._http, "request", new_callable=AsyncMock,
2916
+ return_value=_mock_response(200, {"completed": True})) as mock,
2917
+ pytest.raises(ValueError, match="nonempty commands"),
2918
+ ):
2919
+ await client.complete_workflow_task(
2920
+ task_id="task", lease_owner="owner", workflow_task_attempt=3,
2921
+ commands=[], **metadata,
2922
+ )
2923
+ mock.assert_not_awaited()
2924
+
2906
2925
  @pytest.mark.asyncio
2907
2926
  async def test_fail_workflow_task_matches_polyglot_fixture(self, client: Client) -> None:
2908
2927
  fixture_path = Path(__file__).parent / "fixtures" / "control-plane" / "workflow-task-fail-parity.json"
@@ -2,10 +2,12 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  from typing import Any
5
+ from unittest.mock import AsyncMock, patch
5
6
 
7
+ import httpx
6
8
  import pytest
7
9
 
8
- from durable_workflow import serializer, workflow
10
+ from durable_workflow import Client, Worker, serializer, workflow
9
11
  from durable_workflow.errors import NonDeterministicReplayError
10
12
  from durable_workflow.workflow import CompleteWorkflow, WorkflowContext, query_state, replay
11
13
 
@@ -78,6 +80,43 @@ def test_signal_replay_does_not_reschedule_pending_operation(kind: str) -> None:
78
80
  )
79
81
 
80
82
 
83
+ @pytest.mark.asyncio
84
+ @pytest.mark.parametrize("kind", OPENINGS)
85
+ async def test_worker_acknowledges_pending_signal_replay_with_original_lease(kind: str) -> None:
86
+ history = initial_history() + [OPENINGS[kind], {
87
+ "event_type": "SignalReceived", "payload": {
88
+ "signal_name": "increment", "arguments": serializer.envelope([7]),
89
+ },
90
+ }]
91
+ requests = []
92
+
93
+ async def server(method, path, **kwargs): # type: ignore[no-untyped-def]
94
+ body = kwargs["json"]
95
+ requests.append((method, path, body, kwargs["headers"]["Authorization"]))
96
+ if path.endswith("/complete") and not body["commands"]:
97
+ return httpx.Response(422, json={"message": "The commands field is required."},
98
+ request=httpx.Request(method, "http://test" + path))
99
+ assert path.endswith("/fail")
100
+ return httpx.Response(200, json={"outcome": "waiting_for_history", "recorded": True},
101
+ request=httpx.Request(method, "http://test" + path))
102
+
103
+ async with Client("http://test", worker_token="worker-token") as client:
104
+ worker = Worker(client, task_queue="queue", worker_id="original-owner", workflows=[PendingOperation])
105
+ with patch.object(client._http, "request", new=AsyncMock(side_effect=server)):
106
+ commands = await worker._run_workflow_task({
107
+ "task_id": "original-task", "workflow_task_attempt": 4, "run_id": "original-run",
108
+ "workflow_type": "tests.replay.pending-operation", "history_events": history,
109
+ "payload_codec": "avro", "arguments": serializer.envelope([kind]),
110
+ })
111
+
112
+ assert commands == []
113
+ assert requests == [("POST", "/api/worker/workflow-tasks/original-task/fail", {
114
+ "lease_owner": "original-owner", "workflow_task_attempt": 4,
115
+ "failure": {"message": "Workflow task waiting for scheduled history.",
116
+ "type": "WorkflowTaskWaitingForHistory"},
117
+ }, "Bearer worker-token")]
118
+
119
+
81
120
  @pytest.mark.parametrize("kind", OPENINGS)
82
121
  def test_new_operation_is_still_scheduled(kind: str) -> None:
83
122
  outcome = replay(PendingOperation, initial_history(), [kind])