durable-workflow 2.12.2__tar.gz → 2.12.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.
Files changed (170) hide show
  1. {durable_workflow-2.12.2/src/durable_workflow.egg-info → durable_workflow-2.12.4}/PKG-INFO +9 -3
  2. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/README.md +8 -2
  3. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/pyproject.toml +7 -3
  4. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_activity_process.py +2 -1
  5. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_failure_reporting.py +6 -0
  6. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/invocable.py +4 -3
  7. durable_workflow-2.12.4/src/durable_workflow/server_compatibility.json +7 -0
  8. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/worker.py +11 -10
  9. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/workflow.py +7 -6
  10. {durable_workflow-2.12.2 → durable_workflow-2.12.4/src/durable_workflow.egg-info}/PKG-INFO +9 -3
  11. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow.egg-info/SOURCES.txt +2 -0
  12. durable_workflow-2.12.4/tests/test_empty_callback_failure.py +156 -0
  13. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_release_metadata.py +183 -0
  14. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_replay_regression_corpus.py +7 -0
  15. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/LICENSE +0 -0
  16. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/setup.cfg +0 -0
  17. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/__init__.py +0 -0
  18. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_activity_lease.py +0 -0
  19. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_avro.py +0 -0
  20. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_cancellation_scope.py +0 -0
  21. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  22. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  23. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_metric_routes.py +0 -0
  24. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_poll_transport.py +0 -0
  25. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_prepared_local_activity.py +0 -0
  26. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_query_task_authority.py +0 -0
  27. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  28. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/activity.py +0 -0
  29. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/auth_composition.py +0 -0
  30. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/cancellation.py +0 -0
  31. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/client.py +0 -0
  32. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/errors.py +0 -0
  33. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/external_storage.py +0 -0
  34. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/external_task_input.py +0 -0
  35. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/external_task_result.py +0 -0
  36. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/history_bundle_verify.py +0 -0
  37. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/interceptors.py +0 -0
  38. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/metrics.py +0 -0
  39. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/nexus.py +0 -0
  40. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/py.typed +0 -0
  41. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/python_conformance.py +0 -0
  42. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/replay_conformance.py +0 -0
  43. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/replay_verify.py +0 -0
  44. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/retry_policy.py +0 -0
  45. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/serializer.py +0 -0
  46. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/sync.py +0 -0
  47. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/telemetry.py +0 -0
  48. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/telemetry_carrier.py +0 -0
  49. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/testing.py +0 -0
  50. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/worker_session.py +0 -0
  51. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  52. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  53. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  54. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow.egg-info/requires.txt +0 -0
  55. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/src/durable_workflow.egg-info/top_level.txt +0 -0
  56. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_activity_cancellation_policies.py +0 -0
  57. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_activity_context.py +0 -0
  58. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_activity_heartbeat_authority.py +0 -0
  59. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_activity_lease_authority.py +0 -0
  60. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_activity_lease_renewal.py +0 -0
  61. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_activity_lease_worker.py +0 -0
  62. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_activity_process.py +0 -0
  63. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_activity_retry_info.py +0 -0
  64. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_auth_composition.py +0 -0
  65. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_avro_value_benchmark.py +0 -0
  66. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_avro_value_protocol.py +0 -0
  67. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_cancellation_context.py +0 -0
  68. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_cancellation_remaining_time.py +0 -0
  69. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_cancellation_scope_admission.py +0 -0
  70. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_cancellation_scope_authoring.py +0 -0
  71. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_cancellation_scope_boundary.py +0 -0
  72. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_cancellation_scope_opening.py +0 -0
  73. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_child_workflow_policies.py +0 -0
  74. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_ci_checkout.py +0 -0
  75. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_ci_integration_endpoint.py +0 -0
  76. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_client.py +0 -0
  77. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_client_telemetry.py +0 -0
  78. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_client_timeouts.py +0 -0
  79. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_codec_regression_corpus.py +0 -0
  80. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  81. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_committed_cancellation_scope_groups.py +0 -0
  82. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_committed_cancellation_scope_history.py +0 -0
  83. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_committed_cancellation_scope_replay.py +0 -0
  84. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_condition_replay_drift.py +0 -0
  85. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_control_plane_parity_fixtures.py +0 -0
  86. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_cooperative_cancellation.py +0 -0
  87. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_cooperative_cancellation_client.py +0 -0
  88. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_cooperative_cancellation_worker.py +0 -0
  89. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_cooperative_remote_worker.py +0 -0
  90. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_docstring_cross_references.py +0 -0
  91. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_durable_selection.py +0 -0
  92. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_errors.py +0 -0
  93. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_external_storage.py +0 -0
  94. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_external_task_input.py +0 -0
  95. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_external_task_result.py +0 -0
  96. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_failure_reporting.py +0 -0
  97. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_generator_lifecycle.py +0 -0
  98. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_golden_history_replay.py +0 -0
  99. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_history_bundle_schema2.py +0 -0
  100. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_history_bundle_verify.py +0 -0
  101. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_history_event_contract.py +0 -0
  102. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_invocable.py +0 -0
  103. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_local_failure_reporting.py +0 -0
  104. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_message_streams.py +0 -0
  105. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_metric_routes.py +0 -0
  106. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_metrics.py +0 -0
  107. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_nexus_durable_wait.py +0 -0
  108. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_nexus_namespace.py +0 -0
  109. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_nexus_wire.py +0 -0
  110. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_order_processing_example.py +0 -0
  111. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_parallel_completion_order_replay.py +0 -0
  112. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_parallel_saga_authoring.py +0 -0
  113. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_patch_history.py +0 -0
  114. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_pending_operation_replay.py +0 -0
  115. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_poll_failure_backoff.py +0 -0
  116. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_poll_transport.py +0 -0
  117. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_prepared_local_activity_client.py +0 -0
  118. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_prepared_local_activity_groups.py +0 -0
  119. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_prepared_local_activity_policies.py +0 -0
  120. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_prepared_local_activity_worker.py +0 -0
  121. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_public_boundary_scanner.py +0 -0
  122. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_pypi_project_surface.py +0 -0
  123. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_python_conformance.py +0 -0
  124. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_queries.py +0 -0
  125. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_query_task_authority.py +0 -0
  126. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_query_timeout_retry.py +0 -0
  127. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_readme_quickstart.py +0 -0
  128. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_redrive_replay.py +0 -0
  129. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_registration_ambiguity.py +0 -0
  130. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_replay.py +0 -0
  131. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_replay_clock.py +0 -0
  132. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_replay_conformance.py +0 -0
  133. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_replay_verify.py +0 -0
  134. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_retry_policy.py +0 -0
  135. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_run_cancellation_scope_replay.py +0 -0
  136. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_runtime_external_payload_transport.py +0 -0
  137. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_schedules.py +0 -0
  138. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_scoped_run_cancellation_context.py +0 -0
  139. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_sdk_metrics_contract.py +0 -0
  140. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_serializer.py +0 -0
  141. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_signals.py +0 -0
  142. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_sleep.py +0 -0
  143. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_standalone_activity_client.py +0 -0
  144. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_sticky_execution.py +0 -0
  145. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_storage_admission.py +0 -0
  146. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_sync.py +0 -0
  147. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_sync_activities.py +0 -0
  148. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_telemetry.py +0 -0
  149. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_telemetry_carrier.py +0 -0
  150. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_testing_failures.py +0 -0
  151. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_testing_harness.py +0 -0
  152. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_testing_nexus.py +0 -0
  153. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_threaded_heartbeat_example.py +0 -0
  154. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_tls_trust.py +0 -0
  155. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_transport_diagnostics.py +0 -0
  156. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_update_signal_condition_replay.py +0 -0
  157. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_updates.py +0 -0
  158. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_wait_condition.py +0 -0
  159. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_worker.py +0 -0
  160. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_worker_deregistration.py +0 -0
  161. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_worker_observation.py +0 -0
  162. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_worker_session.py +0 -0
  163. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_worker_telemetry_wire.py +0 -0
  164. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_workflow_entry_arguments.py +0 -0
  165. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_workflow_entrypoint_validation.py +0 -0
  166. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_workflow_handler_validation.py +0 -0
  167. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_workflow_identity.py +0 -0
  168. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_workflow_result_timeout.py +0 -0
  169. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/tests/test_workflow_streams.py +0 -0
  170. {durable_workflow-2.12.2 → durable_workflow-2.12.4}/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.12.2
