pyworkflow-engine 0.3.6__tar.gz → 0.3.7__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 (244) hide show
  1. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/PKG-INFO +1 -1
  2. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyproject.toml +1 -1
  3. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/__init__.py +8 -2
  4. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/celery/singleton.py +5 -1
  5. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/celery/tasks.py +22 -3
  6. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/step_hook.py +72 -9
  7. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_singleton.py +38 -0
  8. pyworkflow_engine-0.3.7/tests/unit/test_step_hook.py +400 -0
  9. pyworkflow_engine-0.3.6/tests/unit/test_step_hook.py +0 -202
  10. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/CLAUDE.md +0 -0
  11. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/DISTRIBUTED.md +0 -0
  12. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/LICENSE +0 -0
  13. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/MANIFEST.in +0 -0
  14. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/README.md +0 -0
  15. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/RELEASING.md +0 -0
  16. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/architecture.md +0 -0
  17. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/cancellation.mdx +0 -0
  18. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/continue-as-new.mdx +0 -0
  19. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/events.mdx +0 -0
  20. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/fault-tolerance.mdx +0 -0
  21. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/hooks.mdx +0 -0
  22. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/limitations.mdx +0 -0
  23. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/schedules.mdx +0 -0
  24. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/sleep.mdx +0 -0
  25. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/step-context.mdx +0 -0
  26. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/steps.mdx +0 -0
  27. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/stream-steps.mdx +0 -0
  28. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/stream-workflows.mdx +0 -0
  29. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/concepts/workflows.mdx +0 -0
  30. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/conventions.md +0 -0
  31. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/guides/brokers.mdx +0 -0
  32. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/guides/cli.mdx +0 -0
  33. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/guides/configuration.mdx +0 -0
  34. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/harness-gaps.md +0 -0
  35. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/introduction.mdx +0 -0
  36. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/layers.md +0 -0
  37. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/docs/quickstart.mdx +0 -0
  38. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/__init__.py +0 -0
  39. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/__init__.py +0 -0
  40. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/docker-compose.yml +0 -0
  41. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/pyworkflow.config.yaml +0 -0
  42. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/__init__.py +0 -0
  43. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/basic.py +0 -0
  44. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/batch_processing.py +0 -0
  45. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/cancellation.py +0 -0
  46. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
  47. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
  48. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/child_workflows.py +0 -0
  49. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/continue_as_new.py +0 -0
  50. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
  51. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/hooks.py +0 -0
  52. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/idempotency.py +0 -0
  53. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/long_running.py +0 -0
  54. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/retries.py +0 -0
  55. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/schedules.py +0 -0
  56. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
  57. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/durable/workflows/step_context.py +0 -0
  58. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/transient/01_basic_workflow.py +0 -0
  59. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/transient/02_fault_tolerance.py +0 -0
  60. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/transient/__init__.py +0 -0
  61. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/celery/transient/pyworkflow.config.yaml +0 -0
  62. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/__init__.py +0 -0
  63. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/01_basic_workflow.py +0 -0
  64. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/02_file_storage.py +0 -0
  65. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/03_retries.py +0 -0
  66. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/04_long_running.py +0 -0
  67. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/05_event_log.py +0 -0
  68. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/06_idempotency.py +0 -0
  69. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/07_hooks.py +0 -0
  70. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/08_cancellation.py +0 -0
  71. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/09_child_workflows.py +0 -0
  72. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/10_child_workflow_patterns.py +0 -0
  73. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/11_continue_as_new.py +0 -0
  74. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/12_schedules.py +0 -0
  75. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/13_step_context.py +0 -0
  76. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/14_child_workflow_from_step.py +0 -0
  77. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/durable/__init__.py +0 -0
  78. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/transient/01_quick_tasks.py +0 -0
  79. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/transient/02_retries.py +0 -0
  80. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/transient/03_sleep.py +0 -0
  81. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/examples/local/transient/__init__.py +0 -0
  82. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/aws/__init__.py +0 -0
  83. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/aws/context.py +0 -0
  84. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/aws/handler.py +0 -0
  85. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/aws/testing.py +0 -0
  86. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/celery/__init__.py +0 -0
  87. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/celery/app.py +0 -0
  88. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/celery/loop.py +0 -0
  89. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/celery/reschedule.py +0 -0
  90. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/celery/scheduler.py +0 -0
  91. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/__init__.py +0 -0
  92. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/__main__.py +0 -0
  93. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/commands/__init__.py +0 -0
  94. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/commands/hooks.py +0 -0
  95. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/commands/quickstart.py +0 -0
  96. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/commands/runs.py +0 -0
  97. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/commands/scheduler.py +0 -0
  98. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/commands/schedules.py +0 -0
  99. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/commands/setup.py +0 -0
  100. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/commands/worker.py +0 -0
  101. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/commands/workflows.py +0 -0
  102. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/output/__init__.py +0 -0
  103. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/output/formatters.py +0 -0
  104. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/output/styles.py +0 -0
  105. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/utils/__init__.py +0 -0
  106. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/utils/async_helpers.py +0 -0
  107. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/utils/config.py +0 -0
  108. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/utils/config_generator.py +0 -0
  109. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/utils/discovery.py +0 -0
  110. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/utils/docker_manager.py +0 -0
  111. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/utils/interactive.py +0 -0
  112. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/cli/utils/storage.py +0 -0
  113. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/config.py +0 -0
  114. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/context/__init__.py +0 -0
  115. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/context/aws.py +0 -0
  116. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/context/base.py +0 -0
  117. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/context/local.py +0 -0
  118. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/context/mock.py +0 -0
  119. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/context/step_context.py +0 -0
  120. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/core/__init__.py +0 -0
  121. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/core/exceptions.py +0 -0
  122. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/core/registry.py +0 -0
  123. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/core/scheduled.py +0 -0
  124. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/core/step.py +0 -0
  125. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/core/validation.py +0 -0
  126. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/core/workflow.py +0 -0
  127. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/discovery.py +0 -0
  128. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/engine/__init__.py +0 -0
  129. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/engine/events.py +0 -0
  130. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/engine/executor.py +0 -0
  131. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/engine/replay.py +0 -0
  132. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/observability/__init__.py +0 -0
  133. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/observability/logging.py +0 -0
  134. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/__init__.py +0 -0
  135. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/child_handle.py +0 -0
  136. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/child_workflow.py +0 -0
  137. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/continue_as_new.py +0 -0
  138. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/define_hook.py +0 -0
  139. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/hooks.py +0 -0
  140. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/resume_hook.py +0 -0
  141. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/schedule.py +0 -0
  142. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/shield.py +0 -0
  143. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/sleep.py +0 -0
  144. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/primitives/step_checkpoint.py +0 -0
  145. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/runtime/__init__.py +0 -0
  146. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/runtime/base.py +0 -0
  147. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/runtime/celery.py +0 -0
  148. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/runtime/factory.py +0 -0
  149. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/runtime/local.py +0 -0
  150. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/scheduler/__init__.py +0 -0
  151. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/scheduler/local.py +0 -0
  152. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/serialization/__init__.py +0 -0
  153. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/serialization/decoder.py +0 -0
  154. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/serialization/encoder.py +0 -0
  155. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/__init__.py +0 -0
  156. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/base.py +0 -0
  157. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/cassandra.py +0 -0
  158. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/citus.py +0 -0
  159. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/config.py +0 -0
  160. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/dynamodb.py +0 -0
  161. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/file.py +0 -0
  162. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/memory.py +0 -0
  163. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/migrations/__init__.py +0 -0
  164. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/migrations/base.py +0 -0
  165. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/mysql.py +0 -0
  166. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/postgres.py +0 -0
  167. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/schemas.py +0 -0
  168. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/storage/sqlite.py +0 -0
  169. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/streams/__init__.py +0 -0
  170. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/streams/checkpoint.py +0 -0
  171. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/streams/consumer.py +0 -0
  172. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/streams/context.py +0 -0
  173. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/streams/decorator.py +0 -0
  174. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/streams/dispatcher.py +0 -0
  175. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/streams/emit.py +0 -0
  176. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/streams/registry.py +0 -0
  177. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/streams/runtime.py +0 -0
  178. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/streams/signal.py +0 -0
  179. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/streams/step_context.py +0 -0
  180. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/tracing/__init__.py +0 -0
  181. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/tracing/base.py +0 -0
  182. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/tracing/factory.py +0 -0
  183. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/tracing/langfuse.py +0 -0
  184. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/tracing/types.py +0 -0
  185. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/utils/__init__.py +0 -0
  186. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/utils/duration.py +0 -0
  187. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/utils/helpers.py +0 -0
  188. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow/utils/schedule.py +0 -0
  189. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/pyworkflow_engine.egg-info/SOURCES.txt +0 -0
  190. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/setup.cfg +0 -0
  191. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/__init__.py +0 -0
  192. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/_reschedule_e2e_app.py +0 -0
  193. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_cancellation.py +0 -0
  194. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_cassandra_storage.py +0 -0
  195. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_child_workflows.py +0 -0
  196. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_continue_as_new.py +0 -0
  197. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_dynamodb_storage.py +0 -0
  198. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_fault_tolerance.py +0 -0
  199. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_list_runs_pagination.py +0 -0
  200. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_reschedule.py +0 -0
  201. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_reschedule_e2e.py +0 -0
  202. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_schedule_storage.py +0 -0
  203. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_schema_migrations.py +0 -0
  204. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_singleton.py +0 -0
  205. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_stream_e2e.py +0 -0
  206. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/integration/test_workflow_suspended.py +0 -0
  207. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/__init__.py +0 -0
  208. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/backends/__init__.py +0 -0
  209. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/backends/test_cassandra_storage.py +0 -0
  210. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/backends/test_citus_storage.py +0 -0
  211. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/backends/test_dynamodb_storage.py +0 -0
  212. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/backends/test_postgres_storage.py +0 -0
  213. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/backends/test_sqlite_storage.py +0 -0
  214. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/conftest.py +0 -0
  215. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/storage/__init__.py +0 -0
  216. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/storage/test_migrations.py +0 -0
  217. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_cancellation.py +0 -0
  218. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_child_workflows.py +0 -0
  219. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_cli_worker.py +0 -0
  220. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_continue_as_new.py +0 -0
  221. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_emit.py +0 -0
  222. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_event_limits.py +0 -0
  223. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_executor.py +0 -0
  224. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_fault_tolerance.py +0 -0
  225. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_force_local.py +0 -0
  226. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_hooks.py +0 -0
  227. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_parent_run_id.py +0 -0
  228. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_primitives_from_steps.py +0 -0
  229. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_registry.py +0 -0
  230. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_replay.py +0 -0
  231. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_reschedule.py +0 -0
  232. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_retention.py +0 -0
  233. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_schedule_schemas.py +0 -0
  234. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_schedule_utils.py +0 -0
  235. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_scheduled_workflow.py +0 -0
  236. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_signal.py +0 -0
  237. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_step.py +0 -0
  238. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_step_checkpoint.py +0 -0
  239. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_step_context.py +0 -0
  240. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_stream_storage.py +0 -0
  241. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_stream_workflow.py +0 -0
  242. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_validation.py +0 -0
  243. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_workflow.py +0 -0
  244. {pyworkflow_engine-0.3.6 → pyworkflow_engine-0.3.7}/tests/unit/test_workflow_suspended.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyworkflow-engine
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: A Python implementation of durable, event-sourced workflows inspired by Vercel Workflow
5
5
  Author: PyWorkflow Contributors
