durable-workflow 2.0.1__tar.gz → 2.0.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 (86) hide show
  1. {durable_workflow-2.0.1/src/durable_workflow.egg-info → durable_workflow-2.0.2}/PKG-INFO +1 -1
  2. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/pyproject.toml +3 -3
  3. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/__init__.py +2 -0
  4. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/client.py +6 -2
  5. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/errors.py +11 -0
  6. {durable_workflow-2.0.1 → durable_workflow-2.0.2/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  7. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
  8. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_pypi_project_surface.py +2 -3
  9. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_release_metadata.py +2 -2
  10. durable_workflow-2.0.2/tests/test_workflow_result_timeout.py +61 -0
  11. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/LICENSE +0 -0
  12. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/README.md +0 -0
  13. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/setup.cfg +0 -0
  14. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/_avro.py +0 -0
  15. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/activity.py +0 -0
  16. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/auth_composition.py +0 -0
  17. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/external_storage.py +0 -0
  18. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/external_task_input.py +0 -0
  19. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/external_task_result.py +0 -0
  20. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/history_bundle_verify.py +0 -0
  21. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/interceptors.py +0 -0
  22. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/invocable.py +0 -0
  23. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/metrics.py +0 -0
  24. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/nexus.py +0 -0
  25. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/py.typed +0 -0
  26. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/python_conformance.py +0 -0
  27. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/replay_conformance.py +0 -0
  28. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/replay_verify.py +0 -0
  29. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/retry_policy.py +0 -0
  30. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/serializer.py +0 -0
  31. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/sync.py +0 -0
  32. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/testing.py +0 -0
  33. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/worker.py +0 -0
  34. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/workflow.py +0 -0
  35. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  36. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  37. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  38. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow.egg-info/requires.txt +0 -0
  39. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/src/durable_workflow.egg-info/top_level.txt +0 -0
  40. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_activity_context.py +0 -0
  41. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_auth_composition.py +0 -0
  42. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_avro_value_benchmark.py +0 -0
  43. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_avro_value_protocol.py +0 -0
  44. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_ci_checkout.py +0 -0
  45. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_ci_integration_endpoint.py +0 -0
  46. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_client.py +0 -0
  47. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_codec_regression_corpus.py +0 -0
  48. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_control_plane_parity_fixtures.py +0 -0
  49. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_docstring_cross_references.py +0 -0
  50. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_durable_selection.py +0 -0
  51. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_errors.py +0 -0
  52. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_external_storage.py +0 -0
  53. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_external_task_input.py +0 -0
  54. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_external_task_result.py +0 -0
  55. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_golden_history_replay.py +0 -0
  56. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_history_bundle_verify.py +0 -0
  57. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_history_event_contract.py +0 -0
  58. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_invocable.py +0 -0
  59. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_message_streams.py +0 -0
  60. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_metrics.py +0 -0
  61. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_order_processing_example.py +0 -0
  62. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_parallel_completion_order_replay.py +0 -0
  63. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_parallel_saga_authoring.py +0 -0
  64. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_public_boundary_scanner.py +0 -0
  65. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_python_conformance.py +0 -0
  66. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_queries.py +0 -0
  67. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_readme_quickstart.py +0 -0
  68. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_replay.py +0 -0
  69. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_replay_conformance.py +0 -0
  70. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_replay_regression_corpus.py +0 -0
  71. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_replay_verify.py +0 -0
  72. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_retry_policy.py +0 -0
  73. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_runtime_external_payload_transport.py +0 -0
  74. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_schedules.py +0 -0
  75. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_serializer.py +0 -0
  76. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_signals.py +0 -0
  77. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_sleep.py +0 -0
  78. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_standalone_activity_client.py +0 -0
  79. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_sync.py +0 -0
  80. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_testing_harness.py +0 -0
  81. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_update_signal_condition_replay.py +0 -0
  82. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_updates.py +0 -0
  83. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_wait_condition.py +0 -0
  84. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_worker.py +0 -0
  85. {durable_workflow-2.0.1 → durable_workflow-2.0.2}/tests/test_workflow_streams.py +0 -0
  86. {durable_workflow-2.0.1 → durable_workflow-2.0.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.0.1
3
+ Version: 2.0.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.0.1"
7
+ version = "2.0.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.0.1"
75
- registry-version = "2.0.1"
74
+ product-train = "2.0.2"
75
+ registry-version = "2.0.2"
76
76
  supported-server-versions = "2.0.0"
77
77
  worker-protocol-version = "1.19"
78
78
  control-plane-version = "2"
@@ -95,6 +95,7 @@ from .errors import (
95
95
  WorkflowNotFound,
96
96
  WorkflowPayloadDecodeError,
97
97
  WorkflowTerminated,
98
+ WorkflowTimedOut,
98
99
  )
99
100
  from .external_storage import (
100
101
  EXTERNAL_PAYLOAD_REFERENCE_SCHEMA,
@@ -374,6 +375,7 @@ __all__ = [
374
375
  "WorkflowFailed",
375
376
  "WorkflowNotFound",
376
377
  "WorkflowTerminated",
378
+ "WorkflowTimedOut",
377
379
  "EXTERNAL_TASK_INPUT_CONTRACT_SCHEMA",
378
380
  "EXTERNAL_TASK_INPUT_MEDIA_TYPE",
379
381
  "EXTERNAL_TASK_INPUT_SCHEMA",
@@ -46,6 +46,7 @@ from .errors import (
46
46
  WorkflowCancelled,
47
47
  WorkflowFailed,
48
48
  WorkflowTerminated,
49
+ WorkflowTimedOut,
49
50
  _raise_for_status,
50
51
  )
51
52
  from .external_storage import (
@@ -4141,8 +4142,9 @@ class Client:
4141
4142
  Raises :class:`~durable_workflow.errors.WorkflowFailed`,
4142
4143
  :class:`~durable_workflow.errors.WorkflowCancelled`, or
4143
4144
  :class:`~durable_workflow.errors.WorkflowTerminated` if the workflow
4144
- ended in a non-success state, or :class:`TimeoutError` if ``timeout``
4145
- seconds elapse before the workflow terminates.
4145
+ ended in a non-success state. A persisted execution or run deadline
4146
+ raises :class:`~durable_workflow.errors.WorkflowTimedOut`; a caller's
4147
+ :class:`TimeoutError` means ``timeout`` seconds elapsed while waiting.
4146
4148
  """
4147
4149
  deadline = asyncio.get_running_loop().time() + timeout
4148
4150
  while True:
@@ -4177,6 +4179,8 @@ class Client:
4177
4179
  raise WorkflowCancelled(
4178
4180
  payload.get("reason", "workflow was cancelled")
4179
4181
  )
4182
+ if etype == "WorkflowTimedOut":
4183
+ raise WorkflowTimedOut()
4180
4184
  return None
4181
4185
  if asyncio.get_running_loop().time() > deadline:
4182
4186
  raise TimeoutError(
@@ -560,6 +560,17 @@ class WorkflowTerminated(DurableWorkflowError):
560
560
  super().__init__(message)
561
561
 
562
562
 
563
+ class WorkflowTimedOut(DurableWorkflowError):
564
+ """A persisted workflow execution or run deadline expired.
565
+
566
+ Unlike a caller's :class:`TimeoutError` while polling, this is a terminal
567
+ workflow outcome recorded by the runtime.
568
+ """
569
+
570
+ def __init__(self, message: str = "workflow execution timed out") -> None:
571
+ super().__init__(message)
572
+
573
+
563
574
  class SagaCompensationFailed(DurableWorkflowError):
564
575
  """A saga compensation failed after an earlier workflow failure.
565
576
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.0.1
3
+ Version: 2.0.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
@@ -79,5 +79,6 @@ tests/test_update_signal_condition_replay.py
79
79
  tests/test_updates.py
80
80
  tests/test_wait_condition.py
81
81
  tests/test_worker.py
82
+ tests/test_workflow_result_timeout.py
82
83
  tests/test_workflow_streams.py
83
84
  tests/test_workflow_updates_conformance.py
@@ -348,7 +348,7 @@ def test_stable_audit_requires_root_and_bare_install(
348
348
  assert evidence["historical_versions"] == ["0.4.105", "0.4.106"]
349
349
 
350
350
 
351
- def test_stable_root_preserves_non_yanked_historical_files() -> None:
351
+ def test_stable_root_preserves_historical_files_without_overriding_yanks() -> None:
352
352
  source = source_metadata(stable=True)
353
353
  exact_urls = verify_exact_version_json(exact_version_payload(source), source)
354
354
 
@@ -356,8 +356,7 @@ def test_stable_root_preserves_non_yanked_historical_files() -> None:
356
356
 
357
357
  payload = stable_project_payload()
358
358
  payload["releases"]["0.4.106"][0]["yanked"] = True
359
- with pytest.raises(ProjectSurfaceError, match="historical release 0.4.106 must remain retained and non-yanked"):
360
- verify_stable_project_json(payload, source, exact_urls)
359
+ assert verify_stable_project_json(payload, source, exact_urls) == ("0.4.105", "0.4.106")
361
360
 
362
361
 
363
362
  def test_prerelease_evidence_records_deferred_default_surface(
@@ -41,9 +41,9 @@ def test_worker_release_identity_matches_supported_server_and_protocol() -> None
41
41
  project = manifest["project"]
42
42
  release = manifest["tool"]["durable-workflow"]
43
43
 
44
- assert project["version"] == "2.0.1"
44
+ assert project["version"] == "2.0.2"
45
45
  assert release["product-train"] == project["version"]
46
- assert release["registry-version"] == "2.0.1"
46
+ assert release["registry-version"] == "2.0.2"
47
47
  assert release["supported-server-versions"] == "2.0.0"
48
48
  assert release["worker-protocol-version"] == PROTOCOL_VERSION == "1.19"
49
49
  assert release["durable-selection"] is True
@@ -0,0 +1,61 @@
1
+ from unittest.mock import AsyncMock
2
+
3
+ import pytest
4
+
5
+ from durable_workflow import Client, WorkflowTimedOut
6
+ from durable_workflow.client import WorkflowExecution, WorkflowHandle
7
+ from durable_workflow.errors import WorkflowCancelled, WorkflowFailed, WorkflowTerminated
8
+
9
+
10
+ def result_client(status: str, event_type: str, payload: dict) -> tuple[Client, WorkflowHandle]:
11
+ client = Client("https://unused.example")
12
+ client.describe_workflow = AsyncMock(
13
+ return_value=WorkflowExecution(
14
+ workflow_id="order", run_id="current-run", workflow_type="order", status=status,
15
+ )
16
+ )
17
+ client.get_history = AsyncMock(
18
+ return_value={"events": [{"event_type": event_type, "payload": payload}]}
19
+ )
20
+ return client, WorkflowHandle(client, workflow_id="order", run_id="selected-run", workflow_type="order")
21
+
22
+
23
+ @pytest.mark.parametrize("kind", ["execution_timeout", "run_timeout"])
24
+ async def test_persisted_deadline_raises_typed_timeout_for_selected_history(kind: str) -> None:
25
+ client, handle = result_client(
26
+ "failed", "WorkflowTimedOut", {"timeout_kind": kind, "deadline_at": "2026-01-01T00:00:00Z"},
27
+ )
28
+ try:
29
+ with pytest.raises(WorkflowTimedOut, match="workflow execution timed out"):
30
+ await client.get_result(handle, timeout=0)
31
+ client.get_history.assert_awaited_once_with("order", "selected-run")
32
+ finally:
33
+ await client.aclose()
34
+
35
+
36
+ async def test_polling_timeout_is_not_a_terminal_workflow_timeout() -> None:
37
+ client, handle = result_client("waiting", "ConditionWaitOpened", {})
38
+ try:
39
+ with pytest.raises(TimeoutError, match="not terminal") as caught:
40
+ await client.get_result(handle, timeout=0)
41
+ assert not isinstance(caught.value, WorkflowTimedOut)
42
+ client.get_history.assert_not_awaited()
43
+ finally:
44
+ await client.aclose()
45
+
46
+
47
+ @pytest.mark.parametrize(
48
+ ("status", "event_type", "exception"),
49
+ [
50
+ ("failed", "WorkflowFailed", WorkflowFailed),
51
+ ("cancelled", "WorkflowCancelled", WorkflowCancelled),
52
+ ("terminated", "WorkflowTerminated", WorkflowTerminated),
53
+ ],
54
+ )
55
+ async def test_other_terminal_failures_preserve_their_exception(status, event_type, exception) -> None:
56
+ client, handle = result_client(status, event_type, {"message": "stopped", "reason": "stopped"})
57
+ try:
58
+ with pytest.raises(exception, match="stopped"):
59
+ await client.get_result(handle, timeout=0)
60
+ finally:
61
+ await client.aclose()