pyworkflow-engine 0.2.15__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.2.15 → pyworkflow_engine-0.3.1}/PKG-INFO +1 -1
  2. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyproject.toml +1 -1
  3. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/__init__.py +1 -1
  4. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/celery/singleton.py +82 -12
  5. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/celery/tasks.py +43 -3
  6. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/config.py +3 -0
  7. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/context/base.py +17 -0
  8. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/core/registry.py +26 -2
  9. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/core/step.py +47 -0
  10. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/core/workflow.py +75 -0
  11. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/engine/executor.py +4 -0
  12. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/base.py +1 -0
  13. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/celery.py +2 -0
  14. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/local.py +2 -0
  15. pyworkflow_engine-0.3.1/pyworkflow/tracing/__init__.py +17 -0
  16. pyworkflow_engine-0.3.1/pyworkflow/tracing/base.py +166 -0
  17. pyworkflow_engine-0.3.1/pyworkflow/tracing/factory.py +46 -0
  18. pyworkflow_engine-0.3.1/pyworkflow/tracing/langfuse.py +184 -0
  19. pyworkflow_engine-0.3.1/pyworkflow/tracing/types.py +69 -0
  20. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow_engine.egg-info/SOURCES.txt +5 -0
  21. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_singleton.py +126 -0
  22. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/CLAUDE.md +0 -0
  23. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/DISTRIBUTED.md +0 -0
  24. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/LICENSE +0 -0
  25. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/MANIFEST.in +0 -0
  26. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/README.md +0 -0
  27. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/RELEASING.md +0 -0
  28. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/architecture.md +0 -0
  29. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/cancellation.mdx +0 -0
  30. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/continue-as-new.mdx +0 -0
  31. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/events.mdx +0 -0
  32. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/fault-tolerance.mdx +0 -0
  33. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/hooks.mdx +0 -0
  34. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/limitations.mdx +0 -0
  35. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/schedules.mdx +0 -0
  36. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/sleep.mdx +0 -0
  37. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/step-context.mdx +0 -0
  38. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/steps.mdx +0 -0
  39. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/stream-steps.mdx +0 -0
  40. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/stream-workflows.mdx +0 -0
  41. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/concepts/workflows.mdx +0 -0
  42. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/conventions.md +0 -0
  43. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/guides/brokers.mdx +0 -0
  44. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/guides/cli.mdx +0 -0
  45. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/guides/configuration.mdx +0 -0
  46. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/harness-gaps.md +0 -0
  47. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/introduction.mdx +0 -0
  48. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/layers.md +0 -0
  49. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/docs/quickstart.mdx +0 -0
  50. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/__init__.py +0 -0
  51. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/__init__.py +0 -0
  52. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/docker-compose.yml +0 -0
  53. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/pyworkflow.config.yaml +0 -0
  54. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/__init__.py +0 -0
  55. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/basic.py +0 -0
  56. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/batch_processing.py +0 -0
  57. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/cancellation.py +0 -0
  58. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
  59. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
  60. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/child_workflows.py +0 -0
  61. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/continue_as_new.py +0 -0
  62. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
  63. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/hooks.py +0 -0
  64. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/idempotency.py +0 -0
  65. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/long_running.py +0 -0
  66. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/retries.py +0 -0
  67. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/schedules.py +0 -0
  68. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
  69. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/step_context.py +0 -0
  70. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/transient/01_basic_workflow.py +0 -0
  71. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/transient/02_fault_tolerance.py +0 -0
  72. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/transient/__init__.py +0 -0
  73. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/celery/transient/pyworkflow.config.yaml +0 -0
  74. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/__init__.py +0 -0
  75. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/01_basic_workflow.py +0 -0
  76. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/02_file_storage.py +0 -0
  77. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/03_retries.py +0 -0
  78. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/04_long_running.py +0 -0
  79. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/05_event_log.py +0 -0
  80. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/06_idempotency.py +0 -0
  81. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/07_hooks.py +0 -0
  82. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/08_cancellation.py +0 -0
  83. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/09_child_workflows.py +0 -0
  84. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/10_child_workflow_patterns.py +0 -0
  85. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/11_continue_as_new.py +0 -0
  86. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/12_schedules.py +0 -0
  87. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/13_step_context.py +0 -0
  88. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/14_child_workflow_from_step.py +0 -0
  89. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/durable/__init__.py +0 -0
  90. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/transient/01_quick_tasks.py +0 -0
  91. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/transient/02_retries.py +0 -0
  92. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/transient/03_sleep.py +0 -0
  93. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/examples/local/transient/__init__.py +0 -0
  94. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/aws/__init__.py +0 -0
  95. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/aws/context.py +0 -0
  96. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/aws/handler.py +0 -0
  97. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/aws/testing.py +0 -0
  98. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/celery/__init__.py +0 -0
  99. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/celery/app.py +0 -0
  100. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/celery/loop.py +0 -0
  101. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/celery/scheduler.py +0 -0
  102. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/__init__.py +0 -0
  103. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/__main__.py +0 -0
  104. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/__init__.py +0 -0
  105. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/hooks.py +0 -0
  106. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/quickstart.py +0 -0
  107. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/runs.py +0 -0
  108. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/scheduler.py +0 -0
  109. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/schedules.py +0 -0
  110. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/setup.py +0 -0
  111. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/worker.py +0 -0
  112. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/workflows.py +0 -0
  113. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/output/__init__.py +0 -0
  114. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/output/formatters.py +0 -0
  115. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/output/styles.py +0 -0
  116. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/__init__.py +0 -0
  117. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/async_helpers.py +0 -0
  118. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/config.py +0 -0
  119. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/config_generator.py +0 -0
  120. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/discovery.py +0 -0
  121. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/docker_manager.py +0 -0
  122. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/interactive.py +0 -0
  123. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/storage.py +0 -0
  124. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/context/__init__.py +0 -0
  125. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/context/aws.py +0 -0
  126. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/context/local.py +0 -0
  127. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/context/mock.py +0 -0
  128. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/context/step_context.py +0 -0
  129. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/core/__init__.py +0 -0
  130. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/core/exceptions.py +0 -0
  131. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/core/scheduled.py +0 -0
  132. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/core/validation.py +0 -0
  133. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/discovery.py +0 -0
  134. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/engine/__init__.py +0 -0
  135. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/engine/events.py +0 -0
  136. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/engine/replay.py +0 -0
  137. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/observability/__init__.py +0 -0
  138. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/observability/logging.py +0 -0
  139. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/__init__.py +0 -0
  140. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/child_handle.py +0 -0
  141. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/child_workflow.py +0 -0
  142. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/continue_as_new.py +0 -0
  143. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/define_hook.py +0 -0
  144. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/hooks.py +0 -0
  145. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/resume_hook.py +0 -0
  146. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/schedule.py +0 -0
  147. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/shield.py +0 -0
  148. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/sleep.py +0 -0
  149. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/step_checkpoint.py +0 -0
  150. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/step_hook.py +0 -0
  151. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/__init__.py +0 -0
  152. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/factory.py +0 -0
  153. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/scheduler/__init__.py +0 -0
  154. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/scheduler/local.py +0 -0
  155. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/serialization/__init__.py +0 -0
  156. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/serialization/decoder.py +0 -0
  157. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/serialization/encoder.py +0 -0
  158. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/__init__.py +0 -0
  159. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/base.py +0 -0
  160. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/cassandra.py +0 -0
  161. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/citus.py +0 -0
  162. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/config.py +0 -0
  163. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/dynamodb.py +0 -0
  164. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/file.py +0 -0
  165. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/memory.py +0 -0
  166. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/migrations/__init__.py +0 -0
  167. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/migrations/base.py +0 -0
  168. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/mysql.py +0 -0
  169. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/postgres.py +0 -0
  170. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/schemas.py +0 -0
  171. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/storage/sqlite.py +0 -0
  172. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/streams/__init__.py +0 -0
  173. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/streams/checkpoint.py +0 -0
  174. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/streams/consumer.py +0 -0
  175. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/streams/context.py +0 -0
  176. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/streams/decorator.py +0 -0
  177. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/streams/dispatcher.py +0 -0
  178. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/streams/emit.py +0 -0
  179. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/streams/registry.py +0 -0
  180. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/streams/runtime.py +0 -0
  181. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/streams/signal.py +0 -0
  182. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/streams/step_context.py +0 -0
  183. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/utils/__init__.py +0 -0
  184. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/utils/duration.py +0 -0
  185. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/utils/helpers.py +0 -0
  186. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/pyworkflow/utils/schedule.py +0 -0
  187. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/setup.cfg +0 -0
  188. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/__init__.py +0 -0
  189. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/test_cancellation.py +0 -0
  190. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/test_cassandra_storage.py +0 -0
  191. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/test_child_workflows.py +0 -0
  192. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/test_continue_as_new.py +0 -0
  193. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/test_dynamodb_storage.py +0 -0
  194. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/test_fault_tolerance.py +0 -0
  195. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/test_schedule_storage.py +0 -0
  196. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/test_schema_migrations.py +0 -0
  197. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/test_singleton.py +0 -0
  198. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/test_stream_e2e.py +0 -0
  199. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/integration/test_workflow_suspended.py +0 -0
  200. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/__init__.py +0 -0
  201. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/backends/__init__.py +0 -0
  202. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_cassandra_storage.py +0 -0
  203. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_citus_storage.py +0 -0
  204. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_dynamodb_storage.py +0 -0
  205. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_postgres_storage.py +0 -0
  206. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_sqlite_storage.py +0 -0
  207. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/conftest.py +0 -0
  208. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/storage/__init__.py +0 -0
  209. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/storage/test_migrations.py +0 -0
  210. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_cancellation.py +0 -0
  211. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_child_workflows.py +0 -0
  212. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_cli_worker.py +0 -0
  213. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_continue_as_new.py +0 -0
  214. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_emit.py +0 -0
  215. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_event_limits.py +0 -0
  216. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_executor.py +0 -0
  217. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_fault_tolerance.py +0 -0
  218. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_force_local.py +0 -0
  219. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_hooks.py +0 -0
  220. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_parent_run_id.py +0 -0
  221. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_primitives_from_steps.py +0 -0
  222. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_registry.py +0 -0
  223. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_replay.py +0 -0
  224. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_retention.py +0 -0
  225. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_schedule_schemas.py +0 -0
  226. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_schedule_utils.py +0 -0
  227. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_scheduled_workflow.py +0 -0
  228. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_signal.py +0 -0
  229. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_step.py +0 -0
  230. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_step_checkpoint.py +0 -0
  231. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_step_context.py +0 -0
  232. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_step_hook.py +0 -0
  233. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_stream_storage.py +0 -0
  234. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_stream_workflow.py +0 -0
  235. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_validation.py +0 -0
  236. {pyworkflow_engine-0.2.15 → pyworkflow_engine-0.3.1}/tests/unit/test_workflow.py +0 -0
  237. {pyworkflow_engine-0.2.15 → 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.2.15
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.2.15"
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.2.15"
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
 
@@ -10,6 +10,7 @@ These tasks enable:
10
10
  """
11
11
 
12
12
  import asyncio
13
+ import hashlib
13
14
  import random
14
15
  import uuid
15
16
  from collections.abc import Callable
@@ -93,6 +94,8 @@ def execute_step_task(
93
94
  context_data: dict[str, Any] | None = None,
94
95
  context_class_name: str | None = None,
95
96
  workflow_name: str | None = None,
97
+ tracing: dict[str, Any] | None = None,
98
+ is_generator: bool = False,
96
99
  ) -> Any:
97
100
  """
98
101
  Execute a workflow step on a Celery worker.
@@ -242,6 +245,7 @@ def execute_step_task(
242
245
  step_workflow_ctx._runtime = "celery"
243
246
  step_workflow_ctx._storage_config = storage_config
244
247
  step_workflow_ctx._is_step_worker = True
248
+ step_workflow_ctx.tracing = tracing
245
249
 
246
250
  # Set parent_run_id if this workflow is a child
247
251
  wf_run = run_async(storage.get_run(run_id))
@@ -284,6 +288,26 @@ def execute_step_task(
284
288
  step_id=step_id,
285
289
  )
286
290
 
291
+ # Record tracing span for this step on the worker
292
+ if tracing and result is not None:
293
+ try:
294
+ from pyworkflow.tracing import create_tracing_provider
295
+
296
+ _tp = create_tracing_provider(tracing)
297
+ if _tp:
298
+ _tp.record_step_span(
299
+ trace_id=hashlib.md5(run_id.encode()).hexdigest(),
300
+ step_name=step_name,
301
+ step_id=step_id,
302
+ is_generator=is_generator,
303
+ result=result,
304
+ step_input=dict(kwargs) if kwargs else None,
305
+ trace_name=workflow_name or "workflow",
306
+ )
307
+ run_async(_tp.shutdown())
308
+ except Exception:
309
+ pass # Never fail step because of tracing
310
+
287
311
  # Record STEP_COMPLETED event and trigger workflow resumption
288
312
  run_async(
289
313
  _record_step_completion_and_resume(
@@ -418,8 +442,12 @@ def execute_step_task(
418
442
  if self.request.retries < max_retries:
419
443
  # Use exponential backoff for unexpected errors
420
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.
421
449
  logger.warning(
422
- 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...",
423
451
  run_id=run_id,
424
452
  step_id=step_id,
425
453
  error=str(e),
@@ -855,6 +883,7 @@ def start_workflow_task(
855
883
  run_id: str,
856
884
  storage_config: dict[str, Any] | None = None,
857
885
  idempotency_key: str | None = None,
886
+ tracing: dict[str, Any] | None = None,
858
887
  ) -> str:
859
888
  """
860
889
  Start a workflow execution.
@@ -906,6 +935,7 @@ def start_workflow_task(
906
935
  storage_config=storage_config,
907
936
  idempotency_key=idempotency_key,
908
937
  run_id=run_id,
938
+ tracing=tracing,
909
939
  )
910
940
  )
911
941
 
@@ -1431,6 +1461,9 @@ async def _recover_workflow_on_worker(
1431
1461
  args = deserialize_args(run.input_args)
1432
1462
  kwargs = deserialize_kwargs(run.input_kwargs)
1433
1463
 
1464
+ # Extract tracing config persisted at start
1465
+ _recover_tracing = kwargs.pop("_tracing_config", None)
1466
+
1434
1467
  # Execute workflow with event replay
1435
1468
  try:
1436
1469
  result = await execute_workflow_with_context(
@@ -1444,6 +1477,7 @@ async def _recover_workflow_on_worker(
1444
1477
  runtime="celery",
1445
1478
  storage_config=storage_config,
1446
1479
  parent_run_id=run.parent_run_id,
1480
+ tracing=workflow_meta.tracing or _recover_tracing,
1447
1481
  )
1448
1482
 
1449
1483
  # Update run status to completed
@@ -1579,6 +1613,7 @@ async def _start_workflow_on_worker(
1579
1613
  storage_config: dict[str, Any] | None = None,
1580
1614
  idempotency_key: str | None = None,
1581
1615
  run_id: str | None = None,
1616
+ tracing: dict[str, Any] | None = None,
1582
1617
  ) -> str:
1583
1618
  """
1584
1619
  Internal function to start workflow on Celery worker.
@@ -1779,10 +1814,10 @@ async def _start_workflow_on_worker(
1779
1814
  created_at=datetime.now(UTC),
1780
1815
  started_at=datetime.now(UTC),
1781
1816
  input_args=serialize_args(*args),
1782
- input_kwargs=serialize_kwargs(**kwargs),
1817
+ input_kwargs=serialize_kwargs(**kwargs, _tracing_config=tracing),
1783
1818
  idempotency_key=idempotency_key,
1784
1819
  max_duration=workflow_meta.max_duration,
1785
- context={}, # Step context (not from decorator)
1820
+ context={}, # Step context data
1786
1821
  recovery_attempts=0,
1787
1822
  max_recovery_attempts=max_recovery_attempts,
1788
1823
  recover_on_worker_loss=recover_on_worker_loss,
@@ -1812,6 +1847,7 @@ async def _start_workflow_on_worker(
1812
1847
  kwargs=kwargs,
1813
1848
  runtime="celery",
1814
1849
  storage_config=storage_config,
1850
+ tracing=tracing or workflow_meta.tracing,
1815
1851
  )
1816
1852
 
1817
1853
  # Update run status to completed
@@ -2392,6 +2428,9 @@ async def _resume_workflow_on_worker(
2392
2428
  args = deserialize_args(run.input_args)
2393
2429
  kwargs = deserialize_kwargs(run.input_kwargs)
2394
2430
 
2431
+ # Extract tracing config persisted at start
2432
+ _resume_tracing = kwargs.pop("_tracing_config", None)
2433
+
2395
2434
  # Execute workflow with event replay
2396
2435
  try:
2397
2436
  result = await execute_workflow_with_context(
@@ -2406,6 +2445,7 @@ async def _resume_workflow_on_worker(
2406
2445
  runtime="celery",
2407
2446
  storage_config=storage_config,
2408
2447
  parent_run_id=run.parent_run_id,
2448
+ tracing=workflow_meta.tracing or _resume_tracing,
2409
2449
  )
2410
2450
 
2411
2451
  # Update run status to completed
@@ -171,6 +171,9 @@ class PyWorkflowConfig:
171
171
  event_hard_limit: int = 50_000 # Fail workflow at this count
172
172
  event_warning_interval: int = 100 # Log warning every N events after soft limit
173
173
 
174
+ # Tracing Configuration
175
+ TRACING_CONFIG: dict | None = None
176
+
174
177
 
175
178
  def _config_from_env_and_yaml() -> PyWorkflowConfig:
176
179
  """
@@ -133,6 +133,9 @@ class WorkflowContext(ABC):
133
133
  """
134
134
  self._run_id = run_id
135
135
  self._workflow_name = workflow_name
136
+ self._tracing: dict[str, Any] | None = None
137
+ self._tracing_provider: Any = None # TracingProvider instance
138
+ self._trace_id: str | None = None # Stable trace ID
136
139
 
137
140
  @property
138
141
  def run_id(self) -> str:
@@ -144,6 +147,20 @@ class WorkflowContext(ABC):
144
147
  """Get the current workflow name."""
145
148
  return self._workflow_name
146
149
 
150
+ @property
151
+ def tracing(self) -> dict[str, Any] | None:
152
+ """Get the tracing provider config (e.g. Langfuse credentials), or None."""
153
+ return self._tracing
154
+
155
+ @tracing.setter
156
+ def tracing(self, value: dict[str, Any] | None) -> None:
157
+ self._tracing = value
158
+
159
+ @property
160
+ def tracing_provider(self) -> Any:
161
+ """Get the active TracingProvider instance, or None."""
162
+ return self._tracing_provider
163
+
147
164
  # =========================================================================
148
165
  # Abstract methods - must be implemented by subclasses
149
166
  # =========================================================================
@@ -23,6 +23,9 @@ class WorkflowMetadata:
23
23
  tags: list[str] | None = None
24
24
  description: str | None = None # Docstring from the workflow function
25
25
  context_class: type | None = None # StepContext subclass for step context access
26
+ tracing: dict[str, Any] | None = (
27
+ None # Optional tracing provider config (e.g. Langfuse credentials)
28
+ )
26
29
 
27
30
  def __post_init__(self) -> None:
28
31
  if self.tags is None:
@@ -44,6 +47,9 @@ class StepMetadata:
44
47
  timeout: int | None = None
45
48
  metadata: dict[str, Any] | None = None
46
49
  force_local: bool = False
50
+ is_generator: bool = (
51
+ False # If True, creates a Langfuse generation span instead of a regular span
52
+ )
47
53
 
48
54
  def __post_init__(self) -> None:
49
55
  if self.metadata is None:
@@ -73,6 +79,7 @@ class WorkflowRegistry:
73
79
  max_duration: str | None = None,
74
80
  tags: list[str] | None = None,
75
81
  context_class: type | None = None,
82
+ tracing: dict[str, Any] | None = None,
76
83
  ) -> None:
77
84
  """
78
85
  Register a workflow.
@@ -84,6 +91,7 @@ class WorkflowRegistry:
84
91
  max_duration: Optional maximum duration
85
92
  tags: Optional list of tags (max 3)
86
93
  context_class: Optional StepContext subclass for step context access
94
+ tracing: Optional tracing provider config dict (e.g. Langfuse credentials)
87
95
  """
88
96
  if name in self._workflows:
89
97
  existing = self._workflows[name]
@@ -101,6 +109,7 @@ class WorkflowRegistry:
101
109
  max_duration=max_duration,
102
110
  tags=tags or [],
103
111
  context_class=context_class,
112
+ tracing=tracing,
104
113
  )
105
114
 
106
115
  self._workflows[name] = workflow_meta
@@ -160,6 +169,7 @@ class WorkflowRegistry:
160
169
  timeout: int | None = None,
161
170
  metadata: dict[str, Any] | None = None,
162
171
  force_local: bool = False,
172
+ is_generator: bool = False,
163
173
  ) -> None:
164
174
  """
165
175
  Register a step.
@@ -173,6 +183,7 @@ class WorkflowRegistry:
173
183
  timeout: Optional timeout in seconds
174
184
  metadata: Optional metadata dict
175
185
  force_local: If True, always execute inline even in distributed runtimes
186
+ is_generator: If True, creates a Langfuse generation span instead of a regular span
176
187
  """
177
188
  if name in self._steps:
178
189
  existing = self._steps[name]
@@ -190,6 +201,7 @@ class WorkflowRegistry:
190
201
  timeout=timeout,
191
202
  metadata=metadata or {},
192
203
  force_local=force_local,
204
+ is_generator=is_generator,
193
205
  )
194
206
 
195
207
  self._steps[name] = step_meta
@@ -259,9 +271,12 @@ def register_workflow(
259
271
  max_duration: str | None = None,
260
272
  tags: list[str] | None = None,
261
273
  context_class: type | None = None,
274
+ tracing: dict[str, Any] | None = None,
262
275
  ) -> None:
263
276
  """Register a workflow in the global registry."""
264
- _registry.register_workflow(name, func, original_func, max_duration, tags, context_class)
277
+ _registry.register_workflow(
278
+ name, func, original_func, max_duration, tags, context_class, tracing
279
+ )
265
280
 
266
281
 
267
282
  def get_workflow(name: str) -> WorkflowMetadata | None:
@@ -293,10 +308,19 @@ def register_step(
293
308
  timeout: int | None = None,
294
309
  metadata: dict[str, Any] | None = None,
295
310
  force_local: bool = False,
311
+ is_generator: bool = False,
296
312
  ) -> None:
297
313
  """Register a step in the global registry."""
298
314
  _registry.register_step(
299
- name, func, original_func, max_retries, retry_delay, timeout, metadata, force_local
315
+ name,
316
+ func,
317
+ original_func,
318
+ max_retries,
319
+ retry_delay,
320
+ timeout,
321
+ metadata,
322
+ force_local,
323
+ is_generator,
300
324
  )
301
325
 
302
326
 
@@ -58,6 +58,7 @@ def step(
58
58
  timeout: int | None = None,
59
59
  metadata: dict[str, Any] | None = None,
60
60
  force_local: bool = False,
61
+ is_generator: bool = False,
61
62
  ) -> Callable:
62
63
  """
63
64
  Decorator to mark functions as workflow steps.
@@ -77,6 +78,8 @@ def step(
77
78
  force_local: If True, always execute inline even in distributed
78
79
  runtimes (e.g., Celery). Useful for lightweight steps where
79
80
  the overhead of message broker dispatch is undesirable.
81
+ is_generator: If True, creates a Langfuse generation span instead of
82
+ a regular span. Use for LLM/AI steps that produce generated content.
80
83
 
81
84
  Returns:
82
85
  Decorated step function
@@ -209,6 +212,7 @@ def step(
209
212
  max_retries=max_retries,
210
213
  retry_delay=retry_delay,
211
214
  timeout=timeout,
215
+ is_generator=is_generator,
212
216
  )
213
217
 
214
218
  # Check if we're resuming from a retry
@@ -297,6 +301,9 @@ def step(
297
301
  step_id=step_id,
298
302
  )
299
303
 
304
+ # Record tracing span for locally executed step
305
+ _record_step_tracing(ctx, step_name, step_id, is_generator, result, args, kwargs)
306
+
300
307
  return result
301
308
 
302
309
  except SuspensionSignal:
@@ -458,6 +465,7 @@ def step(
458
465
  timeout=timeout,
459
466
  metadata=metadata,
460
467
  force_local=force_local,
468
+ is_generator=is_generator,
461
469
  )
462
470
 
463
471
  # Store metadata on wrapper
@@ -468,6 +476,7 @@ def step(
468
476
  wrapper.__step_timeout__ = timeout # type: ignore[attr-defined]
469
477
  wrapper.__step_metadata__ = metadata or {} # type: ignore[attr-defined]
470
478
  wrapper.__step_force_local__ = force_local # type: ignore[attr-defined]
479
+ wrapper.__step_is_generator__ = is_generator # type: ignore[attr-defined]
471
480
 
472
481
  return wrapper
473
482
 
@@ -627,6 +636,7 @@ async def _dispatch_step_to_celery(
627
636
  max_retries: int,
628
637
  retry_delay: str | int | list[int],
629
638
  timeout: int | None,
639
+ is_generator: bool = False,
630
640
  ) -> Any:
631
641
  """
632
642
  Dispatch step execution to Celery step worker.
@@ -736,6 +746,8 @@ async def _dispatch_step_to_celery(
736
746
  context_data=context_data,
737
747
  context_class_name=context_class_name,
738
748
  workflow_name=ctx.workflow_name,
749
+ tracing=ctx.tracing,
750
+ is_generator=is_generator,
739
751
  )
740
752
 
741
753
  logger.info(
@@ -753,3 +765,38 @@ async def _dispatch_step_to_celery(
753
765
  step_name=step_name,
754
766
  task_id=task_result.id,
755
767
  )
768
+
769
+
770
+ def _record_step_tracing(
771
+ ctx: Any,
772
+ step_name: str,
773
+ step_id: str,
774
+ is_generator: bool,
775
+ result: Any,
776
+ step_args: tuple = (),
777
+ step_kwargs: dict | None = None,
778
+ ) -> None:
779
+ """Record a tracing span for a completed step. No-op if tracing is not configured."""
780
+ tp = getattr(ctx, "_tracing_provider", None)
781
+ if not tp:
782
+ return
783
+ try:
784
+ trace_id = getattr(ctx, "_trace_id", None)
785
+ if not trace_id:
786
+ return
787
+
788
+ step_input = dict(step_kwargs) if step_kwargs else {}
789
+ if step_args:
790
+ step_input["_args"] = [str(a)[:500] for a in step_args]
791
+
792
+ tp.record_step_span(
793
+ trace_id=trace_id,
794
+ step_name=step_name,
795
+ step_id=step_id,
796
+ is_generator=is_generator,
797
+ result=result,
798
+ step_input=step_input or None,
799
+ trace_name=ctx.workflow_name,
800
+ )
801
+ except Exception:
802
+ pass # Never fail the workflow because of tracing