durable-workflow 2.12.1__tar.gz → 2.12.3__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 (169) hide show
  1. {durable_workflow-2.12.1/src/durable_workflow.egg-info → durable_workflow-2.12.3}/PKG-INFO +13 -1
  2. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/README.md +9 -0
  3. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/pyproject.toml +6 -3
  4. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_activity_process.py +2 -1
  5. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_failure_reporting.py +6 -0
  6. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/invocable.py +4 -3
  7. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/worker.py +11 -10
  8. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/workflow.py +214 -43
  9. {durable_workflow-2.12.1 → durable_workflow-2.12.3/src/durable_workflow.egg-info}/PKG-INFO +13 -1
  10. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow.egg-info/SOURCES.txt +2 -0
  11. durable_workflow-2.12.3/tests/test_empty_callback_failure.py +156 -0
  12. durable_workflow-2.12.3/tests/test_generator_lifecycle.py +401 -0
  13. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_replay_regression_corpus.py +31 -7
  14. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/LICENSE +0 -0
  15. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/setup.cfg +0 -0
  16. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/__init__.py +0 -0
  17. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_activity_lease.py +0 -0
  18. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_avro.py +0 -0
  19. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_cancellation_scope.py +0 -0
  20. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  21. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  22. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_metric_routes.py +0 -0
  23. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_poll_transport.py +0 -0
  24. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_prepared_local_activity.py +0 -0
  25. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_query_task_authority.py +0 -0
  26. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  27. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/activity.py +0 -0
  28. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/auth_composition.py +0 -0
  29. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/cancellation.py +0 -0
  30. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/client.py +0 -0
  31. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/errors.py +0 -0
  32. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/external_storage.py +0 -0
  33. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/external_task_input.py +0 -0
  34. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/external_task_result.py +0 -0
  35. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/history_bundle_verify.py +0 -0
  36. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/interceptors.py +0 -0
  37. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/metrics.py +0 -0
  38. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/nexus.py +0 -0
  39. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/py.typed +0 -0
  40. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/python_conformance.py +0 -0
  41. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/replay_conformance.py +0 -0
  42. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/replay_verify.py +0 -0
  43. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/retry_policy.py +0 -0
  44. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/serializer.py +0 -0
  45. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/sync.py +0 -0
  46. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/telemetry.py +0 -0
  47. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/telemetry_carrier.py +0 -0
  48. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/testing.py +0 -0
  49. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/worker_session.py +0 -0
  50. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  51. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  52. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  53. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow.egg-info/requires.txt +0 -0
  54. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/src/durable_workflow.egg-info/top_level.txt +0 -0
  55. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_activity_cancellation_policies.py +0 -0
  56. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_activity_context.py +0 -0
  57. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_activity_heartbeat_authority.py +0 -0
  58. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_activity_lease_authority.py +0 -0
  59. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_activity_lease_renewal.py +0 -0
  60. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_activity_lease_worker.py +0 -0
  61. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_activity_process.py +0 -0
  62. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_activity_retry_info.py +0 -0
  63. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_auth_composition.py +0 -0
  64. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_avro_value_benchmark.py +0 -0
  65. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_avro_value_protocol.py +0 -0
  66. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_cancellation_context.py +0 -0
  67. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_cancellation_remaining_time.py +0 -0
  68. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_cancellation_scope_admission.py +0 -0
  69. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_cancellation_scope_authoring.py +0 -0
  70. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_cancellation_scope_boundary.py +0 -0
  71. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_cancellation_scope_opening.py +0 -0
  72. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_child_workflow_policies.py +0 -0
  73. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_ci_checkout.py +0 -0
  74. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_ci_integration_endpoint.py +0 -0
  75. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_client.py +0 -0
  76. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_client_telemetry.py +0 -0
  77. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_client_timeouts.py +0 -0
  78. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_codec_regression_corpus.py +0 -0
  79. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  80. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_committed_cancellation_scope_groups.py +0 -0
  81. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_committed_cancellation_scope_history.py +0 -0
  82. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_committed_cancellation_scope_replay.py +0 -0
  83. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_condition_replay_drift.py +0 -0
  84. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_control_plane_parity_fixtures.py +0 -0
  85. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_cooperative_cancellation.py +0 -0
  86. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_cooperative_cancellation_client.py +0 -0
  87. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_cooperative_cancellation_worker.py +0 -0
  88. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_cooperative_remote_worker.py +0 -0
  89. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_docstring_cross_references.py +0 -0
  90. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_durable_selection.py +0 -0
  91. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_errors.py +0 -0
  92. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_external_storage.py +0 -0
  93. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_external_task_input.py +0 -0
  94. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_external_task_result.py +0 -0
  95. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_failure_reporting.py +0 -0
  96. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_golden_history_replay.py +0 -0
  97. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_history_bundle_schema2.py +0 -0
  98. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_history_bundle_verify.py +0 -0
  99. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_history_event_contract.py +0 -0
  100. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_invocable.py +0 -0
  101. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_local_failure_reporting.py +0 -0
  102. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_message_streams.py +0 -0
  103. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_metric_routes.py +0 -0
  104. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_metrics.py +0 -0
  105. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_nexus_durable_wait.py +0 -0
  106. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_nexus_namespace.py +0 -0
  107. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_nexus_wire.py +0 -0
  108. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_order_processing_example.py +0 -0
  109. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_parallel_completion_order_replay.py +0 -0
  110. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_parallel_saga_authoring.py +0 -0
  111. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_patch_history.py +0 -0
  112. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_pending_operation_replay.py +0 -0
  113. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_poll_failure_backoff.py +0 -0
  114. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_poll_transport.py +0 -0
  115. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_prepared_local_activity_client.py +0 -0
  116. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_prepared_local_activity_groups.py +0 -0
  117. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_prepared_local_activity_policies.py +0 -0
  118. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_prepared_local_activity_worker.py +0 -0
  119. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_public_boundary_scanner.py +0 -0
  120. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_pypi_project_surface.py +0 -0
  121. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_python_conformance.py +0 -0
  122. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_queries.py +0 -0
  123. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_query_task_authority.py +0 -0
  124. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_query_timeout_retry.py +0 -0
  125. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_readme_quickstart.py +0 -0
  126. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_redrive_replay.py +0 -0
  127. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_registration_ambiguity.py +0 -0
  128. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_release_metadata.py +0 -0
  129. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_replay.py +0 -0
  130. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_replay_clock.py +0 -0
  131. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_replay_conformance.py +0 -0
  132. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_replay_verify.py +0 -0
  133. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_retry_policy.py +0 -0
  134. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_run_cancellation_scope_replay.py +0 -0
  135. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_runtime_external_payload_transport.py +0 -0
  136. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_schedules.py +0 -0
  137. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_scoped_run_cancellation_context.py +0 -0
  138. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_sdk_metrics_contract.py +0 -0
  139. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_serializer.py +0 -0
  140. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_signals.py +0 -0
  141. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_sleep.py +0 -0
  142. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_standalone_activity_client.py +0 -0
  143. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_sticky_execution.py +0 -0
  144. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_storage_admission.py +0 -0
  145. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_sync.py +0 -0
  146. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_sync_activities.py +0 -0
  147. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_telemetry.py +0 -0
  148. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_telemetry_carrier.py +0 -0
  149. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_testing_failures.py +0 -0
  150. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_testing_harness.py +0 -0
  151. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_testing_nexus.py +0 -0
  152. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_threaded_heartbeat_example.py +0 -0
  153. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_tls_trust.py +0 -0
  154. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_transport_diagnostics.py +0 -0
  155. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_update_signal_condition_replay.py +0 -0
  156. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_updates.py +0 -0
  157. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_wait_condition.py +0 -0
  158. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_worker.py +0 -0
  159. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_worker_deregistration.py +0 -0
  160. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_worker_observation.py +0 -0
  161. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_worker_session.py +0 -0
  162. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_worker_telemetry_wire.py +0 -0
  163. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_workflow_entry_arguments.py +0 -0
  164. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_workflow_entrypoint_validation.py +0 -0
  165. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_workflow_handler_validation.py +0 -0
  166. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_workflow_identity.py +0 -0
  167. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_workflow_result_timeout.py +0 -0
  168. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/tests/test_workflow_streams.py +0 -0
  169. {durable_workflow-2.12.1 → durable_workflow-2.12.3}/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.1
