durable-workflow 2.8.0__tar.gz → 2.9.1__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.8.0/src/durable_workflow.egg-info → durable_workflow-2.9.1}/PKG-INFO +2 -1
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/pyproject.toml +4 -3
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/workflow.py +17 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1/src/durable_workflow.egg-info}/PKG-INFO +2 -1
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow.egg-info/SOURCES.txt +2 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow.egg-info/requires.txt +1 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_replay_regression_corpus.py +5 -0
- durable_workflow-2.9.1/tests/test_threaded_heartbeat_example.py +125 -0
- durable_workflow-2.9.1/tests/test_workflow_identity.py +72 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/LICENSE +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/README.md +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/setup.cfg +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/__init__.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_activity_lease.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_cancellation_scope_history.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_cooperative_cancellation.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/activity.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/errors.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/telemetry.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/telemetry_carrier.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/testing.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/worker.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_activity_heartbeat_authority.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_activity_lease_authority.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_activity_lease_renewal.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_activity_lease_worker.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_cancellation_remaining_time.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_client.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_client_telemetry.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_committed_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_errors.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_history_event_contract.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_invocable.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_metrics.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_queries.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_replay.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_replay_clock.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_schedules.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_serializer.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_signals.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_sleep.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_sync.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_sync_activities.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_telemetry.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_telemetry_carrier.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_updates.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_wait_condition.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_worker.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_worker_observation.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_worker_telemetry_wire.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_workflow_entry_arguments.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_workflow_handler_validation.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.8.0 → durable_workflow-2.9.1}/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.
|
|
3
|
+
Version: 2.9.1
|
|
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
|
|
@@ -20,6 +20,7 @@ Requires-Python: >=3.10
|
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: certifi>=2026.10.19
|
|
23
24
|
Requires-Dist: fastavro<2,>=1.12.2
|
|
24
25
|
Provides-Extra: telemetry
|
|
25
26
|
Requires-Dist: opentelemetry-api<2,>=1.45.1; extra == "telemetry"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "durable-workflow"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.9.1"
|
|
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"
|
|
@@ -34,6 +34,7 @@ classifiers = [
|
|
|
34
34
|
]
|
|
35
35
|
dependencies = [
|
|
36
36
|
"httpx>=0.27",
|
|
37
|
+
"certifi>=2026.10.19",
|
|
37
38
|
"fastavro>=1.12.2,<2",
|
|
38
39
|
]
|
|
39
40
|
|
|
@@ -75,8 +76,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
|
|
|
75
76
|
durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
|
|
76
77
|
|
|
77
78
|
[tool.durable-workflow]
|
|
78
|
-
product-train = "2.
|
|
79
|
-
registry-version = "2.
|
|
79
|
+
product-train = "2.9.1"
|
|
80
|
+
registry-version = "2.9.1"
|
|
80
81
|
supported-server-versions = "2.8.0"
|
|
81
82
|
worker-protocol-version = "1.20"
|
|
82
83
|
control-plane-version = "2"
|
|
@@ -316,6 +316,13 @@ class ActivityRetryPolicy:
|
|
|
316
316
|
budget for an already-scheduled activity. It is a server-side durable
|
|
317
317
|
retry policy, not the SDK HTTP transport retry policy.
|
|
318
318
|
|
|
319
|
+
``initial_interval_seconds`` and ``maximum_interval_seconds`` accept
|
|
320
|
+
fractional seconds. Each generated interval is capped first, then rounded
|
|
321
|
+
up to a whole second for the durable command. For example, an initial
|
|
322
|
+
interval of ``0.1`` becomes one second. The explicit ``backoff_seconds``
|
|
323
|
+
list instead takes integer seconds. This resolution does not guarantee
|
|
324
|
+
when a worker will start the next attempt.
|
|
325
|
+
|
|
319
326
|
``non_retryable_error_types`` names failure types that should bypass this
|
|
320
327
|
retry budget. An activity worker can also report ``non_retryable=True`` on
|
|
321
328
|
a failure to stop retrying that activity execution.
|
|
@@ -2056,6 +2063,16 @@ class WorkflowContext:
|
|
|
2056
2063
|
self._workflow_stream_command_counter = 0
|
|
2057
2064
|
self.logger = _ReplayLogger(_REPLAY_LOGGER)
|
|
2058
2065
|
|
|
2066
|
+
@property
|
|
2067
|
+
def workflow_id(self) -> str:
|
|
2068
|
+
"""Original workflow identity, retained across continue-as-new runs."""
|
|
2069
|
+
return self._workflow_id
|
|
2070
|
+
|
|
2071
|
+
@property
|
|
2072
|
+
def run_id(self) -> str:
|
|
2073
|
+
"""Current run identity, stable on replay and new after continue-as-new."""
|
|
2074
|
+
return self._run_id
|
|
2075
|
+
|
|
2059
2076
|
def cancellation_scope(
|
|
2060
2077
|
self, body: Callable[[], Any], *, shield_parent: bool = False,
|
|
2061
2078
|
) -> Generator[Any, Any, Any]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: durable-workflow
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.1
|
|
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
|
|
@@ -20,6 +20,7 @@ Requires-Python: >=3.10
|
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: certifi>=2026.10.19
|
|
23
24
|
Requires-Dist: fastavro<2,>=1.12.2
|
|
24
25
|
Provides-Extra: telemetry
|
|
25
26
|
Requires-Dist: opentelemetry-api<2,>=1.45.1; extra == "telemetry"
|
|
@@ -132,6 +132,7 @@ tests/test_sync_activities.py
|
|
|
132
132
|
tests/test_telemetry.py
|
|
133
133
|
tests/test_telemetry_carrier.py
|
|
134
134
|
tests/test_testing_harness.py
|
|
135
|
+
tests/test_threaded_heartbeat_example.py
|
|
135
136
|
tests/test_update_signal_condition_replay.py
|
|
136
137
|
tests/test_updates.py
|
|
137
138
|
tests/test_wait_condition.py
|
|
@@ -142,6 +143,7 @@ tests/test_worker_telemetry_wire.py
|
|
|
142
143
|
tests/test_workflow_entry_arguments.py
|
|
143
144
|
tests/test_workflow_entrypoint_validation.py
|
|
144
145
|
tests/test_workflow_handler_validation.py
|
|
146
|
+
tests/test_workflow_identity.py
|
|
145
147
|
tests/test_workflow_result_timeout.py
|
|
146
148
|
tests/test_workflow_streams.py
|
|
147
149
|
tests/test_workflow_updates_conformance.py
|
|
@@ -46,6 +46,7 @@ from tests.test_update_signal_condition_replay import (
|
|
|
46
46
|
UpdateSignalConditionTimerWorkflow,
|
|
47
47
|
)
|
|
48
48
|
from tests.test_worker import compatible_cluster_info
|
|
49
|
+
from tests.test_workflow_identity import IdentityAcrossReplay, IdentityContinueAsNew
|
|
49
50
|
|
|
50
51
|
FIXTURE_SCHEMA = "durable-workflow.replay-regression/v1"
|
|
51
52
|
FIXTURE_DIR = Path(__file__).parent / "fixtures" / "replay_regressions"
|
|
@@ -340,6 +341,8 @@ class NexusCooperativeDeliveryWorkflow:
|
|
|
340
341
|
|
|
341
342
|
|
|
342
343
|
WORKFLOWS = [
|
|
344
|
+
IdentityAcrossReplay,
|
|
345
|
+
IdentityContinueAsNew,
|
|
343
346
|
PersistedClockWorkflow,
|
|
344
347
|
NexusCooperativeDeliveryWorkflow,
|
|
345
348
|
NexusTerminalDeliveryWorkflow,
|
|
@@ -514,6 +517,8 @@ def _execute_fixture(fixture: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
514
517
|
history,
|
|
515
518
|
start_input,
|
|
516
519
|
workflow_type=workflow_type,
|
|
520
|
+
workflow_id=workflow.get("workflow_id"),
|
|
521
|
+
run_id=workflow.get("run_id", ""),
|
|
517
522
|
payload_codec=("json" if payload_codec is None else payload_codec),
|
|
518
523
|
)
|
|
519
524
|
commands = _command_documents(outcome.commands)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Exercise the public example with real ActivityContext thread/loop behavior."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import importlib.util
|
|
6
|
+
import unittest
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from threading import Event, Thread
|
|
9
|
+
from threading import enumerate as threads
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from durable_workflow import activity
|
|
13
|
+
from durable_workflow.activity import ActivityContext, ActivityInfo, _set_context
|
|
14
|
+
from durable_workflow.errors import ActivityCancelled, ActivityLeaseAuthorityLost
|
|
15
|
+
|
|
16
|
+
SPEC = importlib.util.spec_from_file_location(
|
|
17
|
+
"threaded_heartbeats_example",
|
|
18
|
+
Path(__file__).resolve().parents[1] / "examples" / "threaded_heartbeats.py",
|
|
19
|
+
)
|
|
20
|
+
assert SPEC is not None
|
|
21
|
+
assert SPEC.loader is not None
|
|
22
|
+
EXAMPLE = importlib.util.module_from_spec(SPEC)
|
|
23
|
+
SPEC.loader.exec_module(EXAMPLE)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HeartbeatClient:
|
|
27
|
+
def __init__(self, response: Any = None) -> None:
|
|
28
|
+
self.response = response if response is not None else {"can_continue": True}
|
|
29
|
+
self.calls: list[dict[str, Any]] = []
|
|
30
|
+
self.observed = Event()
|
|
31
|
+
|
|
32
|
+
async def heartbeat_activity_task(self, **request: Any) -> Any:
|
|
33
|
+
self.calls.append(request)
|
|
34
|
+
self.observed.set()
|
|
35
|
+
if isinstance(self.response, Exception):
|
|
36
|
+
raise self.response
|
|
37
|
+
return self.response
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ThreadedHeartbeatExampleTest(unittest.IsolatedAsyncioTestCase):
|
|
41
|
+
async def asyncSetUp(self) -> None:
|
|
42
|
+
self.client = HeartbeatClient()
|
|
43
|
+
self.ctx = ActivityContext(
|
|
44
|
+
info=ActivityInfo(
|
|
45
|
+
"task-original", "export", "attempt-original", 2, "reports", "worker-original",
|
|
46
|
+
),
|
|
47
|
+
client=self.client, # type: ignore[arg-type]
|
|
48
|
+
)
|
|
49
|
+
_set_context(self.ctx)
|
|
50
|
+
|
|
51
|
+
async def asyncTearDown(self) -> None:
|
|
52
|
+
_set_context(None)
|
|
53
|
+
self.assertFalse([thread for thread in threads() if thread.name == "activity-heartbeat"])
|
|
54
|
+
|
|
55
|
+
async def run_work(self, work: Any) -> Any:
|
|
56
|
+
return await asyncio.wait_for(
|
|
57
|
+
asyncio.to_thread(EXAMPLE.call_with_heartbeats, work, interval=0.01),
|
|
58
|
+
timeout=3,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def test_new_thread_needs_the_captured_context(self) -> None:
|
|
62
|
+
errors: list[BaseException] = []
|
|
63
|
+
|
|
64
|
+
def look_up_context() -> None:
|
|
65
|
+
try:
|
|
66
|
+
activity.context()
|
|
67
|
+
except RuntimeError as error:
|
|
68
|
+
errors.append(error)
|
|
69
|
+
|
|
70
|
+
helper = Thread(target=look_up_context)
|
|
71
|
+
helper.start()
|
|
72
|
+
helper.join()
|
|
73
|
+
self.assertEqual(len(errors), 1)
|
|
74
|
+
self.assertIn("outside of an activity", str(errors[0]))
|
|
75
|
+
|
|
76
|
+
async def test_return_keeps_original_attempt_and_joins(self) -> None:
|
|
77
|
+
def work(cancelled: Event) -> dict[str, int]:
|
|
78
|
+
self.assertTrue(self.client.observed.wait(2))
|
|
79
|
+
self.assertFalse(cancelled.is_set())
|
|
80
|
+
return {"count": 7}
|
|
81
|
+
|
|
82
|
+
self.assertEqual(await self.run_work(work), {"count": 7})
|
|
83
|
+
self.assertTrue(self.client.calls)
|
|
84
|
+
for request in self.client.calls:
|
|
85
|
+
self.assertEqual(request["task_id"], "task-original")
|
|
86
|
+
self.assertEqual(request["activity_attempt_id"], "attempt-original")
|
|
87
|
+
self.assertEqual(request["lease_owner"], "worker-original")
|
|
88
|
+
|
|
89
|
+
async def test_work_error_also_joins(self) -> None:
|
|
90
|
+
original = ValueError("business call failed")
|
|
91
|
+
|
|
92
|
+
def work(cancelled: Event) -> None:
|
|
93
|
+
self.assertTrue(self.client.observed.wait(2))
|
|
94
|
+
raise original
|
|
95
|
+
|
|
96
|
+
with self.assertRaises(ValueError) as caught:
|
|
97
|
+
await self.run_work(work)
|
|
98
|
+
self.assertIs(caught.exception, original)
|
|
99
|
+
|
|
100
|
+
def cancelled_work(self, cancelled: Event) -> str:
|
|
101
|
+
self.assertTrue(cancelled.wait(2))
|
|
102
|
+
return "must not be returned"
|
|
103
|
+
|
|
104
|
+
async def test_cancellation_stops_work_and_preserves_type(self) -> None:
|
|
105
|
+
self.client.response = {"cancel_requested": True}
|
|
106
|
+
with self.assertRaises(ActivityCancelled):
|
|
107
|
+
await self.run_work(self.cancelled_work)
|
|
108
|
+
|
|
109
|
+
async def test_authority_loss_keeps_original_identity_and_reason(self) -> None:
|
|
110
|
+
self.client.response = {"can_continue": False, "reason": "activity_attempt_replaced"}
|
|
111
|
+
with self.assertRaises(ActivityLeaseAuthorityLost) as caught:
|
|
112
|
+
await self.run_work(self.cancelled_work)
|
|
113
|
+
self.assertEqual(caught.exception.task_id, "task-original")
|
|
114
|
+
self.assertEqual(caught.exception.reason, "activity_attempt_replaced")
|
|
115
|
+
|
|
116
|
+
async def test_transport_error_is_not_silently_lost(self) -> None:
|
|
117
|
+
original = OSError("transport unavailable")
|
|
118
|
+
self.client.response = original
|
|
119
|
+
with self.assertRaises(OSError) as caught:
|
|
120
|
+
await self.run_work(self.cancelled_work)
|
|
121
|
+
self.assertIs(caught.exception, original)
|
|
122
|
+
|
|
123
|
+
async def test_event_loop_call_refuses_before_work(self) -> None:
|
|
124
|
+
with self.assertRaisesRegex(RuntimeError, "synchronous activity thread"):
|
|
125
|
+
EXAMPLE.call_with_heartbeats(lambda cancelled: self.fail("must not run"))
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from durable_workflow import serializer, workflow
|
|
6
|
+
from durable_workflow.workflow import (
|
|
7
|
+
CompleteWorkflow,
|
|
8
|
+
ContinueAsNew,
|
|
9
|
+
ScheduleActivity,
|
|
10
|
+
WorkflowContext,
|
|
11
|
+
replay,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@workflow.defn(name="identity-across-replay")
|
|
16
|
+
class IdentityAcrossReplay:
|
|
17
|
+
def run(self, ctx: WorkflowContext): # type: ignore[no-untyped-def]
|
|
18
|
+
before = [ctx.workflow_id, ctx.run_id]
|
|
19
|
+
value = yield ctx.schedule_activity("identity", before)
|
|
20
|
+
return {"before": before, "after": [ctx.workflow_id, ctx.run_id], "value": value}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.parametrize("workflow_id", ["workflow-identity", "0", "17", "017"])
|
|
24
|
+
def test_identity_survives_activity_suspension_and_cold_replay(workflow_id: str) -> None:
|
|
25
|
+
run_id = "original-run"
|
|
26
|
+
first = replay(IdentityAcrossReplay, [], [], workflow_id=workflow_id, run_id=run_id)
|
|
27
|
+
assert len(first.commands) == 1
|
|
28
|
+
assert isinstance(first.commands[0], ScheduleActivity)
|
|
29
|
+
assert first.commands[0].arguments == [workflow_id, run_id]
|
|
30
|
+
|
|
31
|
+
history = [{"event_type": "ActivityCompleted", "payload": {
|
|
32
|
+
"result": serializer.envelope("done", codec=serializer.AVRO_CODEC),
|
|
33
|
+
}}]
|
|
34
|
+
resumed = replay(IdentityAcrossReplay, history, [], workflow_id=workflow_id, run_id=run_id)
|
|
35
|
+
assert len(resumed.commands) == 1
|
|
36
|
+
assert isinstance(resumed.commands[0], CompleteWorkflow)
|
|
37
|
+
assert resumed.commands[0].result == {
|
|
38
|
+
"before": [workflow_id, run_id], "after": [workflow_id, run_id], "value": "done",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@workflow.defn(name="identity-continue-as-new")
|
|
43
|
+
class IdentityContinueAsNew:
|
|
44
|
+
def run( # type: ignore[no-untyped-def]
|
|
45
|
+
self, ctx: WorkflowContext, again: bool,
|
|
46
|
+
prior_workflow_id: str | None = None, prior_run_id: str | None = None,
|
|
47
|
+
):
|
|
48
|
+
if again:
|
|
49
|
+
return ctx.continue_as_new(False, ctx.workflow_id, ctx.run_id)
|
|
50
|
+
return {"current": [ctx.workflow_id, ctx.run_id], "prior": [prior_workflow_id, prior_run_id]}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_successor_context_has_same_workflow_and_new_run_identity() -> None:
|
|
54
|
+
original = replay(IdentityContinueAsNew, [], [True], workflow_id="chain", run_id="run-one")
|
|
55
|
+
assert len(original.commands) == 1
|
|
56
|
+
command = original.commands[0]
|
|
57
|
+
assert isinstance(command, ContinueAsNew)
|
|
58
|
+
assert command.arguments == [False, "chain", "run-one"]
|
|
59
|
+
|
|
60
|
+
successor = replay(
|
|
61
|
+
IdentityContinueAsNew, [], command.arguments, workflow_id="chain", run_id="run-two",
|
|
62
|
+
)
|
|
63
|
+
assert len(successor.commands) == 1
|
|
64
|
+
assert isinstance(successor.commands[0], CompleteWorkflow)
|
|
65
|
+
assert successor.commands[0].result == {"current": ["chain", "run-two"], "prior": ["chain", "run-one"]}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize("attribute", ["workflow_id", "run_id"])
|
|
69
|
+
def test_workflow_identity_is_read_only(attribute: str) -> None:
|
|
70
|
+
ctx = WorkflowContext(workflow_id="workflow", run_id="run")
|
|
71
|
+
with pytest.raises(AttributeError):
|
|
72
|
+
setattr(ctx, attribute, "replacement")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_cancellation_scope.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_cooperative_cancellation.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_prepared_local_activity.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/_query_task_authority.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/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
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/external_task_input.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow/external_task_result.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/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.8.0 → durable_workflow-2.9.1}/src/durable_workflow/python_conformance.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/src/durable_workflow.egg-info/top_level.txt
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_activity_cancellation_policies.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_activity_heartbeat_authority.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.8.0 → durable_workflow-2.9.1}/tests/test_cancellation_scope_admission.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/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
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_committed_cancellation_scope_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_committed_cancellation_scope_history.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_committed_cancellation_scope_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_control_plane_parity_fixtures.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_cooperative_cancellation_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_parallel_completion_order_replay.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_prepared_local_activity_client.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_prepared_local_activity_groups.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_prepared_local_activity_policies.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_run_cancellation_scope_replay.py
RENAMED
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_runtime_external_payload_transport.py
RENAMED
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/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
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_workflow_entrypoint_validation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{durable_workflow-2.8.0 → durable_workflow-2.9.1}/tests/test_workflow_updates_conformance.py
RENAMED
|
File without changes
|