durable-workflow 2.14.0__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.0/src/durable_workflow.egg-info → durable_workflow-2.15.0}/PKG-INFO +1 -1
  2. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/pyproject.toml +3 -3
  3. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/__init__.py +8 -0
  4. durable_workflow-2.15.0/src/durable_workflow/_condition_fingerprint.py +229 -0
  5. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_typed_values.py +67 -8
  6. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/activity.py +4 -0
  7. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/client.py +286 -3
  8. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/errors.py +7 -0
  9. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/metrics.py +1 -0
  10. durable_workflow-2.15.0/src/durable_workflow/replay_clock.py +488 -0
  11. durable_workflow-2.15.0/src/durable_workflow/replay_clock_math.py +23 -0
  12. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/worker.py +23 -1
  13. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/workflow.py +242 -74
  14. {durable_workflow-2.14.0 → durable_workflow-2.15.0/src/durable_workflow.egg-info}/PKG-INFO +1 -1
  15. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/SOURCES.txt +21 -0
  16. durable_workflow-2.15.0/tests/test_activity_identity.py +79 -0
  17. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_retry_info.py +2 -1
  18. durable_workflow-2.15.0/tests/test_business_replay_clock.py +445 -0
  19. durable_workflow-2.15.0/tests/test_business_replay_clock_child.py +239 -0
  20. durable_workflow-2.15.0/tests/test_business_replay_clock_parallel.py +456 -0
  21. durable_workflow-2.15.0/tests/test_business_replay_clock_redrive.py +142 -0
  22. durable_workflow-2.15.0/tests/test_business_replay_clock_sequence_claims.py +105 -0
  23. durable_workflow-2.15.0/tests/test_client_iterators.py +207 -0
  24. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_client_timeouts.py +5 -0
  25. durable_workflow-2.15.0/tests/test_condition_fingerprint.py +282 -0
  26. durable_workflow-2.15.0/tests/test_condition_occurrence_id.py +245 -0
  27. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cooperative_remote_worker.py +2 -1
  28. durable_workflow-2.15.0/tests/test_docs_release_channel.py +206 -0
  29. durable_workflow-2.15.0/tests/test_portal_source_split.py +139 -0
  30. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_worker.py +4 -2
  31. durable_workflow-2.15.0/tests/test_redrive_call_ledger.py +259 -0
  32. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_redrive_successor_evidence.py +266 -5
  33. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_replay_clock.py +89 -21
  34. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_replay_regression_corpus.py +24 -0
  35. durable_workflow-2.15.0/tests/test_run_activities.py +161 -0
  36. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_sdk_metrics_contract.py +145 -1
  37. durable_workflow-2.15.0/tests/test_typed_workflow_values.py +339 -0
  38. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_wait_condition.py +65 -1
  39. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_worker.py +19 -7
  40. durable_workflow-2.15.0/tests/test_worker_observation.py +656 -0
  41. durable_workflow-2.15.0/tests/test_worker_startup_buffered.py +235 -0
  42. durable_workflow-2.15.0/tests/test_worker_startup_raw.py +200 -0
  43. durable_workflow-2.15.0/tests/test_worker_startup_read_budget.py +122 -0
  44. durable_workflow-2.15.0/tests/test_worker_startup_snapshot.py +235 -0
  45. durable_workflow-2.14.0/tests/test_worker_observation.py +0 -311
  46. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/LICENSE +0 -0
  47. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/README.md +0 -0
  48. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/setup.cfg +0 -0
  49. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_activity_lease.py +0 -0
  50. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_activity_process.py +0 -0
  51. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_avro.py +0 -0
  52. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_cancellation_scope.py +0 -0
  53. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  54. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_cooperative_cancellation.py +0 -0
  55. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_failure_reporting.py +0 -0
  56. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_metric_routes.py +0 -0
  57. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_poll_transport.py +0 -0
  58. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_prepared_local_activity.py +0 -0
  59. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_query_task_authority.py +0 -0
  60. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  61. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/auth_composition.py +0 -0
  62. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/cancellation.py +0 -0
  63. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/external_storage.py +0 -0
  64. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/external_task_input.py +0 -0
  65. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/external_task_result.py +0 -0
  66. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/history_bundle_verify.py +0 -0
  67. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/interceptors.py +0 -0
  68. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/invocable.py +0 -0
  69. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/nexus.py +0 -0
  70. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/py.typed +0 -0
  71. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/python_conformance.py +0 -0
  72. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/replay_conformance.py +0 -0
  73. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/replay_verify.py +0 -0
  74. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/retry_policy.py +0 -0
  75. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/serializer.py +0 -0
  76. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/server_compatibility.json +0 -0
  77. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/sync.py +0 -0
  78. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/telemetry.py +0 -0
  79. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/telemetry_carrier.py +0 -0
  80. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/testing.py +0 -0
  81. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/worker_session.py +0 -0
  82. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/worker_status.py +0 -0
  83. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  84. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  85. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  86. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/requires.txt +0 -0
  87. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
  88. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_cancellation_policies.py +0 -0
  89. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_context.py +0 -0
  90. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_heartbeat_authority.py +0 -0
  91. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_lease_authority.py +0 -0
  92. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_lease_renewal.py +0 -0
  93. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_lease_worker.py +0 -0
  94. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_activity_process.py +0 -0
  95. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_auth_composition.py +0 -0
  96. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_avro_value_benchmark.py +0 -0
  97. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_avro_value_protocol.py +0 -0
  98. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_context.py +0 -0
  99. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_remaining_time.py +0 -0
  100. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_scope_admission.py +0 -0
  101. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_scope_authoring.py +0 -0
  102. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_scope_boundary.py +0 -0
  103. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cancellation_scope_opening.py +0 -0
  104. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_child_workflow_policies.py +0 -0
  105. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_ci_checkout.py +0 -0
  106. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_ci_integration_endpoint.py +0 -0
  107. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_client.py +0 -0
  108. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_client_telemetry.py +0 -0
  109. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_codec_regression_corpus.py +0 -0
  110. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  111. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_groups.py +0 -0
  112. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_history.py +0 -0
  113. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_committed_cancellation_scope_replay.py +0 -0
  114. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_condition_replay_drift.py +0 -0
  115. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_control_plane_parity_fixtures.py +0 -0
  116. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation.py +0 -0
  117. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation_client.py +0 -0
  118. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_cooperative_cancellation_worker.py +0 -0
  119. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_docstring_cross_references.py +0 -0
  120. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_durable_selection.py +0 -0
  121. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_empty_callback_failure.py +0 -0
  122. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_errors.py +0 -0
  123. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_external_storage.py +0 -0
  124. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_external_task_input.py +0 -0
  125. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_external_task_result.py +0 -0
  126. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_failure_reporting.py +0 -0
  127. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_generator_lifecycle.py +0 -0
  128. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_golden_history_replay.py +0 -0
  129. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_history_bundle_pruned.py +0 -0
  130. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_history_bundle_schema2.py +0 -0
  131. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_history_bundle_verify.py +0 -0
  132. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_history_event_contract.py +0 -0
  133. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_invocable.py +0 -0
  134. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_local_failure_reporting.py +0 -0
  135. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_message_streams.py +0 -0
  136. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_metric_routes.py +0 -0
  137. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_metrics.py +0 -0
  138. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_nexus_durable_wait.py +0 -0
  139. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_nexus_namespace.py +0 -0
  140. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_nexus_wire.py +0 -0
  141. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_order_processing_example.py +0 -0
  142. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_parallel_completion_order_replay.py +0 -0
  143. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_parallel_saga_authoring.py +0 -0
  144. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_patch_history.py +0 -0
  145. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_pending_operation_replay.py +0 -0
  146. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_poll_failure_backoff.py +0 -0
  147. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_poll_transport.py +0 -0
  148. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_client.py +0 -0
  149. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_groups.py +0 -0
  150. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_prepared_local_activity_policies.py +0 -0
  151. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_public_boundary_scanner.py +0 -0
  152. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_publish_workflow_isolation.py +0 -0
  153. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_pypi_project_surface.py +0 -0
  154. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_python_conformance.py +0 -0
  155. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_queries.py +0 -0
  156. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_query_task_authority.py +0 -0
  157. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_query_timeout_retry.py +0 -0
  158. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_readme_quickstart.py +0 -0
  159. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_redrive_replay.py +0 -0
  160. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_registration_ambiguity.py +0 -0
  161. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_release_metadata.py +0 -0
  162. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_replay.py +0 -0
  163. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_replay_conformance.py +0 -0
  164. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_replay_verify.py +0 -0
  165. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_retry_policy.py +0 -0
  166. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_run_cancellation_scope_replay.py +0 -0
  167. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_runtime_external_payload_transport.py +0 -0
  168. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_schedules.py +0 -0
  169. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_scoped_run_cancellation_context.py +0 -0
  170. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_search_attribute_replay_identity.py +0 -0
  171. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_serializer.py +0 -0
  172. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_signals.py +0 -0
  173. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_sleep.py +0 -0
  174. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_standalone_activity_client.py +0 -0
  175. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_sticky_execution.py +0 -0
  176. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_storage_admission.py +0 -0
  177. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_sync.py +0 -0
  178. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_sync_activities.py +0 -0
  179. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_telemetry.py +0 -0
  180. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_telemetry_carrier.py +0 -0
  181. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_testing_failures.py +0 -0
  182. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_testing_harness.py +0 -0
  183. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_testing_nexus.py +0 -0
  184. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_threaded_heartbeat_example.py +0 -0
  185. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_tls_trust.py +0 -0
  186. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_transport_diagnostics.py +0 -0
  187. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_typed_values.py +0 -0
  188. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_update_signal_condition_replay.py +0 -0
  189. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_updates.py +0 -0
  190. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_worker_deregistration.py +0 -0
  191. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_worker_session.py +0 -0
  192. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_worker_status.py +0 -0
  193. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_worker_telemetry_wire.py +0 -0
  194. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_entry_arguments.py +0 -0
  195. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_entrypoint_validation.py +0 -0
  196. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_handler_validation.py +0 -0
  197. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_identity.py +0 -0
  198. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_result_timeout.py +0 -0
  199. {durable_workflow-2.14.0 → durable_workflow-2.15.0}/tests/test_workflow_streams.py +0 -0
  200. {durable_workflow-2.14.0 → 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.0
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.0"
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.0"
83
- registry-version = "2.14.0"
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",
@@ -0,0 +1,229 @@
1
+ """Stable identity for ``wait_condition`` predicates (#258).
2
+
3
+ The Server records the fingerprint a worker sends with ``open_condition_wait``
4
+ and replay compares it, so the value must be the same on every Python version
5
+ and in every process. Version 2 hashes the predicate's module, qualified name
6
+ and a canonical form of its source AST. It never hashes bytecode, constant
7
+ reprs, memory addresses, file paths or line numbers.
8
+
9
+ Version 1 (every release through 2.14.0) hashed bytecode and ``repr`` of code
10
+ constants, which change across Python versions and, for nested functions,
11
+ across processes. Recorded v1 values cannot be recomputed portably, so replay
12
+ treats a v1 value that differs from this process's v1 as unverifiable rather
13
+ than as a changed predicate (see :func:`fingerprint_verdict`).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import ast
19
+ import hashlib
20
+ import inspect
21
+ import linecache
22
+ import re
23
+ from collections.abc import Callable
24
+ from typing import Any, Literal
25
+
26
+ V2_PREFIX = "dwpy2:sha256:"
27
+ _LEGACY = re.compile(r"sha256:[0-9a-f]{64}\Z")
28
+ # Fields that carry no predicate meaning or differ between Python versions.
29
+ _SKIPPED_FIELDS = frozenset({"ctx", "kind", "type_comment", "type_params", "decorator_list", "returns"})
30
+ _parsed: dict[tuple[str, int], ast.Module | None] = {}
31
+ _nodes: dict[Any, str | None] = {}
32
+ # Python 3.12 inlines list, dict and set comprehensions (PEP 709), which drops
33
+ # their segment from a nested function's __qualname__.
34
+ _INLINED_COMPREHENSION = re.compile(r"\.<(?:listcomp|dictcomp|setcomp)>")
35
+
36
+ Verdict = Literal["match", "mismatch", "unverifiable"]
37
+
38
+
39
+ def legacy_fingerprint(predicate: Callable[[], bool]) -> str:
40
+ """The v1 fingerprint, kept only to recognise histories recorded by 2.14.0 and earlier."""
41
+ h = hashlib.sha256()
42
+ h.update(b"durable-workflow-python.wait-condition.v1\0")
43
+ h.update(f"{getattr(predicate, '__module__', '')}\0".encode())
44
+ h.update(f"{getattr(predicate, '__qualname__', '')}\0".encode())
45
+ original = getattr(predicate, "__typed_workflow_method__", None)
46
+ code = getattr(original if original is not None else predicate, "__code__", None)
47
+ if code is None:
48
+ h.update(repr(predicate).encode())
49
+ else:
50
+ h.update(repr((
51
+ code.co_argcount, code.co_posonlyargcount, code.co_kwonlyargcount, code.co_code,
52
+ code.co_consts, code.co_names, code.co_varnames, code.co_freevars,
53
+ )).encode())
54
+ return f"sha256:{h.hexdigest()}"
55
+
56
+
57
+ def is_legacy(value: Any) -> bool:
58
+ return isinstance(value, str) and _LEGACY.match(value) is not None
59
+
60
+
61
+ def fingerprint(predicate: Callable[[], bool]) -> str | None:
62
+ """Return the v2 fingerprint, or None when the predicate's source is unavailable.
63
+
64
+ Without a fingerprint the wait is identified by its key and call order,
65
+ as in sdk-php.
66
+ """
67
+ function = _authored_function(predicate)
68
+ if function is None:
69
+ return None
70
+ code = function.__code__
71
+ if code not in _nodes:
72
+ _nodes[code] = _predicate_node(function)
73
+ node = _nodes[code]
74
+ if node is None:
75
+ return None
76
+ qualname = _INLINED_COMPREHENSION.sub("", getattr(function, "__qualname__", ""))
77
+ h = hashlib.sha256()
78
+ h.update(b"durable-workflow-python.wait-condition.v2\0")
79
+ h.update(f"{getattr(function, '__module__', '')}\0".encode())
80
+ h.update(f"{qualname}\0".encode())
81
+ h.update(node.encode())
82
+ return f"{V2_PREFIX}{h.hexdigest()}"
83
+
84
+
85
+ def fingerprint_verdict(recorded: Any, authored: str | None, predicate: Callable[[], bool]) -> Verdict:
86
+ """Compare a recorded fingerprint with the authored predicate.
87
+
88
+ - No recorded value: nothing to compare.
89
+ - An identity supplied explicitly in another format (not v2, not None)
90
+ is compared exactly, as before.
91
+ - A v2 value is compared exactly; without source it cannot be checked.
92
+ - A v1 value equal to this process's v1 matches. Any other v1 value is
93
+ unverifiable, because v1 differs across Python versions and processes.
94
+ - Any other string is compared exactly, as before.
95
+ """
96
+ if not isinstance(recorded, str):
97
+ return "match"
98
+ if authored is not None and not authored.startswith(V2_PREFIX):
99
+ return "match" if recorded == authored else "mismatch"
100
+ if recorded.startswith(V2_PREFIX):
101
+ if authored is None:
102
+ return "unverifiable"
103
+ return "match" if recorded == authored else "mismatch"
104
+ if is_legacy(recorded):
105
+ return "match" if recorded == legacy_fingerprint(predicate) else "unverifiable"
106
+ return "match" if recorded == authored else "mismatch"
107
+
108
+
109
+ def _authored_function(predicate: Any) -> Any:
110
+ target = getattr(predicate, "__typed_workflow_method__", None) or predicate
111
+ if inspect.ismethod(target):
112
+ target = target.__func__
113
+ target = getattr(target, "__typed_workflow_method__", None) or target
114
+ return target if inspect.isfunction(target) else None
115
+
116
+
117
+ def _module_tree(function: Any) -> ast.Module | None:
118
+ try:
119
+ filename = inspect.getsourcefile(function) or inspect.getfile(function)
120
+ except (OSError, TypeError):
121
+ return None
122
+ lines = linecache.getlines(filename, function.__globals__)
123
+ if not lines:
124
+ return None
125
+ source = "".join(lines)
126
+ key = (filename, hash(source))
127
+ if key not in _parsed:
128
+ try:
129
+ _parsed[key] = ast.parse(source)
130
+ except (SyntaxError, ValueError):
131
+ _parsed[key] = None
132
+ return _parsed[key]
133
+
134
+
135
+ def _predicate_node(function: Any) -> str | None:
136
+ tree = _module_tree(function)
137
+ if tree is None:
138
+ return None
139
+ code = function.__code__
140
+ first_line = code.co_firstlineno
141
+ if function.__name__ == "<lambda>":
142
+ lambdas = [node for node in ast.walk(tree) if isinstance(node, ast.Lambda) and node.lineno == first_line]
143
+ lambdas.sort(key=lambda node: node.col_offset)
144
+ # PEP 709 moves comprehension instructions into the enclosing lambda.
145
+ # Candidates indistinguishable before 3.12 can become distinct after
146
+ # inlining, so keep the combined AST on every version in this case.
147
+ has_inlined_comprehension = any(
148
+ isinstance(child, ast.ListComp | ast.DictComp | ast.SetComp)
149
+ for node in lambdas for child in ast.walk(node)
150
+ )
151
+ if len(lambdas) > 1 and not has_inlined_comprehension:
152
+ # Names and constants are stable across versions; bytecode only
153
+ # breaks a tie, and is compared within this interpreter.
154
+ matching = [node for node in lambdas if _compiles_to(node, code, compare_bytecode=False)]
155
+ if len(matching) > 1:
156
+ matching = [node for node in matching if _compiles_to(node, code, compare_bytecode=True)]
157
+ if len(matching) == 1:
158
+ lambdas = matching
159
+ if not lambdas:
160
+ return None
161
+ # Lambdas that start on the same line and compile identically share
162
+ # one fingerprint covering all of them.
163
+ return "|".join(_canonical(node) for node in lambdas)
164
+ definitions = [
165
+ node for node in ast.walk(tree)
166
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and node.name == function.__name__
167
+ and min([node.lineno, *(decorator.lineno for decorator in node.decorator_list)]) == first_line
168
+ ]
169
+ if len(definitions) != 1:
170
+ return None
171
+ return _canonical(definitions[0])
172
+
173
+
174
+ def _compiles_to(node: ast.Lambda, code: Any, *, compare_bytecode: bool) -> bool:
175
+ """Whether *node* compiles to *code* on this interpreter.
176
+
177
+ The candidate is compiled inside a function that binds the live code's
178
+ free variables, so names resolve the same way. Only the choice of node
179
+ uses bytecode, which is valid within one interpreter; the fingerprint
180
+ hashes the node's AST, so it stays portable.
181
+ """
182
+ free = list(code.co_freevars)
183
+ lines = ["def __dw_outer__():"]
184
+ if free:
185
+ lines.append(f" {' = '.join(free)} = None")
186
+ lines.append(f" return ({ast.unparse(node)})")
187
+ try:
188
+ outer = compile("\n".join(lines), "<condition-fingerprint>", "exec")
189
+ except SyntaxError:
190
+ return False
191
+ function = next((const for const in outer.co_consts if hasattr(const, "co_code")), None)
192
+ candidate = next(
193
+ (const for const in getattr(function, "co_consts", ()) if getattr(const, "co_name", None) == "<lambda>"),
194
+ None,
195
+ )
196
+ return candidate is not None and (
197
+ (not compare_bytecode or candidate.co_code == code.co_code)
198
+ and candidate.co_argcount == code.co_argcount
199
+ and candidate.co_names == code.co_names
200
+ and candidate.co_varnames == code.co_varnames
201
+ and candidate.co_freevars == code.co_freevars
202
+ and _plain_constants(candidate) == _plain_constants(code)
203
+ )
204
+
205
+
206
+ def _plain_constants(code: Any) -> list[Any]:
207
+ """Constants other than nested code and the qualnames Python 3.10 stores for them."""
208
+ return [
209
+ const for const in code.co_consts
210
+ if not hasattr(const, "co_code") and not (isinstance(const, str) and "<locals>" in const)
211
+ ]
212
+
213
+
214
+ def _canonical(value: Any) -> str:
215
+ if isinstance(value, ast.AST):
216
+ parts = [type(value).__name__]
217
+ for name in value._fields:
218
+ if name in _SKIPPED_FIELDS:
219
+ continue
220
+ item = getattr(value, name, None)
221
+ if item is None or item == []:
222
+ continue
223
+ parts.append(f"{name}={_canonical(item)}")
224
+ return "(" + " ".join(parts) + ")"
225
+ if isinstance(value, list):
226
+ return "[" + ",".join(_canonical(item) for item in value) + "]"
227
+ if value is None or value is Ellipsis or isinstance(value, str | bytes | int | float | complex | bool):
228
+ return f"{type(value).__name__}:{value!r}"
229
+ return type(value).__name__
@@ -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: