durable-workflow 2.13.1__tar.gz → 2.14.0__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.13.1/src/durable_workflow.egg-info → durable_workflow-2.14.0}/PKG-INFO +1 -1
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/pyproject.toml +3 -3
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/__init__.py +2 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_cancellation_scope_history.py +6 -1
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_cooperative_cancellation.py +3 -1
- durable_workflow-2.14.0/src/durable_workflow/_typed_values.py +311 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/activity.py +17 -2
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/errors.py +20 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/testing.py +12 -1
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/worker.py +5 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/workflow.py +508 -20
- {durable_workflow-2.13.1 → durable_workflow-2.14.0/src/durable_workflow.egg-info}/PKG-INFO +1 -1
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow.egg-info/SOURCES.txt +5 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_cancellation_remaining_time.py +42 -11
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_committed_cancellation_scope_replay.py +96 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_history_event_contract.py +16 -3
- durable_workflow-2.14.0/tests/test_publish_workflow_isolation.py +149 -0
- durable_workflow-2.14.0/tests/test_redrive_successor_evidence.py +613 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_replay.py +6 -3
- durable_workflow-2.14.0/tests/test_replay_clock.py +998 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_replay_regression_corpus.py +28 -2
- durable_workflow-2.14.0/tests/test_search_attribute_replay_identity.py +675 -0
- durable_workflow-2.14.0/tests/test_typed_values.py +464 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_wait_condition.py +374 -22
- durable_workflow-2.13.1/tests/test_replay_clock.py +0 -133
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/LICENSE +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/README.md +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/setup.cfg +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_activity_lease.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_activity_process.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_avro.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_cancellation_scope.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_failure_reporting.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_metric_routes.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_poll_transport.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_prepared_local_activity.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_query_task_authority.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/auth_composition.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/cancellation.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/client.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/external_storage.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/external_task_input.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/external_task_result.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/history_bundle_verify.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/interceptors.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/invocable.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/metrics.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/nexus.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/py.typed +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/python_conformance.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/replay_conformance.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/replay_verify.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/retry_policy.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/serializer.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/server_compatibility.json +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/sync.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/telemetry.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/telemetry_carrier.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/worker_session.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/worker_status.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow/workflow_updates_conformance.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow.egg-info/entry_points.txt +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow.egg-info/requires.txt +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_activity_cancellation_policies.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_activity_context.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_activity_heartbeat_authority.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_activity_lease_authority.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_activity_lease_renewal.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_activity_lease_worker.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_activity_process.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_activity_retry_info.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_auth_composition.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_avro_value_benchmark.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_avro_value_protocol.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_cancellation_context.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_cancellation_scope_admission.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_cancellation_scope_authoring.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_cancellation_scope_boundary.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_cancellation_scope_opening.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_child_workflow_policies.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_ci_checkout.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_ci_integration_endpoint.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_client.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_client_telemetry.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_client_timeouts.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_codec_regression_corpus.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_committed_cancellation_scope_descendants.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_committed_cancellation_scope_groups.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_committed_cancellation_scope_history.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_condition_replay_drift.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_control_plane_parity_fixtures.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_cooperative_cancellation.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_cooperative_cancellation_client.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_cooperative_cancellation_worker.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_cooperative_remote_worker.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_docstring_cross_references.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_durable_selection.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_empty_callback_failure.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_errors.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_external_storage.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_external_task_input.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_external_task_result.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_failure_reporting.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_generator_lifecycle.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_golden_history_replay.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_history_bundle_pruned.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_history_bundle_schema2.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_history_bundle_verify.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_invocable.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_local_failure_reporting.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_message_streams.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_metric_routes.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_metrics.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_nexus_durable_wait.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_nexus_namespace.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_nexus_wire.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_order_processing_example.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_parallel_completion_order_replay.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_parallel_saga_authoring.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_patch_history.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_pending_operation_replay.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_poll_failure_backoff.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_poll_transport.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_prepared_local_activity_client.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_prepared_local_activity_groups.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_prepared_local_activity_policies.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_prepared_local_activity_worker.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_public_boundary_scanner.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_pypi_project_surface.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_python_conformance.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_queries.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_query_task_authority.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_query_timeout_retry.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_readme_quickstart.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_redrive_replay.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_registration_ambiguity.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_release_metadata.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_replay_conformance.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_replay_verify.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_retry_policy.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_run_cancellation_scope_replay.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_runtime_external_payload_transport.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_schedules.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_scoped_run_cancellation_context.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_sdk_metrics_contract.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_serializer.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_signals.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_sleep.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_standalone_activity_client.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_sticky_execution.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_storage_admission.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_sync.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_sync_activities.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_telemetry.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_telemetry_carrier.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_testing_failures.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_testing_harness.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_testing_nexus.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_threaded_heartbeat_example.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_tls_trust.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_transport_diagnostics.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_update_signal_condition_replay.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_updates.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_worker.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_worker_deregistration.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_worker_observation.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_worker_session.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_worker_status.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_worker_telemetry_wire.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_workflow_entry_arguments.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_workflow_entrypoint_validation.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_workflow_handler_validation.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_workflow_identity.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_workflow_result_timeout.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/tests/test_workflow_streams.py +0 -0
- {durable_workflow-2.13.1 → durable_workflow-2.14.0}/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.
|
|
7
|
+
version = "2.14.0"
|
|
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"
|
|
@@ -79,8 +79,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
|
|
|
79
79
|
durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
|
|
80
80
|
|
|
81
81
|
[tool.durable-workflow]
|
|
82
|
-
product-train = "2.
|
|
83
|
-
registry-version = "2.
|
|
82
|
+
product-train = "2.14.0"
|
|
83
|
+
registry-version = "2.14.0"
|
|
84
84
|
supported-server-versions = "2.8.0"
|
|
85
85
|
supported-server-release-baselines = { "2" = "2.8.0", "3" = "3.0.0" }
|
|
86
86
|
worker-protocol-version = "1.20"
|
|
@@ -98,6 +98,7 @@ from .errors import (
|
|
|
98
98
|
ScheduleNotFound,
|
|
99
99
|
ServerError,
|
|
100
100
|
SignalFailed,
|
|
101
|
+
TypedValueError,
|
|
101
102
|
Unauthorized,
|
|
102
103
|
UpdateFailed,
|
|
103
104
|
UpdateRejected,
|
|
@@ -280,6 +281,7 @@ __all__ = [
|
|
|
280
281
|
"AuthCompositionContract",
|
|
281
282
|
"AuthCompositionContractError",
|
|
282
283
|
"NonRetryableError",
|
|
284
|
+
"TypedValueError",
|
|
283
285
|
"ReplayOutcome",
|
|
284
286
|
"Replayer",
|
|
285
287
|
"Saga",
|
|
@@ -122,10 +122,15 @@ def _cleanup_operation(event: dict[str, Any], prefix: Sequence[dict[str, Any]],
|
|
|
122
122
|
"authority_deadline_at": _canonical_time(authority),
|
|
123
123
|
}
|
|
124
124
|
sequence = payload.get("sequence")
|
|
125
|
+
# Only an explicit null reads as one call, as in Core; any other value
|
|
126
|
+
# that is not a positive integer is refused, never narrowed.
|
|
127
|
+
span = 1 if recorded.get("sequence_span") is None else recorded["sequence_span"]
|
|
128
|
+
if not _positive(span) or not _positive(recorded.get("sequence")):
|
|
129
|
+
raise _invalid(operation + " follows a delivery without a canonical sequence span")
|
|
125
130
|
deadline = scope_timestamp(expected["authority_deadline_at"])
|
|
126
131
|
timestamp = scope_timestamp(event.get("timestamp", event.get("recorded_at")))
|
|
127
132
|
if (snapshot != expected or scope_id != context.scope_id
|
|
128
|
-
or not _positive(sequence) or sequence < recorded["sequence"] +
|
|
133
|
+
or not _positive(sequence) or sequence < recorded["sequence"] + span
|
|
129
134
|
or not _positive(event.get("sequence")) or delivery["sequence"] >= event["sequence"]
|
|
130
135
|
or timestamp < scope_timestamp(delivery.get("timestamp", delivery.get("recorded_at")))
|
|
131
136
|
or timestamp >= deadline or not local and (
|
|
@@ -112,7 +112,9 @@ class CancellationDelivery:
|
|
|
112
112
|
request_id = _text(payload.get("workflow_command_id"), "workflow_command_id")
|
|
113
113
|
sequence = _positive(payload.get("sequence"), "sequence")
|
|
114
114
|
kind = _text(payload.get("call_kind"), "call_kind")
|
|
115
|
-
|
|
115
|
+
# Absent and null both mean one call, as Core's WorkflowStepHistory reads it.
|
|
116
|
+
recorded_span = payload.get("sequence_span")
|
|
117
|
+
span = _positive(1 if recorded_span is None else recorded_span, "sequence_span", maximum=1000)
|
|
116
118
|
operation = payload.get("operation_sequence")
|
|
117
119
|
operation_span = _positive(payload.get("operation_sequence_span", 1), "operation_sequence_span", maximum=1000)
|
|
118
120
|
if kind not in _CALL_KINDS or (kind != "parallel" and span != 1):
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Opt-in conversion between neutral payload values and annotated Python types.
|
|
2
|
+
|
|
3
|
+
The wire format stays the neutral Avro Value: a dataclass travels as the
|
|
4
|
+
string-keyed map of its fields and an ``Enum`` as its value, exactly as
|
|
5
|
+
``dataclasses.asdict()`` and ``.value`` would produce by hand. Decoding is
|
|
6
|
+
driven only by the receiver's own annotations, so no class name, tag or
|
|
7
|
+
pickled object ever enters durable history.
|
|
8
|
+
|
|
9
|
+
Decoding follows payload-evolution rules for rolling deploys: keys a target
|
|
10
|
+
dataclass does not declare are ignored, and a missing field takes its
|
|
11
|
+
default. A value that cannot be converted raises
|
|
12
|
+
:class:`~durable_workflow.errors.TypedValueError`, which names the field path
|
|
13
|
+
and the expected type but never the value.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import dataclasses
|
|
19
|
+
import functools
|
|
20
|
+
import inspect
|
|
21
|
+
import types
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from typing import Any, Union, cast, get_args, get_origin, get_type_hints
|
|
25
|
+
|
|
26
|
+
from .errors import TypedValueError
|
|
27
|
+
|
|
28
|
+
_SCALARS: tuple[type, ...] = (str, int, float, bool, bytes)
|
|
29
|
+
_PASS_THROUGH = (Any, object, inspect.Parameter.empty)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _type_name(annotation: Any) -> str:
|
|
33
|
+
"""A short, module-free name for diagnostics, such as ``list[Item]``."""
|
|
34
|
+
if annotation is type(None) or annotation is None:
|
|
35
|
+
return "None"
|
|
36
|
+
if _is_union(annotation):
|
|
37
|
+
options = get_args(annotation)
|
|
38
|
+
concrete = [option for option in options if option is not type(None)]
|
|
39
|
+
if len(concrete) == 1 and len(options) == 2:
|
|
40
|
+
return f"Optional[{_type_name(concrete[0])}]"
|
|
41
|
+
return " | ".join(_type_name(option) for option in options)
|
|
42
|
+
origin = get_origin(annotation)
|
|
43
|
+
if origin is not None:
|
|
44
|
+
arguments = ", ".join(_type_name(argument) for argument in get_args(annotation))
|
|
45
|
+
name = getattr(origin, "__name__", repr(origin).replace("typing.", ""))
|
|
46
|
+
return f"{name}[{arguments}]" if arguments else name
|
|
47
|
+
if isinstance(annotation, type):
|
|
48
|
+
return annotation.__qualname__
|
|
49
|
+
return repr(annotation).replace("typing.", "")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _json_kind(value: Any) -> str:
|
|
53
|
+
"""The neutral kind of a received value, never the value itself."""
|
|
54
|
+
if value is None:
|
|
55
|
+
return "null"
|
|
56
|
+
if isinstance(value, bool):
|
|
57
|
+
return "boolean"
|
|
58
|
+
if isinstance(value, int):
|
|
59
|
+
return "integer"
|
|
60
|
+
if isinstance(value, float):
|
|
61
|
+
return "number"
|
|
62
|
+
if isinstance(value, str):
|
|
63
|
+
return "string"
|
|
64
|
+
if isinstance(value, bytes):
|
|
65
|
+
return "bytes"
|
|
66
|
+
if isinstance(value, list):
|
|
67
|
+
return "array"
|
|
68
|
+
if isinstance(value, dict):
|
|
69
|
+
return "object"
|
|
70
|
+
return type(value).__name__
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _optional_member(annotation: Any) -> Any:
|
|
74
|
+
"""Return T for Optional[T]; refuse every other union as ambiguous."""
|
|
75
|
+
options = get_args(annotation)
|
|
76
|
+
concrete = [option for option in options if option is not type(None)]
|
|
77
|
+
if len(concrete) != 1 or len(concrete) == len(options):
|
|
78
|
+
raise TypeError(
|
|
79
|
+
f"typed values support only Optional[T] unions; {_type_name(annotation)} is ambiguous"
|
|
80
|
+
)
|
|
81
|
+
return concrete[0]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _is_union(annotation: Any) -> bool:
|
|
85
|
+
return get_origin(annotation) in (Union, types.UnionType)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _dataclass_hints(cls: type) -> dict[str, Any]:
|
|
89
|
+
try:
|
|
90
|
+
return get_type_hints(cls)
|
|
91
|
+
except (NameError, TypeError) as error:
|
|
92
|
+
raise TypeError(f"typed values cannot resolve the annotations of {cls.__qualname__}: {error}") from None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def validate_annotation(annotation: Any, where: str, _seen: frozenset[type] = frozenset()) -> None:
|
|
96
|
+
"""Refuse an annotation this contract cannot decode unambiguously.
|
|
97
|
+
|
|
98
|
+
Called when a typed activity is declared or a ``result_type`` is given, so
|
|
99
|
+
an unsupported type fails at definition time instead of passing a plain
|
|
100
|
+
value through silently.
|
|
101
|
+
"""
|
|
102
|
+
if annotation in _PASS_THROUGH or annotation is None or annotation is type(None):
|
|
103
|
+
return
|
|
104
|
+
if _is_union(annotation):
|
|
105
|
+
validate_annotation(_optional_member(annotation), where, _seen)
|
|
106
|
+
return
|
|
107
|
+
origin = get_origin(annotation)
|
|
108
|
+
if annotation is list or origin is list:
|
|
109
|
+
for item in get_args(annotation):
|
|
110
|
+
validate_annotation(item, where, _seen)
|
|
111
|
+
return
|
|
112
|
+
if annotation is dict or origin is dict:
|
|
113
|
+
arguments = get_args(annotation)
|
|
114
|
+
if arguments:
|
|
115
|
+
if arguments[0] is not str:
|
|
116
|
+
raise TypeError(f"{where}: typed maps need str keys, not {_type_name(arguments[0])}")
|
|
117
|
+
validate_annotation(arguments[1], where, _seen)
|
|
118
|
+
return
|
|
119
|
+
if isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
120
|
+
for member in annotation:
|
|
121
|
+
if not isinstance(member.value, str | int | float):
|
|
122
|
+
raise TypeError(
|
|
123
|
+
f"{where}: {annotation.__qualname__} members must have str, int or float values"
|
|
124
|
+
)
|
|
125
|
+
return
|
|
126
|
+
if isinstance(annotation, type) and dataclasses.is_dataclass(annotation):
|
|
127
|
+
if annotation in _seen:
|
|
128
|
+
return
|
|
129
|
+
hints = _dataclass_hints(annotation)
|
|
130
|
+
for item in dataclasses.fields(annotation):
|
|
131
|
+
if item.init:
|
|
132
|
+
validate_annotation(hints.get(item.name, Any), f"{where}.{item.name}", _seen | {annotation})
|
|
133
|
+
return
|
|
134
|
+
if isinstance(annotation, type) and annotation in _SCALARS:
|
|
135
|
+
return
|
|
136
|
+
raise TypeError(f"{where}: typed values do not support {_type_name(annotation)}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def decode_payload_value(value: Any, annotation: Any, path: str = "value") -> Any:
|
|
140
|
+
"""Decode an untrusted payload value; every refusal is a TypedValueError."""
|
|
141
|
+
try:
|
|
142
|
+
return from_payload_value(value, annotation, path)
|
|
143
|
+
except RecursionError:
|
|
144
|
+
raise TypedValueError(path, _type_name(annotation), "the value is nested too deeply") from None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def from_payload_value(value: Any, annotation: Any, path: str = "value") -> Any:
|
|
148
|
+
"""Return *value* converted to *annotation*, which must be validated."""
|
|
149
|
+
if annotation in _PASS_THROUGH:
|
|
150
|
+
return value
|
|
151
|
+
if annotation is None or annotation is type(None):
|
|
152
|
+
if value is not None:
|
|
153
|
+
raise TypedValueError(path, "None", f"got {_json_kind(value)}")
|
|
154
|
+
return None
|
|
155
|
+
if _is_union(annotation):
|
|
156
|
+
if value is None:
|
|
157
|
+
return None
|
|
158
|
+
return from_payload_value(value, _optional_member(annotation), path)
|
|
159
|
+
if value is None:
|
|
160
|
+
raise TypedValueError(path, _type_name(annotation), "got null")
|
|
161
|
+
|
|
162
|
+
origin = get_origin(annotation)
|
|
163
|
+
if annotation is list or origin is list:
|
|
164
|
+
if not isinstance(value, list):
|
|
165
|
+
raise TypedValueError(path, _type_name(annotation), f"got {_json_kind(value)}")
|
|
166
|
+
(item_type,) = get_args(annotation) or (Any,)
|
|
167
|
+
return [from_payload_value(item, item_type, f"{path}[{index}]") for index, item in enumerate(value)]
|
|
168
|
+
|
|
169
|
+
if annotation is dict or origin is dict:
|
|
170
|
+
if not isinstance(value, dict):
|
|
171
|
+
raise TypedValueError(path, _type_name(annotation), f"got {_json_kind(value)}")
|
|
172
|
+
_, item_type = get_args(annotation) or (str, Any)
|
|
173
|
+
# Map keys are user data (for example email addresses), so a path names
|
|
174
|
+
# an entry by its position, never by its key.
|
|
175
|
+
return {
|
|
176
|
+
key: from_payload_value(item, item_type, f"{path}[entry {index}]")
|
|
177
|
+
for index, (key, item) in enumerate(value.items())
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
181
|
+
return _enum_from_payload(value, annotation, path)
|
|
182
|
+
|
|
183
|
+
if isinstance(annotation, type) and dataclasses.is_dataclass(annotation):
|
|
184
|
+
return _dataclass_from_payload(value, annotation, path)
|
|
185
|
+
|
|
186
|
+
if annotation is float and isinstance(value, int) and not isinstance(value, bool):
|
|
187
|
+
return float(value)
|
|
188
|
+
if annotation is int and isinstance(value, bool):
|
|
189
|
+
raise TypedValueError(path, "int", "got boolean")
|
|
190
|
+
if isinstance(annotation, type) and annotation in _SCALARS:
|
|
191
|
+
if not isinstance(value, annotation):
|
|
192
|
+
raise TypedValueError(path, annotation.__qualname__, f"got {_json_kind(value)}")
|
|
193
|
+
return value
|
|
194
|
+
raise TypeError(f"{path}: typed values do not support {_type_name(annotation)}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _enum_from_payload(value: Any, cls: type[Enum], path: str) -> Enum:
|
|
198
|
+
if isinstance(value, cls):
|
|
199
|
+
return value
|
|
200
|
+
for member in cls:
|
|
201
|
+
expected = member.value
|
|
202
|
+
same_kind = type(expected) is type(value) or (type(expected) is float and type(value) is int)
|
|
203
|
+
if same_kind and expected == value:
|
|
204
|
+
return member
|
|
205
|
+
# The diagnostic must not echo the received value.
|
|
206
|
+
raise TypedValueError(path, cls.__qualname__, f"the {_json_kind(value)} is not a member")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _dataclass_from_payload(value: Any, cls: type, path: str) -> Any:
|
|
210
|
+
if isinstance(value, cls):
|
|
211
|
+
return value
|
|
212
|
+
if not isinstance(value, dict):
|
|
213
|
+
raise TypedValueError(path, cls.__qualname__, f"got {_json_kind(value)}")
|
|
214
|
+
hints = _dataclass_hints(cls)
|
|
215
|
+
arguments: dict[str, Any] = {}
|
|
216
|
+
for item in dataclasses.fields(cls):
|
|
217
|
+
if not item.init:
|
|
218
|
+
# Computed fields are derived again by the dataclass itself.
|
|
219
|
+
continue
|
|
220
|
+
field_path = f"{path}.{item.name}"
|
|
221
|
+
annotation = hints.get(item.name, Any)
|
|
222
|
+
if item.name in value:
|
|
223
|
+
arguments[item.name] = from_payload_value(value[item.name], annotation, field_path)
|
|
224
|
+
elif item.default is dataclasses.MISSING and item.default_factory is dataclasses.MISSING:
|
|
225
|
+
raise TypedValueError(field_path, _type_name(annotation), "the field is missing")
|
|
226
|
+
# Keys the receiver does not declare are ignored for rolling deploys.
|
|
227
|
+
try:
|
|
228
|
+
return cls(**arguments)
|
|
229
|
+
except ValueError:
|
|
230
|
+
# A __post_init__ check refused the decoded fields. Its message can
|
|
231
|
+
# carry the values, so it is not repeated here.
|
|
232
|
+
raise TypedValueError(path, cls.__qualname__, "the dataclass refused the decoded fields") from None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def to_payload_value(value: Any) -> Any:
|
|
236
|
+
"""Return the neutral value for dataclass and ``Enum`` values, recursively.
|
|
237
|
+
|
|
238
|
+
Every other value is returned unchanged, so the codec keeps refusing what
|
|
239
|
+
it refuses today (for example datetimes and tuples). The result encodes
|
|
240
|
+
to the same bytes as ``dataclasses.asdict()`` with each ``Enum`` replaced
|
|
241
|
+
by its ``.value``.
|
|
242
|
+
"""
|
|
243
|
+
if isinstance(value, Enum):
|
|
244
|
+
return to_payload_value(value.value)
|
|
245
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
246
|
+
return {item.name: to_payload_value(getattr(value, item.name)) for item in dataclasses.fields(value)}
|
|
247
|
+
if isinstance(value, list):
|
|
248
|
+
return [to_payload_value(item) for item in value]
|
|
249
|
+
if isinstance(value, dict):
|
|
250
|
+
return {key: to_payload_value(item) for key, item in value.items()}
|
|
251
|
+
return value
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def to_payload_values(values: list[Any]) -> list[Any]:
|
|
255
|
+
return [to_payload_value(value) for value in values]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def typed_callable(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
259
|
+
"""Wrap *fn* so positional arguments decode to its annotations.
|
|
260
|
+
|
|
261
|
+
Annotations are validated here, so an unsupported type fails when the
|
|
262
|
+
activity is declared. The wrapper keeps *fn*'s module and qualified name:
|
|
263
|
+
the decorator binds that name to the wrapper, so supervised callbacks that
|
|
264
|
+
pickle by reference resolve to it.
|
|
265
|
+
"""
|
|
266
|
+
signature = inspect.signature(fn)
|
|
267
|
+
try:
|
|
268
|
+
hints = get_type_hints(fn)
|
|
269
|
+
except (NameError, TypeError) as error:
|
|
270
|
+
raise TypeError(f"typed activity {fn.__qualname__} has unresolvable annotations: {error}") from None
|
|
271
|
+
positional: list[tuple[str, Any]] = []
|
|
272
|
+
variadic: tuple[str, Any] | None = None
|
|
273
|
+
for parameter in signature.parameters.values():
|
|
274
|
+
annotation = hints.get(parameter.name, Any)
|
|
275
|
+
if parameter.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
|
|
276
|
+
validate_annotation(annotation, parameter.name)
|
|
277
|
+
positional.append((parameter.name, annotation))
|
|
278
|
+
elif parameter.kind is inspect.Parameter.VAR_POSITIONAL:
|
|
279
|
+
validate_annotation(annotation, parameter.name)
|
|
280
|
+
variadic = (parameter.name, annotation)
|
|
281
|
+
|
|
282
|
+
def convert(args: tuple[Any, ...]) -> list[Any]:
|
|
283
|
+
converted: list[Any] = []
|
|
284
|
+
for index, value in enumerate(args):
|
|
285
|
+
if index < len(positional):
|
|
286
|
+
name, annotation = positional[index]
|
|
287
|
+
converted.append(decode_payload_value(value, annotation, name))
|
|
288
|
+
elif variadic is not None:
|
|
289
|
+
name, annotation = variadic
|
|
290
|
+
converted.append(decode_payload_value(value, annotation, f"{name}[{index - len(positional)}]"))
|
|
291
|
+
else:
|
|
292
|
+
converted.append(value)
|
|
293
|
+
return converted
|
|
294
|
+
|
|
295
|
+
wrapper: Any
|
|
296
|
+
if inspect.iscoroutinefunction(fn):
|
|
297
|
+
|
|
298
|
+
@functools.wraps(fn)
|
|
299
|
+
async def typed_async(*args: Any, **kwargs: Any) -> Any:
|
|
300
|
+
return to_payload_value(await fn(*convert(args), **kwargs))
|
|
301
|
+
|
|
302
|
+
wrapper = typed_async
|
|
303
|
+
else:
|
|
304
|
+
|
|
305
|
+
@functools.wraps(fn)
|
|
306
|
+
def typed_sync(*args: Any, **kwargs: Any) -> Any:
|
|
307
|
+
return to_payload_value(fn(*convert(args), **kwargs))
|
|
308
|
+
|
|
309
|
+
wrapper = typed_sync
|
|
310
|
+
wrapper.__typed_activity__ = fn
|
|
311
|
+
return cast(Callable[..., Any], wrapper)
|
|
@@ -206,10 +206,25 @@ def _set_context(ctx: ActivityContext | None) -> contextvars.Token[ActivityConte
|
|
|
206
206
|
return _current_context.set(ctx)
|
|
207
207
|
|
|
208
208
|
|
|
209
|
-
def defn(*, name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
210
|
-
"""Register a callable as an activity type under a language-neutral name.
|
|
209
|
+
def defn(*, name: str, typed: bool = False) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
210
|
+
"""Register a callable as an activity type under a language-neutral name.
|
|
211
|
+
|
|
212
|
+
With ``typed=True``, positional arguments are decoded to the callable's
|
|
213
|
+
parameter annotations: dataclasses, ``Enum`` (including ``IntEnum`` and
|
|
214
|
+
``StrEnum``), ``Optional[T]``, ``list[T]``, ``dict[str, T]`` and the
|
|
215
|
+
scalars ``str``, ``int``, ``float``, ``bool`` and ``bytes``. Keys a
|
|
216
|
+
dataclass does not declare are ignored and missing fields take their
|
|
217
|
+
defaults. A value that does not match fails the activity without retries
|
|
218
|
+
with :class:`~durable_workflow.errors.TypedValueError`. An annotation
|
|
219
|
+
outside this list raises ``TypeError`` here, when the activity is
|
|
220
|
+
declared. Untyped activities keep receiving plain values.
|
|
221
|
+
"""
|
|
211
222
|
|
|
212
223
|
def wrap(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
224
|
+
if typed:
|
|
225
|
+
from ._typed_values import typed_callable
|
|
226
|
+
|
|
227
|
+
fn = typed_callable(fn)
|
|
213
228
|
fn.__activity_name__ = name # type: ignore[attr-defined]
|
|
214
229
|
_REGISTRY[name] = fn
|
|
215
230
|
return fn
|
|
@@ -881,6 +881,26 @@ class NonRetryableError(DurableWorkflowError):
|
|
|
881
881
|
self.__cause__ = cause
|
|
882
882
|
|
|
883
883
|
|
|
884
|
+
class TypedValueError(NonRetryableError, TypeError):
|
|
885
|
+
"""A payload value does not match the receiver's declared type.
|
|
886
|
+
|
|
887
|
+
Raised by opt-in typed values: a ``typed=True`` activity's arguments, or an
|
|
888
|
+
activity result decoded to ``result_type``. ``path`` names the field (for
|
|
889
|
+
example ``order.items[2].sku``) and ``expected`` the declared type. The
|
|
890
|
+
message never contains the received value. An activity whose arguments do
|
|
891
|
+
not match fails without retries, because a retry cannot change the payload.
|
|
892
|
+
"""
|
|
893
|
+
|
|
894
|
+
def __init__(self, path: str, expected: str, detail: str) -> None:
|
|
895
|
+
self.path = path
|
|
896
|
+
self.expected = expected
|
|
897
|
+
self.detail = detail
|
|
898
|
+
super().__init__(f"{path}: expected {expected}; {detail}")
|
|
899
|
+
|
|
900
|
+
def __reduce__(self) -> tuple[Any, ...]:
|
|
901
|
+
return (type(self), (self.path, self.expected, self.detail))
|
|
902
|
+
|
|
903
|
+
|
|
884
904
|
class AvroNotInstalledError(DurableWorkflowError, ImportError):
|
|
885
905
|
"""Raised when the core ``avro`` runtime dependency is unavailable."""
|
|
886
906
|
|
|
@@ -400,8 +400,19 @@ class WorkflowEnvironment:
|
|
|
400
400
|
}
|
|
401
401
|
)
|
|
402
402
|
elif isinstance(cmd, UpsertSearchAttributes):
|
|
403
|
+
# Record the JSON wire form, as the Server does; replay
|
|
404
|
+
# checks the workflow's update against it.
|
|
403
405
|
history.append(
|
|
404
|
-
{
|
|
406
|
+
{
|
|
407
|
+
"event_type": "SearchAttributesUpserted",
|
|
408
|
+
"payload": {
|
|
409
|
+
# Every recorded event but a signal is a step.
|
|
410
|
+
"sequence": 1 + sum(
|
|
411
|
+
event["event_type"] != "SignalReceived" for event in history
|
|
412
|
+
),
|
|
413
|
+
"attributes": json.loads(json.dumps(cmd.attributes)),
|
|
414
|
+
},
|
|
415
|
+
}
|
|
405
416
|
)
|
|
406
417
|
elif isinstance(cmd, RecordVersionMarker):
|
|
407
418
|
history.append(
|
|
@@ -49,6 +49,7 @@ from ._failure_reporting import REQUEST_BYTES, exception_message, normalize_loca
|
|
|
49
49
|
from ._prepared_local_activity import PreparedAttempt, PreparedCancellationObserved, PreparedLocalRunner
|
|
50
50
|
from ._query_task_authority import QueryTaskAuthority, current_query_authority
|
|
51
51
|
from ._sticky_workflow_cache import CacheKey, StickyWorkflowCache, complete_history
|
|
52
|
+
from ._typed_values import to_payload_value
|
|
52
53
|
from .activity import ActivityContext, ActivityInfo, _set_context
|
|
53
54
|
from .auth_composition import (
|
|
54
55
|
AUTH_COMPOSITION_CONTRACT_SCHEMA,
|
|
@@ -2505,6 +2506,9 @@ class Worker:
|
|
|
2505
2506
|
"duration_ms": max(0, round((time.monotonic() - attempt_started_at) * 1000)),
|
|
2506
2507
|
"heartbeats": heartbeats,
|
|
2507
2508
|
})
|
|
2509
|
+
# The workflow receives the same neutral value on this first
|
|
2510
|
+
# run as on replay of the recorded result.
|
|
2511
|
+
result = to_payload_value(result)
|
|
2508
2512
|
try:
|
|
2509
2513
|
inline_result = serializer.envelope(result, codec=payload_codec)
|
|
2510
2514
|
except Exception:
|
|
@@ -3270,6 +3274,7 @@ class Worker:
|
|
|
3270
3274
|
finally:
|
|
3271
3275
|
_set_context(None)
|
|
3272
3276
|
|
|
3277
|
+
result = to_payload_value(result)
|
|
3273
3278
|
log.info("completing activity task %s (%s) -> %r", task_id, activity_type, result)
|
|
3274
3279
|
try:
|
|
3275
3280
|
await self.client.complete_activity_task(
|