durable-workflow 2.5.3__tar.gz → 2.5.4__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.3/src/durable_workflow.egg-info → durable_workflow-2.5.4}/PKG-INFO +1 -1
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/pyproject.toml +3 -3
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/__init__.py +2 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/errors.py +24 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/testing.py +5 -1
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/worker.py +13 -2
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/workflow.py +29 -23
- {durable_workflow-2.5.3 → durable_workflow-2.5.4/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
- durable_workflow-2.5.4/tests/test_registration_ambiguity.py +193 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_replay_regression_corpus.py +11 -1
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/LICENSE +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/README.md +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/setup.cfg +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_client.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_errors.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_invocable.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_metrics.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_queries.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_replay.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_schedules.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_serializer.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_signals.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_sleep.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_sync.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_updates.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_worker.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.5.3 → durable_workflow-2.5.4}/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.4"
|
|
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.4"
|
|
75
|
+
registry-version = "2.5.4"
|
|
76
76
|
supported-server-versions = "2.5.16"
|
|
77
77
|
worker-protocol-version = "1.19"
|
|
78
78
|
control-plane-version = "2"
|
|
@@ -70,6 +70,7 @@ from .errors import (
|
|
|
70
70
|
ChildWorkflowCancelled,
|
|
71
71
|
ChildWorkflowFailed,
|
|
72
72
|
ChildWorkflowTerminated,
|
|
73
|
+
DuplicateRegistrationError,
|
|
73
74
|
DurableOperationCancelled,
|
|
74
75
|
DurableWorkflowError,
|
|
75
76
|
ExternalPayloadError,
|
|
@@ -370,6 +371,7 @@ __all__ = [
|
|
|
370
371
|
"NexusOperationResult",
|
|
371
372
|
"NexusServiceCall",
|
|
372
373
|
"NonDeterministicReplayError",
|
|
374
|
+
"DuplicateRegistrationError",
|
|
373
375
|
"InvalidWorkflowDefinition",
|
|
374
376
|
"NoopMetrics",
|
|
375
377
|
"QueryFailed",
|
|
@@ -31,6 +31,30 @@ class InvalidWorkflowDefinition(DurableWorkflowError, ValueError):
|
|
|
31
31
|
"""Workflow authoring uses an unsupported entry point or result."""
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
class DuplicateRegistrationError(DurableWorkflowError, ValueError):
|
|
35
|
+
"""One registration scope assigns a wire name to multiple definitions."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self, registration_kind: str, wire_name: str,
|
|
39
|
+
first_definition: object, second_definition: object,
|
|
40
|
+
) -> None:
|
|
41
|
+
def label(definition: object) -> str:
|
|
42
|
+
if isinstance(definition, str):
|
|
43
|
+
return definition
|
|
44
|
+
module = getattr(definition, "__module__", type(definition).__module__)
|
|
45
|
+
name = getattr(definition, "__qualname__", type(definition).__qualname__)
|
|
46
|
+
return f"{module}.{name}"
|
|
47
|
+
|
|
48
|
+
self.registration_kind = registration_kind
|
|
49
|
+
self.wire_name = wire_name
|
|
50
|
+
self.first_definition = label(first_definition)
|
|
51
|
+
self.second_definition = label(second_definition)
|
|
52
|
+
super().__init__(
|
|
53
|
+
f"duplicate {registration_kind} name {wire_name!r}: "
|
|
54
|
+
f"{self.first_definition} conflicts with {self.second_definition}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
34
58
|
class DurableOperationCancelled(DurableWorkflowError):
|
|
35
59
|
"""A durable selection member was explicitly cancelled."""
|
|
36
60
|
|
|
@@ -39,7 +39,7 @@ from pathlib import Path
|
|
|
39
39
|
from typing import Any
|
|
40
40
|
|
|
41
41
|
from . import serializer
|
|
42
|
-
from .errors import WorkflowCancelled, WorkflowFailed, WorkflowTerminated
|
|
42
|
+
from .errors import DuplicateRegistrationError, WorkflowCancelled, WorkflowFailed, WorkflowTerminated
|
|
43
43
|
from .workflow import (
|
|
44
44
|
CompleteWorkflow,
|
|
45
45
|
ContinueAsNew,
|
|
@@ -52,6 +52,7 @@ from .workflow import (
|
|
|
52
52
|
StartTimer,
|
|
53
53
|
UpsertSearchAttributes,
|
|
54
54
|
_validate_workflow_entrypoint,
|
|
55
|
+
_workflow_handler_registries,
|
|
55
56
|
registry,
|
|
56
57
|
replay,
|
|
57
58
|
)
|
|
@@ -136,6 +137,9 @@ class WorkflowEnvironment:
|
|
|
136
137
|
"decorate it with @workflow.defn(name=...) first."
|
|
137
138
|
)
|
|
138
139
|
_validate_workflow_entrypoint(workflow_cls)
|
|
140
|
+
_workflow_handler_registries(workflow_cls)
|
|
141
|
+
if name in self._registered_workflows and self._registered_workflows[name] is not workflow_cls:
|
|
142
|
+
raise DuplicateRegistrationError("workflow", name, self._registered_workflows[name], workflow_cls)
|
|
139
143
|
self._registered_workflows[name] = workflow_cls
|
|
140
144
|
|
|
141
145
|
def signal(
|
|
@@ -66,6 +66,7 @@ from .errors import (
|
|
|
66
66
|
ActivityCancelled,
|
|
67
67
|
ActivityFailed,
|
|
68
68
|
AvroNotInstalledError,
|
|
69
|
+
DuplicateRegistrationError,
|
|
69
70
|
DurableWorkflowError,
|
|
70
71
|
ExternalPayloadError,
|
|
71
72
|
InvalidArgument,
|
|
@@ -106,6 +107,7 @@ from .workflow import (
|
|
|
106
107
|
StartChildWorkflow,
|
|
107
108
|
UpsertMemo,
|
|
108
109
|
_validate_workflow_entrypoint,
|
|
110
|
+
_workflow_handler_registries,
|
|
109
111
|
apply_update,
|
|
110
112
|
commands_to_server_commands,
|
|
111
113
|
query_state,
|
|
@@ -1028,7 +1030,11 @@ class Worker:
|
|
|
1028
1030
|
self.workflows: dict[str, type] = {}
|
|
1029
1031
|
for definition in workflows:
|
|
1030
1032
|
_validate_workflow_entrypoint(definition)
|
|
1031
|
-
|
|
1033
|
+
_workflow_handler_registries(definition)
|
|
1034
|
+
name = _workflow_name(definition)
|
|
1035
|
+
if name in self.workflows:
|
|
1036
|
+
raise DuplicateRegistrationError("workflow", name, self.workflows[name], definition)
|
|
1037
|
+
self.workflows[name] = definition
|
|
1032
1038
|
self.workflow_definition_fingerprints = {
|
|
1033
1039
|
workflow_type: _workflow_definition_fingerprint(workflow_cls)
|
|
1034
1040
|
for workflow_type, workflow_cls in self.workflows.items()
|
|
@@ -1037,7 +1043,12 @@ class Worker:
|
|
|
1037
1043
|
workflow_type: _workflow_command_contract(workflow_cls)
|
|
1038
1044
|
for workflow_type, workflow_cls in self.workflows.items()
|
|
1039
1045
|
}
|
|
1040
|
-
self.activities
|
|
1046
|
+
self.activities: dict[str, Callable[..., Any]] = {}
|
|
1047
|
+
for activity_definition in activities:
|
|
1048
|
+
name = _activity_name(activity_definition)
|
|
1049
|
+
if name in self.activities:
|
|
1050
|
+
raise DuplicateRegistrationError("activity", name, self.activities[name], activity_definition)
|
|
1051
|
+
self.activities[name] = activity_definition
|
|
1041
1052
|
self.capabilities = tuple(dict.fromkeys(capability.strip() for capability in capabilities))
|
|
1042
1053
|
self._sticky_cache = StickyWorkflowCache(
|
|
1043
1054
|
sticky_cache_capacity, sticky_cache_max_bytes, sticky_cache_ttl_seconds,
|
|
@@ -58,6 +58,7 @@ from .errors import (
|
|
|
58
58
|
ChildWorkflowCancelled,
|
|
59
59
|
ChildWorkflowFailed,
|
|
60
60
|
ChildWorkflowTerminated,
|
|
61
|
+
DuplicateRegistrationError,
|
|
61
62
|
DurableOperationCancelled,
|
|
62
63
|
InvalidWorkflowDefinition,
|
|
63
64
|
NexusOperationFailed,
|
|
@@ -124,6 +125,29 @@ def _validate_workflow_result(value: Any, cls: type) -> None:
|
|
|
124
125
|
)
|
|
125
126
|
|
|
126
127
|
|
|
128
|
+
def _workflow_handler_registries(
|
|
129
|
+
cls: type,
|
|
130
|
+
) -> tuple[dict[str, str], dict[str, str], dict[str, str], dict[str, str]]:
|
|
131
|
+
handlers: dict[str, dict[str, str]] = {kind: {} for kind in ("signal", "query", "update", "update_validator")}
|
|
132
|
+
for attr in dir(cls):
|
|
133
|
+
if attr.startswith("_"):
|
|
134
|
+
continue
|
|
135
|
+
member = getattr(cls, attr, None)
|
|
136
|
+
for kind, registry in handlers.items():
|
|
137
|
+
wire_name = getattr(member, f"__{kind}_name__", None)
|
|
138
|
+
if not isinstance(wire_name, str) or not wire_name:
|
|
139
|
+
continue
|
|
140
|
+
if kind == "signal" and wire_name == MESSAGE_STREAM_SIGNAL:
|
|
141
|
+
raise ValueError(f"signal name {wire_name!r} is reserved by the workflow runtime")
|
|
142
|
+
if wire_name in registry:
|
|
143
|
+
owner = f"{cls.__module__}.{cls.__qualname__}"
|
|
144
|
+
raise DuplicateRegistrationError(
|
|
145
|
+
kind, wire_name, f"{owner}.{registry[wire_name]}", f"{owner}.{attr}",
|
|
146
|
+
)
|
|
147
|
+
registry[wire_name] = attr
|
|
148
|
+
return handlers["signal"], handlers["query"], handlers["update"], handlers["update_validator"]
|
|
149
|
+
|
|
150
|
+
|
|
127
151
|
def defn(*, name: str) -> Callable[[type[_WorkflowT]], type[_WorkflowT]]:
|
|
128
152
|
"""Register a class as a workflow type under a language-neutral name.
|
|
129
153
|
|
|
@@ -136,28 +160,7 @@ def defn(*, name: str) -> Callable[[type[_WorkflowT]], type[_WorkflowT]]:
|
|
|
136
160
|
def wrap(cls: type[_WorkflowT]) -> type[_WorkflowT]:
|
|
137
161
|
cls.__workflow_name__ = name # type: ignore[attr-defined]
|
|
138
162
|
_validate_workflow_entrypoint(cls)
|
|
139
|
-
signals
|
|
140
|
-
queries: dict[str, str] = {}
|
|
141
|
-
updates: dict[str, str] = {}
|
|
142
|
-
update_validators: dict[str, str] = {}
|
|
143
|
-
for attr in dir(cls):
|
|
144
|
-
if attr.startswith("_"):
|
|
145
|
-
continue
|
|
146
|
-
member = getattr(cls, attr, None)
|
|
147
|
-
signal_name = getattr(member, "__signal_name__", None)
|
|
148
|
-
if isinstance(signal_name, str) and signal_name:
|
|
149
|
-
if signal_name == MESSAGE_STREAM_SIGNAL:
|
|
150
|
-
raise ValueError(f"signal name {signal_name!r} is reserved by the workflow runtime")
|
|
151
|
-
signals[signal_name] = attr
|
|
152
|
-
query_name = getattr(member, "__query_name__", None)
|
|
153
|
-
if isinstance(query_name, str) and query_name:
|
|
154
|
-
queries[query_name] = attr
|
|
155
|
-
update_name = getattr(member, "__update_name__", None)
|
|
156
|
-
if isinstance(update_name, str) and update_name:
|
|
157
|
-
updates[update_name] = attr
|
|
158
|
-
update_validator_name = getattr(member, "__update_validator_name__", None)
|
|
159
|
-
if isinstance(update_validator_name, str) and update_validator_name:
|
|
160
|
-
update_validators[update_validator_name] = attr
|
|
163
|
+
signals, queries, updates, update_validators = _workflow_handler_registries(cls)
|
|
161
164
|
cls.__workflow_signals__ = signals # type: ignore[attr-defined]
|
|
162
165
|
cls.__workflow_queries__ = queries # type: ignore[attr-defined]
|
|
163
166
|
cls.__workflow_updates__ = updates # type: ignore[attr-defined]
|
|
@@ -2578,9 +2581,12 @@ class Replayer:
|
|
|
2578
2581
|
def __init__(self, *, workflows: Iterable[type]) -> None:
|
|
2579
2582
|
self._workflows: dict[str, type] = {}
|
|
2580
2583
|
for workflow_cls in workflows:
|
|
2584
|
+
_workflow_handler_registries(workflow_cls)
|
|
2581
2585
|
workflow_type = str(getattr(workflow_cls, "__workflow_name__", workflow_cls.__name__))
|
|
2582
2586
|
if workflow_type in self._workflows:
|
|
2583
|
-
raise
|
|
2587
|
+
raise DuplicateRegistrationError(
|
|
2588
|
+
"workflow", workflow_type, self._workflows[workflow_type], workflow_cls,
|
|
2589
|
+
)
|
|
2584
2590
|
self._workflows[workflow_type] = workflow_cls
|
|
2585
2591
|
if not self._workflows:
|
|
2586
2592
|
raise ValueError("Replayer requires at least one workflow class")
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any
|
|
5
|
+
from unittest.mock import AsyncMock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from durable_workflow import Client, DuplicateRegistrationError, DurableWorkflowError, Worker, activity, workflow
|
|
10
|
+
from durable_workflow.testing import WorkflowEnvironment
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(self: Any, ctx: Any) -> int:
|
|
14
|
+
return 1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def first(self: Any, *args: Any) -> None:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def second(self: Any, *args: Any) -> None:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def definition(name: str, cls_name: str, members: dict[str, Any] | None = None, bases: tuple[type, ...] = ()) -> type:
|
|
26
|
+
return workflow.defn(name=name)(type(cls_name, bases, {"run": run, **(members or {})}))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize("consumer", ["worker", "replayer", "environment"])
|
|
30
|
+
def test_conflicting_workflow_names_report_both_definitions(consumer: str) -> None:
|
|
31
|
+
first_cls = definition("shared-workflow", "FirstWorkflow")
|
|
32
|
+
second_cls = definition("shared-workflow", "SecondWorkflow")
|
|
33
|
+
client = AsyncMock(spec=Client)
|
|
34
|
+
with pytest.raises(ValueError) as captured:
|
|
35
|
+
if consumer == "worker":
|
|
36
|
+
Worker(client, task_queue="registration", workflows=iter([first_cls, second_cls]))
|
|
37
|
+
elif consumer == "replayer":
|
|
38
|
+
workflow.Replayer(workflows=iter([first_cls, second_cls]))
|
|
39
|
+
else:
|
|
40
|
+
env = WorkflowEnvironment()
|
|
41
|
+
env.register_workflow(first_cls)
|
|
42
|
+
env.register_workflow(second_cls)
|
|
43
|
+
assert captured.value.registration_kind == "workflow" # type: ignore[attr-defined]
|
|
44
|
+
assert captured.value.wire_name == "shared-workflow" # type: ignore[attr-defined]
|
|
45
|
+
assert "FirstWorkflow" in str(captured.value)
|
|
46
|
+
assert "SecondWorkflow" in str(captured.value)
|
|
47
|
+
assert client.mock_calls == []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize("same_definition", [False, True])
|
|
51
|
+
def test_worker_rejects_duplicate_activities_before_network(same_definition: bool) -> None:
|
|
52
|
+
first_activity = activity.defn(name="shared-activity")(first)
|
|
53
|
+
second_activity = first_activity if same_definition else activity.defn(name="shared-activity")(second)
|
|
54
|
+
client = AsyncMock(spec=Client)
|
|
55
|
+
with pytest.raises(ValueError) as captured:
|
|
56
|
+
Worker(client, task_queue="registration", activities=iter([first_activity, second_activity]))
|
|
57
|
+
assert captured.value.registration_kind == "activity" # type: ignore[attr-defined]
|
|
58
|
+
assert captured.value.wire_name == "shared-activity" # type: ignore[attr-defined]
|
|
59
|
+
assert "first" in str(captured.value)
|
|
60
|
+
assert ("first" if same_definition else "second") in str(captured.value)
|
|
61
|
+
assert client.mock_calls == []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.parametrize("consumer", ["worker", "replayer"])
|
|
65
|
+
def test_duplicate_list_entry_is_rejected(consumer: str) -> None:
|
|
66
|
+
cls = definition("same-entry", "RepeatedWorkflow")
|
|
67
|
+
with pytest.raises(ValueError, match="same-entry"):
|
|
68
|
+
if consumer == "worker":
|
|
69
|
+
Worker(AsyncMock(spec=Client), task_queue="registration", workflows=[cls, cls])
|
|
70
|
+
else:
|
|
71
|
+
workflow.Replayer(workflows=[cls, cls])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@pytest.mark.parametrize("kind", ["signal", "query", "update", "update_validator"])
|
|
75
|
+
@pytest.mark.parametrize("inherited", [False, True])
|
|
76
|
+
def test_conflicting_handler_names_are_rejected_at_decoration(kind: str, inherited: bool) -> None:
|
|
77
|
+
decorate: Callable[..., Any] = getattr(workflow, kind)
|
|
78
|
+
# Use distinct functions so metadata from one handler kind cannot bleed
|
|
79
|
+
# into the next parameterized definition.
|
|
80
|
+
def first_handler(self: Any, *args: Any) -> None:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def second_handler(self: Any, *args: Any) -> None:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
bases = ()
|
|
87
|
+
members = {"first_handler": decorate("approve")(first_handler)}
|
|
88
|
+
if inherited:
|
|
89
|
+
base = definition(f"base-{kind}", "BaseWorkflow", members)
|
|
90
|
+
bases = (base,)
|
|
91
|
+
members = {}
|
|
92
|
+
members["second_handler"] = decorate("approve")(second_handler)
|
|
93
|
+
with pytest.raises(ValueError) as captured:
|
|
94
|
+
definition(f"ambiguous-{kind}", "AmbiguousWorkflow", members, bases)
|
|
95
|
+
assert captured.value.registration_kind == kind # type: ignore[attr-defined]
|
|
96
|
+
assert captured.value.wire_name == "approve" # type: ignore[attr-defined]
|
|
97
|
+
assert "first_handler" in str(captured.value)
|
|
98
|
+
assert "second_handler" in str(captured.value)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.parametrize("kind", ["signal", "query", "update", "update_validator"])
|
|
102
|
+
def test_overriding_the_same_python_attribute_is_unambiguous(kind: str) -> None:
|
|
103
|
+
decorate: Callable[..., Any] = getattr(workflow, kind)
|
|
104
|
+
def original(self: Any) -> str:
|
|
105
|
+
return "original"
|
|
106
|
+
|
|
107
|
+
def replacement(self: Any) -> str:
|
|
108
|
+
return "replacement"
|
|
109
|
+
|
|
110
|
+
base = definition(f"override-base-{kind}", "OverrideBase", {"handler": decorate("approve")(original)})
|
|
111
|
+
child = definition(
|
|
112
|
+
f"override-child-{kind}", "OverrideChild", {"handler": decorate("approve")(replacement)}, (base,),
|
|
113
|
+
)
|
|
114
|
+
assert child().handler() == "replacement"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.parametrize("kind", ["signal", "query", "update", "update_validator"])
|
|
118
|
+
@pytest.mark.parametrize("consumer", ["worker", "replayer", "environment"])
|
|
119
|
+
def test_programmatic_workflow_admission_cannot_bypass_handler_validation(kind: str, consumer: str) -> None:
|
|
120
|
+
decorate: Callable[..., Any] = getattr(workflow, kind)
|
|
121
|
+
def first_handler(self: Any) -> None:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
def second_handler(self: Any) -> None:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
cls = type("ProgrammaticWorkflow", (), {
|
|
128
|
+
"__workflow_name__": "programmatic-workflow", "run": run,
|
|
129
|
+
"first_handler": decorate("approve")(first_handler),
|
|
130
|
+
"second_handler": decorate("approve")(second_handler),
|
|
131
|
+
})
|
|
132
|
+
client = AsyncMock(spec=Client)
|
|
133
|
+
with pytest.raises(DuplicateRegistrationError) as captured:
|
|
134
|
+
if consumer == "worker":
|
|
135
|
+
Worker(client, task_queue="registration", workflows=[cls])
|
|
136
|
+
elif consumer == "replayer":
|
|
137
|
+
workflow.Replayer(workflows=[cls])
|
|
138
|
+
else:
|
|
139
|
+
WorkflowEnvironment().register_workflow(cls)
|
|
140
|
+
assert captured.value.registration_kind == kind
|
|
141
|
+
assert captured.value.wire_name == "approve"
|
|
142
|
+
assert client.mock_calls == []
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_handler_names_are_scoped_by_kind() -> None:
|
|
146
|
+
cls = definition("scoped-handlers", "ScopedHandlers", {
|
|
147
|
+
"signal": workflow.signal("approve")(lambda self: None),
|
|
148
|
+
"query": workflow.query("approve")(lambda self: None),
|
|
149
|
+
"update": workflow.update("approve")(lambda self: None),
|
|
150
|
+
"validator": workflow.update_validator("approve")(lambda self: None),
|
|
151
|
+
})
|
|
152
|
+
assert cls.__workflow_signals__ == {"approve": "signal"}
|
|
153
|
+
assert cls.__workflow_queries__ == {"approve": "query"}
|
|
154
|
+
assert cls.__workflow_updates__ == {"approve": "update"}
|
|
155
|
+
assert cls.__workflow_update_validators__ == {"approve": "validator"}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_distinct_names_and_separate_workers_remain_supported() -> None:
|
|
159
|
+
first_cls = definition("one", "FirstDistinct")
|
|
160
|
+
second_cls = definition("two", "SecondDistinct")
|
|
161
|
+
alternative = definition("one", "AlternativeDefinition")
|
|
162
|
+
client = AsyncMock(spec=Client)
|
|
163
|
+
worker = Worker(client, task_queue="registration", workflows=iter([first_cls, second_cls]))
|
|
164
|
+
assert worker.workflows == {"one": first_cls, "two": second_cls}
|
|
165
|
+
assert Worker(client, task_queue="other", workflows=[alternative]).workflows == {"one": alternative}
|
|
166
|
+
assert workflow.Replayer(workflows=[first_cls, second_cls]).replay([], workflow_type="one").commands[0].result == 1
|
|
167
|
+
assert client.mock_calls == []
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_environment_registration_is_idempotent_for_automatic_execution() -> None:
|
|
171
|
+
cls = definition("environment-idempotent", "EnvironmentWorkflow")
|
|
172
|
+
env = WorkflowEnvironment()
|
|
173
|
+
env.register_workflow(cls)
|
|
174
|
+
env.register_workflow(cls)
|
|
175
|
+
assert env.execute_workflow(cls) == 1
|
|
176
|
+
assert env.execute_workflow(cls) == 1
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_mock_replacement_is_an_explicit_test_setting() -> None:
|
|
180
|
+
env = WorkflowEnvironment()
|
|
181
|
+
env.register_activity("mock", lambda: "first")
|
|
182
|
+
env.register_activity("mock", lambda: "second")
|
|
183
|
+
assert env._activity_fns["mock"]() == "second"
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_duplicate_error_is_a_public_structured_sdk_error() -> None:
|
|
187
|
+
error = DuplicateRegistrationError("workflow", "duplicate", run, first)
|
|
188
|
+
assert isinstance(error, DurableWorkflowError)
|
|
189
|
+
assert isinstance(error, ValueError)
|
|
190
|
+
assert error.registration_kind == "workflow"
|
|
191
|
+
assert error.wire_name == "duplicate"
|
|
192
|
+
assert error.first_definition.endswith(".run")
|
|
193
|
+
assert error.second_definition.endswith(".first")
|
|
@@ -368,8 +368,18 @@ def _execute_fixture(fixture: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
368
368
|
|
|
369
369
|
query = workflow.get("query")
|
|
370
370
|
query_result: Any = None
|
|
371
|
+
registered_workflows = [WORKFLOW_TYPES[workflow_type]]
|
|
372
|
+
worker_registrations = workflow.get("worker_registrations")
|
|
373
|
+
if worker_registrations is not None:
|
|
374
|
+
assert isinstance(worker_registrations, list) and worker_registrations
|
|
375
|
+
assert all(isinstance(name, str) and name in WORKFLOW_TYPES for name in worker_registrations)
|
|
376
|
+
worker = Worker(
|
|
377
|
+
AsyncMock(spec=Client), task_queue="regression-corpus",
|
|
378
|
+
workflows=[WORKFLOW_TYPES[name] for name in worker_registrations],
|
|
379
|
+
)
|
|
380
|
+
registered_workflows = list(worker.workflows.values())
|
|
371
381
|
if query is None:
|
|
372
|
-
outcome = Replayer(workflows=
|
|
382
|
+
outcome = Replayer(workflows=registered_workflows).replay(
|
|
373
383
|
history,
|
|
374
384
|
start_input,
|
|
375
385
|
workflow_type=workflow_type,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/_cancellation_scope.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/_cooperative_cancellation.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/_prepared_local_activity.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/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.3 → durable_workflow-2.5.4}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/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.3 → durable_workflow-2.5.4}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/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.3 → durable_workflow-2.5.4}/src/durable_workflow.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow.egg-info/requires.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/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.3 → durable_workflow-2.5.4}/tests/test_cancellation_scope_admission.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/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.3 → durable_workflow-2.5.4}/tests/test_committed_cancellation_scope_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_committed_cancellation_scope_history.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_committed_cancellation_scope_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_cooperative_cancellation_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/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.3 → durable_workflow-2.5.4}/tests/test_parallel_completion_order_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_prepared_local_activity_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_prepared_local_activity_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_prepared_local_activity_policies.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/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.3 → durable_workflow-2.5.4}/tests/test_run_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_runtime_external_payload_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/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.3 → durable_workflow-2.5.4}/tests/test_update_signal_condition_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_workflow_entrypoint_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.5.3 → durable_workflow-2.5.4}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|