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.
- {durable_workflow-2.5.1/src/durable_workflow.egg-info → durable_workflow-2.5.2}/PKG-INFO +1 -1
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/pyproject.toml +3 -3
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/__init__.py +2 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/errors.py +4 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/testing.py +2 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/worker.py +5 -1
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/workflow.py +33 -2
- {durable_workflow-2.5.1 → durable_workflow-2.5.2/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_replay_regression_corpus.py +8 -0
- durable_workflow-2.5.2/tests/test_workflow_entrypoint_validation.py +116 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/LICENSE +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/README.md +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/setup.cfg +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_client.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_errors.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_invocable.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_metrics.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_queries.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_schedules.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_serializer.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_signals.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_sleep.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_sync.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_updates.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_worker.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.5.1 → durable_workflow-2.5.2}/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.5.
|
|
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.
|
|
75
|
-
registry-version = "2.5.
|
|
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
|
|
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
|
|
@@ -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]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_cancellation_scope.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_cooperative_cancellation.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_prepared_local_activity.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/_sticky_workflow_cache.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/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.5.1 → durable_workflow-2.5.2}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/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
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/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.5.1 → durable_workflow-2.5.2}/tests/test_cancellation_scope_admission.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/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
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_committed_cancellation_scope_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_committed_cancellation_scope_history.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_committed_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_cooperative_cancellation_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/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
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_parallel_completion_order_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_prepared_local_activity_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_prepared_local_activity_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_prepared_local_activity_policies.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/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
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_run_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/tests/test_runtime_external_payload_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/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
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.1 → durable_workflow-2.5.2}/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.5.1 → durable_workflow-2.5.2}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|