pyworkflow-engine 0.3.0__tar.gz → 0.3.1__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 (237) hide show
  1. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/PKG-INFO +1 -1
  2. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyproject.toml +1 -1
  3. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/__init__.py +1 -1
  4. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/singleton.py +82 -12
  5. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/tasks.py +5 -1
  6. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_singleton.py +126 -0
  7. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/CLAUDE.md +0 -0
  8. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/DISTRIBUTED.md +0 -0
  9. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/LICENSE +0 -0
  10. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/MANIFEST.in +0 -0
  11. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/README.md +0 -0
  12. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/RELEASING.md +0 -0
  13. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/architecture.md +0 -0
  14. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/cancellation.mdx +0 -0
  15. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/continue-as-new.mdx +0 -0
  16. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/events.mdx +0 -0
  17. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/fault-tolerance.mdx +0 -0
  18. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/hooks.mdx +0 -0
  19. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/limitations.mdx +0 -0
  20. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/schedules.mdx +0 -0
  21. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/sleep.mdx +0 -0
  22. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/step-context.mdx +0 -0
  23. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/steps.mdx +0 -0
  24. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/stream-steps.mdx +0 -0
  25. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/stream-workflows.mdx +0 -0
  26. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/workflows.mdx +0 -0
  27. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/conventions.md +0 -0
  28. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/guides/brokers.mdx +0 -0
  29. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/guides/cli.mdx +0 -0
  30. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/guides/configuration.mdx +0 -0
  31. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/harness-gaps.md +0 -0
  32. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/introduction.mdx +0 -0
  33. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/layers.md +0 -0
  34. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/quickstart.mdx +0 -0
  35. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/__init__.py +0 -0
  36. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/__init__.py +0 -0
  37. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/docker-compose.yml +0 -0
  38. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/pyworkflow.config.yaml +0 -0
  39. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/__init__.py +0 -0
  40. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/basic.py +0 -0
  41. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/batch_processing.py +0 -0
  42. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/cancellation.py +0 -0
  43. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
  44. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
  45. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/child_workflows.py +0 -0
  46. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/continue_as_new.py +0 -0
  47. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
  48. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/hooks.py +0 -0
  49. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/idempotency.py +0 -0
  50. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/long_running.py +0 -0
  51. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/retries.py +0 -0
  52. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/schedules.py +0 -0
  53. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
  54. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/step_context.py +0 -0
  55. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/transient/01_basic_workflow.py +0 -0
  56. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/transient/02_fault_tolerance.py +0 -0
  57. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/transient/__init__.py +0 -0
  58. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/transient/pyworkflow.config.yaml +0 -0
  59. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/__init__.py +0 -0
  60. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/01_basic_workflow.py +0 -0
  61. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/02_file_storage.py +0 -0
  62. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/03_retries.py +0 -0
  63. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/04_long_running.py +0 -0
  64. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/05_event_log.py +0 -0
  65. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/06_idempotency.py +0 -0
  66. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/07_hooks.py +0 -0
  67. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/08_cancellation.py +0 -0
  68. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/09_child_workflows.py +0 -0
  69. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/10_child_workflow_patterns.py +0 -0
  70. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/11_continue_as_new.py +0 -0
  71. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/12_schedules.py +0 -0
  72. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/13_step_context.py +0 -0
  73. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/14_child_workflow_from_step.py +0 -0
  74. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/__init__.py +0 -0
  75. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/transient/01_quick_tasks.py +0 -0
  76. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/transient/02_retries.py +0 -0
  77. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/transient/03_sleep.py +0 -0
  78. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/transient/__init__.py +0 -0
  79. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/aws/__init__.py +0 -0
  80. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/aws/context.py +0 -0
  81. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/aws/handler.py +0 -0
  82. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/aws/testing.py +0 -0
  83. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/__init__.py +0 -0
  84. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/app.py +0 -0
  85. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/loop.py +0 -0
  86. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/scheduler.py +0 -0
  87. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/__init__.py +0 -0
  88. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/__main__.py +0 -0
  89. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/__init__.py +0 -0
  90. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/hooks.py +0 -0
  91. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/quickstart.py +0 -0
  92. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/runs.py +0 -0
  93. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/scheduler.py +0 -0
  94. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/schedules.py +0 -0
  95. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/setup.py +0 -0
  96. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/worker.py +0 -0
  97. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/workflows.py +0 -0
  98. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/output/__init__.py +0 -0
  99. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/output/formatters.py +0 -0
  100. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/output/styles.py +0 -0
  101. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/__init__.py +0 -0
  102. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/async_helpers.py +0 -0
  103. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/config.py +0 -0
  104. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/config_generator.py +0 -0
  105. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/discovery.py +0 -0
  106. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/docker_manager.py +0 -0
  107. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/interactive.py +0 -0
  108. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/storage.py +0 -0
  109. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/config.py +0 -0
  110. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/__init__.py +0 -0
  111. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/aws.py +0 -0
  112. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/base.py +0 -0
  113. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/local.py +0 -0
  114. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/mock.py +0 -0
  115. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/step_context.py +0 -0
  116. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/__init__.py +0 -0
  117. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/exceptions.py +0 -0
  118. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/registry.py +0 -0
  119. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/scheduled.py +0 -0
  120. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/step.py +0 -0
  121. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/validation.py +0 -0
  122. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/workflow.py +0 -0
  123. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/discovery.py +0 -0
  124. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/engine/__init__.py +0 -0
  125. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/engine/events.py +0 -0
  126. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/engine/executor.py +0 -0
  127. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/engine/replay.py +0 -0
  128. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/observability/__init__.py +0 -0
  129. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/observability/logging.py +0 -0
  130. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/__init__.py +0 -0
  131. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/child_handle.py +0 -0
  132. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/child_workflow.py +0 -0
  133. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/continue_as_new.py +0 -0
  134. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/define_hook.py +0 -0
  135. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/hooks.py +0 -0
  136. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/resume_hook.py +0 -0
  137. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/schedule.py +0 -0
  138. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/shield.py +0 -0
  139. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/sleep.py +0 -0
  140. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/step_checkpoint.py +0 -0
  141. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/step_hook.py +0 -0
  142. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/__init__.py +0 -0
  143. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/base.py +0 -0
  144. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/celery.py +0 -0
  145. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/factory.py +0 -0
  146. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/local.py +0 -0
  147. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/scheduler/__init__.py +0 -0
  148. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/scheduler/local.py +0 -0
  149. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/serialization/__init__.py +0 -0
  150. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/serialization/decoder.py +0 -0
  151. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/serialization/encoder.py +0 -0
  152. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/__init__.py +0 -0
  153. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/base.py +0 -0
  154. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/cassandra.py +0 -0
  155. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/citus.py +0 -0
  156. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/config.py +0 -0
  157. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/dynamodb.py +0 -0
  158. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/file.py +0 -0
  159. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/memory.py +0 -0
  160. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/migrations/__init__.py +0 -0
  161. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/migrations/base.py +0 -0
  162. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/mysql.py +0 -0
  163. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/postgres.py +0 -0
  164. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/schemas.py +0 -0
  165. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/sqlite.py +0 -0
  166. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/__init__.py +0 -0
  167. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/checkpoint.py +0 -0
  168. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/consumer.py +0 -0
  169. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/context.py +0 -0
  170. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/decorator.py +0 -0
  171. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/dispatcher.py +0 -0
  172. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/emit.py +0 -0
  173. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/registry.py +0 -0
  174. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/runtime.py +0 -0
  175. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/signal.py +0 -0
  176. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/step_context.py +0 -0
  177. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/tracing/__init__.py +0 -0
  178. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/tracing/base.py +0 -0
  179. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/tracing/factory.py +0 -0
  180. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/tracing/langfuse.py +0 -0
  181. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/tracing/types.py +0 -0
  182. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/utils/__init__.py +0 -0
  183. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/utils/duration.py +0 -0
  184. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/utils/helpers.py +0 -0
  185. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/utils/schedule.py +0 -0
  186. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow_engine.egg-info/SOURCES.txt +0 -0
  187. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/setup.cfg +0 -0
  188. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/__init__.py +0 -0
  189. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_cancellation.py +0 -0
  190. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_cassandra_storage.py +0 -0
  191. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_child_workflows.py +0 -0
  192. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_continue_as_new.py +0 -0
  193. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_dynamodb_storage.py +0 -0
  194. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_fault_tolerance.py +0 -0
  195. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_schedule_storage.py +0 -0
  196. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_schema_migrations.py +0 -0
  197. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_singleton.py +0 -0
  198. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_stream_e2e.py +0 -0
  199. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_workflow_suspended.py +0 -0
  200. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/__init__.py +0 -0
  201. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/__init__.py +0 -0
  202. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_cassandra_storage.py +0 -0
  203. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_citus_storage.py +0 -0
  204. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_dynamodb_storage.py +0 -0
  205. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_postgres_storage.py +0 -0
  206. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_sqlite_storage.py +0 -0
  207. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/conftest.py +0 -0
  208. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/storage/__init__.py +0 -0
  209. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/storage/test_migrations.py +0 -0
  210. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_cancellation.py +0 -0
  211. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_child_workflows.py +0 -0
  212. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_cli_worker.py +0 -0
  213. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_continue_as_new.py +0 -0
  214. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_emit.py +0 -0
  215. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_event_limits.py +0 -0
  216. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_executor.py +0 -0
  217. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_fault_tolerance.py +0 -0
  218. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_force_local.py +0 -0
  219. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_hooks.py +0 -0
  220. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_parent_run_id.py +0 -0
  221. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_primitives_from_steps.py +0 -0
  222. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_registry.py +0 -0
  223. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_replay.py +0 -0
  224. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_retention.py +0 -0
  225. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_schedule_schemas.py +0 -0
  226. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_schedule_utils.py +0 -0
  227. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_scheduled_workflow.py +0 -0
  228. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_signal.py +0 -0
  229. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_step.py +0 -0
  230. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_step_checkpoint.py +0 -0
  231. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_step_context.py +0 -0
  232. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_step_hook.py +0 -0
  233. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_stream_storage.py +0 -0
  234. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_stream_workflow.py +0 -0
  235. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_validation.py +0 -0
  236. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_workflow.py +0 -0
  237. {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/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.0
3
+ Version: 0.3.1
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.0"
10
+ version = "0.3.1"
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.0"
32
+ __version__ = "0.3.1"
33
33
 
34
34
  # Configuration
35
35
  from pyworkflow.config import (
@@ -11,8 +11,9 @@ Based on:
11
11
 
12
12
  import inspect
13
13
  import json
14
+ from collections.abc import Callable
14
15
  from hashlib import md5
15
- from typing import TYPE_CHECKING, Any
16
+ from typing import TYPE_CHECKING, Any, TypeVar
16
17
  from uuid import uuid4
17
18
 
18
19
  from celery import Task
@@ -20,8 +21,11 @@ from celery.exceptions import WorkerLostError
20
21
  from loguru import logger
21
22
 
22
23
  if TYPE_CHECKING:
24
+ from redis import Redis
23
25
  from redis.sentinel import Sentinel
24
26
 
27
+ T = TypeVar("T")
28
+
25
29
 
26
30
  def generate_lock_key(
27
31
  task_name: str,
@@ -74,10 +78,17 @@ class SingletonConfig:
74
78
 
75
79
 
76
80
  class RedisLockBackend:
77
- """Redis backend for distributed locking with Sentinel support."""
81
+ """Redis backend for distributed locking with Sentinel support.
82
+
83
+ During Sentinel failover (master rotation), cached connections can raise
84
+ MasterNotFoundError or socket TimeoutError. On those, we refresh the client
85
+ via ``sentinel.master_for()`` and retry the operation once. See
86
+ ``_execute_with_refresh``.
87
+ """
78
88
 
79
89
  _sentinel: "Sentinel | None"
80
90
  _master_name: str | None
91
+ redis: "Redis"
81
92
 
82
93
  def __init__(
83
94
  self,
@@ -91,21 +102,62 @@ class RedisLockBackend:
91
102
  from redis.sentinel import Sentinel
92
103
 
93
104
  sentinels = self._parse_sentinel_url(url)
105
+ # socket_timeout=5 matches FlowHunt's production-tested value.
106
+ # The previous 0.5s was too tight during Sentinel failover windows.
94
107
  self._sentinel = Sentinel(
95
108
  sentinels,
96
- socket_timeout=0.5,
109
+ socket_timeout=5,
97
110
  decode_responses=True,
98
111
  )
99
112
  self._master_name = sentinel_master or "mymaster"
100
- self.redis = self._sentinel.master_for(
101
- self._master_name,
102
- decode_responses=True,
103
- )
113
+ self.redis = self._build_master_client()
104
114
  else:
105
115
  self._sentinel = None
106
116
  self._master_name = None
107
117
  self.redis = redis.from_url(url, decode_responses=True)
108
118
 
119
+ def _build_master_client(self) -> "Redis":
120
+ """Get a fresh master client from Sentinel. Sentinel mode only."""
121
+ assert self._sentinel is not None and self._master_name is not None
122
+ return self._sentinel.master_for(
123
+ self._master_name,
124
+ decode_responses=True,
125
+ )
126
+
127
+ def _refresh_client(self) -> None:
128
+ """Re-resolve master via Sentinel after a failover-related error."""
129
+ if self._sentinel is None:
130
+ return
131
+ self.redis = self._build_master_client()
132
+ logger.info(
133
+ "Refreshed Redis master connection for sentinel master '{}'",
134
+ self._master_name,
135
+ )
136
+
137
+ def _execute_with_refresh(self, fn: Callable[..., T], *args: Any, **kwargs: Any) -> T:
138
+ """Run ``fn``; on Sentinel failover errors, refresh the client and retry once.
139
+
140
+ In non-sentinel mode this is a no-op passthrough — refresh has nothing
141
+ to do, so any exception propagates as before.
142
+ """
143
+ from redis.exceptions import ConnectionError as RedisConnectionError
144
+ from redis.exceptions import TimeoutError as RedisTimeoutError
145
+ from redis.sentinel import MasterNotFoundError
146
+
147
+ try:
148
+ return fn(*args, **kwargs)
149
+ except (MasterNotFoundError, RedisConnectionError, RedisTimeoutError) as exc:
150
+ if self._sentinel is None:
151
+ raise
152
+ logger.warning(
153
+ "Redis lock op hit sentinel failover error ({}: {}). "
154
+ "Refreshing master and retrying once.",
155
+ type(exc).__name__,
156
+ exc,
157
+ )
158
+ self._refresh_client()
159
+ return fn(*args, **kwargs)
160
+
109
161
  @staticmethod
110
162
  def _parse_sentinel_url(url: str) -> list[tuple[str, int]]:
111
163
  """
@@ -142,15 +194,19 @@ class RedisLockBackend:
142
194
 
143
195
  def lock(self, lock_key: str, task_id: str, expiry: int | None = None) -> bool:
144
196
  """Acquire lock atomically. Returns True if acquired."""
145
- return bool(self.redis.set(lock_key, task_id, nx=True, ex=expiry))
197
+ return bool(
198
+ self._execute_with_refresh(
199
+ lambda: self.redis.set(lock_key, task_id, nx=True, ex=expiry)
200
+ )
201
+ )
146
202
 
147
203
  def unlock(self, lock_key: str) -> None:
148
204
  """Release the lock."""
149
- self.redis.delete(lock_key)
205
+ self._execute_with_refresh(lambda: self.redis.delete(lock_key))
150
206
 
151
207
  def get(self, lock_key: str) -> str | None:
152
208
  """Get the task ID holding the lock."""
153
- return self.redis.get(lock_key)
209
+ return self._execute_with_refresh(lambda: self.redis.get(lock_key))
154
210
 
155
211
 
156
212
  class DuplicateTaskError(Exception):
@@ -364,6 +420,16 @@ class SingletonWorkflowTask(Task):
364
420
 
365
421
  # Lock not acquired - check for existing task
366
422
  existing_task_id = self.get_existing_task_id(lock_key)
423
+
424
+ # Self-retry: when celery's Task.retry() calls apply_async with the
425
+ # same task_id we already hold, it's the running task re-queueing
426
+ # itself. Allow the queue write through — on_retry will release the
427
+ # lock before the retried message is picked up. Without this branch,
428
+ # apply_async would silently return AsyncResult without enqueueing,
429
+ # and the retry would be lost.
430
+ if existing_task_id == task_id:
431
+ return super().apply_async(args, kwargs, task_id=task_id, **options)
432
+
367
433
  if existing_task_id:
368
434
  logger.debug(
369
435
  "Singleton: duplicate task blocked",
@@ -427,13 +493,17 @@ class SingletonWorkflowTask(Task):
427
493
  error=str(exc),
428
494
  )
429
495
 
430
- # Log appropriately
496
+ # Log appropriately. Keep str(exc) out of the message string: when
497
+ # kwargs are passed, Loguru calls .format() on the message, which
498
+ # parses any `{` from the exception (e.g. JSON in the message) as a
499
+ # format spec and raises KeyError.
431
500
  if isinstance(exc, WorkerLostError):
432
501
  logger.warning("Task interrupted due to worker loss", task_id=task_id)
433
502
  else:
434
503
  logger.error(
435
- f"Task {self.name} failed: {exc}",
504
+ f"Task {self.name} failed",
436
505
  task_id=task_id,
506
+ error=str(exc),
437
507
  traceback=einfo.traceback if einfo else None,
438
508
  )
439
509
 
@@ -442,8 +442,12 @@ def execute_step_task(
442
442
  if self.request.retries < max_retries:
443
443
  # Use exponential backoff for unexpected errors
444
444
  countdown = _calculate_exponential_backoff(self.request.retries)
445
+ # Don't inline str(e) into the message: Loguru calls .format() on
446
+ # the message when kwargs are present, and any `{` from the
447
+ # exception (e.g. a JSON body) is parsed as a format spec and
448
+ # raises KeyError, which would skip the retry below.
445
449
  logger.warning(
446
- f"Step failed (unexpected): {step_name}, retrying in {countdown:.1f}s...: {str(e)}",
450
+ f"Step failed (unexpected): {step_name}, retrying in {countdown:.1f}s...",
447
451
  run_id=run_id,
448
452
  step_id=step_id,
449
453
  error=str(e),
@@ -785,6 +785,132 @@ class TestRedisLockBackendSentinel:
785
785
  assert backend._sentinel is None
786
786
 
787
787
 
788
+ class TestRedisLockBackendAutoRefresh:
789
+ """Sentinel failover recovery in RedisLockBackend.
790
+
791
+ When the Sentinel master rotates, the cached Redis client can raise
792
+ ``MasterNotFoundError`` or ``TimeoutError``. The backend should transparently
793
+ refresh the client via ``sentinel.master_for()`` and retry the operation once.
794
+ """
795
+
796
+ def _make_sentinel_backend(self, mock_sentinel_class, stale_client, fresh_client):
797
+ """Build a sentinel-backed backend where master_for returns stale then fresh."""
798
+ mock_sentinel = mock_sentinel_class.return_value
799
+ mock_sentinel.master_for.side_effect = [stale_client, fresh_client]
800
+
801
+ return RedisLockBackend(
802
+ "sentinel://host1:26379/0",
803
+ is_sentinel=True,
804
+ sentinel_master="mymaster",
805
+ )
806
+
807
+ def test_lock_recovers_from_master_not_found(self):
808
+ """MasterNotFoundError on lock() triggers refresh and succeeds on retry."""
809
+ from redis.sentinel import MasterNotFoundError
810
+
811
+ with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
812
+ stale = MagicMock()
813
+ stale.set.side_effect = MasterNotFoundError("No master found for 'mymaster'")
814
+ fresh = MagicMock()
815
+ fresh.set.return_value = True
816
+
817
+ backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
818
+ result = backend.lock("test_key", "task_123", expiry=3600)
819
+
820
+ assert result is True
821
+ stale.set.assert_called_once()
822
+ fresh.set.assert_called_once_with("test_key", "task_123", nx=True, ex=3600)
823
+ # master_for called twice: initial + refresh
824
+ assert mock_sentinel_class.return_value.master_for.call_count == 2
825
+
826
+ def test_lock_recovers_from_timeout_error(self):
827
+ """Redis TimeoutError on lock() triggers refresh and succeeds on retry."""
828
+ from redis.exceptions import TimeoutError as RedisTimeoutError
829
+
830
+ with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
831
+ stale = MagicMock()
832
+ stale.set.side_effect = RedisTimeoutError("Timeout reading from socket")
833
+ fresh = MagicMock()
834
+ fresh.set.return_value = True
835
+
836
+ backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
837
+ result = backend.lock("test_key", "task_123", expiry=3600)
838
+
839
+ assert result is True
840
+ assert mock_sentinel_class.return_value.master_for.call_count == 2
841
+
842
+ def test_lock_recovers_from_connection_error(self):
843
+ """Redis ConnectionError on lock() triggers refresh and succeeds on retry."""
844
+ from redis.exceptions import ConnectionError as RedisConnectionError
845
+
846
+ with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
847
+ stale = MagicMock()
848
+ stale.set.side_effect = RedisConnectionError("Connection reset by peer")
849
+ fresh = MagicMock()
850
+ fresh.set.return_value = True
851
+
852
+ backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
853
+ assert backend.lock("test_key", "task_123", expiry=3600) is True
854
+ assert mock_sentinel_class.return_value.master_for.call_count == 2
855
+
856
+ def test_unlock_recovers_from_master_rotation(self):
857
+ """unlock() refreshes and retries once on Sentinel failover errors."""
858
+ from redis.sentinel import MasterNotFoundError
859
+
860
+ with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
861
+ stale = MagicMock()
862
+ stale.delete.side_effect = MasterNotFoundError("No master found")
863
+ fresh = MagicMock()
864
+
865
+ backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
866
+ backend.unlock("test_key")
867
+
868
+ fresh.delete.assert_called_once_with("test_key")
869
+
870
+ def test_get_recovers_from_master_rotation(self):
871
+ """get() refreshes and retries once on Sentinel failover errors."""
872
+ from redis.sentinel import MasterNotFoundError
873
+
874
+ with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
875
+ stale = MagicMock()
876
+ stale.get.side_effect = MasterNotFoundError("No master found")
877
+ fresh = MagicMock()
878
+ fresh.get.return_value = "task_123"
879
+
880
+ backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
881
+ assert backend.get("test_key") == "task_123"
882
+
883
+ def test_second_attempt_failure_propagates(self):
884
+ """If the retry also fails, the exception propagates."""
885
+ from redis.sentinel import MasterNotFoundError
886
+
887
+ with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
888
+ stale = MagicMock()
889
+ stale.set.side_effect = MasterNotFoundError("No master found")
890
+ fresh = MagicMock()
891
+ fresh.set.side_effect = MasterNotFoundError("Still no master")
892
+
893
+ backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
894
+ with pytest.raises(MasterNotFoundError):
895
+ backend.lock("test_key", "task_123", expiry=3600)
896
+
897
+ def test_non_sentinel_mode_does_not_refresh(self):
898
+ """Non-sentinel backend has no refresh path — exceptions propagate."""
899
+ from redis.sentinel import MasterNotFoundError
900
+
901
+ with patch("redis.from_url") as mock_from_url:
902
+ mock_redis = MagicMock()
903
+ mock_redis.set.side_effect = MasterNotFoundError("No master found")
904
+ mock_from_url.return_value = mock_redis
905
+
906
+ backend = RedisLockBackend("redis://localhost:6379/0")
907
+ with pytest.raises(MasterNotFoundError):
908
+ backend.lock("test_key", "task_123", expiry=3600)
909
+
910
+ # Only the initial resolution — no refresh attempts.
911
+ assert mock_redis.set.call_count == 1
912
+
913
+
788
914
  class TestSingletonConfigSentinel:
789
915
  """Test SingletonConfig Sentinel properties."""
790
916