pyworkflow-engine 0.1.37__tar.gz → 0.2.0b2__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.1.37 → pyworkflow_engine-0.2.0b2}/PKG-INFO +1 -1
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyproject.toml +1 -1
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/__init__.py +39 -1
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/celery/scheduler.py +5 -3
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/celery/tasks.py +98 -4
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/commands/hooks.py +15 -15
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/commands/runs.py +9 -9
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/commands/schedules.py +30 -24
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/context/local.py +11 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/core/step.py +28 -9
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/engine/events.py +160 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/engine/replay.py +49 -0
- pyworkflow_engine-0.2.0b2/pyworkflow/primitives/step_checkpoint.py +157 -0
- pyworkflow_engine-0.2.0b2/pyworkflow/primitives/step_hook.py +205 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/base.py +221 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/cassandra.py +59 -3
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/citus.py +17 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/dynamodb.py +89 -39
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/file.py +50 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/memory.py +166 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/mysql.py +46 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/postgres.py +54 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/sqlite.py +54 -3
- pyworkflow_engine-0.2.0b2/pyworkflow/streams/__init__.py +83 -0
- pyworkflow_engine-0.2.0b2/pyworkflow/streams/checkpoint.py +201 -0
- pyworkflow_engine-0.2.0b2/pyworkflow/streams/consumer.py +124 -0
- pyworkflow_engine-0.2.0b2/pyworkflow/streams/context.py +91 -0
- pyworkflow_engine-0.2.0b2/pyworkflow/streams/decorator.py +131 -0
- pyworkflow_engine-0.2.0b2/pyworkflow/streams/dispatcher.py +233 -0
- pyworkflow_engine-0.2.0b2/pyworkflow/streams/emit.py +157 -0
- pyworkflow_engine-0.2.0b2/pyworkflow/streams/registry.py +190 -0
- pyworkflow_engine-0.2.0b2/pyworkflow/streams/signal.py +70 -0
- pyworkflow_engine-0.2.0b2/pyworkflow/streams/step_context.py +79 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow_engine.egg-info/SOURCES.txt +19 -0
- pyworkflow_engine-0.2.0b2/tests/integration/test_stream_e2e.py +308 -0
- pyworkflow_engine-0.2.0b2/tests/unit/test_emit.py +123 -0
- pyworkflow_engine-0.2.0b2/tests/unit/test_signal.py +76 -0
- pyworkflow_engine-0.2.0b2/tests/unit/test_step_checkpoint.py +102 -0
- pyworkflow_engine-0.2.0b2/tests/unit/test_step_hook.py +202 -0
- pyworkflow_engine-0.2.0b2/tests/unit/test_stream_storage.py +188 -0
- pyworkflow_engine-0.2.0b2/tests/unit/test_stream_workflow.py +203 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/CLAUDE.md +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/DISTRIBUTED.md +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/LICENSE +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/MANIFEST.in +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/README.md +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/RELEASING.md +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/architecture.md +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/concepts/cancellation.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/concepts/continue-as-new.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/concepts/events.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/concepts/fault-tolerance.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/concepts/hooks.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/concepts/limitations.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/concepts/schedules.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/concepts/sleep.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/concepts/step-context.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/concepts/steps.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/concepts/workflows.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/conventions.md +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/guides/brokers.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/guides/cli.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/guides/configuration.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/harness-gaps.md +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/introduction.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/layers.md +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/docs/quickstart.mdx +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/docker-compose.yml +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/basic.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/batch_processing.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/cancellation.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/child_workflows.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/continue_as_new.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/hooks.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/idempotency.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/long_running.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/retries.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/schedules.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/durable/workflows/step_context.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/transient/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/transient/02_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/transient/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/celery/transient/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/02_file_storage.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/03_retries.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/04_long_running.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/05_event_log.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/06_idempotency.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/07_hooks.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/08_cancellation.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/09_child_workflows.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/10_child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/11_continue_as_new.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/12_schedules.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/13_step_context.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/14_child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/durable/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/transient/01_quick_tasks.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/transient/02_retries.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/transient/03_sleep.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/examples/local/transient/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/aws/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/aws/context.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/aws/handler.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/aws/testing.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/celery/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/celery/app.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/celery/loop.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/celery/singleton.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/__main__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/commands/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/commands/quickstart.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/commands/scheduler.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/commands/setup.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/commands/worker.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/commands/workflows.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/output/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/output/formatters.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/output/styles.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/utils/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/utils/async_helpers.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/utils/config.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/utils/config_generator.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/utils/discovery.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/utils/docker_manager.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/utils/interactive.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/cli/utils/storage.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/config.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/context/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/context/aws.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/context/base.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/context/mock.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/context/step_context.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/core/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/core/exceptions.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/core/registry.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/core/scheduled.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/core/validation.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/core/workflow.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/discovery.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/engine/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/engine/executor.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/observability/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/observability/logging.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/primitives/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/primitives/child_handle.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/primitives/child_workflow.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/primitives/continue_as_new.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/primitives/define_hook.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/primitives/hooks.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/primitives/resume_hook.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/primitives/schedule.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/primitives/shield.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/primitives/sleep.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/runtime/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/runtime/base.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/runtime/celery.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/runtime/factory.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/runtime/local.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/scheduler/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/scheduler/local.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/serialization/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/serialization/decoder.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/serialization/encoder.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/config.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/migrations/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/migrations/base.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/storage/schemas.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/utils/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/utils/duration.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/utils/helpers.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/pyworkflow/utils/schedule.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/setup.cfg +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/integration/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/integration/test_cancellation.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/integration/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/integration/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/integration/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/integration/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/integration/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/integration/test_schedule_storage.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/integration/test_schema_migrations.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/integration/test_singleton.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/integration/test_workflow_suspended.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/backends/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/backends/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/backends/test_citus_storage.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/backends/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/backends/test_postgres_storage.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/backends/test_sqlite_storage.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/conftest.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/storage/__init__.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/storage/test_migrations.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_cancellation.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_cli_worker.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_event_limits.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_executor.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_force_local.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_hooks.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_parent_run_id.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_primitives_from_steps.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_registry.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_replay.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_retention.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_schedule_schemas.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_schedule_utils.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_scheduled_workflow.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_singleton.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_step.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_step_context.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_validation.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/tests/unit/test_workflow.py +0 -0
- {pyworkflow_engine-0.1.37 → pyworkflow_engine-0.2.0b2}/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.2.0b2"
|
|
11
11
|
description = "A Python implementation of durable, event-sourced workflows inspired by Vercel Workflow"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -29,7 +29,7 @@ Quick Start:
|
|
|
29
29
|
>>> run_id = await start(my_workflow, "Alice")
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
__version__ = "0.
|
|
32
|
+
__version__ = "0.2.0b2"
|
|
33
33
|
|
|
34
34
|
# Configuration
|
|
35
35
|
from pyworkflow.config import (
|
|
@@ -135,6 +135,12 @@ from pyworkflow.primitives.schedule import (
|
|
|
135
135
|
)
|
|
136
136
|
from pyworkflow.primitives.shield import shield
|
|
137
137
|
from pyworkflow.primitives.sleep import sleep
|
|
138
|
+
from pyworkflow.primitives.step_checkpoint import (
|
|
139
|
+
delete_step_checkpoint,
|
|
140
|
+
load_step_checkpoint,
|
|
141
|
+
save_step_checkpoint,
|
|
142
|
+
)
|
|
143
|
+
from pyworkflow.primitives.step_hook import step_hook
|
|
138
144
|
|
|
139
145
|
# Runtime
|
|
140
146
|
from pyworkflow.runtime import LocalRuntime, Runtime, get_runtime, register_runtime
|
|
@@ -156,6 +162,21 @@ from pyworkflow.storage.schemas import (
|
|
|
156
162
|
WorkflowRun,
|
|
157
163
|
)
|
|
158
164
|
|
|
165
|
+
# Streams (pub/sub signal pattern)
|
|
166
|
+
from pyworkflow.streams import (
|
|
167
|
+
CheckpointBackend,
|
|
168
|
+
Signal,
|
|
169
|
+
Stream,
|
|
170
|
+
StreamConsumer,
|
|
171
|
+
StreamStepContext,
|
|
172
|
+
emit,
|
|
173
|
+
get_checkpoint,
|
|
174
|
+
get_current_signal,
|
|
175
|
+
save_checkpoint,
|
|
176
|
+
stream_step,
|
|
177
|
+
stream_workflow,
|
|
178
|
+
)
|
|
179
|
+
|
|
159
180
|
__all__ = [
|
|
160
181
|
# Version
|
|
161
182
|
"__version__",
|
|
@@ -264,4 +285,21 @@ __all__ = [
|
|
|
264
285
|
"get_logger",
|
|
265
286
|
"bind_workflow_context",
|
|
266
287
|
"bind_step_context",
|
|
288
|
+
# Step checkpoint + hooks
|
|
289
|
+
"save_step_checkpoint",
|
|
290
|
+
"load_step_checkpoint",
|
|
291
|
+
"delete_step_checkpoint",
|
|
292
|
+
"step_hook",
|
|
293
|
+
# Streams
|
|
294
|
+
"stream_workflow",
|
|
295
|
+
"stream_step",
|
|
296
|
+
"emit",
|
|
297
|
+
"Signal",
|
|
298
|
+
"Stream",
|
|
299
|
+
"StreamStepContext",
|
|
300
|
+
"StreamConsumer",
|
|
301
|
+
"CheckpointBackend",
|
|
302
|
+
"get_current_signal",
|
|
303
|
+
"get_checkpoint",
|
|
304
|
+
"save_checkpoint",
|
|
267
305
|
]
|
|
@@ -181,9 +181,11 @@ class PyWorkflowScheduler(Scheduler):
|
|
|
181
181
|
execute_scheduled_workflow_task.apply_async(
|
|
182
182
|
kwargs={
|
|
183
183
|
"schedule_id": schedule.schedule_id,
|
|
184
|
-
"scheduled_time":
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
"scheduled_time": (
|
|
185
|
+
schedule.next_run_time.isoformat()
|
|
186
|
+
if schedule.next_run_time
|
|
187
|
+
else now.isoformat()
|
|
188
|
+
),
|
|
187
189
|
"storage_config": self._storage_config,
|
|
188
190
|
},
|
|
189
191
|
queue="pyworkflow.schedules",
|
|
@@ -279,6 +279,18 @@ def execute_step_task(
|
|
|
279
279
|
step_id=step_id,
|
|
280
280
|
)
|
|
281
281
|
|
|
282
|
+
# Set up step execution context for checkpoint/hook primitives
|
|
283
|
+
step_exec_key = f"{run_id}:{step_id}"
|
|
284
|
+
step_exec_tokens = None
|
|
285
|
+
try:
|
|
286
|
+
from pyworkflow.primitives.step_checkpoint import (
|
|
287
|
+
set_step_execution_context,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
step_exec_tokens = set_step_execution_context(step_exec_key, storage)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.warning(f"Failed to set up step execution context: {e}")
|
|
293
|
+
|
|
282
294
|
# Execute step function
|
|
283
295
|
try:
|
|
284
296
|
# Get the original function (unwrapped from decorator)
|
|
@@ -339,10 +351,29 @@ def execute_step_task(
|
|
|
339
351
|
raise
|
|
340
352
|
|
|
341
353
|
except SuspensionSignal as e:
|
|
342
|
-
#
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
354
|
+
# Check if this is a step_hook suspension (supported)
|
|
355
|
+
if e.reason and e.reason.startswith("step_hook:"):
|
|
356
|
+
hook_id = e.data.get("hook_id")
|
|
357
|
+
logger.info(
|
|
358
|
+
f"Step suspended via step_hook: {step_name}",
|
|
359
|
+
run_id=run_id,
|
|
360
|
+
step_id=step_id,
|
|
361
|
+
hook_id=hook_id,
|
|
362
|
+
)
|
|
363
|
+
# Record STEP_SUSPENDED event so the workflow knows this step
|
|
364
|
+
# needs re-dispatch (clears in-progress state during replay)
|
|
365
|
+
run_async(
|
|
366
|
+
_record_step_suspended(
|
|
367
|
+
storage_config=storage_config,
|
|
368
|
+
run_id=run_id,
|
|
369
|
+
step_id=step_id,
|
|
370
|
+
step_name=step_name,
|
|
371
|
+
hook_id=hook_id or "",
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
# Other SuspensionSignals are not supported from steps
|
|
346
377
|
logger.error(
|
|
347
378
|
f"Step raised SuspensionSignal (unsupported from steps): {step_name}",
|
|
348
379
|
run_id=run_id,
|
|
@@ -449,6 +480,12 @@ def execute_step_task(
|
|
|
449
480
|
with contextlib.suppress(Exception):
|
|
450
481
|
_exec_lock_backend.unlock(_exec_lock_key)
|
|
451
482
|
|
|
483
|
+
# Clean up step execution context (checkpoint/hook primitives)
|
|
484
|
+
if step_exec_tokens is not None:
|
|
485
|
+
from pyworkflow.primitives.step_checkpoint import reset_step_execution_context
|
|
486
|
+
|
|
487
|
+
reset_step_execution_context(step_exec_tokens)
|
|
488
|
+
|
|
452
489
|
# Clean up workflow context (must be reset before step context)
|
|
453
490
|
if workflow_context_token is not None:
|
|
454
491
|
from pyworkflow.context.base import reset_context
|
|
@@ -567,6 +604,63 @@ async def _record_step_completion_and_resume(
|
|
|
567
604
|
)
|
|
568
605
|
|
|
569
606
|
|
|
607
|
+
async def _record_step_suspended(
|
|
608
|
+
storage_config: dict[str, Any] | None,
|
|
609
|
+
run_id: str,
|
|
610
|
+
step_id: str,
|
|
611
|
+
step_name: str,
|
|
612
|
+
hook_id: str,
|
|
613
|
+
) -> None:
|
|
614
|
+
"""
|
|
615
|
+
Record STEP_SUSPENDED event when a step suspends via step_hook.
|
|
616
|
+
|
|
617
|
+
This clears the step's in-progress state during replay so that the
|
|
618
|
+
workflow re-dispatches the step on resume (instead of thinking it's
|
|
619
|
+
still running).
|
|
620
|
+
|
|
621
|
+
Does NOT schedule workflow resumption — that happens when resume_hook()
|
|
622
|
+
is called externally.
|
|
623
|
+
"""
|
|
624
|
+
from pyworkflow.engine.events import create_step_suspended_event
|
|
625
|
+
|
|
626
|
+
storage = _get_storage_backend(storage_config)
|
|
627
|
+
if hasattr(storage, "connect"):
|
|
628
|
+
await storage.connect()
|
|
629
|
+
|
|
630
|
+
# Wait for WORKFLOW_SUSPENDED event to avoid sequence number race
|
|
631
|
+
max_wait_attempts = 50
|
|
632
|
+
wait_interval = 0.01
|
|
633
|
+
|
|
634
|
+
for _attempt in range(max_wait_attempts):
|
|
635
|
+
has_suspended = await storage.has_event(
|
|
636
|
+
run_id, EventType.WORKFLOW_SUSPENDED.value, step_id=step_id
|
|
637
|
+
)
|
|
638
|
+
if has_suspended:
|
|
639
|
+
break
|
|
640
|
+
await asyncio.sleep(wait_interval)
|
|
641
|
+
else:
|
|
642
|
+
logger.warning(
|
|
643
|
+
"Timeout waiting for WORKFLOW_SUSPENDED before recording STEP_SUSPENDED",
|
|
644
|
+
run_id=run_id,
|
|
645
|
+
step_id=step_id,
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
event = create_step_suspended_event(
|
|
649
|
+
run_id=run_id,
|
|
650
|
+
step_id=step_id,
|
|
651
|
+
step_name=step_name,
|
|
652
|
+
hook_id=hook_id,
|
|
653
|
+
)
|
|
654
|
+
await storage.record_event(event)
|
|
655
|
+
logger.info(
|
|
656
|
+
"Step suspended event recorded",
|
|
657
|
+
run_id=run_id,
|
|
658
|
+
step_id=step_id,
|
|
659
|
+
step_name=step_name,
|
|
660
|
+
hook_id=hook_id,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
|
|
570
664
|
async def _record_step_failure_and_resume(
|
|
571
665
|
storage_config: dict[str, Any] | None,
|
|
572
666
|
run_id: str,
|
|
@@ -299,12 +299,12 @@ async def list_hooks_cmd(
|
|
|
299
299
|
"Name": hook.name or "-",
|
|
300
300
|
"Status": hook.status.value,
|
|
301
301
|
"Run ID": hook.run_id,
|
|
302
|
-
"Created":
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
"Expires":
|
|
306
|
-
|
|
307
|
-
|
|
302
|
+
"Created": (
|
|
303
|
+
hook.created_at.strftime("%Y-%m-%d %H:%M") if hook.created_at else "-"
|
|
304
|
+
),
|
|
305
|
+
"Expires": (
|
|
306
|
+
hook.expires_at.strftime("%Y-%m-%d %H:%M") if hook.expires_at else "-"
|
|
307
|
+
),
|
|
308
308
|
}
|
|
309
309
|
for hook in hooks_list
|
|
310
310
|
]
|
|
@@ -376,15 +376,15 @@ async def hook_info_cmd(ctx: click.Context, token: str) -> None:
|
|
|
376
376
|
"Run ID": hook.run_id,
|
|
377
377
|
"Name": hook.name or "-",
|
|
378
378
|
"Status": hook.status.value,
|
|
379
|
-
"Created":
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
"Expires":
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
"Received":
|
|
386
|
-
|
|
387
|
-
|
|
379
|
+
"Created": (
|
|
380
|
+
hook.created_at.strftime("%Y-%m-%d %H:%M:%S") if hook.created_at else "-"
|
|
381
|
+
),
|
|
382
|
+
"Expires": (
|
|
383
|
+
hook.expires_at.strftime("%Y-%m-%d %H:%M:%S") if hook.expires_at else "-"
|
|
384
|
+
),
|
|
385
|
+
"Received": (
|
|
386
|
+
hook.received_at.strftime("%Y-%m-%d %H:%M:%S") if hook.received_at else "-"
|
|
387
|
+
),
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
# Show payload if received
|
|
@@ -151,9 +151,9 @@ async def list_runs(
|
|
|
151
151
|
"Run ID": run.run_id,
|
|
152
152
|
"Workflow": run.workflow_name,
|
|
153
153
|
"Status": run.status.value,
|
|
154
|
-
"Started":
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
"Started": (
|
|
155
|
+
run.started_at.strftime("%Y-%m-%d %H:%M:%S") if run.started_at else "-"
|
|
156
|
+
),
|
|
157
157
|
"Duration": durations.get(run.run_id, "-"),
|
|
158
158
|
}
|
|
159
159
|
for run in runs_list
|
|
@@ -238,9 +238,9 @@ async def run_status(ctx: click.Context, run_id: str) -> None:
|
|
|
238
238
|
"Status": run.status.value,
|
|
239
239
|
"Created": run.created_at.strftime("%Y-%m-%d %H:%M:%S") if run.created_at else "-",
|
|
240
240
|
"Started": run.started_at.strftime("%Y-%m-%d %H:%M:%S") if run.started_at else "-",
|
|
241
|
-
"Completed":
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
"Completed": (
|
|
242
|
+
run.completed_at.strftime("%Y-%m-%d %H:%M:%S") if run.completed_at else "-"
|
|
243
|
+
),
|
|
244
244
|
"Duration": duration_str,
|
|
245
245
|
}
|
|
246
246
|
|
|
@@ -622,9 +622,9 @@ async def list_children(
|
|
|
622
622
|
"Workflow": child.workflow_name,
|
|
623
623
|
"Status": child.status.value,
|
|
624
624
|
"Depth": child.nesting_depth,
|
|
625
|
-
"Started":
|
|
626
|
-
|
|
627
|
-
|
|
625
|
+
"Started": (
|
|
626
|
+
child.started_at.strftime("%Y-%m-%d %H:%M:%S") if child.started_at else "-"
|
|
627
|
+
),
|
|
628
628
|
"Duration": _calc_duration(child),
|
|
629
629
|
}
|
|
630
630
|
for child in children
|
|
@@ -123,9 +123,9 @@ async def list_schedules_cmd(
|
|
|
123
123
|
"Workflow": s.workflow_name,
|
|
124
124
|
"Status": s.status.value,
|
|
125
125
|
"Schedule": describe_schedule(s.spec),
|
|
126
|
-
"Next Run":
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
"Next Run": (
|
|
127
|
+
s.next_run_time.strftime("%Y-%m-%d %H:%M:%S") if s.next_run_time else "-"
|
|
128
|
+
),
|
|
129
129
|
"Runs": f"{s.successful_runs}/{s.total_runs}",
|
|
130
130
|
}
|
|
131
131
|
for s in schedules_list
|
|
@@ -241,9 +241,9 @@ async def create_schedule_cmd(
|
|
|
241
241
|
"status": schedule.status.value,
|
|
242
242
|
"spec": describe_schedule(schedule.spec),
|
|
243
243
|
"overlap_policy": schedule.overlap_policy.value,
|
|
244
|
-
"next_run_time":
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
"next_run_time": (
|
|
245
|
+
schedule.next_run_time.isoformat() if schedule.next_run_time else None
|
|
246
|
+
),
|
|
247
247
|
}
|
|
248
248
|
format_json(data)
|
|
249
249
|
else:
|
|
@@ -308,9 +308,9 @@ async def show_schedule_cmd(
|
|
|
308
308
|
"timezone": schedule.spec.timezone,
|
|
309
309
|
},
|
|
310
310
|
"overlap_policy": schedule.overlap_policy.value,
|
|
311
|
-
"next_run_time":
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
"next_run_time": (
|
|
312
|
+
schedule.next_run_time.isoformat() if schedule.next_run_time else None
|
|
313
|
+
),
|
|
314
314
|
"last_run_at": schedule.last_run_at.isoformat() if schedule.last_run_at else None,
|
|
315
315
|
"total_runs": schedule.total_runs,
|
|
316
316
|
"successful_runs": schedule.successful_runs,
|
|
@@ -327,19 +327,25 @@ async def show_schedule_cmd(
|
|
|
327
327
|
"Status": schedule.status.value,
|
|
328
328
|
"Schedule": describe_schedule(schedule.spec),
|
|
329
329
|
"Overlap Policy": schedule.overlap_policy.value,
|
|
330
|
-
"Next Run":
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
330
|
+
"Next Run": (
|
|
331
|
+
schedule.next_run_time.strftime("%Y-%m-%d %H:%M:%S")
|
|
332
|
+
if schedule.next_run_time
|
|
333
|
+
else "-"
|
|
334
|
+
),
|
|
335
|
+
"Last Run": (
|
|
336
|
+
schedule.last_run_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
337
|
+
if schedule.last_run_at
|
|
338
|
+
else "-"
|
|
339
|
+
),
|
|
336
340
|
"Total Runs": schedule.total_runs,
|
|
337
341
|
"Successful": schedule.successful_runs,
|
|
338
342
|
"Failed": schedule.failed_runs,
|
|
339
343
|
"Skipped": schedule.skipped_runs,
|
|
340
|
-
"Created":
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
"Created": (
|
|
345
|
+
schedule.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
346
|
+
if schedule.created_at
|
|
347
|
+
else "-"
|
|
348
|
+
),
|
|
343
349
|
}
|
|
344
350
|
format_key_value(data, title=f"Schedule: {schedule_id}")
|
|
345
351
|
|
|
@@ -440,9 +446,9 @@ async def resume_schedule_cmd(
|
|
|
440
446
|
data = {
|
|
441
447
|
"schedule_id": schedule.schedule_id,
|
|
442
448
|
"status": schedule.status.value,
|
|
443
|
-
"next_run_time":
|
|
444
|
-
|
|
445
|
-
|
|
449
|
+
"next_run_time": (
|
|
450
|
+
schedule.next_run_time.isoformat() if schedule.next_run_time else None
|
|
451
|
+
),
|
|
446
452
|
}
|
|
447
453
|
format_json(data)
|
|
448
454
|
|
|
@@ -774,9 +780,9 @@ async def update_schedule_cmd(
|
|
|
774
780
|
"workflow_name": schedule.workflow_name,
|
|
775
781
|
"spec": describe_schedule(schedule.spec),
|
|
776
782
|
"overlap_policy": schedule.overlap_policy.value,
|
|
777
|
-
"next_run_time":
|
|
778
|
-
|
|
779
|
-
|
|
783
|
+
"next_run_time": (
|
|
784
|
+
schedule.next_run_time.isoformat() if schedule.next_run_time else None
|
|
785
|
+
),
|
|
780
786
|
}
|
|
781
787
|
format_json(data)
|
|
782
788
|
else:
|
|
@@ -94,6 +94,10 @@ class LocalContext(WorkflowContext):
|
|
|
94
94
|
self._cancellation_blocked: bool = False
|
|
95
95
|
self._cancellation_reason: str | None = None
|
|
96
96
|
|
|
97
|
+
# Signal/stream state (used by EventReplayer for stream workflows)
|
|
98
|
+
self._signal_waits: dict[str, dict[str, Any]] = {}
|
|
99
|
+
self._received_signals: dict[str, dict[str, Any]] = {}
|
|
100
|
+
|
|
97
101
|
# Child workflow state
|
|
98
102
|
self._child_results: dict[str, dict[str, Any]] = {}
|
|
99
103
|
self._pending_children: dict[str, str] = {} # child_id -> child_run_id
|
|
@@ -161,6 +165,13 @@ class LocalContext(WorkflowContext):
|
|
|
161
165
|
# Step completed - no longer in progress
|
|
162
166
|
self._steps_in_progress.discard(step_id)
|
|
163
167
|
|
|
168
|
+
elif event.type == EventType.STEP_SUSPENDED:
|
|
169
|
+
step_id = event.data.get("step_id")
|
|
170
|
+
if step_id:
|
|
171
|
+
# Step suspended via step_hook — no longer in progress,
|
|
172
|
+
# needs re-dispatch on workflow resume
|
|
173
|
+
self._steps_in_progress.discard(step_id)
|
|
174
|
+
|
|
164
175
|
elif event.type == EventType.SLEEP_COMPLETED:
|
|
165
176
|
sleep_id = event.data.get("sleep_id")
|
|
166
177
|
self._completed_sleeps.add(sleep_id)
|
|
@@ -21,7 +21,7 @@ from typing import Any
|
|
|
21
21
|
from loguru import logger
|
|
22
22
|
|
|
23
23
|
from pyworkflow.context import get_context, has_context
|
|
24
|
-
from pyworkflow.core.exceptions import FatalError, RetryableError
|
|
24
|
+
from pyworkflow.core.exceptions import FatalError, RetryableError, SuspensionSignal
|
|
25
25
|
from pyworkflow.core.registry import register_step
|
|
26
26
|
from pyworkflow.core.validation import validate_step_parameters
|
|
27
27
|
from pyworkflow.engine.events import (
|
|
@@ -29,6 +29,10 @@ from pyworkflow.engine.events import (
|
|
|
29
29
|
create_step_failed_event,
|
|
30
30
|
create_step_started_event,
|
|
31
31
|
)
|
|
32
|
+
from pyworkflow.primitives.step_checkpoint import (
|
|
33
|
+
reset_step_execution_context,
|
|
34
|
+
set_step_execution_context,
|
|
35
|
+
)
|
|
32
36
|
from pyworkflow.serialization.encoder import serialize, serialize_args, serialize_kwargs
|
|
33
37
|
|
|
34
38
|
|
|
@@ -177,8 +181,6 @@ def step(
|
|
|
177
181
|
step_id=step_id,
|
|
178
182
|
)
|
|
179
183
|
# Re-suspend and wait for existing task to complete
|
|
180
|
-
from pyworkflow.core.exceptions import SuspensionSignal
|
|
181
|
-
|
|
182
184
|
raise SuspensionSignal(
|
|
183
185
|
reason=f"step_dispatch:{step_id}",
|
|
184
186
|
step_id=step_id,
|
|
@@ -229,8 +231,6 @@ def step(
|
|
|
229
231
|
current_attempt=current_attempt,
|
|
230
232
|
resume_at=resume_at.isoformat(),
|
|
231
233
|
)
|
|
232
|
-
from pyworkflow.core.exceptions import SuspensionSignal
|
|
233
|
-
|
|
234
234
|
raise SuspensionSignal(
|
|
235
235
|
reason=f"retry:{step_id}",
|
|
236
236
|
resume_at=resume_at,
|
|
@@ -268,6 +268,10 @@ def step(
|
|
|
268
268
|
# Validate parameters before execution
|
|
269
269
|
validate_step_parameters(func, args, kwargs, step_name)
|
|
270
270
|
|
|
271
|
+
# Set up step execution context for checkpoint/hook primitives
|
|
272
|
+
step_exec_key = f"{ctx.run_id}:{step_id}"
|
|
273
|
+
step_exec_tokens = set_step_execution_context(step_exec_key, ctx.storage)
|
|
274
|
+
|
|
271
275
|
try:
|
|
272
276
|
# Execute step function
|
|
273
277
|
result = await func(*args, **kwargs)
|
|
@@ -295,6 +299,15 @@ def step(
|
|
|
295
299
|
|
|
296
300
|
return result
|
|
297
301
|
|
|
302
|
+
except SuspensionSignal:
|
|
303
|
+
# step_hook() raised SuspensionSignal — propagate to suspend workflow
|
|
304
|
+
logger.info(
|
|
305
|
+
f"Step suspended via step_hook: {step_name}",
|
|
306
|
+
run_id=ctx.run_id,
|
|
307
|
+
step_id=step_id,
|
|
308
|
+
)
|
|
309
|
+
raise
|
|
310
|
+
|
|
298
311
|
except FatalError as e:
|
|
299
312
|
# Fatal error - don't retry
|
|
300
313
|
logger.error(
|
|
@@ -393,8 +406,6 @@ def step(
|
|
|
393
406
|
|
|
394
407
|
# Raise suspension signal to pause workflow
|
|
395
408
|
# Note: The workflow-level exception handler will schedule automatic resumption
|
|
396
|
-
from pyworkflow.core.exceptions import SuspensionSignal
|
|
397
|
-
|
|
398
409
|
raise SuspensionSignal(
|
|
399
410
|
reason=f"retry:{step_id}",
|
|
400
411
|
resume_at=resume_at,
|
|
@@ -434,6 +445,9 @@ def step(
|
|
|
434
445
|
else:
|
|
435
446
|
raise
|
|
436
447
|
|
|
448
|
+
finally:
|
|
449
|
+
reset_step_execution_context(step_exec_tokens)
|
|
450
|
+
|
|
437
451
|
# Register step
|
|
438
452
|
register_step(
|
|
439
453
|
name=step_name,
|
|
@@ -645,7 +659,6 @@ async def _dispatch_step_to_celery(
|
|
|
645
659
|
SuspensionSignal: To pause workflow while step executes on worker
|
|
646
660
|
"""
|
|
647
661
|
from pyworkflow.celery.tasks import execute_step_task
|
|
648
|
-
from pyworkflow.core.exceptions import SuspensionSignal
|
|
649
662
|
from pyworkflow.engine.events import EventType
|
|
650
663
|
|
|
651
664
|
logger.info(
|
|
@@ -657,11 +670,17 @@ async def _dispatch_step_to_celery(
|
|
|
657
670
|
# Defense-in-depth: check if STEP_STARTED was already recorded for this step.
|
|
658
671
|
# This guards against duplicate dispatch when two resume tasks race and both
|
|
659
672
|
# replay past the same step. If already started, re-suspend to wait.
|
|
673
|
+
# Exception: if STEP_SUSPENDED was recorded (step_hook suspension), the step
|
|
674
|
+
# needs re-dispatch so it can re-execute and find the HOOK_RECEIVED event.
|
|
660
675
|
events = await ctx.storage.get_events(ctx.run_id)
|
|
661
676
|
already_started = any(
|
|
662
677
|
evt.type == EventType.STEP_STARTED and evt.data.get("step_id") == step_id for evt in events
|
|
663
678
|
)
|
|
664
|
-
|
|
679
|
+
was_suspended = any(
|
|
680
|
+
evt.type == EventType.STEP_SUSPENDED and evt.data.get("step_id") == step_id
|
|
681
|
+
for evt in events
|
|
682
|
+
)
|
|
683
|
+
if already_started and not was_suspended:
|
|
665
684
|
logger.info(
|
|
666
685
|
f"Step {step_name} already has STEP_STARTED event, re-suspending",
|
|
667
686
|
run_id=ctx.run_id,
|