pyworkflow-engine 0.3.7__tar.gz → 0.4.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.3.7 → pyworkflow_engine-0.4.0}/DISTRIBUTED.md +4 -1
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/PKG-INFO +50 -44
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/README.md +30 -24
- pyworkflow_engine-0.4.0/docs/concepts/step-hooks-and-checkpointing.mdx +325 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/conventions.md +12 -2
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyproject.toml +20 -20
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/__init__.py +7 -1
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/celery/tasks.py +85 -7
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/context/base.py +58 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/context/local.py +48 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/core/registry.py +17 -1
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/core/step.py +41 -16
- pyworkflow_engine-0.4.0/pyworkflow/core/strategy.py +74 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/core/workflow.py +21 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/engine/executor.py +107 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/step_checkpoint.py +27 -2
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/step_hook.py +19 -3
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/runtime/base.py +4 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/runtime/celery.py +3 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/runtime/local.py +70 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/memory.py +5 -2
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow_engine.egg-info/SOURCES.txt +8 -0
- pyworkflow_engine-0.4.0/tests/integration/_run_strategy_e2e_app.py +216 -0
- pyworkflow_engine-0.4.0/tests/integration/_run_strategy_e2e_driver.py +79 -0
- pyworkflow_engine-0.4.0/tests/integration/test_run_strategy_e2e.py +479 -0
- pyworkflow_engine-0.4.0/tests/integration/test_step_hook_inline_suspension_e2e.py +285 -0
- pyworkflow_engine-0.4.0/tests/unit/test_child_completion_notify.py +64 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_step_hook.py +363 -0
- pyworkflow_engine-0.4.0/tests/unit/test_workflow_run_strategy.py +621 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/CLAUDE.md +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/LICENSE +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/MANIFEST.in +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/RELEASING.md +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/architecture.md +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/cancellation.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/continue-as-new.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/events.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/fault-tolerance.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/hooks.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/limitations.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/schedules.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/sleep.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/step-context.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/steps.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/stream-steps.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/stream-workflows.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/concepts/workflows.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/guides/brokers.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/guides/cli.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/guides/configuration.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/harness-gaps.md +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/introduction.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/layers.md +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/docs/quickstart.mdx +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/docker-compose.yml +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/basic.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/batch_processing.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/cancellation.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/child_workflows.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/hooks.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/idempotency.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/long_running.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/retries.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/schedules.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/durable/workflows/step_context.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/transient/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/transient/02_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/transient/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/celery/transient/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/02_file_storage.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/03_retries.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/04_long_running.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/05_event_log.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/06_idempotency.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/07_hooks.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/08_cancellation.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/09_child_workflows.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/10_child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/11_continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/12_schedules.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/13_step_context.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/14_child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/durable/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/transient/01_quick_tasks.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/transient/02_retries.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/transient/03_sleep.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/examples/local/transient/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/aws/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/aws/context.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/aws/handler.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/aws/testing.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/celery/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/celery/app.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/celery/loop.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/celery/reschedule.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/celery/scheduler.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/celery/singleton.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/__main__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/commands/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/commands/hooks.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/commands/quickstart.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/commands/runs.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/commands/scheduler.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/commands/schedules.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/commands/setup.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/commands/worker.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/commands/workflows.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/output/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/output/formatters.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/output/styles.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/utils/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/utils/async_helpers.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/utils/config.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/utils/config_generator.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/utils/discovery.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/utils/docker_manager.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/utils/interactive.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/cli/utils/storage.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/config.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/context/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/context/aws.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/context/mock.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/context/step_context.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/core/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/core/exceptions.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/core/scheduled.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/core/validation.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/discovery.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/engine/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/engine/events.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/engine/replay.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/observability/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/observability/logging.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/child_handle.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/child_workflow.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/define_hook.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/hooks.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/resume_hook.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/schedule.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/shield.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/primitives/sleep.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/runtime/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/runtime/factory.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/scheduler/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/scheduler/local.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/serialization/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/serialization/decoder.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/serialization/encoder.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/base.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/cassandra.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/citus.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/config.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/dynamodb.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/file.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/migrations/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/migrations/base.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/mysql.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/postgres.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/schemas.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/storage/sqlite.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/streams/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/streams/checkpoint.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/streams/consumer.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/streams/context.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/streams/decorator.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/streams/dispatcher.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/streams/emit.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/streams/registry.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/streams/runtime.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/streams/signal.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/streams/step_context.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/tracing/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/tracing/base.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/tracing/factory.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/tracing/langfuse.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/tracing/types.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/utils/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/utils/duration.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/utils/helpers.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/pyworkflow/utils/schedule.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/setup.cfg +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/_reschedule_e2e_app.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_cancellation.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_list_runs_pagination.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_reschedule.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_reschedule_e2e.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_schedule_storage.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_schema_migrations.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_singleton.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_stream_e2e.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/integration/test_workflow_suspended.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/backends/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/backends/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/backends/test_citus_storage.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/backends/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/backends/test_postgres_storage.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/backends/test_sqlite_storage.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/conftest.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/storage/__init__.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/storage/test_migrations.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_cancellation.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_cli_worker.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_emit.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_event_limits.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_executor.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_force_local.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_hooks.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_parent_run_id.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_primitives_from_steps.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_registry.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_replay.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_reschedule.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_retention.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_schedule_schemas.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_schedule_utils.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_scheduled_workflow.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_signal.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_singleton.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_step.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_step_checkpoint.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_step_context.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_stream_storage.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_stream_workflow.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_validation.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_workflow.py +0 -0
- {pyworkflow_engine-0.3.7 → pyworkflow_engine-0.4.0}/tests/unit/test_workflow_suspended.py +0 -0
|
@@ -87,17 +87,20 @@ celery -A pyworkflow.celery.app beat --loglevel=info
|
|
|
87
87
|
```python
|
|
88
88
|
from pyworkflow import workflow, step, start, sleep
|
|
89
89
|
|
|
90
|
+
|
|
90
91
|
@step()
|
|
91
92
|
async def process_item(item_id: int):
|
|
92
93
|
# This runs on any available worker
|
|
93
94
|
return f"Processed {item_id}"
|
|
94
95
|
|
|
96
|
+
|
|
95
97
|
@workflow
|
|
96
98
|
async def my_workflow(item_id: int):
|
|
97
99
|
result = await process_item(item_id)
|
|
98
100
|
await sleep("5m") # Automatically resumes after 5 minutes!
|
|
99
101
|
return result
|
|
100
102
|
|
|
103
|
+
|
|
101
104
|
# Start workflow - executes across workers
|
|
102
105
|
run_id = start(my_workflow, item_id=123)
|
|
103
106
|
```
|
|
@@ -233,7 +236,7 @@ from pyworkflow.celery import create_celery_app
|
|
|
233
236
|
app = create_celery_app(
|
|
234
237
|
broker_url="redis://redis-cluster:6379/0",
|
|
235
238
|
result_backend="redis://redis-cluster:6379/1",
|
|
236
|
-
app_name="my_workflows"
|
|
239
|
+
app_name="my_workflows",
|
|
237
240
|
)
|
|
238
241
|
|
|
239
242
|
# Additional configuration
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyworkflow-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -26,50 +26,50 @@ Requires-Dist: celery<6.0.0,>=5.3.0
|
|
|
26
26
|
Requires-Dist: cloudpickle>=3.0.0
|
|
27
27
|
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
28
28
|
Requires-Dist: loguru>=0.7.0
|
|
29
|
-
Requires-Dist: click>=8.4.
|
|
29
|
+
Requires-Dist: click>=8.4.2
|
|
30
30
|
Requires-Dist: inquirerpy>=0.3.4; python_version < "4.0"
|
|
31
31
|
Requires-Dist: httpx>=0.25.0
|
|
32
32
|
Requires-Dist: python-dateutil>=2.8.0
|
|
33
|
-
Requires-Dist: filelock>=3.
|
|
33
|
+
Requires-Dist: filelock>=3.32.4
|
|
34
34
|
Requires-Dist: pyyaml>=6.0.0
|
|
35
|
-
Requires-Dist: croniter>=2.
|
|
35
|
+
Requires-Dist: croniter>=6.2.4
|
|
36
36
|
Provides-Extra: redis
|
|
37
|
-
Requires-Dist: redis>=
|
|
37
|
+
Requires-Dist: redis>=8.1.0; extra == "redis"
|
|
38
38
|
Provides-Extra: sqlite
|
|
39
39
|
Requires-Dist: aiosqlite>=0.19.0; extra == "sqlite"
|
|
40
40
|
Provides-Extra: postgres
|
|
41
41
|
Requires-Dist: asyncpg>=0.29.0; extra == "postgres"
|
|
42
42
|
Provides-Extra: aws
|
|
43
|
-
Requires-Dist: aws-durable-execution-sdk-python>=
|
|
43
|
+
Requires-Dist: aws-durable-execution-sdk-python>=1.6.0; extra == "aws"
|
|
44
44
|
Provides-Extra: dynamodb
|
|
45
|
-
Requires-Dist: aiobotocore>=3.
|
|
45
|
+
Requires-Dist: aiobotocore>=3.9.0; extra == "dynamodb"
|
|
46
46
|
Provides-Extra: cassandra
|
|
47
|
-
Requires-Dist: cassandra-driver>=3.
|
|
47
|
+
Requires-Dist: cassandra-driver>=3.30.1; extra == "cassandra"
|
|
48
48
|
Provides-Extra: mysql
|
|
49
49
|
Requires-Dist: aiomysql>=0.2.0; extra == "mysql"
|
|
50
50
|
Provides-Extra: all
|
|
51
|
-
Requires-Dist: redis>=
|
|
51
|
+
Requires-Dist: redis>=8.1.0; extra == "all"
|
|
52
52
|
Requires-Dist: aiosqlite>=0.19.0; extra == "all"
|
|
53
53
|
Requires-Dist: asyncpg>=0.29.0; extra == "all"
|
|
54
54
|
Requires-Dist: aiomysql>=0.2.0; extra == "all"
|
|
55
|
-
Requires-Dist: aws-durable-execution-sdk-python>=
|
|
56
|
-
Requires-Dist: cassandra-driver>=3.
|
|
55
|
+
Requires-Dist: aws-durable-execution-sdk-python>=1.6.0; extra == "all"
|
|
56
|
+
Requires-Dist: cassandra-driver>=3.30.1; extra == "all"
|
|
57
57
|
Provides-Extra: dev
|
|
58
|
-
Requires-Dist: pytest>=
|
|
58
|
+
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
59
59
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
60
60
|
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
61
61
|
Requires-Dist: pytest-celery>=0.0.0; extra == "dev"
|
|
62
62
|
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
|
63
|
-
Requires-Dist: moto[dynamodb]>=5.2.
|
|
63
|
+
Requires-Dist: moto[dynamodb]>=5.2.3; extra == "dev"
|
|
64
64
|
Requires-Dist: black>=26.5.1; extra == "dev"
|
|
65
|
-
Requires-Dist: ruff>=0.
|
|
66
|
-
Requires-Dist: mypy>=2.1
|
|
67
|
-
Requires-Dist: pre-commit>=4.6.
|
|
65
|
+
Requires-Dist: ruff>=0.16.4; extra == "dev"
|
|
66
|
+
Requires-Dist: mypy>=2.3.1; extra == "dev"
|
|
67
|
+
Requires-Dist: pre-commit>=4.6.2; extra == "dev"
|
|
68
68
|
Requires-Dist: types-redis>=4.6.0; extra == "dev"
|
|
69
|
-
Requires-Dist: types-python-dateutil>=2.
|
|
70
|
-
Requires-Dist: types-PyYAML>=6.0.
|
|
71
|
-
Requires-Dist: flower>=2.
|
|
72
|
-
Requires-Dist: redis>=
|
|
69
|
+
Requires-Dist: types-python-dateutil>=2.9.0.20260807; extra == "dev"
|
|
70
|
+
Requires-Dist: types-PyYAML>=6.0.12.20260815; extra == "dev"
|
|
71
|
+
Requires-Dist: flower>=2.1.0; extra == "dev"
|
|
72
|
+
Requires-Dist: redis>=8.1.0; extra == "dev"
|
|
73
73
|
Requires-Dist: aiosqlite>=0.19.0; extra == "dev"
|
|
74
74
|
Requires-Dist: asyncpg>=0.29.0; extra == "dev"
|
|
75
75
|
Dynamic: license-file
|
|
@@ -172,17 +172,20 @@ See [DISTRIBUTED.md](DISTRIBUTED.md) for complete deployment guide.
|
|
|
172
172
|
```python
|
|
173
173
|
from pyworkflow import workflow, step, start, sleep
|
|
174
174
|
|
|
175
|
+
|
|
175
176
|
@step()
|
|
176
177
|
async def send_welcome_email(user_id: str):
|
|
177
178
|
# This runs on any available Celery worker
|
|
178
179
|
print(f"Sending welcome email to user {user_id}")
|
|
179
180
|
return f"Email sent to {user_id}"
|
|
180
181
|
|
|
182
|
+
|
|
181
183
|
@step()
|
|
182
184
|
async def send_tips_email(user_id: str):
|
|
183
185
|
print(f"Sending tips email to user {user_id}")
|
|
184
186
|
return f"Tips sent to {user_id}"
|
|
185
187
|
|
|
188
|
+
|
|
186
189
|
@workflow()
|
|
187
190
|
async def onboarding_workflow(user_id: str):
|
|
188
191
|
# Send welcome email immediately
|
|
@@ -196,6 +199,7 @@ async def onboarding_workflow(user_id: str):
|
|
|
196
199
|
|
|
197
200
|
return "Onboarding complete"
|
|
198
201
|
|
|
202
|
+
|
|
199
203
|
# Start workflow - executes across Celery workers
|
|
200
204
|
run_id = start(onboarding_workflow, user_id="user_123")
|
|
201
205
|
print(f"Workflow started: {run_id}")
|
|
@@ -221,6 +225,7 @@ Workflows are the top-level orchestration functions. They coordinate steps, hand
|
|
|
221
225
|
```python
|
|
222
226
|
from pyworkflow import workflow, start
|
|
223
227
|
|
|
228
|
+
|
|
224
229
|
@workflow(name="process_order", max_duration="1h")
|
|
225
230
|
async def process_order(order_id: str):
|
|
226
231
|
"""
|
|
@@ -239,6 +244,7 @@ async def process_order(order_id: str):
|
|
|
239
244
|
|
|
240
245
|
return {"order_id": order_id, "status": "completed"}
|
|
241
246
|
|
|
247
|
+
|
|
242
248
|
# Start the workflow
|
|
243
249
|
run_id = start(process_order, order_id="ORD-123")
|
|
244
250
|
```
|
|
@@ -250,6 +256,7 @@ Steps are the building blocks of workflows. Each step is an isolated, retryable
|
|
|
250
256
|
```python
|
|
251
257
|
from pyworkflow import step, RetryableError, FatalError
|
|
252
258
|
|
|
259
|
+
|
|
253
260
|
@step(max_retries=5, retry_delay="exponential")
|
|
254
261
|
async def call_external_api(url: str):
|
|
255
262
|
"""
|
|
@@ -281,11 +288,13 @@ By default, steps in a Celery runtime are dispatched to worker processes via the
|
|
|
281
288
|
```python
|
|
282
289
|
from pyworkflow import step
|
|
283
290
|
|
|
291
|
+
|
|
284
292
|
@step(force_local=True)
|
|
285
293
|
async def quick_transform(data: dict):
|
|
286
294
|
"""Runs inline even when runtime is Celery."""
|
|
287
295
|
return {k: v.upper() for k, v in data.items()}
|
|
288
296
|
|
|
297
|
+
|
|
289
298
|
@step()
|
|
290
299
|
async def heavy_computation(data: dict):
|
|
291
300
|
"""Dispatched to a Celery worker as usual."""
|
|
@@ -311,6 +320,7 @@ Workflows can sleep for any duration. During sleep, the workflow suspends and co
|
|
|
311
320
|
```python
|
|
312
321
|
from pyworkflow import workflow, sleep
|
|
313
322
|
|
|
323
|
+
|
|
314
324
|
@workflow()
|
|
315
325
|
async def scheduled_reminder(user_id: str):
|
|
316
326
|
# Send immediate reminder
|
|
@@ -407,35 +417,33 @@ Use Python's native `asyncio.gather()` for parallel step execution:
|
|
|
407
417
|
import asyncio
|
|
408
418
|
from pyworkflow import workflow, step
|
|
409
419
|
|
|
420
|
+
|
|
410
421
|
@step()
|
|
411
422
|
async def fetch_user(user_id: str):
|
|
412
423
|
# Fetch user data
|
|
413
424
|
return {"id": user_id, "name": "Alice"}
|
|
414
425
|
|
|
426
|
+
|
|
415
427
|
@step()
|
|
416
428
|
async def fetch_orders(user_id: str):
|
|
417
429
|
# Fetch user orders
|
|
418
430
|
return [{"id": "ORD-1"}, {"id": "ORD-2"}]
|
|
419
431
|
|
|
432
|
+
|
|
420
433
|
@step()
|
|
421
434
|
async def fetch_recommendations(user_id: str):
|
|
422
435
|
# Fetch recommendations
|
|
423
436
|
return ["Product A", "Product B"]
|
|
424
437
|
|
|
438
|
+
|
|
425
439
|
@workflow()
|
|
426
440
|
async def dashboard_data(user_id: str):
|
|
427
441
|
# Fetch all data in parallel
|
|
428
442
|
user, orders, recommendations = await asyncio.gather(
|
|
429
|
-
fetch_user(user_id),
|
|
430
|
-
fetch_orders(user_id),
|
|
431
|
-
fetch_recommendations(user_id)
|
|
443
|
+
fetch_user(user_id), fetch_orders(user_id), fetch_recommendations(user_id)
|
|
432
444
|
)
|
|
433
445
|
|
|
434
|
-
return {
|
|
435
|
-
"user": user,
|
|
436
|
-
"orders": orders,
|
|
437
|
-
"recommendations": recommendations
|
|
438
|
-
}
|
|
446
|
+
return {"user": user, "orders": orders, "recommendations": recommendations}
|
|
439
447
|
```
|
|
440
448
|
|
|
441
449
|
### Error Handling
|
|
@@ -445,6 +453,7 @@ PyWorkflow distinguishes between retriable and fatal errors:
|
|
|
445
453
|
```python
|
|
446
454
|
from pyworkflow import FatalError, RetryableError, step
|
|
447
455
|
|
|
456
|
+
|
|
448
457
|
@step(max_retries=3, retry_delay="exponential")
|
|
449
458
|
async def process_payment(amount: float):
|
|
450
459
|
try:
|
|
@@ -474,14 +483,15 @@ Workflows automatically recover from worker crashes:
|
|
|
474
483
|
```python
|
|
475
484
|
from pyworkflow import workflow, step, sleep
|
|
476
485
|
|
|
486
|
+
|
|
477
487
|
@workflow(
|
|
478
|
-
recover_on_worker_loss=True,
|
|
479
|
-
max_recovery_attempts=5,
|
|
488
|
+
recover_on_worker_loss=True, # Enable recovery (default for durable)
|
|
489
|
+
max_recovery_attempts=5, # Max recovery attempts
|
|
480
490
|
)
|
|
481
491
|
async def resilient_workflow(data_id: str):
|
|
482
|
-
data = await fetch_data(data_id)
|
|
483
|
-
await sleep("10m")
|
|
484
|
-
return await process_data(data)
|
|
492
|
+
data = await fetch_data(data_id) # Completed steps are skipped on recovery
|
|
493
|
+
await sleep("10m") # Sleep state is preserved
|
|
494
|
+
return await process_data(data) # Continues from here after crash
|
|
485
495
|
```
|
|
486
496
|
|
|
487
497
|
**What happens on worker crash:**
|
|
@@ -516,18 +526,10 @@ Prevent duplicate workflow executions with idempotency keys:
|
|
|
516
526
|
from pyworkflow import start
|
|
517
527
|
|
|
518
528
|
# Same idempotency key = same workflow
|
|
519
|
-
run_id_1 = start(
|
|
520
|
-
process_order,
|
|
521
|
-
order_id="ORD-123",
|
|
522
|
-
idempotency_key="order-ORD-123"
|
|
523
|
-
)
|
|
529
|
+
run_id_1 = start(process_order, order_id="ORD-123", idempotency_key="order-ORD-123")
|
|
524
530
|
|
|
525
531
|
# This will return the same run_id, not start a new workflow
|
|
526
|
-
run_id_2 = start(
|
|
527
|
-
process_order,
|
|
528
|
-
order_id="ORD-123",
|
|
529
|
-
idempotency_key="order-ORD-123"
|
|
530
|
-
)
|
|
532
|
+
run_id_2 = start(process_order, order_id="ORD-123", idempotency_key="order-ORD-123")
|
|
531
533
|
|
|
532
534
|
assert run_id_1 == run_id_2 # True!
|
|
533
535
|
```
|
|
@@ -544,7 +546,7 @@ configure_logging(
|
|
|
544
546
|
level="INFO",
|
|
545
547
|
log_file="workflow.log",
|
|
546
548
|
json_logs=True, # JSON format for production
|
|
547
|
-
show_context=True # Include run_id, step_id, etc.
|
|
549
|
+
show_context=True, # Include run_id, step_id, etc.
|
|
548
550
|
)
|
|
549
551
|
|
|
550
552
|
# Logs automatically include:
|
|
@@ -565,15 +567,18 @@ import pytest
|
|
|
565
567
|
from pyworkflow import workflow, step, start, configure, reset_config
|
|
566
568
|
from pyworkflow.storage.memory import InMemoryStorageBackend
|
|
567
569
|
|
|
570
|
+
|
|
568
571
|
@step()
|
|
569
572
|
async def my_step(x: int):
|
|
570
573
|
return x * 2
|
|
571
574
|
|
|
575
|
+
|
|
572
576
|
@workflow()
|
|
573
577
|
async def my_workflow(x: int):
|
|
574
578
|
result = await my_step(x)
|
|
575
579
|
return result + 1
|
|
576
580
|
|
|
581
|
+
|
|
577
582
|
@pytest.fixture(autouse=True)
|
|
578
583
|
def setup_storage():
|
|
579
584
|
reset_config()
|
|
@@ -582,6 +587,7 @@ def setup_storage():
|
|
|
582
587
|
yield storage
|
|
583
588
|
reset_config()
|
|
584
589
|
|
|
590
|
+
|
|
585
591
|
@pytest.mark.asyncio
|
|
586
592
|
async def test_my_workflow(setup_storage):
|
|
587
593
|
storage = setup_storage
|
|
@@ -96,17 +96,20 @@ See [DISTRIBUTED.md](DISTRIBUTED.md) for complete deployment guide.
|
|
|
96
96
|
```python
|
|
97
97
|
from pyworkflow import workflow, step, start, sleep
|
|
98
98
|
|
|
99
|
+
|
|
99
100
|
@step()
|
|
100
101
|
async def send_welcome_email(user_id: str):
|
|
101
102
|
# This runs on any available Celery worker
|
|
102
103
|
print(f"Sending welcome email to user {user_id}")
|
|
103
104
|
return f"Email sent to {user_id}"
|
|
104
105
|
|
|
106
|
+
|
|
105
107
|
@step()
|
|
106
108
|
async def send_tips_email(user_id: str):
|
|
107
109
|
print(f"Sending tips email to user {user_id}")
|
|
108
110
|
return f"Tips sent to {user_id}"
|
|
109
111
|
|
|
112
|
+
|
|
110
113
|
@workflow()
|
|
111
114
|
async def onboarding_workflow(user_id: str):
|
|
112
115
|
# Send welcome email immediately
|
|
@@ -120,6 +123,7 @@ async def onboarding_workflow(user_id: str):
|
|
|
120
123
|
|
|
121
124
|
return "Onboarding complete"
|
|
122
125
|
|
|
126
|
+
|
|
123
127
|
# Start workflow - executes across Celery workers
|
|
124
128
|
run_id = start(onboarding_workflow, user_id="user_123")
|
|
125
129
|
print(f"Workflow started: {run_id}")
|
|
@@ -145,6 +149,7 @@ Workflows are the top-level orchestration functions. They coordinate steps, hand
|
|
|
145
149
|
```python
|
|
146
150
|
from pyworkflow import workflow, start
|
|
147
151
|
|
|
152
|
+
|
|
148
153
|
@workflow(name="process_order", max_duration="1h")
|
|
149
154
|
async def process_order(order_id: str):
|
|
150
155
|
"""
|
|
@@ -163,6 +168,7 @@ async def process_order(order_id: str):
|
|
|
163
168
|
|
|
164
169
|
return {"order_id": order_id, "status": "completed"}
|
|
165
170
|
|
|
171
|
+
|
|
166
172
|
# Start the workflow
|
|
167
173
|
run_id = start(process_order, order_id="ORD-123")
|
|
168
174
|
```
|
|
@@ -174,6 +180,7 @@ Steps are the building blocks of workflows. Each step is an isolated, retryable
|
|
|
174
180
|
```python
|
|
175
181
|
from pyworkflow import step, RetryableError, FatalError
|
|
176
182
|
|
|
183
|
+
|
|
177
184
|
@step(max_retries=5, retry_delay="exponential")
|
|
178
185
|
async def call_external_api(url: str):
|
|
179
186
|
"""
|
|
@@ -205,11 +212,13 @@ By default, steps in a Celery runtime are dispatched to worker processes via the
|
|
|
205
212
|
```python
|
|
206
213
|
from pyworkflow import step
|
|
207
214
|
|
|
215
|
+
|
|
208
216
|
@step(force_local=True)
|
|
209
217
|
async def quick_transform(data: dict):
|
|
210
218
|
"""Runs inline even when runtime is Celery."""
|
|
211
219
|
return {k: v.upper() for k, v in data.items()}
|
|
212
220
|
|
|
221
|
+
|
|
213
222
|
@step()
|
|
214
223
|
async def heavy_computation(data: dict):
|
|
215
224
|
"""Dispatched to a Celery worker as usual."""
|
|
@@ -235,6 +244,7 @@ Workflows can sleep for any duration. During sleep, the workflow suspends and co
|
|
|
235
244
|
```python
|
|
236
245
|
from pyworkflow import workflow, sleep
|
|
237
246
|
|
|
247
|
+
|
|
238
248
|
@workflow()
|
|
239
249
|
async def scheduled_reminder(user_id: str):
|
|
240
250
|
# Send immediate reminder
|
|
@@ -331,35 +341,33 @@ Use Python's native `asyncio.gather()` for parallel step execution:
|
|
|
331
341
|
import asyncio
|
|
332
342
|
from pyworkflow import workflow, step
|
|
333
343
|
|
|
344
|
+
|
|
334
345
|
@step()
|
|
335
346
|
async def fetch_user(user_id: str):
|
|
336
347
|
# Fetch user data
|
|
337
348
|
return {"id": user_id, "name": "Alice"}
|
|
338
349
|
|
|
350
|
+
|
|
339
351
|
@step()
|
|
340
352
|
async def fetch_orders(user_id: str):
|
|
341
353
|
# Fetch user orders
|
|
342
354
|
return [{"id": "ORD-1"}, {"id": "ORD-2"}]
|
|
343
355
|
|
|
356
|
+
|
|
344
357
|
@step()
|
|
345
358
|
async def fetch_recommendations(user_id: str):
|
|
346
359
|
# Fetch recommendations
|
|
347
360
|
return ["Product A", "Product B"]
|
|
348
361
|
|
|
362
|
+
|
|
349
363
|
@workflow()
|
|
350
364
|
async def dashboard_data(user_id: str):
|
|
351
365
|
# Fetch all data in parallel
|
|
352
366
|
user, orders, recommendations = await asyncio.gather(
|
|
353
|
-
fetch_user(user_id),
|
|
354
|
-
fetch_orders(user_id),
|
|
355
|
-
fetch_recommendations(user_id)
|
|
367
|
+
fetch_user(user_id), fetch_orders(user_id), fetch_recommendations(user_id)
|
|
356
368
|
)
|
|
357
369
|
|
|
358
|
-
return {
|
|
359
|
-
"user": user,
|
|
360
|
-
"orders": orders,
|
|
361
|
-
"recommendations": recommendations
|
|
362
|
-
}
|
|
370
|
+
return {"user": user, "orders": orders, "recommendations": recommendations}
|
|
363
371
|
```
|
|
364
372
|
|
|
365
373
|
### Error Handling
|
|
@@ -369,6 +377,7 @@ PyWorkflow distinguishes between retriable and fatal errors:
|
|
|
369
377
|
```python
|
|
370
378
|
from pyworkflow import FatalError, RetryableError, step
|
|
371
379
|
|
|
380
|
+
|
|
372
381
|
@step(max_retries=3, retry_delay="exponential")
|
|
373
382
|
async def process_payment(amount: float):
|
|
374
383
|
try:
|
|
@@ -398,14 +407,15 @@ Workflows automatically recover from worker crashes:
|
|
|
398
407
|
```python
|
|
399
408
|
from pyworkflow import workflow, step, sleep
|
|
400
409
|
|
|
410
|
+
|
|
401
411
|
@workflow(
|
|
402
|
-
recover_on_worker_loss=True,
|
|
403
|
-
max_recovery_attempts=5,
|
|
412
|
+
recover_on_worker_loss=True, # Enable recovery (default for durable)
|
|
413
|
+
max_recovery_attempts=5, # Max recovery attempts
|
|
404
414
|
)
|
|
405
415
|
async def resilient_workflow(data_id: str):
|
|
406
|
-
data = await fetch_data(data_id)
|
|
407
|
-
await sleep("10m")
|
|
408
|
-
return await process_data(data)
|
|
416
|
+
data = await fetch_data(data_id) # Completed steps are skipped on recovery
|
|
417
|
+
await sleep("10m") # Sleep state is preserved
|
|
418
|
+
return await process_data(data) # Continues from here after crash
|
|
409
419
|
```
|
|
410
420
|
|
|
411
421
|
**What happens on worker crash:**
|
|
@@ -440,18 +450,10 @@ Prevent duplicate workflow executions with idempotency keys:
|
|
|
440
450
|
from pyworkflow import start
|
|
441
451
|
|
|
442
452
|
# Same idempotency key = same workflow
|
|
443
|
-
run_id_1 = start(
|
|
444
|
-
process_order,
|
|
445
|
-
order_id="ORD-123",
|
|
446
|
-
idempotency_key="order-ORD-123"
|
|
447
|
-
)
|
|
453
|
+
run_id_1 = start(process_order, order_id="ORD-123", idempotency_key="order-ORD-123")
|
|
448
454
|
|
|
449
455
|
# This will return the same run_id, not start a new workflow
|
|
450
|
-
run_id_2 = start(
|
|
451
|
-
process_order,
|
|
452
|
-
order_id="ORD-123",
|
|
453
|
-
idempotency_key="order-ORD-123"
|
|
454
|
-
)
|
|
456
|
+
run_id_2 = start(process_order, order_id="ORD-123", idempotency_key="order-ORD-123")
|
|
455
457
|
|
|
456
458
|
assert run_id_1 == run_id_2 # True!
|
|
457
459
|
```
|
|
@@ -468,7 +470,7 @@ configure_logging(
|
|
|
468
470
|
level="INFO",
|
|
469
471
|
log_file="workflow.log",
|
|
470
472
|
json_logs=True, # JSON format for production
|
|
471
|
-
show_context=True # Include run_id, step_id, etc.
|
|
473
|
+
show_context=True, # Include run_id, step_id, etc.
|
|
472
474
|
)
|
|
473
475
|
|
|
474
476
|
# Logs automatically include:
|
|
@@ -489,15 +491,18 @@ import pytest
|
|
|
489
491
|
from pyworkflow import workflow, step, start, configure, reset_config
|
|
490
492
|
from pyworkflow.storage.memory import InMemoryStorageBackend
|
|
491
493
|
|
|
494
|
+
|
|
492
495
|
@step()
|
|
493
496
|
async def my_step(x: int):
|
|
494
497
|
return x * 2
|
|
495
498
|
|
|
499
|
+
|
|
496
500
|
@workflow()
|
|
497
501
|
async def my_workflow(x: int):
|
|
498
502
|
result = await my_step(x)
|
|
499
503
|
return result + 1
|
|
500
504
|
|
|
505
|
+
|
|
501
506
|
@pytest.fixture(autouse=True)
|
|
502
507
|
def setup_storage():
|
|
503
508
|
reset_config()
|
|
@@ -506,6 +511,7 @@ def setup_storage():
|
|
|
506
511
|
yield storage
|
|
507
512
|
reset_config()
|
|
508
513
|
|
|
514
|
+
|
|
509
515
|
@pytest.mark.asyncio
|
|
510
516
|
async def test_my_workflow(setup_storage):
|
|
511
517
|
storage = setup_storage
|