6
6
  License: MIT
@@ -7,7 +7,7 @@ packages = [{include = "pyworkflow"}]
7
7
 
8
8
  [project]
9
9
  name = "pyworkflow-engine"
10
- version = "0.3.6"
10
+ version = "0.3.7"
11
11
  description = "A Python implementation of durable, event-sourced workflows inspired by Vercel Workflow"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.11"
@@ -29,7 +29,7 @@ Quick Start:
29
29
  >>> run_id = await start(my_workflow, "Alice")
30
30
  """
31
31
 
32
- __version__ = "0.3.6"
32
+ __version__ = "0.3.7"
33
33
 
34
34
  # Configuration
35
35
  from pyworkflow.config import (
@@ -140,7 +140,11 @@ from pyworkflow.primitives.step_checkpoint import (
140
140
  load_step_checkpoint,
141
141
  save_step_checkpoint,
142
142
  )
143
- from pyworkflow.primitives.step_hook import step_hook
143
+ from pyworkflow.primitives.step_hook import (
144
+ STEP_HOOK_TIMEOUT,
145
+ StepHookTimeout,
146
+ step_hook,
147
+ )
144
148
 
145
149
  # Runtime
146
150
  from pyworkflow.runtime import LocalRuntime, Runtime, get_runtime, register_runtime
@@ -290,6 +294,8 @@ __all__ = [
290
294
  "load_step_checkpoint",
291
295
  "delete_step_checkpoint",
292
296
  "step_hook",
297
+ "StepHookTimeout",
298
+ "STEP_HOOK_TIMEOUT",
293
299
  # Streams
294
300
  "stream_workflow",
295
301
  "stream_step",
@@ -323,7 +323,11 @@ class SingletonWorkflowTask(Task):
323
323
 
324
324
  # Bind arguments to function signature
325
325
  sig = inspect.signature(self.run)
326
- bound = sig.bind(*task_args, **task_kwargs).arguments
326
+ bound_sig = sig.bind(*task_args, **task_kwargs)
327
+ # Include declared defaults so unique_on may reference parameters
328
+ # the caller did not pass explicitly (e.g. dedup_scope).
329
+ bound_sig.apply_defaults()
330
+ bound = bound_sig.arguments
327
331
 
328
332
  unique_args: list[Any] = []
329
333
  for key in unique_on:
@@ -371,6 +371,19 @@ def execute_step_task(
371
371
  hook_id=hook_id or "",
372
372
  )
373
373
  )
374
+ # step_hook(on_timeout="return") carries the hook deadline: schedule
375
+ # a resume at expiry so the workflow wakes even if nobody ever calls
376
+ # resume_hook (the step re-executes and receives STEP_HOOK_TIMEOUT).
377
+ # A racing resume_hook is harmless — duplicate resumes are dropped
378
+ # by try_claim_run.
379
+ resume_at = e.data.get("resume_at")
380
+ if resume_at is not None:
381
+ schedule_workflow_resumption(
382
+ run_id,
383
+ resume_at,
384
+ storage_config,
385
+ triggered_by="step_hook_timeout",
386
+ )
374
387
  return None
375
388
 
376
389
  # Other SuspensionSignals are not supported from steps
@@ -2025,12 +2038,13 @@ async def _start_workflow_on_worker(
2025
2038
  name="pyworkflow.resume_workflow",
2026
2039
  base=SingletonWorkflowTask,
2027
2040
  queue="pyworkflow.schedules",
2028
- unique_on=["run_id"],
2041
+ unique_on=["run_id", "dedup_scope"],
2029
2042
  )
2030
2043
  def resume_workflow_task(
2031
2044
  run_id: str,
2032
2045
  storage_config: dict[str, Any] | None = None,
2033
2046
  triggered_by_hook_id: str | None = None,
2047
+ dedup_scope: str = "immediate",
2034
2048
  ) -> Any | None:
2035
2049
  """