3
+ Version: 2.12.4
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
@@ -160,8 +160,14 @@ portable between both runtime choices.
160
160
  ## Compatibility
161
161
 
162
162
  Stable `2.x` SDK releases follow semantic versioning and negotiate runtime
163
- capabilities with Server at startup. Use stable `2.x` SDK and Server channels
164
- for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
163
+ capabilities with Server at startup. The installed package includes
164
+ `durable_workflow/server_compatibility.json`, matching the release declarations in
165
+ `pyproject.toml`. Its `supported-server-release-baselines` maps each supported
166
+ Server major to its minimum stable release. Readers validate every entry before
167
+ selection and refuse missing majors, malformed entries, or a contradiction with
168
+ the unchanged legacy `supported-server-versions` 2.x baseline. An absent map
169
+ retains legacy reader behavior. This declaration does not replace protocol or
170
+ capability negotiation. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
165
171
  documents protocol and upgrade guarantees.
166
172
 
167
173
  ## Worker protocol and cooperative cancellation
@@ -113,8 +113,14 @@ portable between both runtime choices.
113
113
  ## Compatibility
114
114
 
115
115
  Stable `2.x` SDK releases follow semantic versioning and negotiate runtime
116
- capabilities with Server at startup. Use stable `2.x` SDK and Server channels
117
- for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
116
+ capabilities with Server at startup. The installed package includes
117
+ `durable_workflow/server_compatibility.json`, matching the release declarations in
118
+ `pyproject.toml`. Its `supported-server-release-baselines` maps each supported
119
+ Server major to its minimum stable release. Readers validate every entry before
120
+ selection and refuse missing majors, malformed entries, or a contradiction with
121
+ the unchanged legacy `supported-server-versions` 2.x baseline. An absent map
122
+ retains legacy reader behavior. This declaration does not replace protocol or
123
+ capability negotiation. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
118
124
  documents protocol and upgrade guarantees.
