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.
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/PKG-INFO +1 -1
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyproject.toml +1 -1
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/__init__.py +1 -1
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/tasks.py +53 -2
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/config.py +3 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/base.py +17 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/registry.py +26 -2
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/step.py +47 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/workflow.py +75 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/engine/executor.py +4 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/runtime/base.py +1 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/runtime/celery.py +2 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/runtime/local.py +2 -0
- pyworkflow_engine-0.3.0/pyworkflow/tracing/__init__.py +17 -0
- pyworkflow_engine-0.3.0/pyworkflow/tracing/base.py +166 -0
- pyworkflow_engine-0.3.0/pyworkflow/tracing/factory.py +46 -0
- pyworkflow_engine-0.3.0/pyworkflow/tracing/langfuse.py +184 -0
- pyworkflow_engine-0.3.0/pyworkflow/tracing/types.py +69 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow_engine.egg-info/SOURCES.txt +5 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/CLAUDE.md +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/DISTRIBUTED.md +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/LICENSE +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/MANIFEST.in +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/README.md +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/RELEASING.md +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/architecture.md +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/cancellation.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/continue-as-new.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/events.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/fault-tolerance.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/hooks.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/limitations.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/schedules.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/sleep.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/step-context.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/steps.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/stream-steps.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/stream-workflows.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/concepts/workflows.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/conventions.md +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/guides/brokers.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/guides/cli.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/guides/configuration.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/harness-gaps.md +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/introduction.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/layers.md +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/docs/quickstart.mdx +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/docker-compose.yml +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/basic.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/batch_processing.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/cancellation.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/child_workflows.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/hooks.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/idempotency.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/long_running.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/retries.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/schedules.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/durable/workflows/step_context.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/transient/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/transient/02_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/transient/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/celery/transient/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/02_file_storage.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/03_retries.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/04_long_running.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/05_event_log.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/06_idempotency.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/07_hooks.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/08_cancellation.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/09_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/10_child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/11_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/12_schedules.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/13_step_context.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/14_child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/durable/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/transient/01_quick_tasks.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/transient/02_retries.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/transient/03_sleep.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/examples/local/transient/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/aws/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/aws/context.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/aws/handler.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/aws/testing.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/app.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/loop.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/scheduler.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/celery/singleton.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/__main__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/hooks.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/quickstart.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/runs.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/scheduler.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/schedules.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/setup.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/worker.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/commands/workflows.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/output/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/output/formatters.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/output/styles.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/async_helpers.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/config.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/config_generator.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/discovery.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/docker_manager.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/interactive.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/cli/utils/storage.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/aws.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/local.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/mock.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/context/step_context.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/exceptions.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/scheduled.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/core/validation.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/discovery.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/engine/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/engine/events.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/engine/replay.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/observability/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/observability/logging.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/child_handle.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/child_workflow.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/define_hook.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/hooks.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/resume_hook.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/schedule.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/shield.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/sleep.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/step_checkpoint.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/primitives/step_hook.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/runtime/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/runtime/factory.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/scheduler/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/scheduler/local.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/serialization/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/serialization/decoder.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/serialization/encoder.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/base.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/cassandra.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/citus.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/config.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/dynamodb.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/file.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/memory.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/migrations/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/migrations/base.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/mysql.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/postgres.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/schemas.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/storage/sqlite.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/checkpoint.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/consumer.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/context.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/decorator.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/dispatcher.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/emit.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/registry.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/runtime.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/signal.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/streams/step_context.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/utils/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/utils/duration.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/utils/helpers.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/pyworkflow/utils/schedule.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/setup.cfg +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_cancellation.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_schedule_storage.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_schema_migrations.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_singleton.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_stream_e2e.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/integration/test_workflow_suspended.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/test_citus_storage.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/test_postgres_storage.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/backends/test_sqlite_storage.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/conftest.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/storage/__init__.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/storage/test_migrations.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_cancellation.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_cli_worker.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_emit.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_event_limits.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_executor.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_force_local.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_hooks.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_parent_run_id.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_primitives_from_steps.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_registry.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_replay.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_retention.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_schedule_schemas.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_schedule_utils.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_scheduled_workflow.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_signal.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_singleton.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_step.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_step_checkpoint.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_step_context.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_step_hook.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_stream_storage.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_stream_workflow.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_validation.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_workflow.py +0 -0
- {pyworkflow_engine-0.2.14 → pyworkflow_engine-0.3.0}/tests/unit/test_workflow_suspended.py +0 -0
|
@@ -7,7 +7,7 @@ packages = [{include = "pyworkflow"}]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "pyworkflow-engine"
|
|
10
|
-
version = "0.
|
|
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"
|
|
@@ -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
|
|
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(
|
|
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,
|
|
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
|
|
|
@@ -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
|
+
]
|