durable-workflow 2.5.1__tar.gz → 2.5.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 (125) hide show
  1. {durable_workflow-2.5.1/src/durable_workflow.egg-info → durable_workflow-2.5.2}/PKG-INFO +1 -1
  2. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/pyproject.toml +3 -3
  3. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/__init__.py +2 -0
  4. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/errors.py +4 -0
  5. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/testing.py +2 -0
  6. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/worker.py +5 -1
  7. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/workflow.py +33 -2
  8. {durable_workflow-2.5.1 → durable_workflow-2.5.2/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  9. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
  10. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_replay_regression_corpus.py +8 -0
  11. durable_workflow-2.5.2/tests/test_workflow_entrypoint_validation.py +116 -0
  12. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/LICENSE +0 -0
  13. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/README.md +0 -0
  14. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/setup.cfg +0 -0
  15. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_activity_process.py +0 -0
  16. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_avro.py +0 -0
  17. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_cancellation_scope.py +0 -0
  18. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  19. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  20. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_prepared_local_activity.py +0 -0
  21. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  22. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/activity.py +0 -0
  23. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/auth_composition.py +0 -0
  24. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/cancellation.py +0 -0
  25. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/client.py +0 -0
  26. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/external_storage.py +0 -0
  27. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/external_task_input.py +0 -0
  28. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/external_task_result.py +0 -0
  29. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/history_bundle_verify.py +0 -0
  30. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/interceptors.py +0 -0
  31. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/invocable.py +0 -0
  32. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/metrics.py +0 -0
  33. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/nexus.py +0 -0
  34. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/py.typed +0 -0
  35. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/python_conformance.py +0 -0
  36. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/replay_conformance.py +0 -0
  37. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/replay_verify.py +0 -0
  38. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/retry_policy.py +0 -0
  39. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/serializer.py +0 -0
  40. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/sync.py +0 -0
  41. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/worker_session.py +0 -0
  42. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  43. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  44. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  45. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/requires.txt +0 -0
  46. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/top_level.txt +0 -0
  47. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_activity_cancellation_policies.py +0 -0
  48. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_activity_context.py +0 -0
  49. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_activity_process.py +0 -0
  50. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_auth_composition.py +0 -0
  51. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_avro_value_benchmark.py +0 -0
  52. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_avro_value_protocol.py +0 -0
  53. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_context.py +0 -0
  54. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_remaining_time.py +0 -0
  55. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_scope_admission.py +0 -0
  56. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_scope_authoring.py +0 -0
  57. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_scope_boundary.py +0 -0
  58. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_scope_opening.py +0 -0
  59. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_child_workflow_policies.py +0 -0
  60. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_ci_checkout.py +0 -0
  61. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_ci_integration_endpoint.py +0 -0
  62. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_client.py +0 -0
  63. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_client_timeouts.py +0 -0
  64. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_codec_regression_corpus.py +0 -0
  65. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  66. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_committed_cancellation_scope_groups.py +0 -0
  67. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_committed_cancellation_scope_history.py +0 -0
  68. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_committed_cancellation_scope_replay.py +0 -0
  69. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_control_plane_parity_fixtures.py +0 -0
  70. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cooperative_cancellation.py +0 -0
  71. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cooperative_cancellation_client.py +0 -0
  72. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cooperative_cancellation_worker.py +0 -0
  73. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cooperative_remote_worker.py +0 -0
  74. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_docstring_cross_references.py +0 -0
  75. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_durable_selection.py +0 -0
  76. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_errors.py +0 -0
  77. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_external_storage.py +0 -0
  78. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_external_task_input.py +0 -0
  79. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_external_task_result.py +0 -0
  80. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_golden_history_replay.py +0 -0
  81. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_history_bundle_verify.py +0 -0
  82. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_history_event_contract.py +0 -0
  83. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_invocable.py +0 -0
  84. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_message_streams.py +0 -0
  85. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_metrics.py +0 -0
  86. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_order_processing_example.py +0 -0
  87. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_parallel_completion_order_replay.py +0 -0
  88. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_parallel_saga_authoring.py +0 -0
  89. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_patch_history.py +0 -0
  90. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_pending_operation_replay.py +0 -0
  91. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_prepared_local_activity_client.py +0 -0
  92. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_prepared_local_activity_groups.py +0 -0
  93. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_prepared_local_activity_policies.py +0 -0
  94. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_prepared_local_activity_worker.py +0 -0
  95. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_public_boundary_scanner.py +0 -0
  96. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_pypi_project_surface.py +0 -0
  97. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_python_conformance.py +0 -0
  98. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_queries.py +0 -0
  99. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_readme_quickstart.py +0 -0
  100. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_redrive_replay.py +0 -0
  101. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_release_metadata.py +0 -0
  102. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_replay.py +0 -0
  103. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_replay_conformance.py +0 -0
  104. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_replay_verify.py +0 -0
  105. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_retry_policy.py +0 -0
  106. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_run_cancellation_scope_replay.py +0 -0
  107. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_runtime_external_payload_transport.py +0 -0
  108. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_schedules.py +0 -0
  109. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_scoped_run_cancellation_context.py +0 -0
  110. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_serializer.py +0 -0
  111. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_signals.py +0 -0
  112. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_sleep.py +0 -0
  113. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_standalone_activity_client.py +0 -0
  114. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_sticky_execution.py +0 -0
  115. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_storage_admission.py +0 -0
  116. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_sync.py +0 -0
  117. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_testing_harness.py +0 -0
  118. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_update_signal_condition_replay.py +0 -0
  119. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_updates.py +0 -0
  120. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_wait_condition.py +0 -0
  121. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_worker.py +0 -0
  122. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_worker_session.py +0 -0
  123. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_workflow_result_timeout.py +0 -0
  124. {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_workflow_streams.py +0 -0
  125. {durable_workflow-2.5.1 → durable_workflow-2.5.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.5.1
3
+ Version: 2.5.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.5.1"
7
+ version = "2.5.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.5.1"
75
- registry-version = "2.5.1"
74
+ product-train = "2.5.2"
75
+ registry-version = "2.5.2"
76
76
  supported-server-versions = "2.5.16"
77
77
  worker-protocol-version = "1.19"
78
78
  control-plane-version = "2"
@@ -81,6 +81,7 @@ from .errors import (
81
81
  ExternalPayloadUnavailable,
82
82
  ExternalPayloadUnsupported,
83
83
  InvalidArgument,
84
+ InvalidWorkflowDefinition,
84
85
  NamespaceNotFound,
85
86
  NexusOperationFailed,
86
87
  NonDeterministicReplayError,
@@ -369,6 +370,7 @@ __all__ = [
369
370
  "NexusOperationResult",
370
371
  "NexusServiceCall",
371
372
  "NonDeterministicReplayError",
373
+ "InvalidWorkflowDefinition",
372
374
  "NoopMetrics",
373
375
  "QueryFailed",
374
376
  "RuntimeCapabilityUnsupported",
@@ -27,6 +27,10 @@ class DurableWorkflowError(Exception):
27
27
  """Base class for every exception raised by the SDK."""
28
28
 
29
29
 
30
+ class InvalidWorkflowDefinition(DurableWorkflowError, ValueError):
31
+ """Workflow authoring uses an unsupported entry point or result."""
32
+
33
+
30
34
  class DurableOperationCancelled(DurableWorkflowError):
31
35
  """A durable selection member was explicitly cancelled."""
32
36
 
@@ -51,6 +51,7 @@ from .workflow import (
51
51
  StartChildWorkflow,
52
52
  StartTimer,
53
53
  UpsertSearchAttributes,
54
+ _validate_workflow_entrypoint,
54
55
  registry,
55
56
  replay,
56
57
  )
@@ -134,6 +135,7 @@ class WorkflowEnvironment:
134
135
  f"{workflow_cls!r} is not a workflow class; "
135
136
  "decorate it with @workflow.defn(name=...) first."
136
137
  )
138
+ _validate_workflow_entrypoint(workflow_cls)
137
139
  self._registered_workflows[name] = workflow_cls
138
140
 
139
141
  def signal(
@@ -104,6 +104,7 @@ from .workflow import (
104
104
  ScheduleActivity,
105
105
  StartChildWorkflow,
106
106
  UpsertMemo,
107
+ _validate_workflow_entrypoint,
107
108
  apply_update,
108
109
  commands_to_server_commands,
109
110
  query_state,
@@ -1023,7 +1024,10 @@ class Worker:
1023
1024
  ) -> None:
1024
1025
  self.client = client
1025
1026
  self.task_queue = task_queue
1026
- self.workflows = {_workflow_name(w): w for w in workflows}
1027
+ self.workflows: dict[str, type] = {}
1028
+ for definition in workflows:
1029
+ _validate_workflow_entrypoint(definition)
1030
+ self.workflows[_workflow_name(definition)] = definition
1027
1031
  self.workflow_definition_fingerprints = {
1028
1032
  workflow_type: _workflow_definition_fingerprint(workflow_cls)
1029
1033
  for workflow_type, workflow_cls in self.workflows.items()
@@ -21,6 +21,7 @@ from __future__ import annotations
21
21
  import base64
22
22
  import contextlib
23
23
  import hashlib
24
+ import inspect
24
25
  import logging
25
26
  import math
26
27
  import random
@@ -58,6 +59,7 @@ from .errors import (
58
59
  ChildWorkflowFailed,
59
60
  ChildWorkflowTerminated,
60
61
  DurableOperationCancelled,
62
+ InvalidWorkflowDefinition,
61
63
  NexusOperationFailed,
62
64
  NonDeterministicReplayError,
63
65
  QueryFailed,
@@ -97,6 +99,31 @@ MESSAGE_STREAM_CURSOR_SCHEMA = "durable-workflow.v2.message-stream.cursor"
97
99
  MESSAGE_STREAM_MAX_BATCH = 100
98
100
 
99
101
 
102
+ def _validate_workflow_entrypoint(cls: type) -> None:
103
+ entrypoint = getattr(cls, "run", None)
104
+ if not callable(entrypoint):
105
+ return
106
+ targets = [inspect.unwrap(entrypoint)]
107
+ if not inspect.isroutine(targets[0]):
108
+ targets.append(inspect.unwrap(targets[0].__call__))
109
+ if any(inspect.iscoroutinefunction(target) or inspect.isasyncgenfunction(target) for target in targets):
110
+ name = getattr(cls, "__workflow_name__", cls.__name__)
111
+ raise InvalidWorkflowDefinition(
112
+ f"unsupported_workflow_entrypoint: workflow {name!r} ({cls.__qualname__}.run) "
113
+ "must use a synchronous function or generator; yield durable commands instead of async def."
114
+ )
115
+
116
+
117
+ def _validate_workflow_result(value: Any, cls: type) -> None:
118
+ if inspect.isawaitable(value) or inspect.isasyncgen(value):
119
+ if inspect.iscoroutine(value):
120
+ value.close()
121
+ raise InvalidWorkflowDefinition(
122
+ f"unsupported_workflow_result: {cls.__qualname__}.run returned an async value; "
123
+ "use a synchronous result or yield durable commands."
124
+ )
125
+
126
+
100
127
  def defn(*, name: str) -> Callable[[type[_WorkflowT]], type[_WorkflowT]]:
101
128
  """Register a class as a workflow type under a language-neutral name.
102
129
 
@@ -108,6 +135,7 @@ def defn(*, name: str) -> Callable[[type[_WorkflowT]], type[_WorkflowT]]:
108
135
 
109
136
  def wrap(cls: type[_WorkflowT]) -> type[_WorkflowT]:
110
137
  cls.__workflow_name__ = name # type: ignore[attr-defined]
138
+ _validate_workflow_entrypoint(cls)
111
139
  signals: dict[str, str] = {}
112
140
  queries: dict[str, str] = {}
113
141
  updates: dict[str, str] = {}
@@ -4037,6 +4065,7 @@ def _replay_state(
4037
4065
  allow_cancellation_scope_authoring: bool = False,
4038
4066
  allow_cancellation_scope_delivery: bool = False,
4039
4067
  ) -> _ReplayState:
4068
+ _validate_workflow_entrypoint(workflow_cls)
4040
4069
  if allow_cancellation_scope_delivery and not allow_cancellation_scope_authoring:
4041
4070
  raise LocalActivityExecutionAborted("scope delivery requires canonical scope authoring")
4042
4071
  if prepare_local_activity_groups and not prepare_local_activities:
@@ -5172,6 +5201,7 @@ def _replay_state(
5172
5201
  except Exception as exc:
5173
5202
  return _state([_fail_workflow_from_exception(exc)])
5174
5203
  if not hasattr(gen, "__next__"):
5204
+ _validate_workflow_result(gen, workflow_cls)
5175
5205
  if isinstance(gen, ContinueAsNew):
5176
5206
  _assert_no_unconsumed_history("continue as new")
5177
5207
  return _state([gen])
@@ -6032,6 +6062,7 @@ def _replay_state(
6032
6062
  return next(iter(memberships))
6033
6063
 
6034
6064
  def _terminal_state(value: Any, *, include_pending: bool) -> _ReplayState:
6065
+ _validate_workflow_result(value, workflow_cls)
6035
6066
  _apply_due_receivers()
6036
6067
  _consume_terminal_condition_reopens()
6037
6068
  commands = list(pending) if include_pending else []
@@ -6784,10 +6815,10 @@ def _replay_state(
6784
6815
  try:
6785
6816
  return _terminal_state(stop.value, include_pending=True)
6786
6817
  except Exception as exc:
6787
- if isinstance(exc, NonDeterministicReplayError):
6818
+ if isinstance(exc, (NonDeterministicReplayError, InvalidWorkflowDefinition)):
6788
6819
  raise
6789
6820
  return _state(pending + [_fail_workflow_from_exception(exc)])
6790
- except NonDeterministicReplayError:
6821
+ except (NonDeterministicReplayError, InvalidWorkflowDefinition):
6791
6822
  raise
6792
6823
  except LocalActivityExecutionAborted:
6793
6824
  raise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.5.1
3
+ Version: 2.5.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
@@ -117,6 +117,7 @@ tests/test_updates.py
117
117
  tests/test_wait_condition.py
118
118
  tests/test_worker.py
119
119
  tests/test_worker_session.py
120
+ tests/test_workflow_entrypoint_validation.py
120
121
  tests/test_workflow_result_timeout.py
121
122
  tests/test_workflow_streams.py
122
123
  tests/test_workflow_updates_conformance.py
@@ -251,7 +251,15 @@ class ChildPolicyAuthorWorkflow:
251
251
  return (yield ctx.start_child_workflow("child", []))
252
252
 
253
253
 
254
+ class UnsupportedAsyncWorkflow:
255
+ __workflow_name__ = "tests.replay.unsupported-async"
256
+
257
+ async def run(self, ctx: WorkflowContext) -> str:
258
+ return "body was never executed"
259
+
260
+
254
261
  WORKFLOWS = [
262
+ UnsupportedAsyncWorkflow,
255
263
  ChildPolicyAuthorWorkflow,
256
264
  ColdReplacementSatisfiedConditionWorkflow,
257
265
  CooperativeReopenedConditionCleanupWorkflow,
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Generator
4
+ from functools import partial, wraps
5
+ from typing import Any
6
+
7
+ import pytest
8
+
9
+ from durable_workflow import Client, Worker, workflow
10
+ from durable_workflow.testing import WorkflowEnvironment
11
+
12
+
13
+ class CoroutineWorkflow:
14
+ async def run(self, ctx: Any) -> str:
15
+ return "never executed"
16
+
17
+
18
+ class AsyncGeneratorWorkflow:
19
+ async def run(self, ctx: Any) -> AsyncIterator[str]:
20
+ yield "unsupported"
21
+
22
+
23
+ class AsyncCallable:
24
+ async def __call__(self, ctx: Any) -> str:
25
+ return "unsupported"
26
+
27
+
28
+ class CallableWorkflow:
29
+ run = AsyncCallable()
30
+
31
+
32
+ class PartialWorkflow:
33
+ run = staticmethod(partial(CoroutineWorkflow.run, object()))
34
+
35
+
36
+ ASYNC_DEFINITIONS = [CoroutineWorkflow, AsyncGeneratorWorkflow, CallableWorkflow, PartialWorkflow]
37
+
38
+
39
+ @pytest.mark.parametrize("definition", ASYNC_DEFINITIONS)
40
+ def test_definition_rejects_async_entrypoints(definition: type) -> None:
41
+ with pytest.raises(ValueError, match="unsupported_workflow_entrypoint.*synchronous"):
42
+ workflow.defn(name="invalid-async")(definition)
43
+
44
+
45
+ @pytest.mark.parametrize("definition", ASYNC_DEFINITIONS)
46
+ def test_worker_rejects_undecorated_async_entrypoints_before_network(definition: type) -> None:
47
+ with pytest.raises(ValueError, match=definition.__name__):
48
+ Worker(Client("http://127.0.0.1:1"), task_queue="unused", workflows=[definition])
49
+
50
+
51
+ @pytest.mark.parametrize("definition", ASYNC_DEFINITIONS)
52
+ def test_direct_replay_rejects_async_without_creating_a_coroutine(definition: type) -> None:
53
+ with pytest.raises(ValueError, match="unsupported_workflow_entrypoint"):
54
+ workflow.replay(definition, [], [])
55
+
56
+
57
+ def test_test_environment_revalidates_entrypoint_after_decoration() -> None:
58
+ @workflow.defn(name="changed-entrypoint")
59
+ class ChangedWorkflow:
60
+ def run(self, ctx: Any) -> str:
61
+ return "valid"
62
+
63
+ ChangedWorkflow.run = CoroutineWorkflow.run # type: ignore[assignment]
64
+ with pytest.raises(ValueError, match="changed-entrypoint"):
65
+ WorkflowEnvironment().register_workflow(ChangedWorkflow)
66
+
67
+
68
+ def test_wrapped_and_inherited_async_entrypoints_are_rejected() -> None:
69
+ class InheritedWorkflow(CoroutineWorkflow):
70
+ pass
71
+
72
+ class WrappedWorkflow:
73
+ @wraps(CoroutineWorkflow.run)
74
+ def run(self, ctx: Any) -> Any:
75
+ return CoroutineWorkflow.run(self, ctx)
76
+
77
+ for definition in [InheritedWorkflow, WrappedWorkflow]:
78
+ with pytest.raises(ValueError, match="unsupported_workflow_entrypoint"):
79
+ workflow.replay(definition, [], [])
80
+
81
+
82
+ @pytest.mark.parametrize("generator", [False, True])
83
+ def test_runtime_awaitable_result_is_not_completed(generator: bool) -> None:
84
+ class AwaitableResultWorkflow:
85
+ def run(self, ctx: Any) -> Any:
86
+ return CoroutineWorkflow.run(self, ctx)
87
+
88
+ class GeneratorAwaitableResultWorkflow:
89
+ def run(self, ctx: Any) -> Generator[Any, Any, Any]:
90
+ if False:
91
+ yield None
92
+ return CoroutineWorkflow.run(self, ctx)
93
+
94
+ definition = GeneratorAwaitableResultWorkflow if generator else AwaitableResultWorkflow
95
+ with pytest.raises(ValueError, match="unsupported_workflow_result"):
96
+ workflow.replay(definition, [], [])
97
+
98
+
99
+ def test_synchronous_and_generator_workflows_remain_supported() -> None:
100
+ @workflow.defn(name="valid-sync")
101
+ class SynchronousWorkflow:
102
+ def run(self, ctx: Any) -> str:
103
+ return "done"
104
+
105
+ @workflow.defn(name="valid-generator")
106
+ class GeneratorWorkflow:
107
+ def run(self, ctx: Any) -> Generator[Any, Any, str]:
108
+ if False:
109
+ yield None
110
+ return "done"
111
+
112
+ Worker(Client("http://127.0.0.1:1"), task_queue="unused", workflows=[SynchronousWorkflow, GeneratorWorkflow])
113
+ environment = WorkflowEnvironment()
114
+ for definition in [SynchronousWorkflow, GeneratorWorkflow]:
115
+ assert environment.execute_workflow(definition) == "done"
116
+ assert workflow.replay(definition, [], []).commands[0].result == "done" # type: ignore[union-attr]