pyworkflow-engine 0.2.6__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.6 → pyworkflow_engine-0.2.7}/PKG-INFO +1 -1
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyproject.toml +1 -1
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/__init__.py +1 -1
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/celery/app.py +5 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/celery/tasks.py +100 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/base.py +101 -1
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/cassandra.py +2 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/citus.py +120 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/dynamodb.py +2 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/file.py +2 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/memory.py +108 -5
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/migrations/base.py +38 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/mysql.py +2 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/postgres.py +337 -18
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/sqlite.py +2 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/streams/__init__.py +14 -1
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/streams/consumer.py +41 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/streams/context.py +75 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/streams/dispatcher.py +233 -11
- {pyworkflow_engine-0.2.6 → 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.6 → pyworkflow_engine-0.2.7}/pyworkflow/streams/step_context.py +34 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow_engine.egg-info/SOURCES.txt +1 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/test_schema_migrations.py +1 -1
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/CLAUDE.md +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/DISTRIBUTED.md +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/LICENSE +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/MANIFEST.in +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/README.md +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/RELEASING.md +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/architecture.md +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/concepts/cancellation.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/concepts/continue-as-new.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/concepts/events.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/concepts/fault-tolerance.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/concepts/hooks.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/concepts/limitations.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/concepts/schedules.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/concepts/sleep.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/concepts/step-context.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/concepts/steps.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/concepts/workflows.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/conventions.md +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/guides/brokers.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/guides/cli.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/guides/configuration.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/harness-gaps.md +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/introduction.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/layers.md +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/docs/quickstart.mdx +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/docker-compose.yml +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/basic.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/batch_processing.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/cancellation.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/child_workflows.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/hooks.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/idempotency.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/long_running.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/retries.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/schedules.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/durable/workflows/step_context.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/transient/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/transient/02_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/transient/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/celery/transient/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/02_file_storage.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/03_retries.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/04_long_running.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/05_event_log.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/06_idempotency.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/07_hooks.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/08_cancellation.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/09_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/10_child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/11_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/12_schedules.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/13_step_context.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/14_child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/durable/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/transient/01_quick_tasks.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/transient/02_retries.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/transient/03_sleep.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/examples/local/transient/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/aws/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/aws/context.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/aws/handler.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/aws/testing.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/celery/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/celery/loop.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/celery/scheduler.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/celery/singleton.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/__main__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/hooks.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/quickstart.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/runs.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/scheduler.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/schedules.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/setup.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/worker.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/commands/workflows.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/output/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/output/formatters.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/output/styles.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/async_helpers.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/config.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/config_generator.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/discovery.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/docker_manager.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/interactive.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/cli/utils/storage.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/config.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/context/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/context/aws.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/context/base.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/context/local.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/context/mock.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/context/step_context.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/core/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/core/exceptions.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/core/registry.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/core/scheduled.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/core/step.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/core/validation.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/core/workflow.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/discovery.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/engine/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/engine/events.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/engine/executor.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/engine/replay.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/observability/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/observability/logging.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/child_handle.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/child_workflow.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/define_hook.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/hooks.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/resume_hook.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/schedule.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/shield.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/sleep.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/step_checkpoint.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/primitives/step_hook.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/runtime/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/runtime/base.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/runtime/celery.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/runtime/factory.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/runtime/local.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/scheduler/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/scheduler/local.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/serialization/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/serialization/decoder.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/serialization/encoder.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/config.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/migrations/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/storage/schemas.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/streams/checkpoint.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/streams/decorator.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/streams/registry.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/streams/signal.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/utils/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/utils/duration.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/utils/helpers.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/pyworkflow/utils/schedule.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/setup.cfg +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/test_cancellation.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/test_schedule_storage.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/test_singleton.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/test_stream_e2e.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/integration/test_workflow_suspended.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/backends/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/backends/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/backends/test_citus_storage.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/backends/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/backends/test_postgres_storage.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/backends/test_sqlite_storage.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/conftest.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/storage/__init__.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/storage/test_migrations.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_cancellation.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_cli_worker.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_emit.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_event_limits.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_executor.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_force_local.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_hooks.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_parent_run_id.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_primitives_from_steps.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_registry.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_replay.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_retention.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_schedule_schemas.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_schedule_utils.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_scheduled_workflow.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_signal.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_singleton.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_step.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_step_checkpoint.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_step_context.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_step_hook.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_stream_storage.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_stream_workflow.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_validation.py +0 -0
- {pyworkflow_engine-0.2.6 → pyworkflow_engine-0.2.7}/tests/unit/test_workflow.py +0 -0
- {pyworkflow_engine-0.2.6 → 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}
|
|
@@ -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
|
|
|
@@ -746,6 +746,7 @@ class InMemoryStorageBackend(StorageBackend):
|
|
|
746
746
|
stream_id: str,
|
|
747
747
|
step_run_id: str,
|
|
748
748
|
signal_types: list[str],
|
|
749
|
+
stream_run_id: str | None = None,
|
|
749
750
|
) -> None:
|
|
750
751
|
"""Register a stream step's subscription to signal types."""
|
|
751
752
|
with self._lock:
|
|
@@ -755,34 +756,136 @@ class InMemoryStorageBackend(StorageBackend):
|
|
|
755
756
|
"step_run_id": step_run_id,
|
|
756
757
|
"signal_types": signal_types,
|
|
757
758
|
"status": "waiting",
|
|
759
|
+
"stream_run_id": stream_run_id,
|
|
758
760
|
"created_at": datetime.now(UTC).isoformat(),
|
|
759
761
|
}
|
|
760
762
|
|
|
763
|
+
async def get_subscription_states(
|
|
764
|
+
self,
|
|
765
|
+
stream_id: str,
|
|
766
|
+
stream_run_id: str | None = None,
|
|
767
|
+
) -> list[dict]:
|
|
768
|
+
"""Return all subscriptions and their statuses for a stream run."""
|
|
769
|
+
with self._lock:
|
|
770
|
+
result = []
|
|
771
|
+
for (sid, _), sub in self._subscriptions.items():
|
|
772
|
+
if sid != stream_id:
|
|
773
|
+
continue
|
|
774
|
+
if stream_run_id is not None and sub.get("stream_run_id") not in (
|
|
775
|
+
stream_run_id,
|
|
776
|
+
None,
|
|
777
|
+
):
|
|
778
|
+
continue
|
|
779
|
+
result.append(
|
|
780
|
+
{
|
|
781
|
+
"stream_id": sub["stream_id"],
|
|
782
|
+
"step_run_id": sub["step_run_id"],
|
|
783
|
+
"status": sub["status"],
|
|
784
|
+
"stream_run_id": sub.get("stream_run_id"),
|
|
785
|
+
}
|
|
786
|
+
)
|
|
787
|
+
return result
|
|
788
|
+
|
|
789
|
+
async def schedule_signal(
|
|
790
|
+
self,
|
|
791
|
+
*,
|
|
792
|
+
stream_id: str,
|
|
793
|
+
signal_type: str,
|
|
794
|
+
payload: dict,
|
|
795
|
+
due_at: datetime,
|
|
796
|
+
stream_run_id: str | None = None,
|
|
797
|
+
metadata: dict | None = None,
|
|
798
|
+
) -> str:
|
|
799
|
+
"""Persist a signal to be emitted at ``due_at``."""
|
|
800
|
+
import uuid as _uuid
|
|
801
|
+
|
|
802
|
+
with self._lock:
|
|
803
|
+
if not hasattr(self, "_scheduled_signals"):
|
|
804
|
+
self._scheduled_signals: dict[str, dict] = {}
|
|
805
|
+
sched_id = f"sched_{_uuid.uuid4().hex[:16]}"
|
|
806
|
+
self._scheduled_signals[sched_id] = {
|
|
807
|
+
"id": sched_id,
|
|
808
|
+
"stream_id": stream_id,
|
|
809
|
+
"signal_type": signal_type,
|
|
810
|
+
"payload": payload,
|
|
811
|
+
"due_at": due_at,
|
|
812
|
+
"stream_run_id": stream_run_id,
|
|
813
|
+
"metadata": metadata or {},
|
|
814
|
+
"delivered": False,
|
|
815
|
+
}
|
|
816
|
+
return sched_id
|
|
817
|
+
|
|
818
|
+
async def fetch_due_scheduled_signals(
|
|
819
|
+
self,
|
|
820
|
+
now: datetime,
|
|
821
|
+
limit: int = 100,
|
|
822
|
+
) -> list[dict]:
|
|
823
|
+
"""Return scheduled signals whose ``due_at <= now`` and not delivered."""
|
|
824
|
+
with self._lock:
|
|
825
|
+
if not hasattr(self, "_scheduled_signals"):
|
|
826
|
+
return []
|
|
827
|
+
due = []
|
|
828
|
+
for row in self._scheduled_signals.values():
|
|
829
|
+
if row["delivered"]:
|
|
830
|
+
continue
|
|
831
|
+
if row["due_at"] <= now:
|
|
832
|
+
due.append(dict(row))
|
|
833
|
+
if len(due) >= limit:
|
|
834
|
+
break
|
|
835
|
+
return due
|
|
836
|
+
|
|
837
|
+
async def mark_scheduled_signal_delivered(self, sched_id: str) -> None:
|
|
838
|
+
with self._lock:
|
|
839
|
+
if not hasattr(self, "_scheduled_signals"):
|
|
840
|
+
return
|
|
841
|
+
row = self._scheduled_signals.get(sched_id)
|
|
842
|
+
if row:
|
|
843
|
+
row["delivered"] = True
|
|
844
|
+
|
|
761
845
|
async def get_waiting_steps(
|
|
762
846
|
self,
|
|
763
847
|
stream_id: str,
|
|
764
848
|
signal_type: str,
|
|
849
|
+
stream_run_id: str | None = None,
|
|
765
850
|
) -> list[dict]:
|
|
766
851
|
"""Get step_run_ids waiting for a specific signal type on a stream."""
|
|
767
852
|
with self._lock:
|
|
768
853
|
result = []
|
|
769
854
|
for (sid, _), sub in self._subscriptions.items():
|
|
770
|
-
if sid
|
|
771
|
-
|
|
772
|
-
|
|
855
|
+
if sid != stream_id:
|
|
856
|
+
continue
|
|
857
|
+
if sub["status"] not in ("waiting", "suspended"):
|
|
858
|
+
continue
|
|
859
|
+
if signal_type not in sub["signal_types"]:
|
|
860
|
+
continue
|
|
861
|
+
if stream_run_id is not None and sub.get("stream_run_id") not in (
|
|
862
|
+
stream_run_id,
|
|
863
|
+
None,
|
|
864
|
+
):
|
|
865
|
+
continue
|
|
866
|
+
result.append(sub)
|
|
773
867
|
return result
|
|
774
868
|
|
|
775
869
|
async def get_subscriptions_for_stream(
|
|
776
870
|
self,
|
|
777
871
|
stream_id: str,
|
|
778
872
|
signal_type: str,
|
|
873
|
+
stream_run_id: str | None = None,
|
|
779
874
|
) -> list[dict]:
|
|
780
875
|
"""Get ALL subscriptions for a signal type, regardless of status."""
|
|
781
876
|
with self._lock:
|
|
782
877
|
result = []
|
|
783
878
|
for (sid, _), sub in self._subscriptions.items():
|
|
784
|
-
if sid
|
|
785
|
-
|
|
879
|
+
if sid != stream_id:
|
|
880
|
+
continue
|
|
881
|
+
if signal_type not in sub["signal_types"]:
|
|
882
|
+
continue
|
|
883
|
+
if stream_run_id is not None and sub.get("stream_run_id") not in (
|
|
884
|
+
stream_run_id,
|
|
885
|
+
None,
|
|
886
|
+
):
|
|
887
|
+
continue
|
|
888
|
+
result.append(sub)
|
|
786
889
|
return result
|
|
787
890
|
|
|
788
891
|
async def update_subscription_status(
|