2036
2050
  Resume a suspended workflow.
@@ -2703,10 +2717,15 @@ def schedule_workflow_resumption(
2703
2717
  triggered_by=triggered_by,
2704
2718
  )
2705
2719
 
2706
- # Schedule the resume task
2720
+ # Schedule the resume task. Delayed (deadline) resumes hold their
2721
+ # singleton lock for the whole countdown; scoping them separately keeps
2722
+ # them from swallowing immediate resumes (e.g. resume_hook wake-ups)
2723
+ # enqueued while the countdown is pending. Extra resume executions are
2724
+ # harmless: try_claim_run drops all but the first.
2725
+ dedup_scope = "deadline" if delay_seconds > 0 else "immediate"
2707
2726
  resume_workflow_task.apply_async(
2708
2727
  args=[run_id],
2709
- kwargs={"storage_config": storage_config},
2728
+ kwargs={"storage_config": storage_config, "dedup_scope": dedup_scope},
2710
2729
  countdown=delay_seconds,
2711
2730
  )
2712
2731
 
@@ -25,7 +25,8 @@ Usage:
25
25
  """
26
26
 
27
27
  from collections.abc import Awaitable, Callable
28
- from typing import Any
28
+ from datetime import UTC, datetime, timedelta
29
+ from typing import Any, Literal
29
30
 
30
31
  from loguru import logger
31
32
  from pydantic import BaseModel
@@ -35,12 +36,29 @@ from pyworkflow.core.exceptions import SuspensionSignal
35
36
  from pyworkflow.primitives.step_checkpoint import get_step_run_id
36
37
 
37
38
 
39
+ class StepHookTimeout:
40
+ """Sentinel returned by ``step_hook(on_timeout="return")`` when the hook expires.
41
+
42
+ Check with ``isinstance(result, StepHookTimeout)`` or compare against the
43
+ ``STEP_HOOK_TIMEOUT`` singleton.
44
+ """
45
+
46
+ __slots__ = ()
47
+
48
+ def __repr__(self) -> str:
49
+ return "STEP_HOOK_TIMEOUT"
50
+
51
+
52
+ STEP_HOOK_TIMEOUT = StepHookTimeout()
53
+
54
+
38
55
  async def step_hook(
39
56
  name: str,
40
57
  *,
41
58
  timeout: str | int | None = None,
42
59
  on_created: Callable[[str], Awaitable[None]] | None = None,
43
60
  payload_schema: type[BaseModel] | None = None,
61
+ on_timeout: Literal["suspend", "return"] = "suspend",
44
62
  ) -> Any:
45
63
  """