3
+ Version: 2.12.3
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
@@ -14,6 +14,9 @@ Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
17
20
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
21
  Classifier: Typing :: Typed
19
22
  Requires-Python: >=3.10
@@ -186,6 +189,15 @@ independently of application heartbeats. Existing terminal cancellation remains
186
189
  available. See the [cancellation guide](docs/cooperative-cancellation-design.md)
187
190
  for immutable context, operation policies, shielded cleanup and recovery.
188
191
 
192
+ A suspended replay is not a workflow exit. Python may run `finally` blocks and
193
+ context-manager exits when disposing of that replay. The SDK discards commands
194
+ and `ctx.logger` messages produced only by disposal; queries observe the state
195
+ at the pending command. Actual completion, failure and cooperative cancellation
196
+ run cleanup through the normal history, and cold replay uses its recorded
197
+ results. Put external cleanup in an Activity rather than direct I/O in a
198
+ finalizer, and use `ctx.cancellation_shield()` when that Activity must finish
199
+ after cancellation. The disposal guarantees require SDK 2.12.2 or later.
200
+
189
201
  Both `await activity.context().heartbeat()` and synchronous
190
202
  `activity.context().heartbeat_sync()` stop at an explicit Server refusal.
191
203
  They raise `ActivityLeaseAuthorityLost` with its original `.reason` for an
