durable-workflow 2.12.1__tar.gz → 2.12.2__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 (168) hide show
  1. {durable_workflow-2.12.1/src/durable_workflow.egg-info → durable_workflow-2.12.2}/PKG-INFO +13 -1
  2. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/README.md +9 -0
  3. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/pyproject.toml +6 -3
  4. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/workflow.py +207 -37
  5. {durable_workflow-2.12.1 → durable_workflow-2.12.2/src/durable_workflow.egg-info}/PKG-INFO +13 -1
  6. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/SOURCES.txt +1 -0
  7. durable_workflow-2.12.2/tests/test_generator_lifecycle.py +401 -0
  8. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_replay_regression_corpus.py +24 -7
  9. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/LICENSE +0 -0
  10. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/setup.cfg +0 -0
  11. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/__init__.py +0 -0
  12. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_activity_lease.py +0 -0
  13. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_activity_process.py +0 -0
  14. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_avro.py +0 -0
  15. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_cancellation_scope.py +0 -0
  16. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  17. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  18. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_failure_reporting.py +0 -0
  19. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_metric_routes.py +0 -0
  20. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_poll_transport.py +0 -0
  21. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_prepared_local_activity.py +0 -0
  22. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_query_task_authority.py +0 -0
  23. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  24. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/activity.py +0 -0
  25. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/auth_composition.py +0 -0
  26. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/cancellation.py +0 -0
  27. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/client.py +0 -0
  28. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/errors.py +0 -0
  29. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/external_storage.py +0 -0
  30. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/external_task_input.py +0 -0
  31. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/external_task_result.py +0 -0
  32. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/history_bundle_verify.py +0 -0
  33. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/interceptors.py +0 -0
  34. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/invocable.py +0 -0
  35. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/metrics.py +0 -0
  36. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/nexus.py +0 -0
  37. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/py.typed +0 -0
  38. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/python_conformance.py +0 -0
  39. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/replay_conformance.py +0 -0
  40. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/replay_verify.py +0 -0
  41. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/retry_policy.py +0 -0
  42. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/serializer.py +0 -0
  43. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/sync.py +0 -0
  44. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/telemetry.py +0 -0
  45. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/telemetry_carrier.py +0 -0
  46. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/testing.py +0 -0
  47. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/worker.py +0 -0
  48. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/worker_session.py +0 -0
  49. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  50. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  51. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  52. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/requires.txt +0 -0
  53. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/src/durable_workflow.egg-info/top_level.txt +0 -0
  54. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_cancellation_policies.py +0 -0
  55. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_context.py +0 -0
  56. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_heartbeat_authority.py +0 -0
  57. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_lease_authority.py +0 -0
  58. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_lease_renewal.py +0 -0
  59. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_lease_worker.py +0 -0
  60. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_process.py +0 -0
  61. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_activity_retry_info.py +0 -0
  62. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_auth_composition.py +0 -0
  63. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_avro_value_benchmark.py +0 -0
  64. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_avro_value_protocol.py +0 -0
  65. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_context.py +0 -0
  66. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_remaining_time.py +0 -0
  67. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_scope_admission.py +0 -0
  68. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_scope_authoring.py +0 -0
  69. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_scope_boundary.py +0 -0
  70. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cancellation_scope_opening.py +0 -0
  71. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_child_workflow_policies.py +0 -0
  72. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_ci_checkout.py +0 -0
  73. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_ci_integration_endpoint.py +0 -0
  74. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_client.py +0 -0
  75. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_client_telemetry.py +0 -0
  76. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_client_timeouts.py +0 -0
  77. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_codec_regression_corpus.py +0 -0
  78. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  79. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_groups.py +0 -0
  80. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_history.py +0 -0
  81. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_committed_cancellation_scope_replay.py +0 -0
  82. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_condition_replay_drift.py +0 -0
  83. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_control_plane_parity_fixtures.py +0 -0
  84. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation.py +0 -0
  85. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation_client.py +0 -0
  86. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cooperative_cancellation_worker.py +0 -0
  87. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_cooperative_remote_worker.py +0 -0
  88. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_docstring_cross_references.py +0 -0
  89. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_durable_selection.py +0 -0
  90. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_errors.py +0 -0
  91. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_external_storage.py +0 -0
  92. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_external_task_input.py +0 -0
  93. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_external_task_result.py +0 -0
  94. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_failure_reporting.py +0 -0
  95. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_golden_history_replay.py +0 -0
  96. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_history_bundle_schema2.py +0 -0
  97. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_history_bundle_verify.py +0 -0
  98. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_history_event_contract.py +0 -0
  99. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_invocable.py +0 -0
  100. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_local_failure_reporting.py +0 -0
  101. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_message_streams.py +0 -0
  102. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_metric_routes.py +0 -0
  103. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_metrics.py +0 -0
  104. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_nexus_durable_wait.py +0 -0
  105. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_nexus_namespace.py +0 -0
  106. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_nexus_wire.py +0 -0
  107. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_order_processing_example.py +0 -0
  108. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_parallel_completion_order_replay.py +0 -0
  109. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_parallel_saga_authoring.py +0 -0
  110. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_patch_history.py +0 -0
  111. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_pending_operation_replay.py +0 -0
  112. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_poll_failure_backoff.py +0 -0
  113. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_poll_transport.py +0 -0
  114. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_client.py +0 -0
  115. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_groups.py +0 -0
  116. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_policies.py +0 -0
  117. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_prepared_local_activity_worker.py +0 -0
  118. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_public_boundary_scanner.py +0 -0
  119. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_pypi_project_surface.py +0 -0
  120. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_python_conformance.py +0 -0
  121. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_queries.py +0 -0
  122. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_query_task_authority.py +0 -0
  123. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_query_timeout_retry.py +0 -0
  124. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_readme_quickstart.py +0 -0
  125. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_redrive_replay.py +0 -0
  126. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_registration_ambiguity.py +0 -0
  127. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_release_metadata.py +0 -0
  128. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_replay.py +0 -0
  129. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_replay_clock.py +0 -0
  130. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_replay_conformance.py +0 -0
  131. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_replay_verify.py +0 -0
  132. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_retry_policy.py +0 -0
  133. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_run_cancellation_scope_replay.py +0 -0
  134. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_runtime_external_payload_transport.py +0 -0
  135. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_schedules.py +0 -0
  136. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_scoped_run_cancellation_context.py +0 -0
  137. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_sdk_metrics_contract.py +0 -0
  138. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_serializer.py +0 -0
  139. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_signals.py +0 -0
  140. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_sleep.py +0 -0
  141. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_standalone_activity_client.py +0 -0
  142. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_sticky_execution.py +0 -0
  143. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_storage_admission.py +0 -0
  144. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_sync.py +0 -0
  145. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_sync_activities.py +0 -0
  146. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_telemetry.py +0 -0
  147. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_telemetry_carrier.py +0 -0
  148. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_testing_failures.py +0 -0
  149. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_testing_harness.py +0 -0
  150. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_testing_nexus.py +0 -0
  151. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_threaded_heartbeat_example.py +0 -0
  152. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_tls_trust.py +0 -0
  153. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_transport_diagnostics.py +0 -0
  154. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_update_signal_condition_replay.py +0 -0
  155. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_updates.py +0 -0
  156. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_wait_condition.py +0 -0
  157. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_worker.py +0 -0
  158. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_worker_deregistration.py +0 -0
  159. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_worker_observation.py +0 -0
  160. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_worker_session.py +0 -0
  161. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_worker_telemetry_wire.py +0 -0
  162. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_entry_arguments.py +0 -0
  163. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_entrypoint_validation.py +0 -0
  164. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_handler_validation.py +0 -0
  165. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_identity.py +0 -0
  166. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_result_timeout.py +0 -0
  167. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/tests/test_workflow_streams.py +0 -0
  168. {durable_workflow-2.12.1 → durable_workflow-2.12.2}/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.2
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.2"
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.2"
83
+ registry-version = "2.12.2"
81
84
  supported-server-versions = "2.8.0"