46
64
  Wait for an external event from within a @step function.
@@ -58,9 +76,14 @@ async def step_hook(
58
76
  timeout: Optional max wait time (str duration or seconds)
59
77
  on_created: Optional async callback with the hook token
60
78
  payload_schema: Optional Pydantic model for payload validation
79
+ on_timeout: What to do when ``timeout`` elapses without a resume.
80
+ "suspend" (default): keep waiting for resume_hook() — legacy behavior.
81
+ "return": the runtime schedules a resume at the deadline and this
82
+ call returns the ``STEP_HOOK_TIMEOUT`` sentinel on re-execution.
61
83
 
62
84
  Returns:
63
- Payload from resume_hook()
85
+ Payload from resume_hook(), or ``STEP_HOOK_TIMEOUT`` when the hook
86
+ expired and ``on_timeout="return"``
64
87
 
65
88
  Raises:
66
89
  RuntimeError: If called outside a step context
@@ -107,11 +130,11 @@ async def step_hook(
107
130
 
108
131
  events = await storage.get_events(ctx.run_id)
109
132
  hook_received = None
110
- hook_created = False
133
+ hook_created_event = None
111
134
 
112
135
  for event in events:
113
136
  if event.type == EventType.HOOK_CREATED and event.data.get("hook_id") == hook_id:
114
- hook_created = True
137
+ hook_created_event = event
115
138
  elif event.type == EventType.HOOK_RECEIVED and event.data.get("hook_id") == hook_id:
116
139
  hook_received = event
117
140
 
@@ -127,17 +150,53 @@ async def step_hook(
127
150
  )
128
151
  return payload
129
152
 
130
- # If hook was already created but not received, re-suspend
131
- if hook_created:
153
+ # If hook was already created but not received, check expiry, else re-suspend
154
+ if hook_created_event is not None:
155
+ expires_at: datetime | None = None
156
+ expires_at_raw = hook_created_event.data.get("expires_at")
157
+ if expires_at_raw:
158
+ expires_at = datetime.fromisoformat(expires_at_raw)
159
+
160
+ if on_timeout == "return" and expires_at is not None and datetime.now(UTC) >= expires_at:
161
+ from pyworkflow.engine.events import create_hook_expired_event
162
+ from pyworkflow.storage.schemas import HookStatus
163
+
164
+ await storage.record_event(
165
+ create_hook_expired_event(run_id=ctx.run_id, hook_id=hook_id)
166
+ )
167
+ try:
168
+ await storage.update_hook_status(hook_id, HookStatus.EXPIRED)
169
+ except Exception:
170
+ # Best-effort: the HOOK_EXPIRED event is authoritative for replay;
171
+ # a failed status update must not fail the step.
172
+ logger.warning(
173
+ f"Step hook '{name}' expired but status update failed",
174
+ run_id=ctx.run_id,
175
+ hook_id=hook_id,
176
+ )
177
+ logger.info(
178
+ f"Step hook '{name}' expired, returning timeout sentinel",
179
+ run_id=ctx.run_id,
180
+ hook_id=hook_id,
181
+ )
182
+ return STEP_HOOK_TIMEOUT
183
+
132
184
  logger.debug(
133
185
  f"Step hook '{name}' already created, re-suspending",
134
186
  run_id=ctx.run_id,
135
187
  hook_id=hook_id,
136
188
  )
189
+ # Re-arm the deadline resume only for on_timeout="return" with a future
190
+ # deadline: the runtime schedules a resume at resume_at, and scheduling
191
+ # one in the past would busy-loop resume → re-suspend.
192
+ resume_data: dict[str, Any] = {}
193
+ if on_timeout == "return" and expires_at is not None and datetime.now(UTC) < expires_at:
194
+ resume_data["resume_at"] = expires_at
137
195
  raise SuspensionSignal(
138
196
  reason=f"step_hook:{hook_id}",
139
197
  hook_id=hook_id,
140
198
  step_id=step_run_id,
199
+ **resume_data,
141
200
  )
142
201
 
143
202
  # Parse timeout
@@ -179,8 +238,6 @@ async def step_hook(
179
238
  payload_schema=payload_schema.__name__ if payload_schema else None,
180
239
  )
181
240
  if timeout_seconds:
182
- from datetime import UTC, datetime, timedelta
183
-
184
241
  hook_record.expires_at = datetime.now(UTC) + timedelta(seconds=timeout_seconds)
185
242
 
186
243
  await storage.create_hook(hook_record)
@@ -197,9 +254,15 @@ async def step_hook(
197
254
  if on_created:
198
255
  await on_created(token)
199
256
 
200
- # Raise SuspensionSignal to suspend the step
257
+ # Raise SuspensionSignal to suspend the step. For on_timeout="return",
258
+ # carry the deadline so the runtime schedules a resume at expiry — without
259
+ # it, an expired hook nobody resumes would suspend the workflow forever.
260
+ suspend_data: dict[str, Any] = {}
261
+ if on_timeout == "return" and hook_record.expires_at is not None:
262
+ suspend_data["resume_at"] = hook_record.expires_at
201
263
  raise SuspensionSignal(
202
264
  reason=f"step_hook:{hook_id}",
203
265
  hook_id=hook_id,
204
266
  step_id=step_run_id,
267
+ **suspend_data,
205
268
  )
@@ -1007,3 +1007,41 @@ class TestReleaseLockKeyReproduction:
1007
1007
  key_prefix="pyworkflow:lock:",
1008
1008
  )
1009
1009
  backend.unlock.assert_called_once_with(expected_key)
1010
+
1011
+
1012
+ class TestResumeDedupScope:
1013
+ """The resume task's singleton key must separate immediate vs deadline resumes.
1014
+
1015
+ A deadline (countdown) resume holds its singleton lock for the whole
1016
+ countdown; without scope separation it swallows every immediate resume
1017
+ (e.g. resume_hook wake-ups) enqueued meanwhile.
1018
+ """
1019
+
1020
+ def test_lock_key_differs_by_dedup_scope(self):
1021
+ from pyworkflow.celery.tasks import resume_workflow_task
1022
+
1023
+ immediate = resume_workflow_task.generate_lock(
1024
+ resume_workflow_task.name,
1025
+ ["run_x"],
1026
+ {"storage_config": None, "dedup_scope": "immediate"},
1027
+ )
1028
+ deadline = resume_workflow_task.generate_lock(
1029
+ resume_workflow_task.name,
1030
+ ["run_x"],
1031
+ {"storage_config": None, "dedup_scope": "deadline"},
1032
+ )
1033
+ assert immediate != deadline
1034
+
1035
+ def test_lock_key_defaults_to_immediate_scope(self):
1036
+ """Callers not passing dedup_scope must match explicit 'immediate'."""
1037
+ from pyworkflow.celery.tasks import resume_workflow_task
1038
+
1039
+ default_key = resume_workflow_task.generate_lock(
1040
+ resume_workflow_task.name, ["run_x"], {"storage_config": None}
1041
+ )
1042
+ explicit_key = resume_workflow_task.generate_lock(
1043
+ resume_workflow_task.name,
1044
+ ["run_x"],
1045
+ {"storage_config": None, "dedup_scope": "immediate"},
1046
+ )
1047
+ assert default_key == explicit_key