durable-workflow 2.14.1__tar.gz → 2.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. {durable_workflow-2.14.1/src/durable_workflow.egg-info → durable_workflow-2.15.0}/PKG-INFO +1 -1
  2. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/pyproject.toml +3 -3
  3. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/__init__.py +8 -0
  4. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_typed_values.py +67 -8
  5. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/activity.py +4 -0
  6. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/client.py +286 -3
  7. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/errors.py +7 -0
  8. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/metrics.py +1 -0
  9. durable_workflow-2.15.0/src/durable_workflow/replay_clock.py +488 -0
  10. durable_workflow-2.15.0/src/durable_workflow/replay_clock_math.py +23 -0
  11. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/worker.py +23 -1
  12. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/workflow.py +162 -34
  13. {durable_workflow-2.14.1 → durable_workflow-2.15.0/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  14. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/SOURCES.txt +19 -0
  15. durable_workflow-2.15.0/tests/test_activity_identity.py +79 -0
  16. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_retry_info.py +2 -1
  17. durable_workflow-2.15.0/tests/test_business_replay_clock.py +445 -0
  18. durable_workflow-2.15.0/tests/test_business_replay_clock_child.py +239 -0
  19. durable_workflow-2.15.0/tests/test_business_replay_clock_parallel.py +456 -0
  20. durable_workflow-2.15.0/tests/test_business_replay_clock_redrive.py +142 -0
  21. durable_workflow-2.15.0/tests/test_business_replay_clock_sequence_claims.py +105 -0
  22. durable_workflow-2.15.0/tests/test_client_iterators.py +207 -0
  23. durable_workflow-2.15.0/tests/test_condition_occurrence_id.py +245 -0
  24. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cooperative_remote_worker.py +2 -1
  25. durable_workflow-2.15.0/tests/test_docs_release_channel.py +206 -0
  26. durable_workflow-2.15.0/tests/test_portal_source_split.py +139 -0
  27. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_worker.py +4 -2
  28. durable_workflow-2.15.0/tests/test_redrive_call_ledger.py +259 -0
  29. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_redrive_successor_evidence.py +266 -5
  30. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_replay_clock.py +89 -21
  31. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_replay_regression_corpus.py +20 -0
  32. durable_workflow-2.15.0/tests/test_run_activities.py +161 -0
  33. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_sdk_metrics_contract.py +145 -1
  34. durable_workflow-2.15.0/tests/test_typed_workflow_values.py +339 -0
  35. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_wait_condition.py +63 -0
  36. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_worker.py +5 -2
  37. durable_workflow-2.15.0/tests/test_worker_observation.py +656 -0
  38. durable_workflow-2.15.0/tests/test_worker_startup_buffered.py +235 -0
  39. durable_workflow-2.15.0/tests/test_worker_startup_raw.py +200 -0
  40. durable_workflow-2.15.0/tests/test_worker_startup_read_budget.py +122 -0
  41. durable_workflow-2.15.0/tests/test_worker_startup_snapshot.py +235 -0
  42. durable_workflow-2.14.1/tests/test_worker_observation.py +0 -311
  43. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/LICENSE +0 -0
  44. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/README.md +0 -0
  45. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/setup.cfg +0 -0
  46. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_activity_lease.py +0 -0
  47. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_activity_process.py +0 -0
  48. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_avro.py +0 -0
  49. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_cancellation_scope.py +0 -0
  50. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  51. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_condition_fingerprint.py +0 -0
  52. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  53. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_failure_reporting.py +0 -0
  54. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_metric_routes.py +0 -0
  55. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_poll_transport.py +0 -0
  56. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_prepared_local_activity.py +0 -0
  57. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_query_task_authority.py +0 -0
  58. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  59. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/auth_composition.py +0 -0
  60. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/cancellation.py +0 -0
  61. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/external_storage.py +0 -0
  62. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/external_task_input.py +0 -0
  63. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/external_task_result.py +0 -0
  64. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/history_bundle_verify.py +0 -0
  65. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/interceptors.py +0 -0
  66. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/invocable.py +0 -0
  67. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/nexus.py +0 -0
  68. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/py.typed +0 -0
  69. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/python_conformance.py +0 -0
  70. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/replay_conformance.py +0 -0
  71. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/replay_verify.py +0 -0
  72. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/retry_policy.py +0 -0
  73. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/serializer.py +0 -0
  74. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/server_compatibility.json +0 -0
  75. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/sync.py +0 -0
  76. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/telemetry.py +0 -0
  77. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/telemetry_carrier.py +0 -0
  78. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/testing.py +0 -0
  79. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/worker_session.py +0 -0
  80. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/worker_status.py +0 -0
  81. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  82. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  83. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  84. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/requires.txt +0 -0
  85. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
  86. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_cancellation_policies.py +0 -0
  87. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_context.py +0 -0
  88. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_heartbeat_authority.py +0 -0
  89. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_lease_authority.py +0 -0
  90. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_lease_renewal.py +0 -0
  91. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_lease_worker.py +0 -0
  92. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_activity_process.py +0 -0
  93. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_auth_composition.py +0 -0
  94. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_avro_value_benchmark.py +0 -0
  95. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_avro_value_protocol.py +0 -0
  96. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_context.py +0 -0
  97. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_remaining_time.py +0 -0
  98. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_scope_admission.py +0 -0
  99. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_scope_authoring.py +0 -0
  100. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_scope_boundary.py +0 -0
  101. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cancellation_scope_opening.py +0 -0
  102. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_child_workflow_policies.py +0 -0
  103. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_ci_checkout.py +0 -0
  104. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_ci_integration_endpoint.py +0 -0
  105. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_client.py +0 -0
  106. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_client_telemetry.py +0 -0
  107. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_client_timeouts.py +0 -0
  108. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_codec_regression_corpus.py +0 -0
  109. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  110. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_groups.py +0 -0
  111. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_history.py +0 -0
  112. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_replay.py +0 -0
  113. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_condition_fingerprint.py +0 -0
  114. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_condition_replay_drift.py +0 -0
  115. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_control_plane_parity_fixtures.py +0 -0
  116. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation.py +0 -0
  117. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation_client.py +0 -0
  118. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation_worker.py +0 -0
  119. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_docstring_cross_references.py +0 -0
  120. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_durable_selection.py +0 -0
  121. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_empty_callback_failure.py +0 -0
  122. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_errors.py +0 -0
  123. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_external_storage.py +0 -0
  124. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_external_task_input.py +0 -0
  125. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_external_task_result.py +0 -0
  126. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_failure_reporting.py +0 -0
  127. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_generator_lifecycle.py +0 -0
  128. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_golden_history_replay.py +0 -0
  129. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_history_bundle_pruned.py +0 -0
  130. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_history_bundle_schema2.py +0 -0
  131. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_history_bundle_verify.py +0 -0
  132. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_history_event_contract.py +0 -0
  133. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_invocable.py +0 -0
  134. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_local_failure_reporting.py +0 -0
  135. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_message_streams.py +0 -0
  136. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_metric_routes.py +0 -0
  137. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_metrics.py +0 -0
  138. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_nexus_durable_wait.py +0 -0
  139. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_nexus_namespace.py +0 -0
  140. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_nexus_wire.py +0 -0
  141. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_order_processing_example.py +0 -0
  142. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_parallel_completion_order_replay.py +0 -0
  143. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_parallel_saga_authoring.py +0 -0
  144. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_patch_history.py +0 -0
  145. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_pending_operation_replay.py +0 -0
  146. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_poll_failure_backoff.py +0 -0
  147. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_poll_transport.py +0 -0
  148. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_client.py +0 -0
  149. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_groups.py +0 -0
  150. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_policies.py +0 -0
  151. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_public_boundary_scanner.py +0 -0
  152. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_publish_workflow_isolation.py +0 -0
  153. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_pypi_project_surface.py +0 -0
  154. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_python_conformance.py +0 -0
  155. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_queries.py +0 -0
  156. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_query_task_authority.py +0 -0
  157. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_query_timeout_retry.py +0 -0
  158. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_readme_quickstart.py +0 -0
  159. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_redrive_replay.py +0 -0
  160. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_registration_ambiguity.py +0 -0
  161. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_release_metadata.py +0 -0
  162. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_replay.py +0 -0
  163. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_replay_conformance.py +0 -0
  164. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_replay_verify.py +0 -0
  165. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_retry_policy.py +0 -0
  166. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_run_cancellation_scope_replay.py +0 -0
  167. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_runtime_external_payload_transport.py +0 -0
  168. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_schedules.py +0 -0
  169. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_scoped_run_cancellation_context.py +0 -0
  170. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_search_attribute_replay_identity.py +0 -0
  171. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_serializer.py +0 -0
  172. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_signals.py +0 -0
  173. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_sleep.py +0 -0
  174. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_standalone_activity_client.py +0 -0
  175. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_sticky_execution.py +0 -0
  176. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_storage_admission.py +0 -0
  177. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_sync.py +0 -0
  178. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_sync_activities.py +0 -0
  179. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_telemetry.py +0 -0
  180. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_telemetry_carrier.py +0 -0
  181. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_testing_failures.py +0 -0
  182. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_testing_harness.py +0 -0
  183. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_testing_nexus.py +0 -0
  184. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_threaded_heartbeat_example.py +0 -0
  185. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_tls_trust.py +0 -0
  186. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_transport_diagnostics.py +0 -0
  187. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_typed_values.py +0 -0
  188. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_update_signal_condition_replay.py +0 -0
  189. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_updates.py +0 -0
  190. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_worker_deregistration.py +0 -0
  191. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_worker_session.py +0 -0
  192. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_worker_status.py +0 -0
  193. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_worker_telemetry_wire.py +0 -0
  194. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_entry_arguments.py +0 -0
  195. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_entrypoint_validation.py +0 -0
  196. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_handler_validation.py +0 -0
  197. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_identity.py +0 -0
  198. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_result_timeout.py +0 -0
  199. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/tests/test_workflow_streams.py +0 -0
  200. {durable_workflow-2.14.1 → durable_workflow-2.15.0}/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.14.1
3
+ Version: 2.15.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "2.14.1"
7
+ version = "2.15.0"
8
8
  description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -79,8 +79,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
79
79
  durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
80
80
 
81
81
  [tool.durable-workflow]
82
- product-train = "2.14.1"
83
- registry-version = "2.14.1"
82
+ product-train = "2.15.0"
83
+ registry-version = "2.15.0"
84
84
  supported-server-versions = "2.8.0"
85
85
  supported-server-release-baselines = { "2" = "2.8.0", "3" = "3.0.0" }
86
86
  worker-protocol-version = "1.20"
@@ -52,6 +52,9 @@ from .client import (
52
52
  TaskQueueList,
53
53
  TaskQueueQueryAdmission,
54
54
  TaskQueueTaskAdmission,
55
+ WorkflowActivity,
56
+ WorkflowActivityAttempt,
57
+ WorkflowActivityList,
55
58
  WorkflowCommandResult,
56
59
  WorkflowExecution,
57
60
  WorkflowHandle,
@@ -90,6 +93,7 @@ from .errors import (
90
93
  NonRetryableError,
91
94
  QueryFailed,
92
95
  QueryTaskAuthorityLost,
96
+ ReplayClockUnavailable,
93
97
  RuntimeCapabilityUnsupported,
94
98
  RuntimeDiscoveryUnavailable,
95
99
  SagaCompensationFailed,
@@ -331,6 +335,9 @@ __all__ = [
331
335
  "WorkflowEntryArgumentsMismatchError",
332
336
  "WorkflowRun",
333
337
  "WorkflowRunList",
338
+ "WorkflowActivity",
339
+ "WorkflowActivityAttempt",
340
+ "WorkflowActivityList",
334
341
  "WorkflowStreamAppendItem",
335
342
  "WorkflowStreamAppendResult",
336
343
  "WorkflowStreamDescription",
@@ -383,6 +390,7 @@ __all__ = [
383
390
  "NexusOperationResult",
384
391
  "NexusServiceCall",
385
392
  "NonDeterministicReplayError",
393
+ "ReplayClockUnavailable",
386
394
  "DuplicateRegistrationError",
387
395
  "InvalidWorkflowDefinition",
388
396
  "NoopMetrics",
@@ -255,22 +255,22 @@ def to_payload_values(values: list[Any]) -> list[Any]:
255
255
  return [to_payload_value(value) for value in values]
256
256
 
257
257
 
258
- def typed_callable(fn: Callable[..., Any]) -> Callable[..., Any]:
259
- """Wrap *fn* so positional arguments decode to its annotations.
258
+ def _positional_decoder(
259
+ fn: Callable[..., Any], skip: int, where: str,
260
+ ) -> Callable[[tuple[Any, ...]], list[Any]] | None:
261
+ """Build a decoder for *fn*'s positional arguments after the first *skip*.
260
262
 
261
- Annotations are validated here, so an unsupported type fails when the
262
- activity is declared. The wrapper keeps *fn*'s module and qualified name:
263
- the decorator binds that name to the wrapper, so supervised callbacks that
264
- pickle by reference resolve to it.
263
+ Returns None when no argument needs decoding (none, or only ``Any`` /
264
+ unannotated ones), so the caller can leave *fn* unwrapped.
265
265
  """
266
266
  signature = inspect.signature(fn)
267
267
  try:
268
268
  hints = get_type_hints(fn)
269
269
  except (NameError, TypeError) as error:
270
- raise TypeError(f"typed activity {fn.__qualname__} has unresolvable annotations: {error}") from None
270
+ raise TypeError(f"{where} {fn.__qualname__} has unresolvable annotations: {error}") from None
271
271
  positional: list[tuple[str, Any]] = []
272
272
  variadic: tuple[str, Any] | None = None
273
- for parameter in signature.parameters.values():
273
+ for parameter in list(signature.parameters.values())[skip:]:
274
274
  annotation = hints.get(parameter.name, Any)
275
275
  if parameter.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
276
276
  validate_annotation(annotation, parameter.name)
@@ -279,6 +279,10 @@ def typed_callable(fn: Callable[..., Any]) -> Callable[..., Any]:
279
279
  validate_annotation(annotation, parameter.name)
280
280
  variadic = (parameter.name, annotation)
281
281
 
282
+ annotations = [annotation for _, annotation in positional] + ([variadic[1]] if variadic is not None else [])
283
+ if all(annotation in _PASS_THROUGH for annotation in annotations):
284
+ return None
285
+
282
286
  def convert(args: tuple[Any, ...]) -> list[Any]:
283
287
  converted: list[Any] = []
284
288
  for index, value in enumerate(args):
@@ -292,6 +296,22 @@ def typed_callable(fn: Callable[..., Any]) -> Callable[..., Any]:
292
296
  converted.append(value)
293
297
  return converted
294
298
 
299
+ return convert
300
+
301
+
302
+ def typed_callable(fn: Callable[..., Any]) -> Callable[..., Any]:
303
+ """Wrap an activity so positional arguments decode to its annotations.
304
+
305
+ Annotations are validated here, so an unsupported type fails when the
306
+ activity is declared. The wrapper keeps *fn*'s module and qualified name:
307
+ the decorator binds that name to the wrapper, so supervised callbacks that
308
+ pickle by reference resolve to it.
309
+ """
310
+ decoder = _positional_decoder(fn, 0, "typed activity")
311
+
312
+ def convert(args: tuple[Any, ...]) -> list[Any]:
313
+ return list(args) if decoder is None else decoder(args)
314
+
295
315
  wrapper: Any
296
316
  if inspect.iscoroutinefunction(fn):
297
317
 
@@ -309,3 +329,42 @@ def typed_callable(fn: Callable[..., Any]) -> Callable[..., Any]:
309
329
  wrapper = typed_sync
310
330
  wrapper.__typed_activity__ = fn
311
331
  return cast(Callable[..., Any], wrapper)
332
+
333
+
334
+ def typed_method(fn: Callable[..., Any], skip: int) -> Callable[..., Any]:
335
+ """Wrap a workflow method so arguments after the first *skip* decode.
336
+
337
+ ``skip`` is 1 for handlers (``self``) and 2 for ``run`` (``self, ctx``). A
338
+ generator ``run`` stays a generator. Results are returned unchanged; the
339
+ commands that carry them send dataclass and Enum values as neutral values.
340
+ A method with nothing to decode is returned unwrapped.
341
+ """
342
+ convert = _positional_decoder(fn, skip, "typed workflow method")
343
+ if convert is None:
344
+ # Nothing to decode: keep the original function, so a handler used as a
345
+ # wait_condition predicate keeps its recorded fingerprint.
346
+ return fn
347
+ wrapper: Any
348
+ if inspect.isgeneratorfunction(fn):
349
+
350
+ @functools.wraps(fn)
351
+ def typed_generator(*args: Any, **kwargs: Any) -> Any:
352
+ return (yield from fn(*args[:skip], *convert(args[skip:]), **kwargs))
353
+
354
+ wrapper = typed_generator
355
+ elif inspect.iscoroutinefunction(fn):
356
+
357
+ @functools.wraps(fn)
358
+ async def typed_async(*args: Any, **kwargs: Any) -> Any:
359
+ return await fn(*args[:skip], *convert(args[skip:]), **kwargs)
360
+
361
+ wrapper = typed_async
362
+ else:
363
+
364
+ @functools.wraps(fn)
365
+ def typed_sync(*args: Any, **kwargs: Any) -> Any:
366
+ return fn(*args[:skip], *convert(args[skip:]), **kwargs)
367
+
368
+ wrapper = typed_sync
369
+ wrapper.__typed_workflow_method__ = fn
370
+ return cast(Callable[..., Any], wrapper)
@@ -82,6 +82,7 @@ class ActivityInfo:
82
82
  retry_info: ActivityRetryInfo = field(init=False)
83
83
  workflow_id: str | None = field(init=False)
84
84
  run_id: str | None = field(init=False)
85
+ activity_execution_id: str | None = field(init=False)
85
86
 
86
87
  def __post_init__(self, task_metadata: Mapping[str, Any] | None) -> None:
87
88
  task = task_metadata if task_metadata is not None else {}
@@ -89,6 +90,9 @@ class ActivityInfo:
89
90
  for name in ("workflow_id", "run_id"):
90
91
  value = task.get(name)
91
92
  object.__setattr__(self, name, value if isinstance(value, str) and value else None)
93
+ execution_id = task.get("activity_execution_id")
94
+ object.__setattr__(self, "activity_execution_id",
95
+ execution_id if isinstance(execution_id, str) and execution_id.strip() else None)
92
96
 
93
97
 
94
98
  class ActivityContext:
@@ -62,6 +62,7 @@ from ._poll_transport import POLL_PATHS as _POLL_PATHS
62
62
  from ._poll_transport import unwrap_response as _unwrap_poll_response
63
63
  from ._poll_transport import warn_early_cutoff as _warn_poll_cutoff
64
64
  from ._query_task_authority import QueryTaskAuthority, current_query_authority
65
+ from ._typed_values import decode_payload_value, to_payload_values, validate_annotation
65
66
  from .errors import (
66
67
  REQUEST_ID_CONFLICT,
67
68
  ExternalPayloadIntegrityMismatch,
@@ -377,6 +378,33 @@ class _RuntimeExternalPayloadTransport:
377
378
  prepared_completion_context: bool = False
378
379
 
379
380
 
381
+ class _PageTokens:
382
+ """Detect cyclic opaque continuations with constant-space checkpoints."""
383
+
384
+ def __init__(self, token: str | None) -> None:
385
+ self.current = token
386
+ self._checkpoint = token
387
+ self._distance = 0
388
+ self._power = 1
389
+
390
+ def advance(self, token: Any) -> bool:
391
+ if token is None or token == "":
392
+ self.current = None
393
+ return False
394
+ if not isinstance(token, str):
395
+ raise RuntimeError("pagination returned an invalid page token")
396
+ if token == self.current or token == self._checkpoint:
397
+ raise RuntimeError("pagination repeated a page token")
398
+ self.current = token
399
+ self._distance += 1
400
+ # Brent's cycle detector retains one checkpoint, not every old token.
401
+ if self._distance == self._power:
402
+ self._checkpoint = token
403
+ self._power *= 2
404
+ self._distance = 0
405
+ return True
406
+
407
+
380
408
  @dataclass
381
409
  class WorkflowExecution:
382
410
  """Current server view of one workflow execution."""
@@ -698,6 +726,133 @@ class WorkflowRunList:
698
726
  runs: list[WorkflowRun]
699
727
 
700
728
 
729
+ @dataclass
730
+ class WorkflowActivityAttempt:
731
+ """Received attempt visibility. Missing metadata stays unavailable."""
732
+
733
+ id: str | None = None
734
+ worker_attempt_id: str | None = None
735
+ attempt_number: int | None = None
736
+ status: str | None = None
737
+ task_id: str | None = None
738
+ lease_owner: str | None = None
739
+ lease_expires_at: str | None = None
740
+ started_at: str | None = None
741
+ last_heartbeat_at: str | None = None
742
+ last_heartbeat_progress: dict[str, Any] | list[Any] | None = None
743
+ closed_at: str | None = None
744
+ can_continue: bool | None = None
745
+ cancel_requested: bool | None = None
746
+ stop_reason: str | None = None
747
+ failure_reason: str | None = None
748
+ raw: dict[str, Any] | None = None
749
+
750
+ @classmethod
751
+ def from_dict(cls, data: dict[str, Any]) -> WorkflowActivityAttempt:
752
+ values = {name: _run_activity_field(data, name, str) for name in (
753
+ "id", "worker_attempt_id", "status", "task_id", "lease_owner", "lease_expires_at", "started_at",
754
+ "last_heartbeat_at", "closed_at", "stop_reason", "failure_reason",
755
+ )}
756
+ return cls(**values, attempt_number=_run_activity_field(data, "attempt_number", int),
757
+ can_continue=_run_activity_field(data, "can_continue", bool),
758
+ cancel_requested=_run_activity_field(data, "cancel_requested", bool),
759
+ last_heartbeat_progress=_run_activity_progress(data), raw=copy.deepcopy(data))
760
+
761
+
762
+ @dataclass
763
+ class WorkflowActivity:
764
+ """Activity metadata from one run, retaining Server diagnostic authority."""
765
+
766
+ id: str | None = None
767
+ idempotency_key: str | None = None
768
+ sequence: int | None = None
769
+ activity_type: str | None = None
770
+ activity_class: str | None = None
771
+ execution_mode: str | None = None
772
+ local_activity: bool | None = None
773
+ history_authority: str | None = None
774
+ diagnostic_only: bool | None = None
775
+ history_event_types: list[str] | None = None
776
+ history_unsupported_reason: str | None = None
777
+ reused_from_run_id: str | None = None
778
+ reused_activity_execution_id: str | None = None
779
+ reused_recorded_at: str | None = None
780
+ row_status: str | None = None
781
+ parallel_group_kind: str | None = None
782
+ parallel_group_id: str | None = None
783
+ parallel_group_base_sequence: int | None = None
784
+ parallel_group_size: int | None = None
785
+ parallel_group_index: int | None = None
786
+ parallel_group_path: list[dict[str, Any]] | None = None
787
+ attempt_id: str | None = None
788
+ worker_attempt_id: str | None = None
789
+ status: str | None = None
790
+ attempt_count: int | None = None
791
+ retry_policy: Any = None
792
+ connection: str | None = None
793
+ queue: str | None = None
794
+ last_heartbeat_at: str | None = None
795
+ last_heartbeat_progress: dict[str, Any] | list[Any] | None = None
796
+ created_at: str | None = None
797
+ started_at: str | None = None
798
+ closed_at: str | None = None
799
+ attempts: list[WorkflowActivityAttempt] | None = None
800
+ raw: dict[str, Any] | None = None
801
+
802
+ @classmethod
803
+ def from_dict(cls, data: dict[str, Any]) -> WorkflowActivity:
804
+ values = {name: _run_activity_field(data, name, str) for name in (
805
+ "id", "idempotency_key", "execution_mode", "history_authority", "history_unsupported_reason",
806
+ "reused_from_run_id", "reused_activity_execution_id", "reused_recorded_at", "row_status",
807
+ "parallel_group_kind", "parallel_group_id", "attempt_id", "worker_attempt_id", "status",
808
+ "connection", "queue", "last_heartbeat_at", "created_at", "started_at", "closed_at",
809
+ )}
810
+ for name in ("sequence", "parallel_group_base_sequence", "parallel_group_size", "parallel_group_index",
811
+ "attempt_count"):
812
+ values[name] = _run_activity_field(data, name, int)
813
+ for name in ("local_activity", "diagnostic_only"):
814
+ values[name] = _run_activity_field(data, name, bool)
815
+ attempts = _run_activity_items(data, "attempts", dict)
816
+ return cls(**values, activity_type=_run_activity_field(data, "type", str),
817
+ activity_class=_run_activity_field(data, "class", str),
818
+ history_event_types=_run_activity_items(data, "history_event_types", str),
819
+ parallel_group_path=_run_activity_items(data, "parallel_group_path", dict),
820
+ attempts=[WorkflowActivityAttempt.from_dict(item) for item in attempts]
821
+ if attempts is not None else None,
822
+ last_heartbeat_progress=_run_activity_progress(data),
823
+ retry_policy=copy.deepcopy(data.get("retry_policy")), raw=copy.deepcopy(data))
824
+
825
+
826
+ @dataclass
827
+ class WorkflowActivityList:
828
+ """One explicit run's activity visibility, distinct from standalone jobs."""
829
+
830
+ workflow_id: str
831
+ run_id: str
832
+ activities: list[WorkflowActivity]
833
+
834
+
835
+ def _run_activity_field(data: dict[str, Any], name: str, kind: type[Any]) -> Any:
836
+ value = data.get(name)
837
+ if value is not None and type(value) is not kind:
838
+ raise RuntimeError(f"invalid run activity field: {name}")
839
+ return value
840
+
841
+
842
+ def _run_activity_items(data: dict[str, Any], name: str, kind: type[Any]) -> Any:
843
+ items = _run_activity_field(data, name, list)
844
+ if items is not None and any(type(item) is not kind for item in items):
845
+ raise RuntimeError(f"invalid run activity items: {name}")
846
+ return items
847
+
848
+
849
+ def _run_activity_progress(data: dict[str, Any]) -> Any:
850
+ value = data.get("last_heartbeat_progress")
851
+ if value is not None and not isinstance(value, (dict, list)):
852
+ raise RuntimeError("invalid run activity field: last_heartbeat_progress")
853
+ return copy.deepcopy(value)
854
+
855
+
701
856
  @dataclass
702
857
  class StandaloneActivityExecution:
703
858
  """Server view of one standalone activity execution.
@@ -1328,6 +1483,12 @@ class BridgeAdapterOutcome:
1328
1483
  )
1329
1484
 
1330
1485
 
1486
+ # Client payloads that carry caller arguments.
1487
+ _ARGUMENT_PAYLOAD_KINDS = frozenset({
1488
+ "workflow_input", "signal", "update", "query", "activity_input", "schedule_input",
1489
+ })
1490
+
1491
+
1331
1492
  class WorkflowHandle:
1332
1493
  """Convenience wrapper for operating on one workflow ID."""
1333
1494
 
@@ -1337,9 +1498,11 @@ class WorkflowHandle:
1337
1498
  self.run_id = run_id
1338
1499
  self.workflow_type = workflow_type
1339
1500
 
1340
- async def result(self, *, poll_interval: float = 0.5, timeout: float = 30.0) -> Any:
1501
+ async def result(self, *, poll_interval: float = 0.5, timeout: float = 30.0, result_type: Any = None) -> Any:
1341
1502
  """Block until this workflow terminates and return its result. See :meth:`Client.get_result`."""
1342
- return await self._client.get_result(self, poll_interval=poll_interval, timeout=timeout)
1503
+ return await self._client.get_result(
1504
+ self, poll_interval=poll_interval, timeout=timeout, result_type=result_type,
1505
+ )
1343
1506
 
1344
1507
  async def describe(self) -> WorkflowExecution:
1345
1508
  """Return the server's current view of this workflow. See :meth:`Client.describe_workflow`."""
@@ -1367,6 +1530,17 @@ class WorkflowHandle:
1367
1530
  raise ValueError("run_id is required to export workflow history from a handle")
1368
1531
  return await self._client.export_history(self.workflow_id, self.run_id)
1369
1532
 
1533
+ async def iter_history(
1534
+ self, *, page_size: int | None = None, next_page_token: str | None = None,
1535
+ ) -> AsyncIterator[dict[str, Any]]:
1536
+ """Yield this explicit run's history. See :meth:`Client.iter_history`."""
1537
+ if self.run_id is None:
1538
+ raise ValueError("run_id is required to iterate workflow history from a handle")
1539
+ async for event in self._client.iter_history(
1540
+ self.workflow_id, self.run_id, page_size=page_size, next_page_token=next_page_token,
1541
+ ):
1542
+ yield event
1543
+
1370
1544
  async def list_runs(self) -> WorkflowRunList:
1371
1545
  """List all runs in this workflow execution chain. See :meth:`Client.list_workflow_runs`."""
1372
1546
  return await self._client.list_workflow_runs(self.workflow_id)
@@ -1378,6 +1552,13 @@ class WorkflowHandle:
1378
1552
  raise ValueError("run_id is required to describe a workflow run from a handle")
1379
1553
  return await self._client.describe_workflow_run(self.workflow_id, selected_run_id)
1380
1554
 
1555
+ async def get_activities(self, run_id: str | None = None) -> WorkflowActivityList:
1556
+ """Read activities for an explicit run. See :meth:`Client.get_run_activities`."""
1557
+ selected_run_id = self.run_id if run_id is None else run_id
1558
+ if selected_run_id is None:
1559
+ raise ValueError("run_id is required to fetch workflow activities from a handle")
1560
+ return await self._client.get_run_activities(self.workflow_id, selected_run_id)
1561
+
1381
1562
  async def signal(
1382
1563
  self,
1383
1564
  signal_name: str,
@@ -1807,6 +1988,11 @@ class Client:
1807
1988
  external_storage: ExternalStorageDriver | None = None,
1808
1989
  external_storage_threshold_bytes: int | None = None,
1809
1990
  ) -> dict[str, Any]:
1991
+ if kind in _ARGUMENT_PAYLOAD_KINDS and isinstance(value, list):
1992
+ # Dataclass and Enum arguments travel as their neutral values, the
1993
+ # bytes dataclasses.asdict() and .value give. The codec refuses
1994
+ # those values, so no existing caller depends on this conversion.
1995
+ value = to_payload_values(value)
1810
1996
  warning_context = self._payload_context(
1811
1997
  kind=kind,
1812
1998
  workflow_id=workflow_id,
@@ -3857,6 +4043,39 @@ class Client:
3857
4043
  next_page_token=data.get("next_page_token"),
3858
4044
  )
3859
4045
 
4046
+ async def iter_workflows(
4047
+ self,
4048
+ *,
4049
+ workflow_type: str | None = None,
4050
+ status: str | None = None,
4051
+ query: str | None = None,
4052
+ page_size: int | None = None,
4053
+ next_page_token: str | None = None,
4054
+ ) -> AsyncIterator[WorkflowExecution]:
4055
+ """Yield workflow list results lazily, retaining no previous pages.
4056
+
4057
+ Filters and namespace stay fixed across pages. An empty page with a
4058
+ continuation is followed; missing/empty continuation ends iteration.
4059
+ Cancellation, malformed/cyclic continuations and HTTP errors propagate,
4060
+ possibly after earlier results have been yielded. This is a traversal
4061
+ of Server pages, not a snapshot of concurrently changing workflows.
4062
+ """
4063
+ tokens = _PageTokens(next_page_token)
4064
+ namespace = self.namespace
4065
+ while True:
4066
+ if self.namespace != namespace:
4067
+ raise RuntimeError("client namespace changed during pagination")
4068
+ page = await self.list_workflows(
4069
+ workflow_type=workflow_type, status=status, query=query, page_size=page_size,
4070
+ next_page_token=tokens.current,
4071
+ )
4072
+ more = tokens.advance(page.next_page_token)
4073
+ for execution in page.executions:
4074
+ yield execution
4075
+ del page
4076
+ if not more:
4077
+ return
4078
+
3860
4079
  async def get_history(
3861
4080
  self,
3862
4081
  workflow_id: str,
@@ -3879,6 +4098,39 @@ class Client:
3879
4098
  "GET", path, context=workflow_id
3880
4099
  )
3881
4100
 
4101
+ async def iter_history(
4102
+ self,
4103
+ workflow_id: str,
4104
+ run_id: str,
4105
+ *,
4106
+ page_size: int | None = None,
4107
+ next_page_token: str | None = None,
4108
+ ) -> AsyncIterator[dict[str, Any]]:
4109
+ """Yield run-history events in Server order, fetching one page at a time.
4110
+
4111
+ Workflow/run/namespace identity and page size stay fixed. Stopping the
4112
+ iterator fetches no subsequent page. Empty continued pages are followed;
4113
+ malformed/cyclic continuations and HTTP errors propagate rather than
4114
+ appearing as a complete history. Already yielded events are not undone.
4115
+ """
4116
+ tokens = _PageTokens(next_page_token)
4117
+ namespace = self.namespace
4118
+ while True:
4119
+ if self.namespace != namespace:
4120
+ raise RuntimeError("client namespace changed during pagination")
4121
+ page = await self.get_history(
4122
+ workflow_id, run_id, page_size=page_size, next_page_token=tokens.current,
4123
+ )
4124
+ if (not isinstance(page, dict) or not isinstance(page.get("events"), list)
4125
+ or any(not isinstance(event, dict) for event in page["events"])):
4126
+ raise RuntimeError("history pagination returned invalid events")
4127
+ more = tokens.advance(page.get("next_page_token"))
4128
+ for event in page["events"]:
4129
+ yield event
4130
+ del page
4131
+ if not more:
4132
+ return
4133
+
3882
4134
  async def export_history(self, workflow_id: str, run_id: str) -> Any:
3883
4135
  """Export one workflow run history as a replay bundle."""
3884
4136
  return await self._request(
@@ -3898,6 +4150,25 @@ class Client:
3898
4150
  runs=runs,
3899
4151
  )
3900
4152
 
4153
+ async def get_run_activities(self, workflow_id: str, run_id: str) -> WorkflowActivityList:
4154
+ """Read typed activity/attempt metadata for this namespace and exact run.
4155
+
4156
+ Missing optional metadata stays None. Diagnostic/unsupported states and
4157
+ unknown future status strings are preserved; this visibility snapshot
4158
+ does not grant attempt authority. Payloads are not fetched or decoded.
4159
+ """
4160
+ data = await self._request(
4161
+ "GET", f"/workflows/{quote(workflow_id, safe='._:-')}/runs/{quote(run_id, safe='._:-')}/activities",
4162
+ context=workflow_id,
4163
+ )
4164
+ if (not isinstance(data, dict) or data.get("workflow_id") != workflow_id
4165
+ or data.get("run_id") != run_id):
4166
+ raise RuntimeError("run activity response changed workflow/run identity")
4167
+ items = _run_activity_items(data, "activities", dict)
4168
+ if items is None:
4169
+ raise RuntimeError("run activity response omitted activities")
4170
+ return WorkflowActivityList(workflow_id, run_id, [WorkflowActivity.from_dict(item) for item in items])
4171
+
3901
4172
  async def describe_workflow_run(self, workflow_id: str, run_id: str) -> WorkflowRun:
3902
4173
  """Return detailed status, payload, and actionability for one specific workflow run."""
3903
4174
  data = await self._request("GET", f"/workflows/{workflow_id}/runs/{run_id}", context=workflow_id)
@@ -4760,9 +5031,16 @@ class Client:
4760
5031
  *,
4761
5032
  poll_interval: float = 0.5,
4762
5033
  timeout: float = 30.0,
5034
+ result_type: Any = None,
4763
5035
  ) -> Any:
4764
5036
  """Poll a workflow until it reaches a terminal state and return its result.
4765
5037
 
5038
+ ``result_type`` opts in to decoding the result to that annotation
5039
+ (the types :func:`durable_workflow.activity.defn` lists for
5040
+ ``typed=True``); a mismatch raises
5041
+ :class:`~durable_workflow.errors.TypedValueError`, which names the
5042
+ field path but never the value. Without it the plain value is returned.
5043
+
4766
5044
  Raises :class:`~durable_workflow.errors.WorkflowFailed`,
4767
5045
  :class:`~durable_workflow.errors.WorkflowCancelled`, or
4768
5046
  :class:`~durable_workflow.errors.WorkflowTerminated` if the workflow
@@ -4770,6 +5048,8 @@ class Client:
4770
5048
  raises :class:`~durable_workflow.errors.WorkflowTimedOut`; a caller's
4771
5049
  :class:`TimeoutError` means ``timeout`` seconds elapsed while waiting.
4772
5050
  """
5051
+ if result_type is not None:
5052
+ validate_annotation(result_type, "result_type")
4773
5053
  deadline = asyncio.get_running_loop().time() + timeout
4774
5054
  while True:
4775
5055
  desc = await self.describe_workflow(handle.workflow_id)
@@ -4789,12 +5069,15 @@ class Client:
4789
5069
  etype = ev.get("event_type")
4790
5070
  payload = ev.get("payload") or {}
4791
5071
  if etype == "WorkflowCompleted":
4792
- return serializer.decode_envelope(
5072
+ output = serializer.decode_envelope(
4793
5073
  payload.get("output"),
4794
5074
  codec=payload.get("payload_codec") or desc.payload_codec,
4795
5075
  external_storage=self.external_storage,
4796
5076
  external_storage_cache=self.external_storage_cache,
4797
5077
  )
5078
+ if result_type is None:
5079
+ return output
5080
+ return decode_payload_value(output, result_type, "workflow result")
4798
5081
  if etype == "WorkflowFailed":
4799
5082
  raise WorkflowFailed(
4800
5083
  payload.get("message", "workflow failed"),
@@ -27,6 +27,13 @@ class DurableWorkflowError(Exception):
27
27
  """Base class for every exception raised by the SDK."""
28
28
 
29
29
 
30
+ class ReplayClockUnavailable(DurableWorkflowError):
31
+ """The current execution has no proved business replay-time boundary."""
32
+
33
+ def __init__(self) -> None:
34
+ super().__init__("business replay time is unavailable at this execution boundary")
35
+
36
+
30
37
  class FailureReportTooLarge(DurableWorkflowError, ValueError):
31
38
  """Required failure identity or semantics cannot fit the reporting budget.
32
39
 
@@ -26,6 +26,7 @@ WORKER_POLLS = "durable_workflow_worker_polls"
26
26
  WORKER_POLL_DURATION_SECONDS = "durable_workflow_worker_poll_duration_seconds"
27
27
  WORKER_TASKS = "durable_workflow_worker_tasks"
28
28
  WORKER_TASK_DURATION_SECONDS = "durable_workflow_worker_task_duration_seconds"
29
+ WORKER_TASK_SCHEDULE_TO_START_SECONDS = "durable_workflow_worker_task_schedule_to_start_seconds"
29
30
 
30
31
 
31
32
  class MetricsRecorder(Protocol):