pyworkflow-engine 0.2.14__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/PKG-INFO +1 -1
  2. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyproject.toml +1 -1
  3. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/__init__.py +1 -1
  4. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/tasks.py +53 -2
  5. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/config.py +3 -0
  6. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/base.py +17 -0
  7. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/registry.py +26 -2
  8. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/step.py +47 -0
  9. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/workflow.py +75 -0
  10. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/engine/executor.py +4 -0
  11. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/runtime/base.py +1 -0
  12. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/runtime/celery.py +2 -0
  13. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/runtime/local.py +2 -0
  14. pyworkflow_engine-0.3.0/pyworkflow/tracing/__init__.py +17 -0
  15. pyworkflow_engine-0.3.0/pyworkflow/tracing/base.py +166 -0
  16. pyworkflow_engine-0.3.0/pyworkflow/tracing/factory.py +46 -0
  17. pyworkflow_engine-0.3.0/pyworkflow/tracing/langfuse.py +184 -0
  18. pyworkflow_engine-0.3.0/pyworkflow/tracing/types.py +69 -0
  19. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow_engine.egg-info/SOURCES.txt +5 -0
  20. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/CLAUDE.md +0 -0
  21. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/DISTRIBUTED.md +0 -0
  22. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/LICENSE +0 -0
  23. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/MANIFEST.in +0 -0
  24. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/README.md +0 -0
  25. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/RELEASING.md +0 -0
  26. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/architecture.md +0 -0
  27. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/cancellation.mdx +0 -0
  28. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/continue-as-new.mdx +0 -0
  29. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/events.mdx +0 -0
  30. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/fault-tolerance.mdx +0 -0
  31. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/hooks.mdx +0 -0
  32. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/limitations.mdx +0 -0
  33. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/schedules.mdx +0 -0
  34. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/sleep.mdx +0 -0
  35. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/step-context.mdx +0 -0
  36. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/steps.mdx +0 -0
  37. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/stream-steps.mdx +0 -0
  38. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/stream-workflows.mdx +0 -0
  39. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/workflows.mdx +0 -0
  40. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/conventions.md +0 -0
  41. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/guides/brokers.mdx +0 -0
  42. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/guides/cli.mdx +0 -0
  43. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/guides/configuration.mdx +0 -0
  44. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/harness-gaps.md +0 -0
  45. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/introduction.mdx +0 -0
  46. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/layers.md +0 -0
  47. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/quickstart.mdx +0 -0
  48. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/__init__.py +0 -0
  49. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/__init__.py +0 -0
  50. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/docker-compose.yml +0 -0
  51. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/pyworkflow.config.yaml +0 -0
  52. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/__init__.py +0 -0
  53. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/basic.py +0 -0
  54. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/batch_processing.py +0 -0
  55. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/cancellation.py +0 -0
  56. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
  57. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
  58. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/child_workflows.py +0 -0
  59. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/continue_as_new.py +0 -0
  60. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
  61. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/hooks.py +0 -0
  62. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/idempotency.py +0 -0
  63. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/long_running.py +0 -0
  64. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/retries.py +0 -0
  65. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/schedules.py +0 -0
  66. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
  67. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/step_context.py +0 -0
  68. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/transient/01_basic_workflow.py +0 -0
  69. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/transient/02_fault_tolerance.py +0 -0
  70. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/transient/__init__.py +0 -0
  71. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/transient/pyworkflow.config.yaml +0 -0
  72. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/__init__.py +0 -0
  73. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/01_basic_workflow.py +0 -0
  74. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/02_file_storage.py +0 -0
  75. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/03_retries.py +0 -0
  76. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/04_long_running.py +0 -0
  77. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/05_event_log.py +0 -0
  78. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/06_idempotency.py +0 -0
  79. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/07_hooks.py +0 -0
  80. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/08_cancellation.py +0 -0
  81. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/09_child_workflows.py +0 -0
  82. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/10_child_workflow_patterns.py +0 -0
  83. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/11_continue_as_new.py +0 -0
  84. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/12_schedules.py +0 -0
  85. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/13_step_context.py +0 -0
  86. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/14_child_workflow_from_step.py +0 -0
  87. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/__init__.py +0 -0
  88. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/transient/01_quick_tasks.py +0 -0
  89. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/transient/02_retries.py +0 -0
  90. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/transient/03_sleep.py +0 -0
  91. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/transient/__init__.py +0 -0
  92. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/aws/__init__.py +0 -0
  93. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/aws/context.py +0 -0
  94. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/aws/handler.py +0 -0
  95. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/aws/testing.py +0 -0
  96. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/__init__.py +0 -0
  97. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/app.py +0 -0
  98. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/loop.py +0 -0
  99. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/scheduler.py +0 -0
  100. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/singleton.py +0 -0
  101. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/__init__.py +0 -0
  102. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/__main__.py +0 -0
  103. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/__init__.py +0 -0
  104. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/hooks.py +0 -0
  105. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/quickstart.py +0 -0
  106. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/runs.py +0 -0
  107. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/scheduler.py +0 -0
  108. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/schedules.py +0 -0
  109. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/setup.py +0 -0
  110. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/worker.py +0 -0
  111. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/workflows.py +0 -0
  112. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/output/__init__.py +0 -0
  113. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/output/formatters.py +0 -0
  114. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/output/styles.py +0 -0
  115. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/__init__.py +0 -0
  116. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/async_helpers.py +0 -0
  117. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/config.py +0 -0
  118. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/config_generator.py +0 -0
  119. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/discovery.py +0 -0
  120. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/docker_manager.py +0 -0
  121. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/interactive.py +0 -0
  122. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/storage.py +0 -0
  123. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/__init__.py +0 -0
  124. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/aws.py +0 -0
  125. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/local.py +0 -0
  126. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/mock.py +0 -0
  127. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/step_context.py +0 -0
  128. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/__init__.py +0 -0
  129. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/exceptions.py +0 -0
  130. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/scheduled.py +0 -0
  131. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/validation.py +0 -0
  132. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/discovery.py +0 -0
  133. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/engine/__init__.py +0 -0
  134. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/engine/events.py +0 -0
  135. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/engine/replay.py +0 -0
  136. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/observability/__init__.py +0 -0
  137. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/observability/logging.py +0 -0
  138. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/__init__.py +0 -0
  139. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/child_handle.py +0 -0
  140. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/child_workflow.py +0 -0
  141. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/continue_as_new.py +0 -0
  142. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/define_hook.py +0 -0
  143. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/hooks.py +0 -0
  144. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/resume_hook.py +0 -0
  145. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/schedule.py +0 -0
  146. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/shield.py +0 -0
  147. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/sleep.py +0 -0
  148. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/step_checkpoint.py +0 -0
  149. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/step_hook.py +0 -0
  150. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/runtime/__init__.py +0 -0
  151. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/runtime/factory.py +0 -0
  152. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/scheduler/__init__.py +0 -0
  153. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/scheduler/local.py +0 -0
  154. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/serialization/__init__.py +0 -0
  155. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/serialization/decoder.py +0 -0
  156. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/serialization/encoder.py +0 -0
  157. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/__init__.py +0 -0
  158. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/base.py +0 -0
  159. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/cassandra.py +0 -0
  160. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/citus.py +0 -0
  161. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/config.py +0 -0
  162. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/dynamodb.py +0 -0
  163. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/file.py +0 -0
  164. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/memory.py +0 -0
  165. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/migrations/__init__.py +0 -0
  166. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/migrations/base.py +0 -0
  167. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/mysql.py +0 -0
  168. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/postgres.py +0 -0
  169. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/schemas.py +0 -0
  170. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/sqlite.py +0 -0
  171. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/__init__.py +0 -0
  172. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/checkpoint.py +0 -0
  173. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/consumer.py +0 -0
  174. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/context.py +0 -0
  175. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/decorator.py +0 -0
  176. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/dispatcher.py +0 -0
  177. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/emit.py +0 -0
  178. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/registry.py +0 -0
  179. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/runtime.py +0 -0
  180. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/signal.py +0 -0
  181. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/step_context.py +0 -0
  182. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/utils/__init__.py +0 -0
  183. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/utils/duration.py +0 -0
  184. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/utils/helpers.py +0 -0
  185. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/utils/schedule.py +0 -0
  186. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/setup.cfg +0 -0
  187. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/__init__.py +0 -0
  188. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_cancellation.py +0 -0
  189. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_cassandra_storage.py +0 -0
  190. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_child_workflows.py +0 -0
  191. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_continue_as_new.py +0 -0
  192. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_dynamodb_storage.py +0 -0
  193. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_fault_tolerance.py +0 -0
  194. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_schedule_storage.py +0 -0
  195. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_schema_migrations.py +0 -0
  196. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_singleton.py +0 -0
  197. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_stream_e2e.py +0 -0
  198. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_workflow_suspended.py +0 -0
  199. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/__init__.py +0 -0
  200. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/__init__.py +0 -0
  201. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/test_cassandra_storage.py +0 -0
  202. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/test_citus_storage.py +0 -0
  203. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/test_dynamodb_storage.py +0 -0
  204. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/test_postgres_storage.py +0 -0
  205. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/test_sqlite_storage.py +0 -0
  206. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/conftest.py +0 -0
  207. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/storage/__init__.py +0 -0
  208. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/storage/test_migrations.py +0 -0
  209. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_cancellation.py +0 -0
  210. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_child_workflows.py +0 -0
  211. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_cli_worker.py +0 -0
  212. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_continue_as_new.py +0 -0
  213. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_emit.py +0 -0
  214. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_event_limits.py +0 -0
  215. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_executor.py +0 -0
  216. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_fault_tolerance.py +0 -0
  217. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_force_local.py +0 -0
  218. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_hooks.py +0 -0
  219. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_parent_run_id.py +0 -0
  220. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_primitives_from_steps.py +0 -0
  221. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_registry.py +0 -0
  222. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_replay.py +0 -0
  223. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_retention.py +0 -0
  224. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_schedule_schemas.py +0 -0
  225. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_schedule_utils.py +0 -0
  226. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_scheduled_workflow.py +0 -0
  227. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_signal.py +0 -0
  228. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_singleton.py +0 -0
  229. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_step.py +0 -0
  230. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_step_checkpoint.py +0 -0
  231. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_step_context.py +0 -0
  232. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_step_hook.py +0 -0
  233. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_stream_storage.py +0 -0
  234. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_stream_workflow.py +0 -0
  235. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_validation.py +0 -0
  236. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_workflow.py +0 -0
  237. {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/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.14
3
+ Version: 0.3.0
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.14"
10
+ version = "0.3.0"
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.14"
32
+ __version__ = "0.3.0"
33
33
 
34
34
  # Configuration
35
35
  from pyworkflow.config import (
@@ -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(
@@ -580,6 +604,15 @@ async def _record_step_completion_and_resume(
580
604
  )
581
605
  await storage.record_event(completion_event)
582
606
 
607
+ # Release the resume_workflow_task singleton lock before scheduling the
608
+ # next resume. The parent resume task that dispatched this step may still
609
+ # be holding the (run_id,) lock during its final teardown; without this
610
+ # release the apply_async below sees the lock held and silently drops
611
+ # the schedule as a duplicate, leaving the workflow stuck in SUSPENDED.
612
+ resume_workflow_task.release_lock(
613
+ task_args=[run_id], task_kwargs={"storage_config": storage_config}
614
+ )
615
+
583
616
  # Schedule workflow resumption
584
617
  schedule_workflow_resumption(
585
618
  run_id, datetime.now(UTC), storage_config, triggered_by="step_completed"
@@ -769,6 +802,12 @@ async def _record_step_failure_and_resume(
769
802
  )
770
803
  await storage.record_event(failure_event)
771
804
 
805
+ # Release the resume_workflow_task singleton lock before scheduling the
806
+ # next resume (see _record_step_completion_and_resume for details on why).
807
+ resume_workflow_task.release_lock(
808
+ task_args=[run_id], task_kwargs={"storage_config": storage_config}
809
+ )
810
+
772
811
  # Schedule workflow resumption
773
812
  schedule_workflow_resumption(
774
813
  run_id, datetime.now(UTC), storage_config, triggered_by="step_failed"
@@ -840,6 +879,7 @@ def start_workflow_task(
840
879
  run_id: str,
841
880
  storage_config: dict[str, Any] | None = None,
842
881
  idempotency_key: str | None = None,
882
+ tracing: dict[str, Any] | None = None,
843
883
  ) -> str:
844
884
  """
845
885
  Start a workflow execution.
@@ -891,6 +931,7 @@ def start_workflow_task(
891
931
  storage_config=storage_config,
892
932
  idempotency_key=idempotency_key,
893
933
  run_id=run_id,
934
+ tracing=tracing,
894
935
  )
895
936
  )
896
937
 
@@ -1416,6 +1457,9 @@ async def _recover_workflow_on_worker(
1416
1457
  args = deserialize_args(run.input_args)
1417
1458
  kwargs = deserialize_kwargs(run.input_kwargs)
1418
1459
 
1460
+ # Extract tracing config persisted at start
1461
+ _recover_tracing = kwargs.pop("_tracing_config", None)
1462
+
1419
1463
  # Execute workflow with event replay
1420
1464
  try:
1421
1465
  result = await execute_workflow_with_context(
@@ -1429,6 +1473,7 @@ async def _recover_workflow_on_worker(
1429
1473
  runtime="celery",
1430
1474
  storage_config=storage_config,
1431
1475
  parent_run_id=run.parent_run_id,
1476
+ tracing=workflow_meta.tracing or _recover_tracing,
1432
1477
  )
1433
1478
 
1434
1479
  # Update run status to completed
@@ -1564,6 +1609,7 @@ async def _start_workflow_on_worker(
1564
1609
  storage_config: dict[str, Any] | None = None,
1565
1610
  idempotency_key: str | None = None,
1566
1611
  run_id: str | None = None,
1612
+ tracing: dict[str, Any] | None = None,
1567
1613
  ) -> str:
1568
1614
  """
1569
1615
  Internal function to start workflow on Celery worker.
@@ -1764,10 +1810,10 @@ async def _start_workflow_on_worker(
1764
1810
  created_at=datetime.now(UTC),
1765
1811
  started_at=datetime.now(UTC),
1766
1812
  input_args=serialize_args(*args),
1767
- input_kwargs=serialize_kwargs(**kwargs),
1813
+ input_kwargs=serialize_kwargs(**kwargs, _tracing_config=tracing),
1768
1814
  idempotency_key=idempotency_key,
1769
1815
  max_duration=workflow_meta.max_duration,
1770
- context={}, # Step context (not from decorator)
1816
+ context={}, # Step context data
1771
1817
  recovery_attempts=0,
1772
1818
  max_recovery_attempts=max_recovery_attempts,
1773
1819
  recover_on_worker_loss=recover_on_worker_loss,
@@ -1797,6 +1843,7 @@ async def _start_workflow_on_worker(
1797
1843
  kwargs=kwargs,
1798
1844
  runtime="celery",
1799
1845
  storage_config=storage_config,
1846
+ tracing=tracing or workflow_meta.tracing,
1800
1847
  )
1801
1848
 
1802
1849
  # Update run status to completed
@@ -2377,6 +2424,9 @@ async def _resume_workflow_on_worker(
2377
2424
  args = deserialize_args(run.input_args)
2378
2425
  kwargs = deserialize_kwargs(run.input_kwargs)
2379
2426
 
2427
+ # Extract tracing config persisted at start
2428
+ _resume_tracing = kwargs.pop("_tracing_config", None)
2429
+
2380
2430
  # Execute workflow with event replay
2381
2431
  try:
2382
2432
  result = await execute_workflow_with_context(
@@ -2391,6 +2441,7 @@ async def _resume_workflow_on_worker(
2391
2441
  runtime="celery",
2392
2442
  storage_config=storage_config,
2393
2443
  parent_run_id=run.parent_run_id,
2444
+ tracing=workflow_meta.tracing or _resume_tracing,
2394
2445
  )
2395
2446
 
2396
2447
  # 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
@@ -9,7 +9,9 @@ decorated with @workflow to enable:
9
9
  - Fault tolerance
10
10
  """
11
11
 
12
+ import contextlib
12
13
  import functools
14
+ import hashlib
13
15
  from collections.abc import Callable
14
16
  from typing import Any
15
17
 
@@ -34,6 +36,7 @@ def workflow(
34
36
  recover_on_worker_loss: bool | None = None,
35
37
  max_recovery_attempts: int | None = None,
36
38
  context_class: type | None = None,
39
+ tracing: dict[str, Any] | None = None,
37
40
  ) -> Callable:
38
41
  """
39
42
  Decorator to mark async functions as workflows.
@@ -53,6 +56,9 @@ def workflow(
53
56
  context_class: Optional StepContext subclass for step context access.
54
57
  When provided, enables get_step_context() and set_step_context()
55
58
  for passing context data to steps running on remote workers.
59
+ tracing: Optional tracing provider config dict. When provided, enables
60
+ observability tracing (e.g. Langfuse) for the workflow and its steps.
61
+ Example: {"provider": "langfuse", "public_key": "...", "secret_key": "...", "host": "..."}
56
62
 
57
63
  Returns:
58
64
  Decorated workflow function
@@ -127,6 +133,7 @@ def workflow(
127
133
  max_duration=max_duration,
128
134
  tags=validated_tags,
129
135
  context_class=context_class,
136
+ tracing=tracing,
130
137
  )
131
138
 
132
139
  # Store metadata on wrapper
@@ -142,6 +149,7 @@ def workflow(
142
149
  max_recovery_attempts # None = use config default
143
150
  )
144
151
  wrapper.__workflow_context_class__ = context_class # type: ignore[attr-defined]
152
+ wrapper.__workflow_tracing__ = tracing # type: ignore[attr-defined]
145
153
 
146
154
  return wrapper
147
155
 
@@ -161,6 +169,7 @@ async def execute_workflow_with_context(
161
169
  runtime: str | None = None,
162
170
  storage_config: dict | None = None,
163
171
  parent_run_id: str | None = None,
172
+ tracing: dict | None = None,
164
173
  ) -> Any:
165
174
  """
166
175
  Execute workflow function with proper context setup.
@@ -210,6 +219,31 @@ async def execute_workflow_with_context(
210
219
  ctx._storage_config = storage_config
211
220
  ctx._parent_run_id = parent_run_id
212
221
 
222
+ # Tracing config resolution order:
223
+ # 1. pyworkflow.start(tracing={...}) — runtime creds (e.g. from FlowHunt)
224
+ # 2. @workflow(tracing={...}) — decorator-level config
225
+ # 3. configure(TRACING_CONFIG={...}) — global config
226
+ ctx.tracing = tracing or getattr(
227
+ workflow_func, "__workflow_tracing__", None
228
+ ) # 1. start(tracing=) or 2. @workflow(tracing=)
229
+
230
+ # Initialize tracing provider
231
+ tracing_provider = None
232
+ from pyworkflow.tracing import create_tracing_provider
233
+
234
+ if ctx.tracing:
235
+ tracing_provider = create_tracing_provider(ctx.tracing) # source 1 or 2
236
+ if not tracing_provider:
237
+ from pyworkflow.config import get_config
238
+
239
+ _global_tracing = get_config().TRACING_CONFIG
240
+ tracing_provider = create_tracing_provider(_global_tracing) # source 3
241
+ if tracing_provider:
242
+ ctx.tracing = _global_tracing # propagate to Celery workers
243
+ if tracing_provider:
244
+ ctx._trace_id = hashlib.md5(run_id.encode()).hexdigest()
245
+ ctx._tracing_provider = tracing_provider
246
+
213
247
  # Set cancellation state if requested before execution
214
248
  if cancellation_requested:
215
249
  ctx.request_cancellation(reason="Cancellation requested before execution")
@@ -243,6 +277,7 @@ async def execute_workflow_with_context(
243
277
  try:
244
278
  # Note: Event replay is handled by LocalContext in its constructor
245
279
  # when event_log is provided
280
+ result = None # For tracing in finally block
246
281
 
247
282
  logger.info(
248
283
  f"Executing workflow: {workflow_name}",
@@ -276,6 +311,7 @@ async def execute_workflow_with_context(
276
311
 
277
312
  except SuspensionSignal as e:
278
313
  # Workflow suspended (sleep/hook) - only happens in durable mode
314
+ # Don't end tracing - will continue on resume
279
315
  logger.info(
280
316
  f"Workflow suspended: {e.reason}",
281
317
  run_id=run_id,
@@ -341,6 +377,45 @@ async def execute_workflow_with_context(
341
377
  raise
342
378
 
343
379
  finally:
380
+ # Flush tracing data — wrapped in broad try/except to never crash the workflow
381
+ if tracing_provider:
382
+ try:
383
+ _trace_input = None
384
+ _trace_output = None
385
+ try:
386
+ if args and isinstance(args[0], dict):
387
+ _trace_input = args[0].get("input_value")
388
+ if result is not None and isinstance(result, dict):
389
+ _out = result.get("outputs", result)
390
+ if isinstance(_out, dict):
391
+ for v in _out.values():
392
+ if isinstance(v, dict) and v.get("text_output"):
393
+ _trace_output = v["text_output"]
394
+ break
395
+ if not _trace_output:
396
+ _trace_output = result.get("text_output")
397
+ except Exception:
398
+ pass
399
+ with contextlib.suppress(Exception):
400
+ await tracing_provider.shutdown()
401
+ try:
402
+ if ctx._trace_id:
403
+ trace_params = (ctx.tracing or {}).get("trace_params", {})
404
+ meta = trace_params.get("metadata") or {}
405
+ meta["run_id"] = run_id
406
+ trace_params["metadata"] = meta
407
+ await tracing_provider.update_trace(
408
+ trace_id=ctx._trace_id,
409
+ name=workflow_name,
410
+ input=_trace_input,
411
+ output=_trace_output,
412
+ trace_params=trace_params,
413
+ )
414
+ except Exception as e:
415
+ logger.error(f"TRACING: update_trace_via_api failed: {e}")
416
+ except Exception as e:
417
+ logger.error(f"TRACING: unexpected error in tracing finally block: {e}")
418
+
344
419
  # Clear step context if it was set
345
420
  if step_context_token is not None:
346
421
  from pyworkflow.context.step_context import _reset_step_context
@@ -47,6 +47,7 @@ async def start(
47
47
  durable: bool | None = None,
48
48
  storage: StorageBackend | None = None,
49
49
  idempotency_key: str | None = None,
50
+ tracing: dict | None = None,
50
51
  **kwargs: Any,
51
52
  ) -> str:
52
53
  """
@@ -62,6 +63,8 @@ async def start(
62
63
  durable: Whether workflow is durable (None = use workflow/config default)
63
64
  storage: Storage backend instance (None = use configured storage)
64
65
  idempotency_key: Optional key for idempotent execution
66
+ tracing: Optional tracing provider config dict (e.g. Langfuse credentials).
67
+ Overrides the @workflow decorator's tracing parameter if provided.
65
68
  **kwargs: Keyword arguments for workflow
66
69
 
67
70
  Returns:
@@ -168,6 +171,7 @@ async def start(
168
171
  idempotency_key=idempotency_key,
169
172
  max_duration=workflow_meta.max_duration,
170
173
  metadata={}, # Run-level metadata
174
+ tracing=tracing,
171
175
  )
172
176
 
173
177
 
@@ -37,6 +37,7 @@ class Runtime(ABC):
37
37
  idempotency_key: str | None = None,
38
38
  max_duration: str | None = None,
39
39
  metadata: dict | None = None,
40
+ tracing: dict | None = None,
40
41
  ) -> str:
41
42
  """
42
43
  Start a new workflow execution.
@@ -116,6 +116,7 @@ class CeleryRuntime(Runtime):
116
116
  idempotency_key: str | None = None,
117
117
  max_duration: str | None = None,
118
118
  metadata: dict | None = None,
119
+ tracing: dict | None = None,
119
120
  ) -> str:
120
121
  """
121
122
  Start a workflow execution by dispatching to Celery workers.
@@ -152,6 +153,7 @@ class CeleryRuntime(Runtime):
152
153
  run_id=run_id,
153
154
  storage_config=storage_config,
154
155
  idempotency_key=idempotency_key,
156
+ tracing=tracing,
155
157
  )
156
158
 
157
159
  logger.info(
@@ -134,6 +134,7 @@ class LocalRuntime(Runtime):
134
134
  idempotency_key: str | None = None,
135
135
  max_duration: str | None = None,
136
136
  metadata: dict | None = None,
137
+ tracing: dict | None = None,
137
138
  ) -> str:
138
139
  """Start a workflow execution in the current process."""
139
140
  from pyworkflow.core.workflow import execute_workflow_with_context
@@ -190,6 +191,7 @@ class LocalRuntime(Runtime):
190
191
  args=args,
191
192
  kwargs=kwargs,
192
193
  durable=durable,
194
+ tracing=tracing,
193
195
  )
194
196
 
195
197
  if durable and storage is not None:
@@ -0,0 +1,17 @@
1
+ """
2
+ Tracing module for PyWorkflow.
3
+
4
+ Provides observability tracing (e.g. Langfuse) for workflows and steps.
5
+ """
6
+
7
+ from pyworkflow.tracing.base import BaseTracingProvider
8
+ from pyworkflow.tracing.factory import create_tracing_provider
9
+ from pyworkflow.tracing.types import LLMCallData, ToolCallData, TracingStepResult
10
+
11
+ __all__ = [
12
+ "BaseTracingProvider",
13
+ "create_tracing_provider",
14
+ "LLMCallData",
15
+ "ToolCallData",
16
+ "TracingStepResult",
17
+ ]