@@ -142,6 +142,15 @@ independently of application heartbeats. Existing terminal cancellation remains
142
142
  available. See the [cancellation guide](docs/cooperative-cancellation-design.md)
143
143
  for immutable context, operation policies, shielded cleanup and recovery.
144
144
 
145
+ A suspended replay is not a workflow exit. Python may run `finally` blocks and
146
+ context-manager exits when disposing of that replay. The SDK discards commands
147
+ and `ctx.logger` messages produced only by disposal; queries observe the state
148
+ at the pending command. Actual completion, failure and cooperative cancellation
149
+ run cleanup through the normal history, and cold replay uses its recorded
150
+ results. Put external cleanup in an Activity rather than direct I/O in a
151
+ finalizer, and use `ctx.cancellation_shield()` when that Activity must finish
152
+ after cancellation. The disposal guarantees require SDK 2.12.2 or later.
153
+
145
154
  Both `await activity.context().heartbeat()` and synchronous
146
155
  `activity.context().heartbeat_sync()` stop at an explicit Server refusal.
147
156
  They raise `ActivityLeaseAuthorityLost` with its original `.reason` for an
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "2.12.1"
7
+ version = "2.12.3"
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"
@@ -29,6 +29,9 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3.10",
30
30
  "Programming Language :: Python :: 3.11",
31
31
  "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
33
+ "Programming Language :: Python :: 3.14",
34
+ "Programming Language :: Python :: 3.15",
32
35
  "Topic :: Software Development :: Libraries :: Python Modules",
33
36
  "Typing :: Typed",
34
37
  ]
@@ -76,8 +79,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
76
79
  durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
77
80
 
78
81
  [tool.durable-workflow]
79
- product-train = "2.12.1"
80
- registry-version = "2.12.1"
82
+ product-train = "2.12.3"
83
+ registry-version = "2.12.3"
81
84
  supported-server-versions = "2.8.0"
82
85
  worker-protocol-version = "1.20"
83
86
  control-plane-version = "2"
@@ -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,
@@ -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(),