119
125
 
120
126
  ## Worker protocol and cooperative cancellation
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "2.12.2"
7
+ version = "2.12.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"
@@ -79,9 +79,10 @@ 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.12.2"
83
- registry-version = "2.12.2"
82
+ product-train = "2.12.4"
83
+ registry-version = "2.12.4"
84
84
  supported-server-versions = "2.8.0"
85
+ supported-server-release-baselines = { "2" = "2.8.0", "3" = "3.0.0" }
85
86
  worker-protocol-version = "1.20"
86
87
  control-plane-version = "2"
87
88
  durable-selection = true
@@ -106,6 +107,9 @@ saga-cancellation = "cooperative"
106
107
  [tool.setuptools.packages.find]
107
108
  where = ["src"]
108
109
 
110
+ [tool.setuptools.package-data]
111
+ durable_workflow = ["server_compatibility.json"]
112
+
109
113
  [tool.pytest.ini_options]
110
114
  testpaths = ["tests"]
111
115
  asyncio_mode = "auto"
@@ -24,6 +24,7 @@ from multiprocessing.process import BaseProcess
24
24
  from pathlib import Path
25
25
  from typing import Any, cast
26
26
 
27
+ from ._failure_reporting import exception_message
27
28
  from .activity import ActivityContext, ActivityInfo, _set_context
28
29
  from .errors import ActivityCancelled, NonRetryableError
29
30
  from .interceptors import ActivityInterceptorContext, WorkerInterceptor
@@ -139,7 +140,7 @@ def _callback(directory: str, channel: socket.socket) -> None:
139
140
  except BaseException as error:
140
141
  code = getattr(error, "code", None)
