pyworkflow-engine 0.2.5__tar.gz → 0.2.7__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.5 → pyworkflow_engine-0.2.7}/PKG-INFO +1 -1
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyproject.toml +1 -1
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/__init__.py +1 -1
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/celery/app.py +5 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/celery/tasks.py +100 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/context/local.py +0 -38
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/base.py +101 -1
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/cassandra.py +2 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/citus.py +120 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/dynamodb.py +2 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/file.py +2 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/memory.py +108 -5
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/migrations/base.py +38 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/mysql.py +2 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/postgres.py +337 -18
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/sqlite.py +2 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/streams/__init__.py +14 -1
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/streams/consumer.py +41 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/streams/context.py +75 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/streams/dispatcher.py +233 -11
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/streams/emit.py +94 -3
- pyworkflow_engine-0.2.7/pyworkflow/streams/runtime.py +272 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/streams/step_context.py +34 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow_engine.egg-info/SOURCES.txt +1 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/test_schema_migrations.py +1 -1
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/CLAUDE.md +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/DISTRIBUTED.md +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/LICENSE +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/MANIFEST.in +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/README.md +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/RELEASING.md +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/architecture.md +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/concepts/cancellation.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/concepts/continue-as-new.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/concepts/events.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/concepts/fault-tolerance.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/concepts/hooks.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/concepts/limitations.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/concepts/schedules.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/concepts/sleep.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/concepts/step-context.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/concepts/steps.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/concepts/workflows.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/conventions.md +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/guides/brokers.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/guides/cli.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/guides/configuration.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/harness-gaps.md +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/introduction.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/layers.md +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/docs/quickstart.mdx +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/docker-compose.yml +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/basic.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/batch_processing.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/cancellation.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/child_workflows.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/hooks.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/idempotency.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/long_running.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/retries.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/schedules.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/step_context.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/transient/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/transient/02_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/transient/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/celery/transient/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/02_file_storage.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/03_retries.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/04_long_running.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/05_event_log.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/06_idempotency.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/07_hooks.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/08_cancellation.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/09_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/10_child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/11_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/12_schedules.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/13_step_context.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/14_child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/durable/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/transient/01_quick_tasks.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/transient/02_retries.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/transient/03_sleep.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/examples/local/transient/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/aws/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/aws/context.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/aws/handler.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/aws/testing.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/celery/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/celery/loop.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/celery/scheduler.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/celery/singleton.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/__main__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/hooks.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/quickstart.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/runs.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/scheduler.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/schedules.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/setup.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/worker.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/workflows.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/output/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/output/formatters.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/output/styles.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/async_helpers.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/config.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/config_generator.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/discovery.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/docker_manager.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/interactive.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/storage.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/config.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/context/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/context/aws.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/context/base.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/context/mock.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/context/step_context.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/core/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/core/exceptions.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/core/registry.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/core/scheduled.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/core/step.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/core/validation.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/core/workflow.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/discovery.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/engine/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/engine/events.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/engine/executor.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/engine/replay.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/observability/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/observability/logging.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/child_handle.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/child_workflow.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/define_hook.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/hooks.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/resume_hook.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/schedule.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/shield.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/sleep.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/step_checkpoint.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/step_hook.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/runtime/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/runtime/base.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/runtime/celery.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/runtime/factory.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/runtime/local.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/scheduler/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/scheduler/local.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/serialization/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/serialization/decoder.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/serialization/encoder.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/config.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/migrations/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/storage/schemas.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/streams/checkpoint.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/streams/decorator.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/streams/registry.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/streams/signal.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/utils/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/utils/duration.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/utils/helpers.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/pyworkflow/utils/schedule.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/setup.cfg +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/test_cancellation.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/test_schedule_storage.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/test_singleton.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/test_stream_e2e.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/integration/test_workflow_suspended.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/backends/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/backends/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/backends/test_citus_storage.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/backends/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/backends/test_postgres_storage.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/backends/test_sqlite_storage.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/conftest.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/storage/__init__.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/storage/test_migrations.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_cancellation.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_cli_worker.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_emit.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_event_limits.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_executor.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_force_local.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_hooks.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_parent_run_id.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_primitives_from_steps.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_registry.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_replay.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_retention.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_schedule_schemas.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_schedule_utils.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_scheduled_workflow.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_signal.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_singleton.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_step.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_step_checkpoint.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_step_context.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_step_hook.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_stream_storage.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_stream_workflow.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_validation.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/tests/unit/test_workflow.py +0 -0
- {pyworkflow_engine-0.2.5 → pyworkflow_engine-0.2.7}/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.2.
|
|
10
|
+
version = "0.2.7"
|
|
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"
|
|
@@ -335,6 +335,11 @@ def create_celery_app(
|
|
|
335
335
|
"schedule": timedelta(hours=24),
|
|
336
336
|
"options": {"queue": "pyworkflow.default"},
|
|
337
337
|
},
|
|
338
|
+
"pyworkflow-streams-drain-scheduled": {
|
|
339
|
+
"task": "pyworkflow.streams.drain_scheduled_signals",
|
|
340
|
+
"schedule": timedelta(seconds=2),
|
|
341
|
+
"options": {"queue": "pyworkflow.default"},
|
|
342
|
+
},
|
|
338
343
|
},
|
|
339
344
|
# Logging
|
|
340
345
|
"worker_log_format": "[%(asctime)s: %(levelname)s/%(processName)s] %(message)s",
|
|
@@ -2840,3 +2840,103 @@ def run_data_retention_task(self: SingletonWorkflowTask) -> dict[str, Any]:
|
|
|
2840
2840
|
count = run_async(backend.delete_old_runs(cutoff))
|
|
2841
2841
|
logger.info("Data retention complete: deleted {} runs", count)
|
|
2842
2842
|
return {"deleted": count, "skipped": False}
|
|
2843
|
+
|
|
2844
|
+
|
|
2845
|
+
# =============================================================================
|
|
2846
|
+
# Stream-only celery tasks (private to pyworkflow.streams)
|
|
2847
|
+
# =============================================================================
|
|
2848
|
+
|
|
2849
|
+
|
|
2850
|
+
@celery_app.task(
|
|
2851
|
+
name="pyworkflow.streams.dispatch_signal",
|
|
2852
|
+
queue="pyworkflow.default",
|
|
2853
|
+
)
|
|
2854
|
+
def dispatch_stream_signal_task(
|
|
2855
|
+
signal_id: str,
|
|
2856
|
+
stream_id: str,
|
|
2857
|
+
signal_type: str,
|
|
2858
|
+
payload: dict[str, Any],
|
|
2859
|
+
sequence: int,
|
|
2860
|
+
source_run_id: str | None,
|
|
2861
|
+
stream_run_id: str | None,
|
|
2862
|
+
metadata: dict[str, Any] | None,
|
|
2863
|
+
storage_config: dict[str, Any] | None = None,
|
|
2864
|
+
) -> None:
|
|
2865
|
+
"""Dispatch a previously persisted signal to its subscribed stream steps.
|
|
2866
|
+
|
|
2867
|
+
Reconstructs the Signal from inline kwargs (avoids a DB round-trip),
|
|
2868
|
+
runs the dispatcher logic on the worker, and after step execution
|
|
2869
|
+
completes calls _maybe_resume_stream_parent so the parent @workflow
|
|
2870
|
+
can be resumed via resume_hook() if the aggregate is now terminal.
|
|
2871
|
+
"""
|
|
2872
|
+
from pyworkflow.celery.app import _configure_worker_logging
|
|
2873
|
+
|
|
2874
|
+
_configure_worker_logging()
|
|
2875
|
+
|
|
2876
|
+
storage = _get_storage_backend(storage_config)
|
|
2877
|
+
|
|
2878
|
+
from pyworkflow.streams.dispatcher import dispatch_signal
|
|
2879
|
+
from pyworkflow.streams.signal import Signal
|
|
2880
|
+
|
|
2881
|
+
signal = Signal(
|
|
2882
|
+
signal_id=signal_id,
|
|
2883
|
+
stream_id=stream_id,
|
|
2884
|
+
signal_type=signal_type,
|
|
2885
|
+
payload=payload or {},
|
|
2886
|
+
sequence=sequence,
|
|
2887
|
+
source_run_id=source_run_id,
|
|
2888
|
+
stream_run_id=stream_run_id,
|
|
2889
|
+
metadata=metadata or {},
|
|
2890
|
+
)
|
|
2891
|
+
|
|
2892
|
+
run_async(dispatch_signal(signal, storage))
|
|
2893
|
+
|
|
2894
|
+
|
|
2895
|
+
@celery_app.task(
|
|
2896
|
+
name="pyworkflow.streams.drain_scheduled_signals",
|
|
2897
|
+
base=SingletonWorkflowTask,
|
|
2898
|
+
bind=True,
|
|
2899
|
+
queue="pyworkflow.default",
|
|
2900
|
+
release_lock_on_failure=True,
|
|
2901
|
+
lock_expiry=300,
|
|
2902
|
+
)
|
|
2903
|
+
def drain_scheduled_signals_task(self: SingletonWorkflowTask) -> dict[str, Any]:
|
|
2904
|
+
"""Periodic task: deliver any due rows from the ``scheduled_signals``
|
|
2905
|
+
table by calling :func:`pyworkflow.streams.emit.emit`. Replaces the
|
|
2906
|
+
in-process poller that previously ran inside ``run_stream_workflow``."""
|
|
2907
|
+
from pyworkflow.celery.app import _configure_worker_logging
|
|
2908
|
+
from pyworkflow.config import get_config
|
|
2909
|
+
|
|
2910
|
+
_configure_worker_logging()
|
|
2911
|
+
|
|
2912
|
+
config = get_config()
|
|
2913
|
+
storage = config.storage
|
|
2914
|
+
if storage is None:
|
|
2915
|
+
return {"delivered": 0, "skipped": True}
|
|
2916
|
+
|
|
2917
|
+
async def _drain() -> int:
|
|
2918
|
+
from pyworkflow.streams.emit import emit
|
|
2919
|
+
|
|
2920
|
+
try:
|
|
2921
|
+
due = await storage.fetch_due_scheduled_signals(datetime.now(UTC), limit=100)
|
|
2922
|
+
except NotImplementedError:
|
|
2923
|
+
return 0
|
|
2924
|
+
delivered = 0
|
|
2925
|
+
for row in due or []:
|
|
2926
|
+
try:
|
|
2927
|
+
await emit(
|
|
2928
|
+
stream_id=row["stream_id"],
|
|
2929
|
+
signal_type=row["signal_type"],
|
|
2930
|
+
payload=row.get("payload") or {},
|
|
2931
|
+
storage=storage,
|
|
2932
|
+
stream_run_id=row.get("stream_run_id"),
|
|
2933
|
+
metadata=row.get("metadata") or {},
|
|
2934
|
+
)
|
|
2935
|
+
await storage.mark_scheduled_signal_delivered(row["id"])
|
|
2936
|
+
delivered += 1
|
|
2937
|
+
except Exception as e: # noqa: BLE001
|
|
2938
|
+
logger.error(f"[drain_scheduled_signals] failed to deliver {row.get('id')}: {e}")
|
|
2939
|
+
return delivered
|
|
2940
|
+
|
|
2941
|
+
count = run_async(_drain())
|
|
2942
|
+
return {"delivered": count, "skipped": False}
|
|
@@ -121,46 +121,12 @@ class LocalContext(WorkflowContext):
|
|
|
121
121
|
self._replay_events(event_log)
|
|
122
122
|
self._is_replaying = False
|
|
123
123
|
|
|
124
|
-
def _extract_counter_from_id(self, id_string: str) -> int:
|
|
125
|
-
"""Extract counter value from hook_id or sleep_id.
|
|
126
|
-
|
|
127
|
-
Formats:
|
|
128
|
-
- hook_{name}_{counter}
|
|
129
|
-
- sleep_{counter}_{duration}s
|
|
130
|
-
|
|
131
|
-
Args:
|
|
132
|
-
id_string: The hook_id or sleep_id string
|
|
133
|
-
|
|
134
|
-
Returns:
|
|
135
|
-
The counter value, or 0 if parsing fails
|
|
136
|
-
"""
|
|
137
|
-
try:
|
|
138
|
-
parts = id_string.split("_")
|
|
139
|
-
if id_string.startswith("hook_"):
|
|
140
|
-
# hook_{name}_{counter} - counter is last part
|
|
141
|
-
return int(parts[-1])
|
|
142
|
-
elif id_string.startswith("sleep_"):
|
|
143
|
-
# sleep_{counter}_{duration}s - counter is second part
|
|
144
|
-
return int(parts[1])
|
|
145
|
-
except (ValueError, IndexError):
|
|
146
|
-
pass
|
|
147
|
-
return 0
|
|
148
|
-
|
|
149
124
|
def _replay_events(self, events: list[Any]) -> None:
|
|
150
125
|
"""Replay events to restore state."""
|
|
151
126
|
from pyworkflow.engine.events import EventType
|
|
152
127
|
from pyworkflow.serialization.decoder import deserialize
|
|
153
128
|
|
|
154
|
-
max_counter = 0
|
|
155
129
|
for event in events:
|
|
156
|
-
# Track step counter from sleep and hook IDs to avoid collisions
|
|
157
|
-
for key in ("sleep_id", "hook_id"):
|
|
158
|
-
id_val = event.data.get(key) if event.data else None
|
|
159
|
-
if id_val:
|
|
160
|
-
c = self._extract_counter_from_id(id_val)
|
|
161
|
-
if c > max_counter:
|
|
162
|
-
max_counter = c
|
|
163
|
-
|
|
164
130
|
if event.type == EventType.STEP_STARTED:
|
|
165
131
|
# Track step as in-progress (dispatched but not completed)
|
|
166
132
|
step_id = event.data.get("step_id")
|
|
@@ -280,10 +246,6 @@ class LocalContext(WorkflowContext):
|
|
|
280
246
|
}
|
|
281
247
|
self._pending_children.pop(child_id, None)
|
|
282
248
|
|
|
283
|
-
# Restore step counter so new sleeps/hooks don't collide with replayed ones
|
|
284
|
-
if max_counter > self._step_counter:
|
|
285
|
-
self._step_counter = max_counter
|
|
286
|
-
|
|
287
249
|
@property
|
|
288
250
|
def is_durable(self) -> bool:
|
|
289
251
|
return self._durable
|
|
@@ -7,6 +7,7 @@ across different backends (File, Redis, SQLite, PostgreSQL).
|
|
|
7
7
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
9
|
from datetime import datetime
|
|
10
|
+
from typing import Any
|
|
10
11
|
|
|
11
12
|
from pyworkflow.engine.events import Event
|
|
12
13
|
from pyworkflow.storage.schemas import (
|
|
@@ -841,6 +842,7 @@ class StorageBackend(ABC):
|
|
|
841
842
|
stream_id: str,
|
|
842
843
|
step_run_id: str,
|
|
843
844
|
signal_types: list[str],
|
|
845
|
+
stream_run_id: str | None = None,
|
|
844
846
|
) -> None:
|
|
845
847
|
"""
|
|
846
848
|
Register a stream step's subscription to signal types.
|
|
@@ -859,6 +861,7 @@ class StorageBackend(ABC):
|
|
|
859
861
|
self,
|
|
860
862
|
stream_id: str,
|
|
861
863
|
signal_type: str,
|
|
864
|
+
stream_run_id: str | None = None,
|
|
862
865
|
) -> list[dict]:
|
|
863
866
|
"""
|
|
864
867
|
Get step_run_ids waiting for a specific signal type on a stream.
|
|
@@ -879,6 +882,7 @@ class StorageBackend(ABC):
|
|
|
879
882
|
self,
|
|
880
883
|
stream_id: str,
|
|
881
884
|
signal_type: str,
|
|
885
|
+
stream_run_id: str | None = None,
|
|
882
886
|
) -> list[dict]:
|
|
883
887
|
"""
|
|
884
888
|
Get ALL subscriptions for a signal type on a stream, regardless of status.
|
|
@@ -897,6 +901,40 @@ class StorageBackend(ABC):
|
|
|
897
901
|
"""
|
|
898
902
|
raise NotImplementedError("This storage backend does not support streams")
|
|
899
903
|
|
|
904
|
+
async def set_subscription_result(
|
|
905
|
+
self,
|
|
906
|
+
stream_id: str,
|
|
907
|
+
step_run_id: str,
|
|
908
|
+
result: Any,
|
|
909
|
+
) -> None:
|
|
910
|
+
"""Persist a stream step's result payload on its subscription row.
|
|
911
|
+
|
|
912
|
+
Returned to the parent ``@workflow`` via
|
|
913
|
+
``StreamWorkflowResult.step_results`` so it doesn't have to fish
|
|
914
|
+
through checkpoints to read step output.
|
|
915
|
+
"""
|
|
916
|
+
raise NotImplementedError("This storage backend does not support streams")
|
|
917
|
+
|
|
918
|
+
async def set_stream_parent_link(
|
|
919
|
+
self,
|
|
920
|
+
stream_id: str,
|
|
921
|
+
stream_run_id: str,
|
|
922
|
+
parent_run_id: str,
|
|
923
|
+
parent_hook_token: str,
|
|
924
|
+
) -> None:
|
|
925
|
+
"""Record (parent_run_id, parent_hook_token) for a stream run so the
|
|
926
|
+
stream-step dispatcher can resume the parent @workflow via resume_hook()
|
|
927
|
+
once the stream reaches a terminal aggregate state."""
|
|
928
|
+
raise NotImplementedError("This storage backend does not support streams")
|
|
929
|
+
|
|
930
|
+
async def get_stream_parent_link(
|
|
931
|
+
self,
|
|
932
|
+
stream_id: str,
|
|
933
|
+
stream_run_id: str,
|
|
934
|
+
) -> tuple[str, str] | None:
|
|
935
|
+
"""Return the (parent_run_id, parent_hook_token) recorded for a stream run."""
|
|
936
|
+
raise NotImplementedError("This storage backend does not support streams")
|
|
937
|
+
|
|
900
938
|
async def update_subscription_status(
|
|
901
939
|
self,
|
|
902
940
|
stream_id: str,
|
|
@@ -909,13 +947,40 @@ class StorageBackend(ABC):
|
|
|
909
947
|
Args:
|
|
910
948
|
stream_id: Stream identifier
|
|
911
949
|
step_run_id: The stream step's run ID
|
|
912
|
-
status: New status
|
|
950
|
+
status: New status. One of:
|
|
951
|
+
- "waiting": ready for next signal
|
|
952
|
+
- "running": currently executing lifecycle
|
|
953
|
+
- "suspended": alive but waiting for an external resume condition
|
|
954
|
+
(e.g. HITL). Contributes "suspended" to the stream-workflow
|
|
955
|
+
aggregate state.
|
|
956
|
+
- "terminated": permanently done; dispatcher will skip it.
|
|
957
|
+
- "completed": legacy alias for "terminated".
|
|
913
958
|
|
|
914
959
|
Raises:
|
|
915
960
|
NotImplementedError: If backend doesn't support streams
|
|
916
961
|
"""
|
|
917
962
|
raise NotImplementedError("This storage backend does not support streams")
|
|
918
963
|
|
|
964
|
+
async def get_subscription_states(
|
|
965
|
+
self,
|
|
966
|
+
stream_id: str,
|
|
967
|
+
stream_run_id: str | None = None,
|
|
968
|
+
) -> list[dict]:
|
|
969
|
+
"""
|
|
970
|
+
Return all subscriptions and their statuses for a stream.
|
|
971
|
+
|
|
972
|
+
Used by the stream-workflow runtime to compute the aggregate state
|
|
973
|
+
across all registered ``@stream_step``s.
|
|
974
|
+
|
|
975
|
+
Args:
|
|
976
|
+
stream_id: Stream identifier
|
|
977
|
+
stream_run_id: Optional stream run scope
|
|
978
|
+
|
|
979
|
+
Returns:
|
|
980
|
+
List of dicts with at least ``step_run_id`` and ``status`` keys.
|
|
981
|
+
"""
|
|
982
|
+
raise NotImplementedError("This storage backend does not support streams")
|
|
983
|
+
|
|
919
984
|
async def acknowledge_signal(
|
|
920
985
|
self,
|
|
921
986
|
signal_id: str,
|
|
@@ -953,6 +1018,41 @@ class StorageBackend(ABC):
|
|
|
953
1018
|
"""
|
|
954
1019
|
raise NotImplementedError("This storage backend does not support streams")
|
|
955
1020
|
|
|
1021
|
+
# Scheduled Signal Operations (for schedule_signal primitive)
|
|
1022
|
+
|
|
1023
|
+
async def schedule_signal(
|
|
1024
|
+
self,
|
|
1025
|
+
*,
|
|
1026
|
+
stream_id: str,
|
|
1027
|
+
signal_type: str,
|
|
1028
|
+
payload: dict,
|
|
1029
|
+
due_at: "datetime", # noqa: F821 (forward ref; datetime imported by backends)
|
|
1030
|
+
stream_run_id: str | None = None,
|
|
1031
|
+
metadata: dict | None = None,
|
|
1032
|
+
) -> str:
|
|
1033
|
+
"""
|
|
1034
|
+
Persist a signal to be emitted at ``due_at``.
|
|
1035
|
+
|
|
1036
|
+
Used by the ``schedule_signal`` streams primitive for time-delayed
|
|
1037
|
+
signal delivery (e.g. supervisor sleep/wakeup).
|
|
1038
|
+
|
|
1039
|
+
Returns:
|
|
1040
|
+
Opaque scheduled-signal ID.
|
|
1041
|
+
"""
|
|
1042
|
+
raise NotImplementedError("This storage backend does not support scheduled signals")
|
|
1043
|
+
|
|
1044
|
+
async def fetch_due_scheduled_signals(
|
|
1045
|
+
self,
|
|
1046
|
+
now: "datetime", # noqa: F821
|
|
1047
|
+
limit: int = 100,
|
|
1048
|
+
) -> list[dict]:
|
|
1049
|
+
"""Return scheduled signals whose ``due_at <= now`` and not yet delivered."""
|
|
1050
|
+
raise NotImplementedError("This storage backend does not support scheduled signals")
|
|
1051
|
+
|
|
1052
|
+
async def mark_scheduled_signal_delivered(self, sched_id: str) -> None:
|
|
1053
|
+
"""Mark a scheduled signal as delivered so it is not emitted twice."""
|
|
1054
|
+
raise NotImplementedError("This storage backend does not support scheduled signals")
|
|
1055
|
+
|
|
956
1056
|
# Checkpoint Operations
|
|
957
1057
|
|
|
958
1058
|
async def save_checkpoint(self, step_run_id: str, data: dict) -> None:
|
|
@@ -2101,6 +2101,7 @@ class CassandraStorageBackend(StorageBackend):
|
|
|
2101
2101
|
stream_id: str,
|
|
2102
2102
|
step_run_id: str,
|
|
2103
2103
|
signal_types: list[str],
|
|
2104
|
+
stream_run_id: str | None = None,
|
|
2104
2105
|
) -> None:
|
|
2105
2106
|
"""Register a stream step's subscription to signal types."""
|
|
2106
2107
|
session = self._ensure_connected()
|
|
@@ -2119,6 +2120,7 @@ class CassandraStorageBackend(StorageBackend):
|
|
|
2119
2120
|
self,
|
|
2120
2121
|
stream_id: str,
|
|
2121
2122
|
signal_type: str,
|
|
2123
|
+
stream_run_id: str | None = None,
|
|
2122
2124
|
) -> list[dict]:
|
|
2123
2125
|
"""Get step_run_ids waiting for a specific signal type on a stream."""
|
|
2124
2126
|
session = self._ensure_connected()
|
|
@@ -110,6 +110,95 @@ class CitusMigrationRunner(PostgresMigrationRunner):
|
|
|
110
110
|
# Re-distribute the new tables
|
|
111
111
|
await conn.execute("SELECT create_distributed_table('signals', 'stream_run_id')")
|
|
112
112
|
await conn.execute("SELECT create_reference_table('signal_acknowledgments')")
|
|
113
|
+
elif migration.version == 4:
|
|
114
|
+
# V4: Drop FK constraints referencing streams table.
|
|
115
|
+
# Citus never created these FKs (cross-shard FKs unsupported),
|
|
116
|
+
# so this is a no-op but we still record the version.
|
|
117
|
+
pass
|
|
118
|
+
elif migration.version == 5:
|
|
119
|
+
# V5: Add stream_run_id to stream_subscriptions + scheduled_signals table
|
|
120
|
+
await conn.execute("""
|
|
121
|
+
DO $$
|
|
122
|
+
BEGIN
|
|
123
|
+
IF EXISTS (
|
|
124
|
+
SELECT FROM information_schema.tables
|
|
125
|
+
WHERE table_name = 'stream_subscriptions'
|
|
126
|
+
) AND NOT EXISTS (
|
|
127
|
+
SELECT 1 FROM information_schema.columns
|
|
128
|
+
WHERE table_name = 'stream_subscriptions'
|
|
129
|
+
AND column_name = 'stream_run_id'
|
|
130
|
+
) THEN
|
|
131
|
+
ALTER TABLE stream_subscriptions ADD COLUMN stream_run_id TEXT NULL;
|
|
132
|
+
END IF;
|
|
133
|
+
END $$
|
|
134
|
+
""")
|
|
135
|
+
await conn.execute(
|
|
136
|
+
"CREATE INDEX IF NOT EXISTS idx_subscriptions_stream_run "
|
|
137
|
+
"ON stream_subscriptions(stream_id, stream_run_id)"
|
|
138
|
+
)
|
|
139
|
+
await conn.execute("""
|
|
140
|
+
CREATE TABLE IF NOT EXISTS scheduled_signals (
|
|
141
|
+
id TEXT PRIMARY KEY,
|
|
142
|
+
stream_id TEXT NOT NULL,
|
|
143
|
+
signal_type TEXT NOT NULL,
|
|
144
|
+
payload JSONB NOT NULL DEFAULT '{}',
|
|
145
|
+
due_at TIMESTAMPTZ NOT NULL,
|
|
146
|
+
stream_run_id TEXT,
|
|
147
|
+
metadata JSONB DEFAULT '{}',
|
|
148
|
+
delivered BOOLEAN NOT NULL DEFAULT FALSE,
|
|
149
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
150
|
+
)
|
|
151
|
+
""")
|
|
152
|
+
await conn.execute(
|
|
153
|
+
"CREATE INDEX IF NOT EXISTS idx_scheduled_signals_due "
|
|
154
|
+
"ON scheduled_signals(delivered, due_at)"
|
|
155
|
+
)
|
|
156
|
+
# Distribute as reference table if not already distributed
|
|
157
|
+
already = await conn.fetchval("""
|
|
158
|
+
SELECT EXISTS (
|
|
159
|
+
SELECT 1 FROM pg_dist_partition dp
|
|
160
|
+
JOIN pg_class c ON dp.logicalrelid = c.oid
|
|
161
|
+
WHERE c.relname = 'scheduled_signals'
|
|
162
|
+
)
|
|
163
|
+
""")
|
|
164
|
+
if not already:
|
|
165
|
+
await conn.execute("SELECT create_reference_table('scheduled_signals')")
|
|
166
|
+
elif migration.version == 6:
|
|
167
|
+
# V6: Add parent_run_id + parent_hook_token to stream_subscriptions
|
|
168
|
+
await conn.execute("""
|
|
169
|
+
DO $$
|
|
170
|
+
BEGIN
|
|
171
|
+
IF EXISTS (
|
|
172
|
+
SELECT FROM information_schema.tables
|
|
173
|
+
WHERE table_name = 'stream_subscriptions'
|
|
174
|
+
) AND NOT EXISTS (
|
|
175
|
+
SELECT 1 FROM information_schema.columns
|
|
176
|
+
WHERE table_name = 'stream_subscriptions'
|
|
177
|
+
AND column_name = 'parent_run_id'
|
|
178
|
+
) THEN
|
|
179
|
+
ALTER TABLE stream_subscriptions
|
|
180
|
+
ADD COLUMN parent_run_id TEXT NULL,
|
|
181
|
+
ADD COLUMN parent_hook_token TEXT NULL;
|
|
182
|
+
END IF;
|
|
183
|
+
END $$
|
|
184
|
+
""")
|
|
185
|
+
elif migration.version == 7:
|
|
186
|
+
await conn.execute("""
|
|
187
|
+
DO $$
|
|
188
|
+
BEGIN
|
|
189
|
+
IF EXISTS (
|
|
190
|
+
SELECT FROM information_schema.tables
|
|
191
|
+
WHERE table_name = 'stream_subscriptions'
|
|
192
|
+
) AND NOT EXISTS (
|
|
193
|
+
SELECT 1 FROM information_schema.columns
|
|
194
|
+
WHERE table_name = 'stream_subscriptions'
|
|
195
|
+
AND column_name = 'result'
|
|
196
|
+
) THEN
|
|
197
|
+
ALTER TABLE stream_subscriptions
|
|
198
|
+
ADD COLUMN result JSONB NULL;
|
|
199
|
+
END IF;
|
|
200
|
+
END $$
|
|
201
|
+
""")
|
|
113
202
|
elif migration.up_func:
|
|
114
203
|
await migration.up_func(conn)
|
|
115
204
|
elif migration.up_sql and migration.up_sql != "SELECT 1":
|
|
@@ -410,6 +499,10 @@ class CitusStorageBackend(PostgresStorageBackend):
|
|
|
410
499
|
step_run_id TEXT NOT NULL,
|
|
411
500
|
signal_types JSONB NOT NULL DEFAULT '[]',
|
|
412
501
|
status TEXT NOT NULL DEFAULT 'waiting',
|
|
502
|
+
stream_run_id TEXT NULL,
|
|
503
|
+
parent_run_id TEXT NULL,
|
|
504
|
+
parent_hook_token TEXT NULL,
|
|
505
|
+
result JSONB NULL,
|
|
413
506
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
414
507
|
PRIMARY KEY (stream_id, step_run_id)
|
|
415
508
|
)
|
|
@@ -417,6 +510,29 @@ class CitusStorageBackend(PostgresStorageBackend):
|
|
|
417
510
|
await conn.execute(
|
|
418
511
|
"CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON stream_subscriptions(stream_id, status)"
|
|
419
512
|
)
|
|
513
|
+
await conn.execute(
|
|
514
|
+
"CREATE INDEX IF NOT EXISTS idx_subscriptions_stream_run "
|
|
515
|
+
"ON stream_subscriptions(stream_id, stream_run_id)"
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# scheduled_signals — reference table (polled globally by runtime)
|
|
519
|
+
await conn.execute("""
|
|
520
|
+
CREATE TABLE IF NOT EXISTS scheduled_signals (
|
|
521
|
+
id TEXT PRIMARY KEY,
|
|
522
|
+
stream_id TEXT NOT NULL,
|
|
523
|
+
signal_type TEXT NOT NULL,
|
|
524
|
+
payload JSONB NOT NULL DEFAULT '{}',
|
|
525
|
+
due_at TIMESTAMPTZ NOT NULL,
|
|
526
|
+
stream_run_id TEXT,
|
|
527
|
+
metadata JSONB DEFAULT '{}',
|
|
528
|
+
delivered BOOLEAN NOT NULL DEFAULT FALSE,
|
|
529
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
530
|
+
)
|
|
531
|
+
""")
|
|
532
|
+
await conn.execute(
|
|
533
|
+
"CREATE INDEX IF NOT EXISTS idx_scheduled_signals_due "
|
|
534
|
+
"ON scheduled_signals(delivered, due_at)"
|
|
535
|
+
)
|
|
420
536
|
|
|
421
537
|
# signal_acknowledgments — reference table (needs cross-shard joins)
|
|
422
538
|
# Differences vs postgres:
|
|
@@ -496,3 +612,7 @@ class CitusStorageBackend(PostgresStorageBackend):
|
|
|
496
612
|
# signal_acknowledgments: reference table (needs cross-shard joins)
|
|
497
613
|
if "signal_acknowledgments" not in distributed:
|
|
498
614
|
await conn.execute("SELECT create_reference_table('signal_acknowledgments')")
|
|
615
|
+
|
|
616
|
+
# scheduled_signals: reference table (polled globally by runtime)
|
|
617
|
+
if "scheduled_signals" not in distributed:
|
|
618
|
+
await conn.execute("SELECT create_reference_table('scheduled_signals')")
|
|
@@ -1686,6 +1686,7 @@ class DynamoDBStorageBackend(StorageBackend):
|
|
|
1686
1686
|
stream_id: str,
|
|
1687
1687
|
step_run_id: str,
|
|
1688
1688
|
signal_types: list[str],
|
|
1689
|
+
stream_run_id: str | None = None,
|
|
1689
1690
|
) -> None:
|
|
1690
1691
|
"""Register a stream step's subscription to signal types."""
|
|
1691
1692
|
async with self._get_client() as client:
|
|
@@ -1710,6 +1711,7 @@ class DynamoDBStorageBackend(StorageBackend):
|
|
|
1710
1711
|
self,
|
|
1711
1712
|
stream_id: str,
|
|
1712
1713
|
signal_type: str,
|
|
1714
|
+
stream_run_id: str | None = None,
|
|
1713
1715
|
) -> list[dict]:
|
|
1714
1716
|
"""Get step_run_ids waiting for a specific signal type on a stream."""
|
|
1715
1717
|
async with self._get_client() as client:
|
|
@@ -1187,6 +1187,7 @@ class FileStorageBackend(StorageBackend):
|
|
|
1187
1187
|
stream_id: str,
|
|
1188
1188
|
step_run_id: str,
|
|
1189
1189
|
signal_types: list[str],
|
|
1190
|
+
stream_run_id: str | None = None,
|
|
1190
1191
|
) -> None:
|
|
1191
1192
|
"""Register a stream step's subscription to signal types."""
|
|
1192
1193
|
sub_file = self.subscriptions_dir / f"{stream_id}__{step_run_id}.json"
|
|
@@ -1210,6 +1211,7 @@ class FileStorageBackend(StorageBackend):
|
|
|
1210
1211
|
self,
|
|
1211
1212
|
stream_id: str,
|
|
1212
1213
|
signal_type: str,
|
|
1214
|
+
stream_run_id: str | None = None,
|
|
1213
1215
|
) -> list[dict]:
|
|
1214
1216
|
"""Get step_run_ids waiting for a specific signal type on a stream."""
|
|
1215
1217
|
|