82
85
  worker-protocol-version = "1.20"
83
86
  control-plane-version = "2"
@@ -30,8 +30,8 @@ import uuid
30
30
  import weakref
31
31
  from collections.abc import Callable, Generator, Iterable, Mapping, Sequence
32
32
  from contextvars import ContextVar
33
- from copy import copy
34
- from dataclasses import dataclass, field
33
+ from copy import copy, deepcopy
34
+ from dataclasses import dataclass, field, replace
35
35
  from datetime import datetime, timezone
36
36
  from typing import Any, TypeVar, cast
37
37
 
@@ -2682,6 +2682,66 @@ class WorkflowContext:
2682
2682
 
2683
2683
 
2684
2684
  # ── Replay ───────────────────────────────────────────────────────────
2685
+ _ReplayCommandT = TypeVar("_ReplayCommandT", bound=Command)
2686
+
2687
+
2688
+ def _snapshot_command(command: _ReplayCommandT) -> _ReplayCommandT:
2689
+ """Detach payload containers while preserving predicates and group metadata."""
2690
+ snapshot = copy(command)
2691
+ for name, value in vars(command).items():
2692
+ if isinstance(value, dict | list | tuple | set | bytearray):
2693
+ object.__setattr__(snapshot, name, deepcopy(value))
2694
+ return snapshot
2695
+
2696
+
2697
+ def _snapshot_local_call(call: PreparedLocalActivityCall) -> PreparedLocalActivityCall:
2698
+ return replace(call, command=_snapshot_command(call.command), cleanup=deepcopy(call.cleanup))
2699
+
2700
+
2701
+ class _ReplayLifetime:
2702
+ """Own one suspended replay until its result or handler is consumed."""
2703
+
2704
+ def __init__(self, logger: _ReplayLogger) -> None:
2705
+ self.logger = logger
2706
+ self.generator: Any = None
2707
+ self.awaitable: Any = None
2708
+
2709
+ def close(self) -> None:
2710
+ self.logger._set_replaying(True)
2711
+ awaitable, self.awaitable = self.awaitable, None
2712
+ if inspect.iscoroutine(awaitable):
2713
+ awaitable.close()
2714
+ generator, self.generator = self.generator, None
2715
+ if generator is None:
2716
+ return
2717
+
2718
+ # Keep delegated generators alive while closing from the inside out.
2719
+ # A finally block may yield during GeneratorExit; inject exit again
2720
+ # instead of advancing or submitting that discarded durable command.
2721
+ pending = [generator]
2722
+ finished: list[Any] = []
2723
+ while pending:
2724
+ current = pending[-1]
2725
+ child = getattr(current, "gi_yieldfrom", None)
2726
+ if child is not None and callable(getattr(child, "close", None)) and not any(
2727
+ child is closed for closed in finished
2728
+ ):
2729
+ pending.append(child)
2730
+ continue
2731
+ close = getattr(current, "close", None)
2732
+ if close is not None:
2733
+ # Discard errors are not a logical workflow failure. Actual
2734
+ # success/failure/cancellation still runs through the driver.
2735
+ with contextlib.suppress(BaseException):
2736
+ close()
2737
+ if not inspect.isgenerator(current) or current.gi_frame is None:
2738
+ finished.append(pending.pop())
2739
+
2740
+ def __del__(self) -> None:
2741
+ with contextlib.suppress(BaseException):
2742
+ self.close()
2743
+
2744
+
2685
2745
  @dataclass
