pyworkflow-engine 0.2.3__tar.gz → 0.2.6__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.3 → pyworkflow_engine-0.2.6}/PKG-INFO +1 -1
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyproject.toml +1 -1
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/__init__.py +1 -1
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/celery/tasks.py +13 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/context/local.py +0 -25
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/base.py +58 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/cassandra.py +320 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/citus.py +111 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/dynamodb.py +304 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/file.py +266 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/memory.py +57 -5
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/migrations/base.py +20 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/mysql.py +341 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/postgres.py +483 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/sqlite.py +425 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/streams/context.py +20 -2
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/streams/decorator.py +20 -3
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/streams/dispatcher.py +98 -13
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/streams/emit.py +18 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/streams/signal.py +1 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/test_schema_migrations.py +1 -1
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/CLAUDE.md +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/DISTRIBUTED.md +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/LICENSE +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/MANIFEST.in +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/README.md +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/RELEASING.md +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/architecture.md +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/concepts/cancellation.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/concepts/continue-as-new.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/concepts/events.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/concepts/fault-tolerance.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/concepts/hooks.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/concepts/limitations.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/concepts/schedules.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/concepts/sleep.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/concepts/step-context.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/concepts/steps.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/concepts/workflows.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/conventions.md +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/guides/brokers.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/guides/cli.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/guides/configuration.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/harness-gaps.md +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/introduction.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/layers.md +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/docs/quickstart.mdx +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/docker-compose.yml +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/basic.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/batch_processing.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/cancellation.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/child_workflows.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/hooks.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/idempotency.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/long_running.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/retries.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/schedules.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/durable/workflows/step_context.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/transient/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/transient/02_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/transient/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/celery/transient/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/02_file_storage.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/03_retries.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/04_long_running.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/05_event_log.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/06_idempotency.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/07_hooks.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/08_cancellation.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/09_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/10_child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/11_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/12_schedules.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/13_step_context.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/14_child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/durable/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/transient/01_quick_tasks.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/transient/02_retries.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/transient/03_sleep.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/examples/local/transient/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/aws/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/aws/context.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/aws/handler.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/aws/testing.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/celery/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/celery/app.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/celery/loop.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/celery/scheduler.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/celery/singleton.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/__main__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/commands/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/commands/hooks.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/commands/quickstart.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/commands/runs.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/commands/scheduler.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/commands/schedules.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/commands/setup.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/commands/worker.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/commands/workflows.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/output/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/output/formatters.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/output/styles.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/utils/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/utils/async_helpers.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/utils/config.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/utils/config_generator.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/utils/discovery.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/utils/docker_manager.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/utils/interactive.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/cli/utils/storage.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/config.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/context/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/context/aws.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/context/base.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/context/mock.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/context/step_context.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/core/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/core/exceptions.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/core/registry.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/core/scheduled.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/core/step.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/core/validation.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/core/workflow.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/discovery.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/engine/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/engine/events.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/engine/executor.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/engine/replay.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/observability/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/observability/logging.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/child_handle.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/child_workflow.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/define_hook.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/hooks.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/resume_hook.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/schedule.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/shield.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/sleep.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/step_checkpoint.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/primitives/step_hook.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/runtime/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/runtime/base.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/runtime/celery.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/runtime/factory.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/runtime/local.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/scheduler/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/scheduler/local.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/serialization/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/serialization/decoder.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/serialization/encoder.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/config.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/migrations/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/storage/schemas.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/streams/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/streams/checkpoint.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/streams/consumer.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/streams/registry.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/streams/step_context.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/utils/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/utils/duration.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/utils/helpers.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow/utils/schedule.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/pyworkflow_engine.egg-info/SOURCES.txt +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/setup.cfg +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/test_cancellation.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/test_schedule_storage.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/test_singleton.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/test_stream_e2e.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/integration/test_workflow_suspended.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/backends/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/backends/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/backends/test_citus_storage.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/backends/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/backends/test_postgres_storage.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/backends/test_sqlite_storage.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/conftest.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/storage/__init__.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/storage/test_migrations.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_cancellation.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_cli_worker.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_emit.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_event_limits.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_executor.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_force_local.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_hooks.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_parent_run_id.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_primitives_from_steps.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_registry.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_replay.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_retention.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_schedule_schemas.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_schedule_utils.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_scheduled_workflow.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_signal.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_singleton.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_step.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_step_checkpoint.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_step_context.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_step_hook.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_stream_storage.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_stream_workflow.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_validation.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/tests/unit/test_workflow.py +0 -0
- {pyworkflow_engine-0.2.3 → pyworkflow_engine-0.2.6}/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.6"
|
|
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"
|
|
@@ -2462,6 +2462,9 @@ async def _resume_workflow_on_worker(
|
|
|
2462
2462
|
run_id=run_id,
|
|
2463
2463
|
step_id=step_id,
|
|
2464
2464
|
)
|
|
2465
|
+
resume_workflow_task.release_lock(
|
|
2466
|
+
task_args=[run_id], task_kwargs={"storage_config": storage_config}
|
|
2467
|
+
)
|
|
2465
2468
|
schedule_workflow_resumption(
|
|
2466
2469
|
run_id,
|
|
2467
2470
|
datetime.now(UTC),
|
|
@@ -2481,6 +2484,9 @@ async def _resume_workflow_on_worker(
|
|
|
2481
2484
|
run_id=run_id,
|
|
2482
2485
|
hook_id=hook_id,
|
|
2483
2486
|
)
|
|
2487
|
+
resume_workflow_task.release_lock(
|
|
2488
|
+
task_args=[run_id], task_kwargs={"storage_config": storage_config}
|
|
2489
|
+
)
|
|
2484
2490
|
schedule_workflow_resumption(
|
|
2485
2491
|
run_id,
|
|
2486
2492
|
datetime.now(UTC),
|
|
@@ -2492,6 +2498,13 @@ async def _resume_workflow_on_worker(
|
|
|
2492
2498
|
# Schedule automatic resumption if we have a resume_at time
|
|
2493
2499
|
resume_at = e.data.get("resume_at") if e.data else None
|
|
2494
2500
|
if resume_at:
|
|
2501
|
+
# Release the singleton lock BEFORE scheduling the next resume.
|
|
2502
|
+
# Without this, the new resume_workflow_task.apply_async() sees
|
|
2503
|
+
# the lock still held by the current (calling) task and silently
|
|
2504
|
+
# drops the schedule as a duplicate.
|
|
2505
|
+
resume_workflow_task.release_lock(
|
|
2506
|
+
task_args=[run_id], task_kwargs={"storage_config": storage_config}
|
|
2507
|
+
)
|
|
2495
2508
|
schedule_workflow_resumption(
|
|
2496
2509
|
run_id, resume_at, storage_config=storage_config, triggered_by="start_sleep_hook"
|
|
2497
2510
|
)
|
|
@@ -121,31 +121,6 @@ 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
|
|
@@ -755,6 +755,7 @@ class StorageBackend(ABC):
|
|
|
755
755
|
signal_type: str,
|
|
756
756
|
payload: dict,
|
|
757
757
|
source_run_id: str | None = None,
|
|
758
|
+
stream_run_id: str | None = None,
|
|
758
759
|
metadata: dict | None = None,
|
|
759
760
|
) -> int:
|
|
760
761
|
"""
|
|
@@ -766,6 +767,7 @@ class StorageBackend(ABC):
|
|
|
766
767
|
signal_type: Signal type (e.g., "task.created")
|
|
767
768
|
payload: Signal payload data
|
|
768
769
|
source_run_id: Optional source workflow run ID
|
|
770
|
+
stream_run_id: Optional stream run identifier for grouping signals
|
|
769
771
|
metadata: Optional signal metadata
|
|
770
772
|
|
|
771
773
|
Returns:
|
|
@@ -798,6 +800,40 @@ class StorageBackend(ABC):
|
|
|
798
800
|
"""
|
|
799
801
|
raise NotImplementedError("This storage backend does not support streams")
|
|
800
802
|
|
|
803
|
+
async def query_stream_signals(
|
|
804
|
+
self,
|
|
805
|
+
stream_id: str,
|
|
806
|
+
stream_run_id: str,
|
|
807
|
+
*,
|
|
808
|
+
source_run_id: str | None = None,
|
|
809
|
+
signal_type: str | None = None,
|
|
810
|
+
after_sequence: int | None = None,
|
|
811
|
+
before_sequence: int | None = None,
|
|
812
|
+
last_n: int | None = None,
|
|
813
|
+
limit: int = 50,
|
|
814
|
+
) -> list[dict]:
|
|
815
|
+
"""
|
|
816
|
+
Query signals from a stream with rich filtering.
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
stream_id: Stream identifier.
|
|
820
|
+
source_run_id: Filter to signals from a specific workflow run.
|
|
821
|
+
stream_run_id: Filter to signals from a specific stream run.
|
|
822
|
+
signal_type: Filter by signal type (e.g. ``"task.completed"``).
|
|
823
|
+
after_sequence: Only include signals with ``sequence >= value``.
|
|
824
|
+
before_sequence: Only include signals with ``sequence <= value``.
|
|
825
|
+
last_n: Return the N most recent signals (overrides
|
|
826
|
+
``after_sequence`` and ``limit``).
|
|
827
|
+
limit: Maximum number of signals to return.
|
|
828
|
+
|
|
829
|
+
Returns:
|
|
830
|
+
List of signal dicts ordered by sequence ASC.
|
|
831
|
+
|
|
832
|
+
Raises:
|
|
833
|
+
NotImplementedError: If backend doesn't support streams.
|
|
834
|
+
"""
|
|
835
|
+
raise NotImplementedError("This storage backend does not support streams")
|
|
836
|
+
|
|
801
837
|
# Subscription Operations
|
|
802
838
|
|
|
803
839
|
async def register_stream_subscription(
|
|
@@ -839,6 +875,28 @@ class StorageBackend(ABC):
|
|
|
839
875
|
"""
|
|
840
876
|
raise NotImplementedError("This storage backend does not support streams")
|
|
841
877
|
|
|
878
|
+
async def get_subscriptions_for_stream(
|
|
879
|
+
self,
|
|
880
|
+
stream_id: str,
|
|
881
|
+
signal_type: str,
|
|
882
|
+
) -> list[dict]:
|
|
883
|
+
"""
|
|
884
|
+
Get ALL subscriptions for a signal type on a stream, regardless of status.
|
|
885
|
+
|
|
886
|
+
Unlike ``get_waiting_steps`` which only returns ``"waiting"``
|
|
887
|
+
subscriptions, this returns subscriptions in any status (waiting,
|
|
888
|
+
running, etc.). Used to avoid creating duplicate subscriptions
|
|
889
|
+
when a step is currently executing.
|
|
890
|
+
|
|
891
|
+
Args:
|
|
892
|
+
stream_id: Stream identifier
|
|
893
|
+
signal_type: Signal type to match
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
List of dicts with step subscription info
|
|
897
|
+
"""
|
|
898
|
+
raise NotImplementedError("This storage backend does not support streams")
|
|
899
|
+
|
|
842
900
|
async def update_subscription_status(
|
|
843
901
|
self,
|
|
844
902
|
stream_id: str,
|
|
@@ -179,6 +179,7 @@ class CassandraStorageBackend(StorageBackend):
|
|
|
179
179
|
await self._create_hooks_tables(session)
|
|
180
180
|
await self._create_cancellation_flags_table(session)
|
|
181
181
|
await self._create_checkpoints_table(session)
|
|
182
|
+
await self._create_streams_tables(session)
|
|
182
183
|
await self._create_schedules_tables(session)
|
|
183
184
|
|
|
184
185
|
# Create query pattern tables
|
|
@@ -325,6 +326,52 @@ class CassandraStorageBackend(StorageBackend):
|
|
|
325
326
|
)
|
|
326
327
|
""")
|
|
327
328
|
|
|
329
|
+
async def _create_streams_tables(self, session: Session) -> None:
|
|
330
|
+
"""Create stream-related tables."""
|
|
331
|
+
session.execute("""
|
|
332
|
+
CREATE TABLE IF NOT EXISTS streams (
|
|
333
|
+
stream_id TEXT PRIMARY KEY,
|
|
334
|
+
status TEXT,
|
|
335
|
+
created_at TIMESTAMP,
|
|
336
|
+
metadata TEXT
|
|
337
|
+
)
|
|
338
|
+
""")
|
|
339
|
+
|
|
340
|
+
session.execute("""
|
|
341
|
+
CREATE TABLE IF NOT EXISTS signals (
|
|
342
|
+
stream_run_id TEXT,
|
|
343
|
+
sequence INT,
|
|
344
|
+
signal_id TEXT,
|
|
345
|
+
stream_id TEXT,
|
|
346
|
+
signal_type TEXT,
|
|
347
|
+
payload TEXT,
|
|
348
|
+
published_at TIMESTAMP,
|
|
349
|
+
source_run_id TEXT,
|
|
350
|
+
metadata TEXT,
|
|
351
|
+
PRIMARY KEY ((stream_id, stream_run_id), sequence)
|
|
352
|
+
) WITH CLUSTERING ORDER BY (sequence ASC)
|
|
353
|
+
""")
|
|
354
|
+
|
|
355
|
+
session.execute("""
|
|
356
|
+
CREATE TABLE IF NOT EXISTS stream_subscriptions (
|
|
357
|
+
stream_id TEXT,
|
|
358
|
+
step_run_id TEXT,
|
|
359
|
+
signal_types TEXT,
|
|
360
|
+
status TEXT,
|
|
361
|
+
created_at TIMESTAMP,
|
|
362
|
+
PRIMARY KEY (stream_id, step_run_id)
|
|
363
|
+
)
|
|
364
|
+
""")
|
|
365
|
+
|
|
366
|
+
session.execute("""
|
|
367
|
+
CREATE TABLE IF NOT EXISTS signal_acknowledgments (
|
|
368
|
+
signal_id TEXT,
|
|
369
|
+
step_run_id TEXT,
|
|
370
|
+
acknowledged_at TIMESTAMP,
|
|
371
|
+
PRIMARY KEY (signal_id, step_run_id)
|
|
372
|
+
)
|
|
373
|
+
""")
|
|
374
|
+
|
|
328
375
|
async def _create_schedules_tables(self, session: Session) -> None:
|
|
329
376
|
"""Create schedules tables."""
|
|
330
377
|
# Main schedules table
|
|
@@ -1931,3 +1978,276 @@ class CassandraStorageBackend(StorageBackend):
|
|
|
1931
1978
|
running_run_ids=json.loads(row.running_run_ids) if row.running_run_ids else [],
|
|
1932
1979
|
buffered_count=row.buffered_count or 0,
|
|
1933
1980
|
)
|
|
1981
|
+
|
|
1982
|
+
# Stream Operations
|
|
1983
|
+
|
|
1984
|
+
async def create_stream(self, stream_id: str, metadata: dict | None = None) -> None:
|
|
1985
|
+
"""Create a new stream."""
|
|
1986
|
+
session = self._ensure_connected()
|
|
1987
|
+
now = datetime.now(UTC)
|
|
1988
|
+
|
|
1989
|
+
session.execute(
|
|
1990
|
+
SimpleStatement(
|
|
1991
|
+
"INSERT INTO streams (stream_id, status, created_at, metadata) "
|
|
1992
|
+
"VALUES (%s, %s, %s, %s)",
|
|
1993
|
+
consistency_level=self.write_consistency,
|
|
1994
|
+
),
|
|
1995
|
+
(stream_id, "active", now, json.dumps(metadata or {})),
|
|
1996
|
+
)
|
|
1997
|
+
|
|
1998
|
+
async def get_stream(self, stream_id: str) -> dict | None:
|
|
1999
|
+
"""Get a stream by ID."""
|
|
2000
|
+
session = self._ensure_connected()
|
|
2001
|
+
|
|
2002
|
+
row = session.execute(
|
|
2003
|
+
SimpleStatement(
|
|
2004
|
+
"SELECT stream_id, status, created_at, metadata FROM streams WHERE stream_id = %s",
|
|
2005
|
+
consistency_level=self.read_consistency,
|
|
2006
|
+
),
|
|
2007
|
+
(stream_id,),
|
|
2008
|
+
).one()
|
|
2009
|
+
|
|
2010
|
+
if row is None:
|
|
2011
|
+
return None
|
|
2012
|
+
return {
|
|
2013
|
+
"stream_id": row.stream_id,
|
|
2014
|
+
"status": row.status,
|
|
2015
|
+
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
2016
|
+
"metadata": json.loads(row.metadata) if row.metadata else {},
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
async def publish_signal(
|
|
2020
|
+
self,
|
|
2021
|
+
signal_id: str,
|
|
2022
|
+
stream_id: str,
|
|
2023
|
+
signal_type: str,
|
|
2024
|
+
payload: dict,
|
|
2025
|
+
source_run_id: str | None = None,
|
|
2026
|
+
stream_run_id: str | None = None,
|
|
2027
|
+
metadata: dict | None = None,
|
|
2028
|
+
) -> int:
|
|
2029
|
+
"""Publish a signal to a stream."""
|
|
2030
|
+
session = self._ensure_connected()
|
|
2031
|
+
now = datetime.now(UTC)
|
|
2032
|
+
|
|
2033
|
+
# Get next sequence number (scoped per stream_run_id)
|
|
2034
|
+
run_key = stream_run_id
|
|
2035
|
+
row = session.execute(
|
|
2036
|
+
SimpleStatement(
|
|
2037
|
+
"SELECT MAX(sequence) AS max_seq FROM signals WHERE stream_run_id = %s",
|
|
2038
|
+
consistency_level=self.read_consistency,
|
|
2039
|
+
),
|
|
2040
|
+
(run_key,),
|
|
2041
|
+
).one()
|
|
2042
|
+
|
|
2043
|
+
seq = (row.max_seq + 1) if row and row.max_seq is not None else 0
|
|
2044
|
+
|
|
2045
|
+
session.execute(
|
|
2046
|
+
SimpleStatement(
|
|
2047
|
+
"INSERT INTO signals (stream_run_id, sequence, signal_id, stream_id, signal_type, payload, "
|
|
2048
|
+
"published_at, source_run_id, metadata) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
|
|
2049
|
+
consistency_level=self.write_consistency,
|
|
2050
|
+
),
|
|
2051
|
+
(
|
|
2052
|
+
run_key,
|
|
2053
|
+
seq,
|
|
2054
|
+
signal_id,
|
|
2055
|
+
stream_id,
|
|
2056
|
+
signal_type,
|
|
2057
|
+
json.dumps(payload),
|
|
2058
|
+
now,
|
|
2059
|
+
source_run_id,
|
|
2060
|
+
json.dumps(metadata or {}),
|
|
2061
|
+
),
|
|
2062
|
+
)
|
|
2063
|
+
return seq
|
|
2064
|
+
|
|
2065
|
+
async def get_signals(
|
|
2066
|
+
self,
|
|
2067
|
+
stream_id: str,
|
|
2068
|
+
after_sequence: int = 0,
|
|
2069
|
+
limit: int = 100,
|
|
2070
|
+
) -> list[dict]:
|
|
2071
|
+
"""Get signals from a stream after a given sequence number."""
|
|
2072
|
+
session = self._ensure_connected()
|
|
2073
|
+
|
|
2074
|
+
rows = session.execute(
|
|
2075
|
+
SimpleStatement(
|
|
2076
|
+
"SELECT signal_id, stream_id, signal_type, payload, published_at, "
|
|
2077
|
+
"sequence, source_run_id, stream_run_id, metadata FROM signals "
|
|
2078
|
+
"WHERE stream_id = %s AND sequence >= %s LIMIT %s ALLOW FILTERING",
|
|
2079
|
+
consistency_level=self.read_consistency,
|
|
2080
|
+
),
|
|
2081
|
+
(stream_id, after_sequence, limit),
|
|
2082
|
+
)
|
|
2083
|
+
|
|
2084
|
+
return [
|
|
2085
|
+
{
|
|
2086
|
+
"signal_id": row.signal_id,
|
|
2087
|
+
"stream_id": row.stream_id,
|
|
2088
|
+
"signal_type": row.signal_type,
|
|
2089
|
+
"payload": json.loads(row.payload) if row.payload else {},
|
|
2090
|
+
"published_at": row.published_at.isoformat() if row.published_at else None,
|
|
2091
|
+
"sequence": row.sequence,
|
|
2092
|
+
"source_run_id": row.source_run_id,
|
|
2093
|
+
"stream_run_id": row.stream_run_id,
|
|
2094
|
+
"metadata": json.loads(row.metadata) if row.metadata else {},
|
|
2095
|
+
}
|
|
2096
|
+
for row in rows
|
|
2097
|
+
]
|
|
2098
|
+
|
|
2099
|
+
async def register_stream_subscription(
|
|
2100
|
+
self,
|
|
2101
|
+
stream_id: str,
|
|
2102
|
+
step_run_id: str,
|
|
2103
|
+
signal_types: list[str],
|
|
2104
|
+
) -> None:
|
|
2105
|
+
"""Register a stream step's subscription to signal types."""
|
|
2106
|
+
session = self._ensure_connected()
|
|
2107
|
+
now = datetime.now(UTC)
|
|
2108
|
+
|
|
2109
|
+
session.execute(
|
|
2110
|
+
SimpleStatement(
|
|
2111
|
+
"INSERT INTO stream_subscriptions (stream_id, step_run_id, signal_types, "
|
|
2112
|
+
"status, created_at) VALUES (%s, %s, %s, %s, %s)",
|
|
2113
|
+
consistency_level=self.write_consistency,
|
|
2114
|
+
),
|
|
2115
|
+
(stream_id, step_run_id, json.dumps(signal_types), "waiting", now),
|
|
2116
|
+
)
|
|
2117
|
+
|
|
2118
|
+
async def get_waiting_steps(
|
|
2119
|
+
self,
|
|
2120
|
+
stream_id: str,
|
|
2121
|
+
signal_type: str,
|
|
2122
|
+
) -> list[dict]:
|
|
2123
|
+
"""Get step_run_ids waiting for a specific signal type on a stream."""
|
|
2124
|
+
session = self._ensure_connected()
|
|
2125
|
+
|
|
2126
|
+
rows = session.execute(
|
|
2127
|
+
SimpleStatement(
|
|
2128
|
+
"SELECT stream_id, step_run_id, signal_types, status, created_at "
|
|
2129
|
+
"FROM stream_subscriptions WHERE stream_id = %s",
|
|
2130
|
+
consistency_level=self.read_consistency,
|
|
2131
|
+
),
|
|
2132
|
+
(stream_id,),
|
|
2133
|
+
)
|
|
2134
|
+
|
|
2135
|
+
result = []
|
|
2136
|
+
for row in rows:
|
|
2137
|
+
if row.status != "waiting":
|
|
2138
|
+
continue
|
|
2139
|
+
types = json.loads(row.signal_types) if row.signal_types else []
|
|
2140
|
+
if signal_type in types:
|
|
2141
|
+
result.append(
|
|
2142
|
+
{
|
|
2143
|
+
"stream_id": row.stream_id,
|
|
2144
|
+
"step_run_id": row.step_run_id,
|
|
2145
|
+
"signal_types": types,
|
|
2146
|
+
"status": row.status,
|
|
2147
|
+
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
2148
|
+
}
|
|
2149
|
+
)
|
|
2150
|
+
return result
|
|
2151
|
+
|
|
2152
|
+
async def update_subscription_status(
|
|
2153
|
+
self,
|
|
2154
|
+
stream_id: str,
|
|
2155
|
+
step_run_id: str,
|
|
2156
|
+
status: str,
|
|
2157
|
+
) -> None:
|
|
2158
|
+
"""Update a subscription's status."""
|
|
2159
|
+
session = self._ensure_connected()
|
|
2160
|
+
|
|
2161
|
+
session.execute(
|
|
2162
|
+
SimpleStatement(
|
|
2163
|
+
"UPDATE stream_subscriptions SET status = %s "
|
|
2164
|
+
"WHERE stream_id = %s AND step_run_id = %s",
|
|
2165
|
+
consistency_level=self.write_consistency,
|
|
2166
|
+
),
|
|
2167
|
+
(status, stream_id, step_run_id),
|
|
2168
|
+
)
|
|
2169
|
+
|
|
2170
|
+
async def acknowledge_signal(
|
|
2171
|
+
self,
|
|
2172
|
+
signal_id: str,
|
|
2173
|
+
step_run_id: str,
|
|
2174
|
+
) -> None:
|
|
2175
|
+
"""Acknowledge that a signal has been processed by a step."""
|
|
2176
|
+
session = self._ensure_connected()
|
|
2177
|
+
now = datetime.now(UTC)
|
|
2178
|
+
|
|
2179
|
+
session.execute(
|
|
2180
|
+
SimpleStatement(
|
|
2181
|
+
"INSERT INTO signal_acknowledgments (signal_id, step_run_id, acknowledged_at) "
|
|
2182
|
+
"VALUES (%s, %s, %s)",
|
|
2183
|
+
consistency_level=self.write_consistency,
|
|
2184
|
+
),
|
|
2185
|
+
(signal_id, step_run_id, now),
|
|
2186
|
+
)
|
|
2187
|
+
|
|
2188
|
+
async def get_pending_signals(
|
|
2189
|
+
self,
|
|
2190
|
+
stream_id: str,
|
|
2191
|
+
step_run_id: str,
|
|
2192
|
+
) -> list[dict]:
|
|
2193
|
+
"""Get signals that arrived for a step but haven't been acknowledged."""
|
|
2194
|
+
session = self._ensure_connected()
|
|
2195
|
+
|
|
2196
|
+
# Get subscription
|
|
2197
|
+
sub_row = session.execute(
|
|
2198
|
+
SimpleStatement(
|
|
2199
|
+
"SELECT signal_types FROM stream_subscriptions "
|
|
2200
|
+
"WHERE stream_id = %s AND step_run_id = %s",
|
|
2201
|
+
consistency_level=self.read_consistency,
|
|
2202
|
+
),
|
|
2203
|
+
(stream_id, step_run_id),
|
|
2204
|
+
).one()
|
|
2205
|
+
|
|
2206
|
+
if not sub_row:
|
|
2207
|
+
return []
|
|
2208
|
+
|
|
2209
|
+
signal_types = json.loads(sub_row.signal_types) if sub_row.signal_types else []
|
|
2210
|
+
if not signal_types:
|
|
2211
|
+
return []
|
|
2212
|
+
|
|
2213
|
+
# Get all signals for the stream
|
|
2214
|
+
signals = session.execute(
|
|
2215
|
+
SimpleStatement(
|
|
2216
|
+
"SELECT signal_id, stream_id, signal_type, payload, published_at, "
|
|
2217
|
+
"sequence, source_run_id, metadata FROM signals "
|
|
2218
|
+
"WHERE stream_id = %s ORDER BY sequence ASC",
|
|
2219
|
+
consistency_level=self.read_consistency,
|
|
2220
|
+
),
|
|
2221
|
+
(stream_id,),
|
|
2222
|
+
)
|
|
2223
|
+
|
|
2224
|
+
pending = []
|
|
2225
|
+
for sig in signals:
|
|
2226
|
+
if sig.signal_type not in signal_types:
|
|
2227
|
+
continue
|
|
2228
|
+
|
|
2229
|
+
# Check if acknowledged
|
|
2230
|
+
ack = session.execute(
|
|
2231
|
+
SimpleStatement(
|
|
2232
|
+
"SELECT signal_id FROM signal_acknowledgments "
|
|
2233
|
+
"WHERE signal_id = %s AND step_run_id = %s",
|
|
2234
|
+
consistency_level=self.read_consistency,
|
|
2235
|
+
),
|
|
2236
|
+
(sig.signal_id, step_run_id),
|
|
2237
|
+
).one()
|
|
2238
|
+
|
|
2239
|
+
if ack is None:
|
|
2240
|
+
pending.append(
|
|
2241
|
+
{
|
|
2242
|
+
"signal_id": sig.signal_id,
|
|
2243
|
+
"stream_id": sig.stream_id,
|
|
2244
|
+
"signal_type": sig.signal_type,
|
|
2245
|
+
"payload": json.loads(sig.payload) if sig.payload else {},
|
|
2246
|
+
"published_at": sig.published_at.isoformat() if sig.published_at else None,
|
|
2247
|
+
"sequence": sig.sequence,
|
|
2248
|
+
"source_run_id": sig.source_run_id,
|
|
2249
|
+
"metadata": json.loads(sig.metadata) if sig.metadata else {},
|
|
2250
|
+
}
|
|
2251
|
+
)
|
|
2252
|
+
|
|
2253
|
+
return pending
|
|
@@ -79,6 +79,37 @@ class CitusMigrationRunner(PostgresMigrationRunner):
|
|
|
79
79
|
""")
|
|
80
80
|
# NOTE: No UNIQUE constraints added here — Citus requires distribution
|
|
81
81
|
# column in every unique constraint, which V2 doesn't add.
|
|
82
|
+
elif migration.version == 3:
|
|
83
|
+
# V3: Restructure signals table — PK changes to (stream_id, stream_run_id, sequence).
|
|
84
|
+
# Must undistribute before dropping on Citus.
|
|
85
|
+
await conn.execute("DROP TABLE IF EXISTS signal_acknowledgments")
|
|
86
|
+
await conn.execute("DROP TABLE IF EXISTS signals")
|
|
87
|
+
await conn.execute("""
|
|
88
|
+
CREATE TABLE signals (
|
|
89
|
+
stream_run_id TEXT NOT NULL,
|
|
90
|
+
sequence INTEGER NOT NULL,
|
|
91
|
+
signal_id TEXT NOT NULL,
|
|
92
|
+
stream_id TEXT NOT NULL,
|
|
93
|
+
signal_type TEXT NOT NULL,
|
|
94
|
+
payload JSONB NOT NULL DEFAULT '{}',
|
|
95
|
+
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
96
|
+
source_run_id TEXT,
|
|
97
|
+
metadata JSONB DEFAULT '{}',
|
|
98
|
+
PRIMARY KEY (stream_id, stream_run_id, sequence)
|
|
99
|
+
)
|
|
100
|
+
""")
|
|
101
|
+
await conn.execute("CREATE INDEX idx_signals_signal_id ON signals(signal_id)")
|
|
102
|
+
await conn.execute("""
|
|
103
|
+
CREATE TABLE signal_acknowledgments (
|
|
104
|
+
signal_id TEXT NOT NULL,
|
|
105
|
+
step_run_id TEXT NOT NULL,
|
|
106
|
+
acknowledged_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
107
|
+
PRIMARY KEY (signal_id, step_run_id)
|
|
108
|
+
)
|
|
109
|
+
""")
|
|
110
|
+
# Re-distribute the new tables
|
|
111
|
+
await conn.execute("SELECT create_distributed_table('signals', 'stream_run_id')")
|
|
112
|
+
await conn.execute("SELECT create_reference_table('signal_acknowledgments')")
|
|
82
113
|
elif migration.up_func:
|
|
83
114
|
await migration.up_func(conn)
|
|
84
115
|
elif migration.up_sql and migration.up_sql != "SELECT 1":
|
|
@@ -337,6 +368,68 @@ class CitusStorageBackend(PostgresStorageBackend):
|
|
|
337
368
|
)
|
|
338
369
|
""")
|
|
339
370
|
|
|
371
|
+
# streams — reference table (independent, keyed by stream_id)
|
|
372
|
+
await conn.execute("""
|
|
373
|
+
CREATE TABLE IF NOT EXISTS streams (
|
|
374
|
+
stream_id TEXT PRIMARY KEY,
|
|
375
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
376
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
377
|
+
metadata JSONB DEFAULT '{}'
|
|
378
|
+
)
|
|
379
|
+
""")
|
|
380
|
+
|
|
381
|
+
# signals — distributed on stream_run_id
|
|
382
|
+
# Differences vs postgres:
|
|
383
|
+
# - PK: (stream_id, stream_run_id, sequence) — distribution column is stream_run_id
|
|
384
|
+
# - No FK constraints (cross-shard FKs unsupported)
|
|
385
|
+
await conn.execute("""
|
|
386
|
+
CREATE TABLE IF NOT EXISTS signals (
|
|
387
|
+
stream_run_id TEXT NOT NULL,
|
|
388
|
+
sequence INTEGER NOT NULL,
|
|
389
|
+
signal_id TEXT NOT NULL,
|
|
390
|
+
stream_id TEXT NOT NULL,
|
|
391
|
+
signal_type TEXT NOT NULL,
|
|
392
|
+
payload JSONB NOT NULL DEFAULT '{}',
|
|
393
|
+
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
394
|
+
source_run_id TEXT,
|
|
395
|
+
metadata JSONB DEFAULT '{}',
|
|
396
|
+
PRIMARY KEY (stream_id, stream_run_id, sequence)
|
|
397
|
+
)
|
|
398
|
+
""")
|
|
399
|
+
await conn.execute(
|
|
400
|
+
"CREATE INDEX IF NOT EXISTS idx_signals_signal_id ON signals(signal_id)"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# stream_subscriptions — distributed on stream_id, co-located with signals
|
|
404
|
+
# Differences vs postgres:
|
|
405
|
+
# - PK: (stream_id, step_run_id) to include distribution column
|
|
406
|
+
# - No FK constraints (cross-shard FKs unsupported)
|
|
407
|
+
await conn.execute("""
|
|
408
|
+
CREATE TABLE IF NOT EXISTS stream_subscriptions (
|
|
409
|
+
stream_id TEXT NOT NULL,
|
|
410
|
+
step_run_id TEXT NOT NULL,
|
|
411
|
+
signal_types JSONB NOT NULL DEFAULT '[]',
|
|
412
|
+
status TEXT NOT NULL DEFAULT 'waiting',
|
|
413
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
414
|
+
PRIMARY KEY (stream_id, step_run_id)
|
|
415
|
+
)
|
|
416
|
+
""")
|
|
417
|
+
await conn.execute(
|
|
418
|
+
"CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON stream_subscriptions(stream_id, status)"
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# signal_acknowledgments — reference table (needs cross-shard joins)
|
|
422
|
+
# Differences vs postgres:
|
|
423
|
+
# - No FK constraints (cross-shard FKs unsupported)
|
|
424
|
+
await conn.execute("""
|
|
425
|
+
CREATE TABLE IF NOT EXISTS signal_acknowledgments (
|
|
426
|
+
signal_id TEXT NOT NULL,
|
|
427
|
+
step_run_id TEXT NOT NULL,
|
|
428
|
+
acknowledged_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
429
|
+
PRIMARY KEY (signal_id, step_run_id)
|
|
430
|
+
)
|
|
431
|
+
""")
|
|
432
|
+
|
|
340
433
|
# Step 4: Distribute tables (idempotent — already-distributed tables are skipped)
|
|
341
434
|
await self._distribute_tables(pool)
|
|
342
435
|
|
|
@@ -385,3 +478,21 @@ class CitusStorageBackend(PostgresStorageBackend):
|
|
|
385
478
|
# checkpoints: reference table (keyed by step_run_id, can't co-locate with run_id)
|
|
386
479
|
if "checkpoints" not in distributed:
|
|
387
480
|
await conn.execute("SELECT create_reference_table('checkpoints')")
|
|
481
|
+
|
|
482
|
+
# streams: reference table (independent, small)
|
|
483
|
+
if "streams" not in distributed:
|
|
484
|
+
await conn.execute("SELECT create_reference_table('streams')")
|
|
485
|
+
|
|
486
|
+
# signals: distributed on stream_run_id
|
|
487
|
+
if "signals" not in distributed:
|
|
488
|
+
await conn.execute("SELECT create_distributed_table('signals', 'stream_run_id')")
|
|
489
|
+
|
|
490
|
+
# stream_subscriptions: distributed on stream_id (independent from signals)
|
|
491
|
+
if "stream_subscriptions" not in distributed:
|
|
492
|
+
await conn.execute(
|
|
493
|
+
"SELECT create_distributed_table('stream_subscriptions', 'stream_id')"
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# signal_acknowledgments: reference table (needs cross-shard joins)
|
|
497
|
+
if "signal_acknowledgments" not in distributed:
|
|
498
|
+
await conn.execute("SELECT create_reference_table('signal_acknowledgments')")
|