141
142
  _write_payload(directory, "outcome", CallbackOutcome(failure=CallbackFailure(
142
- message=str(error), failure_type=type(error).__name__,
143
+ message=exception_message(error), failure_type=type(error).__name__,
143
144
  failure_class=f"{type(error).__module__}.{type(error).__qualname__}",
144
145
  failure_code=code if isinstance(code, int) and not isinstance(code, bool) else None,
145
146
  stack_trace=traceback.format_exc(), non_retryable=isinstance(error, NonRetryableError),
@@ -26,6 +26,12 @@ _EXCEPTION_IDENTITY = frozenset({
26
26
  })
27
27
 
28
28
 
29
+ def exception_message(error: BaseException, *, fallback: str | None = None) -> str:
30
+ """Keep useful diagnostic text; blank exceptions still need a valid report."""
31
+ message = str(error)
32
+ return message if message.strip() else fallback or type(error).__name__
33
+
34
+
29
35
  def encoded_json(value: Any) -> bytes:
30
36
  """Use HTTPX's actual JSON settings, including escaping and NaN refusal."""
31
37
  return httpx.Request("POST", "https://failure.invalid", json=value).content
@@ -12,6 +12,7 @@ from datetime import datetime, timezone
12
12
  from typing import Any, cast
13
13
 
14
14
  from . import serializer
15
+ from ._failure_reporting import exception_message
15
16
  from .errors import ActivityCancelled, NonRetryableError
16
17
  from .external_storage import ExternalPayloadCache, ExternalStorageDriver
17
18
  from .external_task_input import ExternalTaskInput, parse_external_task_input
@@ -105,7 +106,7 @@ class InvocableActivityHandler:
105
106
  started,
106
107
  kind="application",
107
108
  classification="application_error",
108
- message=str(exc),
109
+ message=exception_message(exc),
109
110
  failure_type=type(exc).__name__,
110
111
  stack_trace=traceback.format_exc(),
111
112
  retryable=False,
@@ -116,7 +117,7 @@ class InvocableActivityHandler:
116
117
  started,
117
118
  kind="timeout",
118
119
  classification="deadline_exceeded",
119
- message=str(exc) or "Invocable activity handler exceeded its deadline.",
120
+ message=exception_message(exc, fallback="Invocable activity handler exceeded its deadline."),
120
121
  failure_type=type(exc).__name__,
121
122
  stack_trace=traceback.format_exc(),
122
123
  timeout_type="deadline_exceeded",
@@ -140,7 +141,7 @@ class InvocableActivityHandler:
140
141
  started,
141
142
  kind="application",
142
143
  classification="application_error",
143
- message=str(exc),
144
+ message=exception_message(exc),
144
145
  failure_type=type(exc).__name__,
145
146
  stack_trace=traceback.format_exc(),
146
147
  retryable=True,
@@ -0,0 +1,7 @@
1
+ {
2
+ "supported-server-versions": "2.8.0",
3
+ "supported-server-release-baselines": {
4
+ "2": "2.8.0",
5
+ "3": "3.0.0"
6
+ }
7
+ }
@@ -44,7 +44,7 @@ from ._activity_lease import activity_lease_renewal_supported, current_activity_
44
44
  from ._activity_process import CallbackFailure, CallbackInvocation, CallbackProcessLost, SupervisedCallback
45
45
  from ._cancellation_scope_history import CancellationScopeBudget, CancellationScopeDeliveryReceipt
46
46
  from ._cooperative_cancellation import CancellationRequest, read_cancellation_history
47
- from ._failure_reporting import REQUEST_BYTES, normalize_local_failure
47
+ from ._failure_reporting import REQUEST_BYTES, exception_message, normalize_local_failure
48
48
  from ._prepared_local_activity import PreparedAttempt, PreparedCancellationObserved, PreparedLocalRunner
49
49
  from ._query_task_authority import QueryTaskAuthority, current_query_authority
50
50
  from ._sticky_workflow_cache import CacheKey, StickyWorkflowCache, complete_history
@@ -3240,7 +3240,7 @@ class Worker:
3240
3240
  except NonRetryableError as e:
3241
3241
  log.exception("activity failed (non-retryable)")
3242
3242
  await self._report_activity_task_failure(
3243
- task_id, attempt_id, "activity failure", message=str(e), failure_type=type(e).__name__,
3243
+ task_id, attempt_id, "activity failure", message=exception_message(e), failure_type=type(e).__name__,
3244
3244
  failure_class=_exception_class_name(e), failure_code=_exception_code(e),
3245
3245
  stack_trace=traceback.format_exc(), non_retryable=True,
3246
3246
  )
@@ -3271,7 +3271,8 @@ class Worker:
3271
3271
  raise error
3272
3272
  log.error("activity failed", exc_info=(type(error), error, error.__traceback__))
3273
3273
  await self._report_activity_task_failure(
3274
- task_id, attempt_id, "activity failure", message=str(error), failure_type=type(error).__name__,
3274
+ task_id, attempt_id, "activity failure", message=exception_message(error),
3275
+ failure_type=type(error).__name__,
3275
3276
  failure_class=_exception_class_name(error), failure_code=_exception_code(error),
3276
3277
  stack_trace="".join(traceback.format_exception(type(error), error, error.__traceback__)),
3277
3278
  )
@@ -3573,7 +3574,7 @@ class Worker:
3573
3574
  )
3574
3575
  return "failed"
3575
3576
  except QueryFailed as e:
3576
- message = str(e)
3577
+ message = exception_message(e)
3577
3578
  if "unknown query" in message:
3578
3579
  reason = "rejected_unknown_query"
3579
3580
  elif message.startswith("workflow replay failed before query:"):
@@ -3594,7 +3595,7 @@ class Worker:
3594
3595
  await self._fail_query_task(
3595
3596
  query_task_id,
3596
3597
  attempt,
3597
- str(e) or "query execution failed",
3598
+ exception_message(e, fallback="query execution failed"),
3598
3599
  reason="query_rejected",
3599
3600
  failure_type=type(e).__name__,
3600
3601
  stack_trace=traceback.format_exc(),
@@ -3631,7 +3632,7 @@ class Worker:
3631
3632
  await self._fail_query_task(
3632
3633
  query_task_id,
3633
3634
  attempt,
3634
- str(e) or "query result completion was rejected by the server",
3635
+ exception_message(e, fallback="query result completion was rejected by the server"),
3635
3636
  reason=server_reason if server_reason else "query_result_completion_failed",
3636
3637
  failure_type=type(e).__name__,
3637
3638
  stack_trace=traceback.format_exc(),
@@ -3667,7 +3668,7 @@ class Worker:
3667
3668
  await self._fail_query_task(
3668
3669
  query_task_id,
3669
3670
  attempt,
3670
- str(e) or "query result completion failed",
3671
+ exception_message(e, fallback="query result completion failed"),
3671
3672
  reason="query_result_completion_failed",
3672
3673
  failure_type=type(e).__name__,
3673
3674
  stack_trace=traceback.format_exc(),
@@ -4075,7 +4076,7 @@ class Worker:
4075
4076
  return await self._reject_update_validation_task(
4076
4077
  task_id,
4077
4078
  attempt,
4078
- str(exc) or "update validator rejected the request",
4079
+ exception_message(exc, fallback="update validator rejected the request"),
4079
4080
  reason="update_validator_rejected",
4080
4081
  failure_type=exc.exception_type or type(exc).__name__,
4081
4082
  validation_errors=exc.validation_errors,
@@ -4085,7 +4086,7 @@ class Worker:
4085
4086
  return await self._reject_update_validation_task(
4086
4087
  task_id,
4087
4088
  attempt,
4088
- str(exc),
4089
+ exception_message(exc),
4089
4090
  reason="update_validation_worker_failed",
4090
4091
  failure_type=type(exc).__name__,
4091
4092
  stack_trace=traceback.format_exc(),
@@ -4094,7 +4095,7 @@ class Worker:
4094
4095
  return await self._reject_update_validation_task(
4095
4096
  task_id,
4096
4097
  attempt,
4097
- str(exc) or "update validator rejected the request",
4098
+ exception_message(exc, fallback="update validator rejected the request"),
4098
4099
  reason="update_validator_rejected",
4099
4100
  failure_type=type(exc).__name__,
4100
4101
  stack_trace=traceback.format_exc(),
@@ -43,6 +43,7 @@ from ._cancellation_scope_history import (
43
43
  ScopeBoundary,
44
44
  )
45
45
  from ._cooperative_cancellation import CancellationDelivery, read_cancellation_history
46
+ from ._failure_reporting import exception_message
46
47
  from .cancellation import (
47
48
  CancellationContext,
48
49
  CancellationPolicy,
@@ -3292,7 +3293,7 @@ def _prepare_query_state(
3292
3293
  raise
3293
3294
  except Exception as exc:
3294
3295
  state.outcome.close()
3295
- raise QueryFailed(str(exc) or f"query {query_name!r} failed") from exc
3296
+ raise QueryFailed(exception_message(exc, fallback=f"query {query_name!r} failed")) from exc
3296
3297
  except BaseException:
3297
3298
  state.outcome.close()
3298
3299
  raise
@@ -3594,7 +3595,7 @@ def _invoke_update_validator(
3594
3595
  except Exception as exc:
3595
3596
  errors = exc.errors if isinstance(exc, InvalidArgument) else None
3596
3597
  raise UpdateRejected(
3597
- str(exc) or f"update validator {update_name!r} rejected the request",
3598
+ exception_message(exc, fallback=f"update validator {update_name!r} rejected the request"),
3598
3599
  exception_type=type(exc).__name__,
3599
3600
  validation_errors=errors,
3600
3601
  ) from exc
@@ -3614,7 +3615,7 @@ def _accepted_update_payload(
3614
3615
 
3615
3616
 
3616
3617
  def _fail_update_from_exception(update_id: str, prefix: str, exc: Exception) -> FailUpdate:
3617
- message = str(exc) or type(exc).__name__
3618
+ message = exception_message(exc)
3618
3619
  return FailUpdate(
3619
3620
  update_id=update_id,
3620
3621
  message=f"{prefix}: {message}",
@@ -3628,7 +3629,7 @@ def _exception_class_name(exc: BaseException) -> str:
3628
3629
 
3629
3630
 
3630
3631
  def _fail_workflow_from_exception(exc: BaseException, *, prefix: str | None = None) -> FailWorkflow:
3631
- message = str(exc) or type(exc).__name__
3632
+ message = exception_message(exc)
3632
3633
  if prefix:
3633
3634
  message = f"{prefix}: {message}"
3634
3635
 
@@ -3645,12 +3646,12 @@ def _fail_workflow_from_exception(exc: BaseException, *, prefix: str | None = No
3645
3646
  "initiating_failure": {
3646
3647
  "type": type(initiating).__name__,
3647
3648
  "class": _exception_class_name(initiating),
3648
- "message": str(initiating) or type(initiating).__name__,
3649
+ "message": exception_message(initiating),
3649
3650
  },
3650
3651
  "compensation_failure": {
3651
3652
  "type": type(compensation).__name__,
3652
3653
  "class": _exception_class_name(compensation),
3653
- "message": str(compensation) or type(compensation).__name__,
3654
+ "message": exception_message(compensation),
3654
3655
  "activity_type": exc.compensation_activity_type,
3655
3656
  "registration_order": exc.compensation_registration_order,
3656
3657
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.12.2
3
+ Version: 2.12.4
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
@@ -160,8 +160,14 @@ portable between both runtime choices.
160
160
  ## Compatibility
161
161
 
162
162
  Stable `2.x` SDK releases follow semantic versioning and negotiate runtime
163
- capabilities with Server at startup. Use stable `2.x` SDK and Server channels
164
- for new applications. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
163
+ capabilities with Server at startup. The installed package includes
164
+ `durable_workflow/server_compatibility.json`, matching the release declarations in
165
+ `pyproject.toml`. Its `supported-server-release-baselines` maps each supported
166
+ Server major to its minimum stable release. Readers validate every entry before
167
+ selection and refuse missing majors, malformed entries, or a contradiction with
168
+ the unchanged legacy `supported-server-versions` 2.x baseline. An absent map
169
+ retains legacy reader behavior. This declaration does not replace protocol or
170
+ capability negotiation. The [compatibility guide](https://durable-workflow.com/docs/2.0/compatibility/)
165
171
  documents protocol and upgrade guarantees.
166
172
 
167
173
  ## Worker protocol and cooperative cancellation
@@ -33,6 +33,7 @@ src/durable_workflow/replay_conformance.py
33
33
  src/durable_workflow/replay_verify.py
34
34
  src/durable_workflow/retry_policy.py
35
35
  src/durable_workflow/serializer.py
36
+ src/durable_workflow/server_compatibility.json
36
37
  src/durable_workflow/sync.py
37
38
  src/durable_workflow/telemetry.py
38
39
  src/durable_workflow/telemetry_carrier.py
@@ -83,6 +84,7 @@ tests/test_cooperative_cancellation_worker.py
83
84
  tests/test_cooperative_remote_worker.py
84
85
  tests/test_docstring_cross_references.py
85
86
  tests/test_durable_selection.py
87
+ tests/test_empty_callback_failure.py
86
88
  tests/test_errors.py
87
89
  tests/test_external_storage.py
88
90
  tests/test_external_task_input.py
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from unittest.mock import AsyncMock
5
+
6
+ import pytest
7
+
8
+ from durable_workflow import serializer, workflow
9
+ from durable_workflow.client import Client
10
+ from durable_workflow.errors import NonRetryableError
11
+ from durable_workflow.external_task_result import parse_external_task_result
12
+ from durable_workflow.invocable import handle_invocable_activity
13
+ from durable_workflow.worker import Worker
14
+ from durable_workflow.workflow import _fail_update_from_exception, _fail_workflow_from_exception
15
+ from tests.query_task_helpers import leased_query_task
16
+ from tests.test_cooperative_remote_worker import make_owner, task
17
+ from tests.test_invocable import activity_input
18
+ from tests.test_worker import mock_client # noqa: F401 (shared fixture)
19
+
20
+
21
+ async def application_failure(kind: str, message: str) -> None:
22
+ if kind == "NonRetryableError":
23
+ raise NonRetryableError(message)
24
+ raise AssertionError(message)
25
+
26
+
27
+ @pytest.mark.parametrize("message", ["", " \t\n", " original failure "])
28
+ @pytest.mark.parametrize("kind", ["AssertionError", "NonRetryableError"])
29
+ @pytest.mark.parametrize("supervised", [False, True])
30
+ async def test_callback_failure_reports_a_nonempty_message_with_original_authority(
31
+ monkeypatch: pytest.MonkeyPatch, message: str, kind: str, supervised: bool,
32
+ ) -> None:
33
+ if supervised:
34
+ worker, client = await make_owner(monkeypatch)
35
+ else:
36
+ client = AsyncMock(spec=Client)
37
+ worker = Worker(client, task_queue="queue", worker_id="owner")
38
+ worker.activities["remote"] = application_failure
39
+ try:
40
+ outcome = await worker._run_activity_task(task(kind, message))
41
+ assert outcome == ("failed_non_retryable" if kind == "NonRetryableError" else "failed")
42
+ client.fail_activity_task.assert_awaited_once()
43
+ report = client.fail_activity_task.await_args.kwargs
44
+ assert report["message"] == (message if message.strip() else kind)
45
+ assert report["failure_type"] == kind
46
+ assert report["failure_class"].endswith("." + kind)
47
+ assert report.get("non_retryable", False) is (kind == "NonRetryableError")
48
+ assert report["task_id"] == "remote-task"
49
+ assert report["activity_attempt_id"] == "attempt"
50
+ assert report["lease_owner"] == "owner"
51
+ assert "application_failure" in report["stack_trace"]
52
+ client.complete_activity_task.assert_not_awaited()
53
+ finally:
54
+ await worker.stop()
55
+
56
+
57
+ @pytest.mark.parametrize("message", ["", " \t\n", " original failure "])
58
+ @pytest.mark.parametrize("kind", ["AssertionError", "NonRetryableError"])
59
+ async def test_invocable_failure_keeps_a_valid_typed_envelope(message: str, kind: str) -> None:
60
+ output = await handle_invocable_activity(
61
+ activity_input(kind, message), {"billing.charge-card": application_failure},
62
+ )
63
+ failure: Any = parse_external_task_result(output).failure
64
+ assert failure.message == (message if message.strip() else kind)
65
+ assert failure.type == kind
66
+ assert "application_failure" in failure.stack_trace
67
+
68
+
69
+ @pytest.mark.parametrize("message", ["", " \t\n", " original failure "])
70
+ def test_workflow_and_update_failures_have_useful_messages(message: str) -> None:
71
+ error = AssertionError(message)
72
+ expected = message if message.strip() else "AssertionError"
73
+ failed = _fail_workflow_from_exception(error)
74
+ assert failed.message == expected
75
+ assert failed.exception_type == "AssertionError"
76
+ update = _fail_update_from_exception("update", "update rejected", error)
77
+ assert update.message == "update rejected: " + expected
78
+ assert update.exception_type == "AssertionError"
79
+
80
+
81
+ @workflow.defn(name="tests.blank-query-and-validator")
82
+ class BlankQueryAndValidatorWorkflow:
83
+ message = ""
84
+
85
+ @workflow.query("status")
86
+ def status(self) -> str:
87
+ raise AssertionError(type(self).message)
88
+
89
+ @workflow.update("approve")
90
+ def approve(self) -> None:
91
+ return None
92
+
93
+ @approve.validator # type: ignore[attr-defined]
94
+ def validate_approve(self) -> None:
95
+ raise AssertionError(type(self).message)
96
+
97
+ def run(self, ctx: Any) -> str:
98
+ return "waiting"
99
+
100
+
101
+ def blank_workflow_task(**fields: Any) -> dict[str, Any]:
102
+ return {
103
+ "workflow_type": "tests.blank-query-and-validator",
104
+ "history_events": [],
105
+ "workflow_arguments": serializer.envelope([], codec="avro"),
106
+ "payload_codec": "avro",
107
+ **fields,
108
+ }
109
+
110
+
111
+ @pytest.mark.parametrize("message", ["", " \t\n", " original failure "])
112
+ async def test_query_failure_reports_a_nonempty_message(
113
+ mock_client: AsyncMock, message: str, monkeypatch: pytest.MonkeyPatch, # noqa: F811
114
+ ) -> None:
115
+ monkeypatch.setattr(BlankQueryAndValidatorWorkflow, "message", message)
116
+ worker = Worker(mock_client, task_queue="q1", workflows=[BlankQueryAndValidatorWorkflow])
117
+ query = blank_workflow_task(
118
+ query_task_id="qt-blank", query_task_attempt=3, query_name="status",
119
+ query_arguments=serializer.envelope([], codec="avro"),
120
+ )
121
+
122
+ assert await worker._run_query_task(leased_query_task(worker, query)) == "failed"
123
+
124
+ report = mock_client.fail_query_task.await_args.kwargs
125
+ assert report["message"] == (message if message.strip() else "query 'status' failed")
126
+ assert report["reason"] == "query_rejected"
127
+ assert report["query_task_id"] == "qt-blank"
128
+ assert report["query_task_attempt"] == 3
129
+ mock_client.complete_query_task.assert_not_awaited()
130
+
131
+
132
+ @pytest.mark.parametrize("message", ["", " \t\n", " original failure "])
133
+ async def test_update_validator_rejection_reports_a_nonempty_message(
134
+ mock_client: AsyncMock, message: str, monkeypatch: pytest.MonkeyPatch, # noqa: F811
135
+ ) -> None:
136
+ monkeypatch.setattr(BlankQueryAndValidatorWorkflow, "message", message)
137
+ worker = Worker(
138
+ mock_client, task_queue="q1", workflows=[BlankQueryAndValidatorWorkflow], worker_id="validator",
139
+ )
140
+ validation = blank_workflow_task(
141
+ task_kind="update_validation", update_validation_task_id="uv-blank", update_validation_attempt=2,
142
+ update_name="approve", update_arguments=serializer.envelope([], codec="avro"),
143
+ workflow_id="wf", run_id="run",
144
+ )
145
+
146
+ assert await worker._run_update_validation_task(validation) == "rejected"
147
+
148
+ rejection = mock_client.reject_update_validation_task.await_args.kwargs
149
+ expected = message if message.strip() else "update validator 'approve' rejected the request"
150
+ assert expected in rejection["message"]
151
+ assert rejection["message"].strip()
152
+ assert rejection["reason"] == "update_validator_rejected"
153
+ assert rejection["failure_type"] == "AssertionError"
154
+ assert rejection["update_validation_task_id"] == "uv-blank"
155
+ assert rejection["update_validation_attempt"] == 2
156
+ mock_client.approve_update_validation_task.assert_not_awaited()