2686
2746
  class ReplayOutcome:
2687
2747
  commands: list[Command]
@@ -2693,6 +2753,19 @@ class ReplayOutcome:
2693
2753
  cancellation_scope_opening: CancellationScopeOpening | None = None
2694
2754
  cancellation_scope_delivery: CancellationScopeDeliveryIntent | None = None
2695
2755
 
2756
+ def __post_init__(self) -> None:
2757
+ # This ownership is not a dataclass/wire field.
2758
+ self._lifetime: _ReplayLifetime | None = None
2759
+
2760
+ def close(self) -> None:
2761
+ lifetime, self._lifetime = self._lifetime, None
2762
+ if lifetime is not None:
2763
+ lifetime.close()
2764
+
2765
+ def __del__(self) -> None:
2766
+ with contextlib.suppress(BaseException):
2767
+ self.close()
2768
+
2696
2769
 
2697
2770
  @dataclass(frozen=True)
2698
2771
  class _ServiceCallDelivery:
@@ -2815,6 +2888,25 @@ class _ReplayState:
2815
2888
  instance: Any
2816
2889
 
2817
2890
 
2891
+ def _consume_replay_result(state: _ReplayState, result: Any) -> Any:
2892
+ """Detach a handler value before discarded replay cleanup can mutate it."""
2893
+ if inspect.isawaitable(result):
2894
+ if state.outcome._lifetime is not None:
2895
+ state.outcome._lifetime.awaitable = result
2896
+
2897
+ async def consume() -> Any:
2898
+ try:
2899
+ return deepcopy(await result)
2900
+ finally:
2901
+ state.outcome.close()
2902
+
2903
+ return consume()
2904
+ try:
2905
+ return deepcopy(result)
2906
+ finally:
2907
+ state.outcome.close()
2908
+
2909
+
2818
2910
  @dataclass
2819
2911
  class _PendingReceiver:
2820
2912
  history_index: int
@@ -3160,6 +3252,7 @@ def _prepare_query_state(
3160
3252
  workflow_cls,
3161
3253
  history_events,
3162
3254
  start_input,
3255
+ reconstruction_only=True,
3163
3256
  workflow_id=workflow_id,
3164
3257
  run_id=run_id,
3165
3258
  payload_codec=payload_codec,
@@ -3174,26 +3267,35 @@ def _prepare_query_state(
3174
3267
  raise QueryFailed(f"workflow replay failed before query: {exc}") from exc
3175
3268
  if state.outcome.commands and isinstance(state.outcome.commands[0], FailWorkflow):
3176
3269
  failure = state.outcome.commands[0]
3270
+ state.outcome.close()
3177
3271
  raise QueryFailed(f"workflow replay failed before query: {failure.message}") from None
3178
3272
 
3179
3273
  query_registry: dict[str, str] = getattr(workflow_cls, "__workflow_queries__", {}) or {}
3180
3274
  method_name = query_registry.get(query_name)
3181
3275
  if method_name is None:
3276
+ state.outcome.close()
3182
3277
  raise QueryFailed(f"unknown query {query_name!r}")
3183
3278
 
3184
3279
  handler = getattr(state.instance, method_name, None)
3185
3280
  if handler is None:
3281
+ state.outcome.close()
3186
3282
  raise QueryFailed(f"query handler {query_name!r} is not available")
3187
3283
 
3188
3284
  def invoke() -> Any:
3189
3285
  try:
3190
- return _invoke_workflow_handler(
3286
+ result = _invoke_workflow_handler(
3191
3287
  handler, list(args) if args is not None else [], workflow_cls, method_name, allow_async=True,
3192
3288
  )
3289
+ return _consume_replay_result(state, result)
3193
3290
  except QueryFailed:
3291
+ state.outcome.close()
3194
3292
  raise
3195
3293
  except Exception as exc:
3294
+ state.outcome.close()
3196
3295
  raise QueryFailed(str(exc) or f"query {query_name!r} failed") from exc
3296
+ except BaseException:
3297
+ state.outcome.close()
3298
+ raise
3197
3299
 
3198
3300
  return invoke
3199
3301
 
@@ -3263,6 +3365,7 @@ def apply_update(
3263
3365
  workflow_cls,
3264
3366
  events,
3265
3367
  start_input,
3368
+ reconstruction_only=True,
3266
3369
  workflow_id=workflow_id,
3267
3370
  run_id=run_id,
3268
3371
  payload_codec=payload_codec,
@@ -3337,10 +3440,14 @@ def apply_update(
3337
3440
  )
3338
3441
 
3339
3442
  try:
3443
+ if state.outcome._lifetime is not None:
3444
+ state.outcome._lifetime.logger._set_replaying(False)
3340
3445
  result = _invoke_workflow_handler(handler, args, workflow_cls, method_name)
3341
- return CompleteUpdate(update_id=update_id, result=result)
3446
+ return CompleteUpdate(update_id=update_id, result=deepcopy(result))
3342
3447
  except Exception as exc:
3343
3448
  return _fail_update_from_exception(update_id, "update handler failed", exc)
3449
+ finally:
3450
+ state.outcome.close()
3344
3451
 
3345
3452
 
3346
3453
  @dataclass(frozen=True)
@@ -3414,6 +3521,7 @@ def _prepare_update_validation(
3414
3521
  workflow_cls,
3415
3522
  events,
3416
3523
  start_input,
3524
+ reconstruction_only=True,
3417
3525
  workflow_id=replay_options.workflow_id,
3418
3526
  run_id=replay_options.run_id,
3419
3527
  payload_codec=replay_options.payload_codec,
@@ -3430,10 +3538,12 @@ def _prepare_update_validation(
3430
3538
 
3431
3539
  if state.outcome.commands and isinstance(state.outcome.commands[0], FailWorkflow):
3432
3540
  failure = state.outcome.commands[0]
3541
+ state.outcome.close()
3433
3542
  raise UpdateValidationFailed(f"workflow replay failed before update validation: {failure.message}")
3434
3543
 
3435
3544
  update_registry: dict[str, str] = getattr(workflow_cls, "__workflow_updates__", {}) or {}
3436
3545
  if update_name not in update_registry:
3546
+ state.outcome.close()
3437
3547
  raise UpdateValidationFailed(f"unknown update {update_name!r}")
3438
3548
 
3439
3549
  validator_registry: dict[str, str] = (
@@ -3446,13 +3556,23 @@ def _prepare_update_validation(
3446
3556
  )
3447
3557
  validator_name = validator_registry.get(update_name)
3448
3558
  if validator_name is None:
3559
+ state.outcome.close()
3449
3560
  raise UpdateValidationFailed(f"update validator {update_name!r} is not declared")
3450
3561
 
3451
3562
  validator = getattr(state.instance, validator_name, None)
3452
3563
  if validator is None:
3564
+ state.outcome.close()
3453
3565
  raise UpdateValidationFailed(f"update validator {update_name!r} is not available")
3454
3566
 
3455
- return lambda: _invoke_update_validator(validator, args, workflow_cls, validator_name, update_name)
3567
+ def invoke() -> Any:
3568
+ try:
3569
+ result = _invoke_update_validator(validator, args, workflow_cls, validator_name, update_name)
3570
+ return _consume_replay_result(state, result)
3571
+ except BaseException:
3572
+ state.outcome.close()
3573
+ raise
3574
+
3575
+ return invoke
3456
3576
 
3457
3577
 
3458
3578
  def _invoke_update_validator(
@@ -4351,6 +4471,7 @@ def _replay_state(
4351
4471
  prepare_local_activity_groups: bool = False,
4352
4472
  local_activity_cancellation_policies: tuple[str, ...] = (),
4353
4473
  stop_at_uncommitted_cancellation: bool = False,
4474
+ reconstruction_only: bool = False,
4354
4475
  allow_cancellation_scope_authoring: bool = False,
4355
4476
  allow_cancellation_scope_delivery: bool = False,
4356
4477
  ) -> _ReplayState:
@@ -4582,6 +4703,11 @@ def _replay_state(
4582
4703
  _allow_wall_clock=not events,
4583
4704
  )
4584
4705
  ctx._replay_clock_error = replay_clock_error if workflow_start_time is None else None
4706
+ lifetime = _ReplayLifetime(ctx.logger)
4707
+
4708
+ def _set_replaying(replaying: bool) -> None:
4709
+ ctx.logger._set_replaying(replaying or reconstruction_only)
4710
+
4585
4711
  # Continue-as-new cursor checkpoints describe instance state that predates
4586
4712
  # this run. Seed them before the workflow opens its first stream wait so
4587
4713
  # both the durable wait key and worker acknowledgement use the global
@@ -4614,25 +4740,32 @@ def _replay_state(
4614
4740
  prepared_local_activity_group: PreparedLocalActivityGroup | None = None,
4615
4741
  cancellation_scope_opening: CancellationScopeOpening | None = None,
4616
4742
  ) -> _ReplayState:
4617
- return _ReplayState(
4618
- outcome=ReplayOutcome(
4619
- commands=commands,
4620
- cancellation_delivery=cancellation_intent,
4621
- prepared_local_activity=prepared_local_activity,
4622
- prepared_local_activity_group=prepared_local_activity_group,
4623
- cancellation_scope_opening=cancellation_scope_opening,
4624
- cancellation_scope_delivery=scope_intent,
4625
- message_stream_cursors=[
4626
- {"stream_name": name, "through_position": position}
4627
- for name, position in sorted(ctx._message_stream_cursors.items())
4628
- ],
4629
- message_stream_waits=[
4630
- {"stream_name": name, "after_position": position}
4631
- for name, position in sorted(ctx._message_stream_waits.items())
4632
- ],
4633
- ),
4634
- instance=instance,
4743
+ if prepared_local_activity is not None:
4744
+ prepared_local_activity = _snapshot_local_call(prepared_local_activity)
4745
+ if prepared_local_activity_group is not None:
4746
+ prepared_local_activity_group = replace(
4747
+ prepared_local_activity_group,
4748
+ commands=tuple(_snapshot_command(command) for command in prepared_local_activity_group.commands),
4749
+ calls=tuple(_snapshot_local_call(call) for call in prepared_local_activity_group.calls),
4750
+ )
4751
+ outcome = ReplayOutcome(
4752
+ commands=[_snapshot_command(command) for command in commands],
4753
+ cancellation_delivery=cancellation_intent,
4754
+ prepared_local_activity=prepared_local_activity,
4755
+ prepared_local_activity_group=prepared_local_activity_group,
4756
+ cancellation_scope_opening=cancellation_scope_opening,
4757
+ cancellation_scope_delivery=scope_intent,
4758
+ message_stream_cursors=[
4759
+ {"stream_name": name, "through_position": position}
4760
+ for name, position in sorted(ctx._message_stream_cursors.items())
4761
+ ],
4762
+ message_stream_waits=[
4763
+ {"stream_name": name, "after_position": position}
4764
+ for name, position in sorted(ctx._message_stream_waits.items())
4765
+ ],
4635
4766
  )
4767
+ outcome._lifetime = lifetime
4768
+ return _ReplayState(outcome=outcome, instance=instance)
4636
4769
 
4637
4770
  resolved_results: list[Any] = []
4638
4771
  recorded_steps: list[_RecordedStep] = []
@@ -5409,7 +5542,7 @@ def _replay_state(
5409
5542
  if receiver.kind == "signal":
5410
5543
  return
5411
5544
  raise TypeError(f"update handler {receiver.name!r} is not available")
5412
- ctx.logger._set_replaying(True)
5545
+ _set_replaying(True)
5413
5546
  _invoke_workflow_handler(handler, receiver.args, workflow_cls, method_name)
5414
5547
 
5415
5548
  def _receiver_due(receiver: _PendingReceiver, *, before_consuming_result: bool) -> bool:
@@ -5517,6 +5650,25 @@ def _replay_state(
5517
5650
  result_cursor = 0
5518
5651
  pending_step_cursor = 0
5519
5652
  wait_yield_count = 0
5653
+ has_terminal_history = any(_history_event_type(event) in {
5654
+ "WorkflowCompleted", "WorkflowFailed", "WorkflowCancelled", "WorkflowTerminated",
5655
+ "WorkflowContinuedAsNew", "WorkflowTimedOut",
5656
+ } for event in events)
5657
+
5658
+ def _refresh_replay_logging() -> None:
5659
+ # The resumed Python code is live before it yields its next command.
5660
+ # Read-only reconstruction and discarded-generator disposal stay quiet.
5661
+ _set_replaying(
5662
+ has_terminal_history
5663
+ or result_cursor < len(recorded_steps)
5664
+ or wait_yield_count < len(recorded_wait_steps)
5665
+ or pending_step_cursor < len(recorded_pending_steps)
5666
+ or selection_marker_cursor < len(selection_markers)
5667
+ or bool(pending_receivers)
5668
+ or (cancellation.delivery is not None and not cancellation_consumed)
5669
+ or len(consumed_scope_deliveries) < len(committed_scopes.deliveries)
5670
+ )
5671
+
5520
5672
  entry = instance.run
5521
5673
  workflow_type = str(getattr(workflow_cls, "__workflow_name__", workflow_cls.__name__))
5522
5674
  try:
@@ -5531,6 +5683,7 @@ def _replay_state(
5531
5683
  # Binding happens outside the application-error path. A TypeError raised
5532
5684
  # by a successfully bound body is still an actual workflow failure.
5533
5685
  try:
5686
+ _refresh_replay_logging()
5534
5687
  gen = entry(ctx, *start_input)
5535
5688
  except NonDeterministicReplayError:
5536
5689
  raise
@@ -5548,7 +5701,9 @@ def _replay_state(
5548
5701
  _assert_no_unconsumed_history("complete workflow")
5549
5702
  return _state([CompleteWorkflow(result=gen)])
5550
5703
 
5551
- ctx.logger._set_replaying(True)
5704
+ lifetime.generator = gen
5705
+
5706
+ _set_replaying(True)
5552
5707
 
5553
5708
  next_value: Any = None
5554
5709
  first = True
@@ -5631,7 +5786,7 @@ def _replay_state(
5631
5786
  version = command.version
5632
5787
  if type(version) is not int:
5633
5788
  raise _version_error(f"Invalid version for change ID {change_id!r}.")
5634
- ctx.logger._set_replaying(False)
5789
+ _set_replaying(False)
5635
5790
  pending.append(command)
5636
5791
  authored_sequence += 1
5637
5792
  if not command.min_supported <= version <= command.max_supported:
@@ -6454,6 +6609,7 @@ def _replay_state(
6454
6609
  if isinstance(command, DurableOperationHandle):
6455
6610
  authored_sequence += 1
6456
6611
  try:
6612
+ _refresh_replay_logging()
6457
6613
  advanced_cmd = gen.throw(WorkflowCancelled(
6458
6614
  "workflow cancellation was requested", request_id=boundary.request_id,
6459
6615
  context=ctx._cancellation_context,
@@ -6510,6 +6666,7 @@ def _replay_state(
6510
6666
  ctx._cancellation_context = active_context
6511
6667
  _apply_due_receivers()
6512
6668
  try:
6669
+ _refresh_replay_logging()
6513
6670
  advanced_cmd = gen.throw(WorkflowCancelled(
6514
6671
  "scope cancellation was requested", request_id=active_context.request_id, context=active_context,
6515
6672
  ))
@@ -6545,6 +6702,7 @@ def _replay_state(
6545
6702
  advanced_cmd = None
6546
6703
  else:
6547
6704
  try:
6705
+ _refresh_replay_logging()
6548
6706
  cmd = gen.send(None) if first else gen.send(next_value)
6549
6707
  except StopIteration as stop:
6550
6708
  return _terminal_state(stop.value, include_pending=True)
@@ -6588,7 +6746,7 @@ def _replay_state(
6588
6746
  current_call_sequence, "cancellation scope", next_step.event_types if next_step else [],
6589
6747
  detail="cancellation_scope_opening_changed: authored scope has no original canonical opening",
6590
6748
  )
6591
- ctx.logger._set_replaying(False)
6749
+ _set_replaying(False)
6592
6750
  return _state(pending, cancellation_scope_opening=CancellationScopeOpening(
6593
6751
  current_call_sequence, cmd.parent_scope_id, cmd.shield_parent,
6594
6752
  ))
@@ -6710,7 +6868,7 @@ def _replay_state(
6710
6868
  if marker is None:
6711
6869
  if has_recorded_member_history:
6712
6870
  return _state(pending)
6713
- ctx.logger._set_replaying(False)
6871
+ _set_replaying(False)
6714
6872
  pending.extend(annotated)
6715
6873
  return _state(pending)
6716
6874
 
@@ -6799,7 +6957,7 @@ def _replay_state(
6799
6957
  if prepared_group:
6800
6958
  group = _prepared_group(cmd)
6801
6959
  if group is not None:
6802
- ctx.logger._set_replaying(False)
6960
+ _set_replaying(False)
6803
6961
  return _state(pending, prepared_local_activity_group=group)
6804
6962
  if any(current_call_sequence + offset not in resolved_sequences for offset in range(needed)):
6805
6963
  # Local results are durable. Remote members own their
@@ -6829,13 +6987,14 @@ def _replay_state(
6829
6987
  _advance_cancellation_clock(consumed_indexes)
6830
6988
  if failed is not None:
6831
6989
  try:
6990
+ _refresh_replay_logging()
6832
6991
  advanced_cmd = gen.throw(failed)
6833
6992
  continue
6834
6993
  except StopIteration as stop:
6835
6994
  return _terminal_state(stop.value, include_pending=False)
6836
6995
  next_value = _parallel_result_shape(result_shape, vals)
6837
6996
  continue
6838
- ctx.logger._set_replaying(False)
6997
+ _set_replaying(False)
6839
6998
  for offset, child_command in enumerate(cmd):
6840
6999
  _assert_pending_step_matches(child_command, offset)
6841
7000
  pending.extend(cmd)
@@ -6847,6 +7006,7 @@ def _replay_state(
6847
7006
  cancelled_selection_indexes[(cmd.selection_group_id, cmd.base_sequence)],
6848
7007
  ])
6849
7008
  try:
7009
+ _refresh_replay_logging()
6850
7010
  advanced_cmd = gen.throw(
6851
7011
  DurableOperationCancelled(
6852
7012
  cmd.selection_group_id,
@@ -6874,11 +7034,12 @@ def _replay_state(
6874
7034
  selection_shape,
6875
7035
  )
6876
7036
  if not resolved:
6877
- ctx.logger._set_replaying(False)
7037
+ _set_replaying(False)
6878
7038
  return _state(pending)
6879
7039
  _advance_selection_clock(cmd.base_sequence, cmd.size, failure)
6880
7040
  if failure is not None:
6881
7041
  try:
7042
+ _refresh_replay_logging()
6882
7043
  advanced_cmd = gen.throw(failure)
6883
7044
  continue
6884
7045
  except StopIteration as stop:
@@ -6943,12 +7104,13 @@ def _replay_state(
6943
7104
  )
6944
7105
  except NexusOperationFailed as exc:
6945
7106
  try:
7107
+ _refresh_replay_logging()
6946
7108
  advanced_cmd = gen.throw(exc)
6947
7109
  continue
6948
7110
  except StopIteration as stop:
6949
7111
  return _terminal_state(stop.value, include_pending=False)
6950
7112
  continue
6951
- ctx.logger._set_replaying(False)
7113
+ _set_replaying(False)
6952
7114
  _assert_pending_step_matches(cmd)
6953
7115
  if _next_unconsumed_recorded_step() is not None:
6954
7116
  # Admission already has its durable sequence. A later task
@@ -6963,13 +7125,19 @@ def _replay_state(
6963
7125
  next_value = resolved_results[result_cursor]
6964
7126
  result_cursor += 1
6965
7127
  continue
6966
- ctx.logger._set_replaying(False)
7128
+ _set_replaying(False)
6967
7129
  _assert_pending_step_matches(cmd)
7130
+ if reconstruction_only:
7131
+ # A query/update rebuild cannot author a missing marker or
7132
+ # execute its callback. Only a workflow task may do that.
7133
+ pending.append(cmd)
7134
+ return _state(pending)
6968
7135
  if cmd._effect is not None:
6969
7136
  try:
6970
7137
  cmd.result = cmd._effect()
6971
7138
  except Exception as exc:
6972
7139
  try:
7140
+ _refresh_replay_logging()
6973
7141
  advanced_cmd = gen.throw(exc)
6974
7142
  continue
6975
7143
  except StopIteration as stop:
@@ -6984,7 +7152,7 @@ def _replay_state(
6984
7152
  next_value = resolved_results[result_cursor]
6985
7153
  result_cursor += 1
6986
7154
  continue
6987
- ctx.logger._set_replaying(False)
7155
+ _set_replaying(False)
6988
7156
  _assert_pending_step_matches(cmd)
6989
7157
  pending.append(cmd)
6990
7158
  next_value = None
@@ -7026,7 +7194,7 @@ def _replay_state(
7026
7194
  step = _next_unconsumed_recorded_step()
7027
7195
  if step is not None:
7028
7196
  _assert_step_matches(cmd, step)
7029
- ctx.logger._set_replaying(False)
7197
+ _set_replaying(False)
7030
7198
  pending.append(cmd)
7031
7199
  return _state(pending)
7032
7200
  # Terminal cleanup may only skip a later open after this
@@ -7133,7 +7301,7 @@ def _replay_state(
7133
7301
  wait_yield_count = next_wait_index
7134
7302
  continue
7135
7303
 
7136
- ctx.logger._set_replaying(False)
7304
+ _set_replaying(False)
7137
7305
  pending.append(cmd)
7138
7306
  wait_yield_count = next_wait_index
7139
7307
  return _state(pending)
@@ -7147,13 +7315,14 @@ def _replay_state(
7147
7315
  result_cursor += 1
7148
7316
  if isinstance(val, ActivityFailed | ChildWorkflowFailed):
7149
7317
  try:
7318
+ _refresh_replay_logging()
7150
7319
  advanced_cmd = gen.throw(val)
7151
7320
  continue
7152
7321
  except StopIteration as stop:
7153
7322
  return _terminal_state(stop.value, include_pending=False)
7154
7323
  next_value = val
7155
7324
  continue
7156
- ctx.logger._set_replaying(False)
7325
+ _set_replaying(False)
7157
7326
  _assert_pending_step_matches(cmd)
7158
7327
  if not isinstance(cmd, RecordLocalActivity) and _next_unconsumed_recorded_step() is not None:
7159
7328
  # A signal may wake a workflow while its existing operation is
@@ -7169,6 +7338,7 @@ def _replay_state(
7169
7338
  pending.append(cmd)
7170
7339
  if isinstance(local_result, ActivityFailed):
7171
7340
  try:
7341
+ _refresh_replay_logging()
7172
7342
  advanced_cmd = gen.throw(local_result)
7173
7343
  continue
7174
7344
  except StopIteration as stop:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.12.1
3
+ Version: 2.12.2
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
@@ -88,6 +88,7 @@ tests/test_external_storage.py
88
88
  tests/test_external_task_input.py
89
89
  tests/test_external_task_result.py
90
90
  tests/test_failure_reporting.py
91
+ tests/test_generator_lifecycle.py
91
92
  tests/test_golden_history_replay.py
92
93
  tests/test_history_bundle_schema2.py
93
94
  tests/test_history_bundle_verify.py