pyworkflow-engine 0.3.2__tar.gz → 0.3.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/PKG-INFO +8 -8
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyproject.toml +8 -8
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/__init__.py +1 -1
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/interactive.py +18 -24
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/citus.py +8 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/mysql.py +53 -5
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/postgres.py +61 -5
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/sqlite.py +61 -5
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow_engine.egg-info/SOURCES.txt +1 -0
- pyworkflow_engine-0.3.4/tests/integration/test_list_runs_pagination.py +94 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/test_postgres_storage.py +25 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/test_sqlite_storage.py +28 -22
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/CLAUDE.md +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/DISTRIBUTED.md +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/LICENSE +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/MANIFEST.in +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/README.md +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/RELEASING.md +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/architecture.md +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/cancellation.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/continue-as-new.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/events.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/fault-tolerance.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/hooks.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/limitations.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/schedules.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/sleep.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/step-context.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/steps.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/stream-steps.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/stream-workflows.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/workflows.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/conventions.md +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/guides/brokers.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/guides/cli.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/guides/configuration.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/harness-gaps.md +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/introduction.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/layers.md +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/quickstart.mdx +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/docker-compose.yml +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/basic.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/batch_processing.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/cancellation.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/child_workflows.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/hooks.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/idempotency.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/long_running.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/retries.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/schedules.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/step_context.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/transient/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/transient/02_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/transient/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/transient/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/02_file_storage.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/03_retries.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/04_long_running.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/05_event_log.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/06_idempotency.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/07_hooks.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/08_cancellation.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/09_child_workflows.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/10_child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/11_continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/12_schedules.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/13_step_context.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/14_child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/transient/01_quick_tasks.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/transient/02_retries.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/transient/03_sleep.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/transient/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/aws/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/aws/context.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/aws/handler.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/aws/testing.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/app.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/loop.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/scheduler.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/singleton.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/tasks.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/__main__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/hooks.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/quickstart.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/runs.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/scheduler.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/schedules.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/setup.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/worker.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/workflows.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/output/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/output/formatters.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/output/styles.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/async_helpers.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/config.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/config_generator.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/discovery.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/docker_manager.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/storage.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/config.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/aws.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/base.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/local.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/mock.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/step_context.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/exceptions.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/registry.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/scheduled.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/step.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/validation.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/workflow.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/discovery.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/engine/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/engine/events.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/engine/executor.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/engine/replay.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/observability/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/observability/logging.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/child_handle.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/child_workflow.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/define_hook.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/hooks.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/resume_hook.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/schedule.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/shield.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/sleep.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/step_checkpoint.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/step_hook.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/runtime/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/runtime/base.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/runtime/celery.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/runtime/factory.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/runtime/local.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/scheduler/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/scheduler/local.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/serialization/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/serialization/decoder.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/serialization/encoder.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/base.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/cassandra.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/config.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/dynamodb.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/file.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/memory.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/migrations/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/migrations/base.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/schemas.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/checkpoint.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/consumer.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/context.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/decorator.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/dispatcher.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/emit.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/registry.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/runtime.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/signal.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/step_context.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/tracing/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/tracing/base.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/tracing/factory.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/tracing/langfuse.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/tracing/types.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/utils/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/utils/duration.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/utils/helpers.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/utils/schedule.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/setup.cfg +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_cancellation.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_schedule_storage.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_schema_migrations.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_singleton.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_stream_e2e.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_workflow_suspended.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/test_citus_storage.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/conftest.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/storage/__init__.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/storage/test_migrations.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_cancellation.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_cli_worker.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_emit.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_event_limits.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_executor.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_force_local.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_hooks.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_parent_run_id.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_primitives_from_steps.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_registry.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_replay.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_retention.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_schedule_schemas.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_schedule_utils.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_scheduled_workflow.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_signal.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_singleton.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_step.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_step_checkpoint.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_step_context.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_step_hook.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_stream_storage.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_stream_workflow.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_validation.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_workflow.py +0 -0
- {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_workflow_suspended.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyworkflow-engine
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: A Python implementation of durable, event-sourced workflows inspired by Vercel Workflow
|
|
5
5
|
Author: PyWorkflow Contributors
|
|
6
6
|
License: MIT
|
|
@@ -26,11 +26,11 @@ Requires-Dist: celery<6.0.0,>=5.3.0
|
|
|
26
26
|
Requires-Dist: cloudpickle>=3.0.0
|
|
27
27
|
Requires-Dist: pydantic<3.0.0,>=2.0.0
|
|
28
28
|
Requires-Dist: loguru>=0.7.0
|
|
29
|
-
Requires-Dist: click>=8.
|
|
29
|
+
Requires-Dist: click>=8.4.1
|
|
30
30
|
Requires-Dist: inquirerpy>=0.3.4; python_version < "4.0"
|
|
31
31
|
Requires-Dist: httpx>=0.25.0
|
|
32
32
|
Requires-Dist: python-dateutil>=2.8.0
|
|
33
|
-
Requires-Dist: filelock>=3.
|
|
33
|
+
Requires-Dist: filelock>=3.29.3
|
|
34
34
|
Requires-Dist: pyyaml>=6.0.0
|
|
35
35
|
Requires-Dist: croniter>=2.0.0
|
|
36
36
|
Provides-Extra: redis
|
|
@@ -42,7 +42,7 @@ Requires-Dist: asyncpg>=0.29.0; extra == "postgres"
|
|
|
42
42
|
Provides-Extra: aws
|
|
43
43
|
Requires-Dist: aws-durable-execution-sdk-python>=0.1.0; extra == "aws"
|
|
44
44
|
Provides-Extra: dynamodb
|
|
45
|
-
Requires-Dist: aiobotocore>=3.
|
|
45
|
+
Requires-Dist: aiobotocore>=3.7.0; extra == "dynamodb"
|
|
46
46
|
Provides-Extra: cassandra
|
|
47
47
|
Requires-Dist: cassandra-driver>=3.29.0; extra == "cassandra"
|
|
48
48
|
Provides-Extra: mysql
|
|
@@ -60,10 +60,10 @@ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
|
60
60
|
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
61
61
|
Requires-Dist: pytest-celery>=0.0.0; extra == "dev"
|
|
62
62
|
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
|
63
|
-
Requires-Dist: moto[dynamodb]>=5.2.
|
|
64
|
-
Requires-Dist: black>=
|
|
65
|
-
Requires-Dist: ruff>=0.15.
|
|
66
|
-
Requires-Dist: mypy>=1.
|
|
63
|
+
Requires-Dist: moto[dynamodb]>=5.2.2; extra == "dev"
|
|
64
|
+
Requires-Dist: black>=26.5.1; extra == "dev"
|
|
65
|
+
Requires-Dist: ruff>=0.15.17; extra == "dev"
|
|
66
|
+
Requires-Dist: mypy>=2.1.0; extra == "dev"
|
|
67
67
|
Requires-Dist: pre-commit>=4.6.0; extra == "dev"
|
|
68
68
|
Requires-Dist: types-redis>=4.6.0; extra == "dev"
|
|
69
69
|
Requires-Dist: types-python-dateutil>=2.8.0; extra == "dev"
|
|
@@ -7,7 +7,7 @@ packages = [{include = "pyworkflow"}]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "pyworkflow-engine"
|
|
10
|
-
version = "0.3.
|
|
10
|
+
version = "0.3.4"
|
|
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"
|
|
@@ -37,13 +37,13 @@ dependencies = [
|
|
|
37
37
|
"loguru>=0.7.0",
|
|
38
38
|
|
|
39
39
|
# CLI dependencies
|
|
40
|
-
"click>=8.
|
|
40
|
+
"click>=8.4.1",
|
|
41
41
|
"inquirerpy>=0.3.4; python_version < '4.0'",
|
|
42
42
|
|
|
43
43
|
# Utilities
|
|
44
44
|
"httpx>=0.25.0",
|
|
45
45
|
"python-dateutil>=2.8.0",
|
|
46
|
-
"filelock>=3.
|
|
46
|
+
"filelock>=3.29.3",
|
|
47
47
|
"pyyaml>=6.0.0",
|
|
48
48
|
"croniter>=2.0.0",
|
|
49
49
|
]
|
|
@@ -67,7 +67,7 @@ aws = [
|
|
|
67
67
|
"aws-durable-execution-sdk-python>=0.1.0",
|
|
68
68
|
]
|
|
69
69
|
dynamodb = [
|
|
70
|
-
"aiobotocore>=3.
|
|
70
|
+
"aiobotocore>=3.7.0",
|
|
71
71
|
]
|
|
72
72
|
|
|
73
73
|
cassandra = [
|
|
@@ -96,12 +96,12 @@ dev = [
|
|
|
96
96
|
"pytest-cov>=4.1.0",
|
|
97
97
|
"pytest-celery>=0.0.0",
|
|
98
98
|
"pytest-mock>=3.12.0",
|
|
99
|
-
"moto[dynamodb]>=5.2.
|
|
99
|
+
"moto[dynamodb]>=5.2.2", # AWS mocking for DynamoDB tests
|
|
100
100
|
|
|
101
101
|
# Code quality
|
|
102
|
-
"black>=
|
|
103
|
-
"ruff>=0.15.
|
|
104
|
-
"mypy>=1.
|
|
102
|
+
"black>=26.5.1",
|
|
103
|
+
"ruff>=0.15.17",
|
|
104
|
+
"mypy>=2.1.0",
|
|
105
105
|
"pre-commit>=4.6.0",
|
|
106
106
|
|
|
107
107
|
# Type stubs
|
|
@@ -76,15 +76,12 @@ def select(
|
|
|
76
76
|
try:
|
|
77
77
|
# Convert string list to Choice objects
|
|
78
78
|
choice_objects: list[Choice] = []
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
for c in choices:
|
|
86
|
-
if isinstance(c, dict):
|
|
87
|
-
choice_objects.append(Choice(value=c.get("value", c["name"]), name=c["name"]))
|
|
79
|
+
for c in choices:
|
|
80
|
+
if isinstance(c, str):
|
|
81
|
+
choice_objects.append(Choice(value=c, name=c))
|
|
82
|
+
else:
|
|
83
|
+
# Dict format: {"name": "...", "value": "..."}
|
|
84
|
+
choice_objects.append(Choice(value=c.get("value", c["name"]), name=c["name"]))
|
|
88
85
|
|
|
89
86
|
return inquirer.select(
|
|
90
87
|
message=message,
|
|
@@ -222,22 +219,19 @@ def multiselect(
|
|
|
222
219
|
try:
|
|
223
220
|
# Convert string list to Choice objects
|
|
224
221
|
choice_objects: list[Choice] = []
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
value=value,
|
|
237
|
-
name=c["name"],
|
|
238
|
-
enabled=(value in (default or [])),
|
|
239
|
-
)
|
|
222
|
+
for c in choices:
|
|
223
|
+
if isinstance(c, str):
|
|
224
|
+
choice_objects.append(Choice(value=c, name=c, enabled=(c in (default or []))))
|
|
225
|
+
else:
|
|
226
|
+
# Dict format: {"name": "...", "value": "..."}
|
|
227
|
+
value = c.get("value", c["name"])
|
|
228
|
+
choice_objects.append(
|
|
229
|
+
Choice(
|
|
230
|
+
value=value,
|
|
231
|
+
name=c["name"],
|
|
232
|
+
enabled=(value in (default or [])),
|
|
240
233
|
)
|
|
234
|
+
)
|
|
241
235
|
|
|
242
236
|
return inquirer.checkbox(
|
|
243
237
|
message=message,
|
|
@@ -308,6 +308,14 @@ class CitusStorageBackend(PostgresStorageBackend):
|
|
|
308
308
|
await conn.execute(
|
|
309
309
|
"CREATE INDEX IF NOT EXISTS idx_runs_created_at ON workflow_runs(created_at DESC)"
|
|
310
310
|
)
|
|
311
|
+
# Composite index backing keyset pagination + ORDER BY created_at DESC, run_id DESC
|
|
312
|
+
# for the unfiltered Runs list (issue #482). The summary column projection
|
|
313
|
+
# (RUN_SUMMARY_COLUMNS) already drops the heavy payload columns from the scatter-gather;
|
|
314
|
+
# no INCLUDE covering columns, to avoid duplicating the summary columns into the index.
|
|
315
|
+
await conn.execute(
|
|
316
|
+
"CREATE INDEX IF NOT EXISTS idx_runs_created_at_run_id "
|
|
317
|
+
"ON workflow_runs(created_at DESC, run_id DESC)"
|
|
318
|
+
)
|
|
311
319
|
# Non-unique index: Citus cannot enforce global uniqueness on non-distribution columns
|
|
312
320
|
await conn.execute(
|
|
313
321
|
"CREATE INDEX IF NOT EXISTS idx_runs_idempotency_key ON workflow_runs(idempotency_key) WHERE idempotency_key IS NOT NULL"
|
|
@@ -31,6 +31,15 @@ from pyworkflow.storage.schemas import (
|
|
|
31
31
|
WorkflowRun,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
+
# Columns needed by list/summary views. Excludes the heavy payload columns
|
|
35
|
+
# (input_args, input_kwargs, result, metadata) which a list view never renders. See issue #482.
|
|
36
|
+
RUN_SUMMARY_COLUMNS = (
|
|
37
|
+
"run_id, workflow_name, status, created_at, updated_at, started_at, completed_at, "
|
|
38
|
+
"error, idempotency_key, max_duration, recovery_attempts, max_recovery_attempts, "
|
|
39
|
+
"recover_on_worker_loss, parent_run_id, nesting_depth, continued_from_run_id, "
|
|
40
|
+
"continued_to_run_id"
|
|
41
|
+
)
|
|
42
|
+
|
|
34
43
|
|
|
35
44
|
class MySQLMigrationRunner(MigrationRunner):
|
|
36
45
|
"""MySQL-specific migration runner."""
|
|
@@ -286,6 +295,7 @@ class MySQLStorageBackend(StorageBackend):
|
|
|
286
295
|
INDEX idx_runs_status (status),
|
|
287
296
|
INDEX idx_runs_workflow_name (workflow_name),
|
|
288
297
|
INDEX idx_runs_created_at (created_at DESC),
|
|
298
|
+
INDEX idx_runs_created_at_run_id (created_at DESC, run_id DESC),
|
|
289
299
|
UNIQUE INDEX idx_runs_idempotency_key (idempotency_key),
|
|
290
300
|
INDEX idx_runs_parent_run_id (parent_run_id),
|
|
291
301
|
FOREIGN KEY (parent_run_id) REFERENCES workflow_runs(run_id)
|
|
@@ -607,15 +617,23 @@ class MySQLStorageBackend(StorageBackend):
|
|
|
607
617
|
limit: int = 100,
|
|
608
618
|
cursor: str | None = None,
|
|
609
619
|
) -> tuple[list[WorkflowRun], str | None]:
|
|
610
|
-
"""List workflow runs with optional filtering and pagination.
|
|
620
|
+
"""List workflow runs with optional filtering and pagination.
|
|
621
|
+
|
|
622
|
+
Note: the returned WorkflowRun objects are *summaries* — the heavy payload columns
|
|
623
|
+
(input_args, input_kwargs, result, metadata) are NOT fetched and carry placeholder
|
|
624
|
+
defaults. Use get_run() when the full payload is required. See issue #482.
|
|
625
|
+
"""
|
|
611
626
|
pool = self._ensure_connected()
|
|
612
627
|
|
|
613
628
|
conditions = []
|
|
614
629
|
params: list[Any] = []
|
|
615
630
|
|
|
616
631
|
if cursor:
|
|
632
|
+
# Keyset pagination on (created_at, run_id) — created_at is not unique, so paginating on
|
|
633
|
+
# it alone would skip/duplicate rows sharing a timestamp. See issue #482.
|
|
617
634
|
conditions.append(
|
|
618
|
-
"
|
|
635
|
+
"(created_at, run_id) < "
|
|
636
|
+
"(SELECT created_at, run_id FROM workflow_runs WHERE run_id = %s)"
|
|
619
637
|
)
|
|
620
638
|
params.append(cursor)
|
|
621
639
|
|
|
@@ -640,9 +658,9 @@ class MySQLStorageBackend(StorageBackend):
|
|
|
640
658
|
params.append(limit + 1) # Fetch one extra to determine if there are more
|
|
641
659
|
|
|
642
660
|
sql = f"""
|
|
643
|
-
SELECT
|
|
661
|
+
SELECT {RUN_SUMMARY_COLUMNS} FROM workflow_runs
|
|
644
662
|
{where_clause}
|
|
645
|
-
ORDER BY created_at DESC
|
|
663
|
+
ORDER BY created_at DESC, run_id DESC
|
|
646
664
|
LIMIT %s
|
|
647
665
|
"""
|
|
648
666
|
|
|
@@ -654,7 +672,7 @@ class MySQLStorageBackend(StorageBackend):
|
|
|
654
672
|
if has_more:
|
|
655
673
|
rows = rows[:limit]
|
|
656
674
|
|
|
657
|
-
runs = [self.
|
|
675
|
+
runs = [self._row_to_workflow_run_summary(row) for row in rows]
|
|
658
676
|
next_cursor = runs[-1].run_id if runs and has_more else None
|
|
659
677
|
|
|
660
678
|
return runs, next_cursor
|
|
@@ -1486,6 +1504,36 @@ class MySQLStorageBackend(StorageBackend):
|
|
|
1486
1504
|
continued_to_run_id=row["continued_to_run_id"],
|
|
1487
1505
|
)
|
|
1488
1506
|
|
|
1507
|
+
def _row_to_workflow_run_summary(self, row: dict) -> WorkflowRun:
|
|
1508
|
+
"""Convert a summary row (RUN_SUMMARY_COLUMNS) to a WorkflowRun.
|
|
1509
|
+
|
|
1510
|
+
Heavy payload columns are not selected by list_runs, so they are filled with their schema
|
|
1511
|
+
defaults. Callers needing the real payload must use get_run(). See issue #482.
|
|
1512
|
+
"""
|
|
1513
|
+
return WorkflowRun(
|
|
1514
|
+
run_id=row["run_id"],
|
|
1515
|
+
workflow_name=row["workflow_name"],
|
|
1516
|
+
status=RunStatus(row["status"]),
|
|
1517
|
+
created_at=row["created_at"],
|
|
1518
|
+
updated_at=row["updated_at"],
|
|
1519
|
+
started_at=row["started_at"],
|
|
1520
|
+
completed_at=row["completed_at"],
|
|
1521
|
+
input_args="[]",
|
|
1522
|
+
input_kwargs="{}",
|
|
1523
|
+
result=None,
|
|
1524
|
+
error=row["error"],
|
|
1525
|
+
idempotency_key=row["idempotency_key"],
|
|
1526
|
+
max_duration=row["max_duration"],
|
|
1527
|
+
context={},
|
|
1528
|
+
recovery_attempts=row["recovery_attempts"],
|
|
1529
|
+
max_recovery_attempts=row["max_recovery_attempts"],
|
|
1530
|
+
recover_on_worker_loss=bool(row["recover_on_worker_loss"]),
|
|
1531
|
+
parent_run_id=row["parent_run_id"],
|
|
1532
|
+
nesting_depth=row["nesting_depth"],
|
|
1533
|
+
continued_from_run_id=row["continued_from_run_id"],
|
|
1534
|
+
continued_to_run_id=row["continued_to_run_id"],
|
|
1535
|
+
)
|
|
1536
|
+
|
|
1489
1537
|
def _row_to_event(self, row: dict) -> Event:
|
|
1490
1538
|
"""Convert database row to Event object."""
|
|
1491
1539
|
return Event(
|
|
@@ -39,6 +39,17 @@ from pyworkflow.storage.schemas import (
|
|
|
39
39
|
WorkflowRun,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
+
# Columns needed by list/summary views (the Runs list, CLI table). Deliberately excludes the heavy
|
|
43
|
+
# unbounded payload columns (input_args, input_kwargs, result, metadata) which a list view never
|
|
44
|
+
# renders. Selecting only these avoids scatter-gathering wide rows across every Citus shard.
|
|
45
|
+
# See issue #482. Keep `error` — it is shown in the list view and is normally short.
|
|
46
|
+
RUN_SUMMARY_COLUMNS = (
|
|
47
|
+
"run_id, workflow_name, status, created_at, updated_at, started_at, completed_at, "
|
|
48
|
+
"error, idempotency_key, max_duration, recovery_attempts, max_recovery_attempts, "
|
|
49
|
+
"recover_on_worker_loss, parent_run_id, nesting_depth, continued_from_run_id, "
|
|
50
|
+
"continued_to_run_id"
|
|
51
|
+
)
|
|
52
|
+
|
|
42
53
|
|
|
43
54
|
class PostgresMigrationRunner(MigrationRunner):
|
|
44
55
|
"""PostgreSQL-specific migration runner."""
|
|
@@ -429,6 +440,12 @@ class PostgresStorageBackend(StorageBackend):
|
|
|
429
440
|
await conn.execute(
|
|
430
441
|
"CREATE INDEX IF NOT EXISTS idx_runs_created_at ON workflow_runs(created_at DESC)"
|
|
431
442
|
)
|
|
443
|
+
# Composite index backing keyset pagination + ORDER BY created_at DESC, run_id DESC
|
|
444
|
+
# (see list_runs / issue #482).
|
|
445
|
+
await conn.execute(
|
|
446
|
+
"CREATE INDEX IF NOT EXISTS idx_runs_created_at_run_id "
|
|
447
|
+
"ON workflow_runs(created_at DESC, run_id DESC)"
|
|
448
|
+
)
|
|
432
449
|
await conn.execute(
|
|
433
450
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_idempotency_key ON workflow_runs(idempotency_key) WHERE idempotency_key IS NOT NULL"
|
|
434
451
|
)
|
|
@@ -852,7 +869,12 @@ class PostgresStorageBackend(StorageBackend):
|
|
|
852
869
|
limit: int = 100,
|
|
853
870
|
cursor: str | None = None,
|
|
854
871
|
) -> tuple[list[WorkflowRun], str | None]:
|
|
855
|
-
"""List workflow runs with optional filtering and pagination.
|
|
872
|
+
"""List workflow runs with optional filtering and pagination.
|
|
873
|
+
|
|
874
|
+
Note: the returned WorkflowRun objects are *summaries* — the heavy payload columns
|
|
875
|
+
(input_args, input_kwargs, result, metadata) are NOT fetched and carry placeholder
|
|
876
|
+
defaults. Use get_run() when the full payload is required. See issue #482.
|
|
877
|
+
"""
|
|
856
878
|
pool = await self._get_pool()
|
|
857
879
|
|
|
858
880
|
conditions = []
|
|
@@ -860,8 +882,12 @@ class PostgresStorageBackend(StorageBackend):
|
|
|
860
882
|
param_idx = 1
|
|
861
883
|
|
|
862
884
|
if cursor:
|
|
885
|
+
# Keyset pagination on (created_at, run_id). created_at is not unique, so paginating on
|
|
886
|
+
# it alone would skip/duplicate rows that share a timestamp. The composite tuple compare
|
|
887
|
+
# is a stable total order matching ORDER BY created_at DESC, run_id DESC.
|
|
863
888
|
conditions.append(
|
|
864
|
-
f"
|
|
889
|
+
f"(created_at, run_id) < "
|
|
890
|
+
f"(SELECT created_at, run_id FROM workflow_runs WHERE run_id = ${param_idx})"
|
|
865
891
|
)
|
|
866
892
|
params.append(cursor)
|
|
867
893
|
param_idx += 1
|
|
@@ -893,9 +919,9 @@ class PostgresStorageBackend(StorageBackend):
|
|
|
893
919
|
params.append(limit + 1) # Fetch one extra to determine if there are more
|
|
894
920
|
|
|
895
921
|
sql = f"""
|
|
896
|
-
SELECT
|
|
922
|
+
SELECT {RUN_SUMMARY_COLUMNS} FROM workflow_runs
|
|
897
923
|
{where_clause}
|
|
898
|
-
ORDER BY created_at DESC
|
|
924
|
+
ORDER BY created_at DESC, run_id DESC
|
|
899
925
|
LIMIT ${param_idx}
|
|
900
926
|
"""
|
|
901
927
|
|
|
@@ -906,7 +932,7 @@ class PostgresStorageBackend(StorageBackend):
|
|
|
906
932
|
if has_more:
|
|
907
933
|
rows = rows[:limit]
|
|
908
934
|
|
|
909
|
-
runs = [self.
|
|
935
|
+
runs = [self._row_to_workflow_run_summary(row) for row in rows]
|
|
910
936
|
next_cursor = runs[-1].run_id if runs and has_more else None
|
|
911
937
|
|
|
912
938
|
return runs, next_cursor
|
|
@@ -1747,6 +1773,36 @@ class PostgresStorageBackend(StorageBackend):
|
|
|
1747
1773
|
continued_to_run_id=row["continued_to_run_id"],
|
|
1748
1774
|
)
|
|
1749
1775
|
|
|
1776
|
+
def _row_to_workflow_run_summary(self, row: asyncpg.Record) -> WorkflowRun:
|
|
1777
|
+
"""Convert a summary row (RUN_SUMMARY_COLUMNS) to a WorkflowRun.
|
|
1778
|
+
|
|
1779
|
+
The heavy payload columns are not selected by list_runs, so they are filled with their
|
|
1780
|
+
schema defaults. Callers needing the real payload must use get_run(). See issue #482.
|
|
1781
|
+
"""
|
|
1782
|
+
return WorkflowRun(
|
|
1783
|
+
run_id=row["run_id"],
|
|
1784
|
+
workflow_name=row["workflow_name"],
|
|
1785
|
+
status=RunStatus(row["status"]),
|
|
1786
|
+
created_at=row["created_at"],
|
|
1787
|
+
updated_at=row["updated_at"],
|
|
1788
|
+
started_at=row["started_at"],
|
|
1789
|
+
completed_at=row["completed_at"],
|
|
1790
|
+
input_args="[]",
|
|
1791
|
+
input_kwargs="{}",
|
|
1792
|
+
result=None,
|
|
1793
|
+
error=row["error"],
|
|
1794
|
+
idempotency_key=row["idempotency_key"],
|
|
1795
|
+
max_duration=row["max_duration"],
|
|
1796
|
+
context={},
|
|
1797
|
+
recovery_attempts=row["recovery_attempts"],
|
|
1798
|
+
max_recovery_attempts=row["max_recovery_attempts"],
|
|
1799
|
+
recover_on_worker_loss=row["recover_on_worker_loss"],
|
|
1800
|
+
parent_run_id=row["parent_run_id"],
|
|
1801
|
+
nesting_depth=row["nesting_depth"],
|
|
1802
|
+
continued_from_run_id=row["continued_from_run_id"],
|
|
1803
|
+
continued_to_run_id=row["continued_to_run_id"],
|
|
1804
|
+
)
|
|
1805
|
+
|
|
1750
1806
|
def _row_to_event(self, row: asyncpg.Record) -> Event:
|
|
1751
1807
|
"""Convert database row to Event object."""
|
|
1752
1808
|
return Event(
|
|
@@ -31,6 +31,16 @@ from pyworkflow.storage.schemas import (
|
|
|
31
31
|
WorkflowRun,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
+
# Columns needed by list/summary views, in the exact order consumed by
|
|
35
|
+
# _row_to_workflow_run_summary (positional). Excludes the heavy payload columns
|
|
36
|
+
# (input_args, input_kwargs, result, metadata) which a list view never renders. See issue #482.
|
|
37
|
+
RUN_SUMMARY_COLUMNS = (
|
|
38
|
+
"run_id, workflow_name, status, created_at, updated_at, started_at, completed_at, "
|
|
39
|
+
"error, idempotency_key, max_duration, recovery_attempts, max_recovery_attempts, "
|
|
40
|
+
"recover_on_worker_loss, parent_run_id, nesting_depth, continued_from_run_id, "
|
|
41
|
+
"continued_to_run_id"
|
|
42
|
+
)
|
|
43
|
+
|
|
34
44
|
|
|
35
45
|
class SQLiteMigrationRunner(MigrationRunner):
|
|
36
46
|
"""SQLite-specific migration runner."""
|
|
@@ -249,6 +259,12 @@ class SQLiteStorageBackend(StorageBackend):
|
|
|
249
259
|
await db.execute(
|
|
250
260
|
"CREATE INDEX IF NOT EXISTS idx_runs_created_at ON workflow_runs(created_at DESC)"
|
|
251
261
|
)
|
|
262
|
+
# Composite index backing keyset pagination + ORDER BY created_at DESC, run_id DESC
|
|
263
|
+
# (see list_runs / issue #482).
|
|
264
|
+
await db.execute(
|
|
265
|
+
"CREATE INDEX IF NOT EXISTS idx_runs_created_at_run_id "
|
|
266
|
+
"ON workflow_runs(created_at DESC, run_id DESC)"
|
|
267
|
+
)
|
|
252
268
|
await db.execute(
|
|
253
269
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_runs_idempotency_key ON workflow_runs(idempotency_key) WHERE idempotency_key IS NOT NULL"
|
|
254
270
|
)
|
|
@@ -597,15 +613,24 @@ class SQLiteStorageBackend(StorageBackend):
|
|
|
597
613
|
limit: int = 100,
|
|
598
614
|
cursor: str | None = None,
|
|
599
615
|
) -> tuple[list[WorkflowRun], str | None]:
|
|
600
|
-
"""List workflow runs with optional filtering and pagination.
|
|
616
|
+
"""List workflow runs with optional filtering and pagination.
|
|
617
|
+
|
|
618
|
+
Note: the returned WorkflowRun objects are *summaries* — the heavy payload columns
|
|
619
|
+
(input_args, input_kwargs, result, metadata) are NOT fetched and carry placeholder
|
|
620
|
+
defaults. Use get_run() when the full payload is required. See issue #482.
|
|
621
|
+
"""
|
|
601
622
|
db = self._ensure_connected()
|
|
602
623
|
|
|
603
624
|
conditions = []
|
|
604
625
|
params: list[Any] = []
|
|
605
626
|
|
|
606
627
|
if cursor:
|
|
628
|
+
# Keyset pagination on (created_at, run_id) — created_at is not unique, so paginating on
|
|
629
|
+
# it alone would skip/duplicate rows sharing a timestamp. SQLite supports row-value
|
|
630
|
+
# comparison (>= 3.15). See issue #482.
|
|
607
631
|
conditions.append(
|
|
608
|
-
"
|
|
632
|
+
"(created_at, run_id) < "
|
|
633
|
+
"(SELECT created_at, run_id FROM workflow_runs WHERE run_id = ?)"
|
|
609
634
|
)
|
|
610
635
|
params.append(cursor)
|
|
611
636
|
|
|
@@ -630,9 +655,9 @@ class SQLiteStorageBackend(StorageBackend):
|
|
|
630
655
|
params.append(limit + 1) # Fetch one extra to determine if there are more
|
|
631
656
|
|
|
632
657
|
sql = f"""
|
|
633
|
-
SELECT
|
|
658
|
+
SELECT {RUN_SUMMARY_COLUMNS} FROM workflow_runs
|
|
634
659
|
{where_clause}
|
|
635
|
-
ORDER BY created_at DESC
|
|
660
|
+
ORDER BY created_at DESC, run_id DESC
|
|
636
661
|
LIMIT ?
|
|
637
662
|
"""
|
|
638
663
|
|
|
@@ -643,7 +668,7 @@ class SQLiteStorageBackend(StorageBackend):
|
|
|
643
668
|
if has_more:
|
|
644
669
|
rows = rows[:limit]
|
|
645
670
|
|
|
646
|
-
runs = [self.
|
|
671
|
+
runs = [self._row_to_workflow_run_summary(row) for row in rows]
|
|
647
672
|
next_cursor = runs[-1].run_id if runs and has_more else None
|
|
648
673
|
|
|
649
674
|
return runs, next_cursor
|
|
@@ -1801,6 +1826,37 @@ class SQLiteStorageBackend(StorageBackend):
|
|
|
1801
1826
|
continued_to_run_id=row[20],
|
|
1802
1827
|
)
|
|
1803
1828
|
|
|
1829
|
+
def _row_to_workflow_run_summary(self, row: Any) -> WorkflowRun:
|
|
1830
|
+
"""Convert a summary row to a WorkflowRun.
|
|
1831
|
+
|
|
1832
|
+
Positional mapping MUST match the order of RUN_SUMMARY_COLUMNS. The heavy payload columns
|
|
1833
|
+
are not selected by list_runs, so they are filled with their schema defaults. Callers
|
|
1834
|
+
needing the real payload must use get_run(). See issue #482.
|
|
1835
|
+
"""
|
|
1836
|
+
return WorkflowRun(
|
|
1837
|
+
run_id=row[0],
|
|
1838
|
+
workflow_name=row[1],
|
|
1839
|
+
status=RunStatus(row[2]),
|
|
1840
|
+
created_at=datetime.fromisoformat(row[3]),
|
|
1841
|
+
updated_at=datetime.fromisoformat(row[4]),
|
|
1842
|
+
started_at=datetime.fromisoformat(row[5]) if row[5] else None,
|
|
1843
|
+
completed_at=datetime.fromisoformat(row[6]) if row[6] else None,
|
|
1844
|
+
input_args="[]",
|
|
1845
|
+
input_kwargs="{}",
|
|
1846
|
+
result=None,
|
|
1847
|
+
error=row[7],
|
|
1848
|
+
idempotency_key=row[8],
|
|
1849
|
+
max_duration=row[9],
|
|
1850
|
+
context={},
|
|
1851
|
+
recovery_attempts=row[10],
|
|
1852
|
+
max_recovery_attempts=row[11],
|
|
1853
|
+
recover_on_worker_loss=bool(row[12]),
|
|
1854
|
+
parent_run_id=row[13],
|
|
1855
|
+
nesting_depth=row[14],
|
|
1856
|
+
continued_from_run_id=row[15],
|
|
1857
|
+
continued_to_run_id=row[16],
|
|
1858
|
+
)
|
|
1859
|
+
|
|
1804
1860
|
def _row_to_event(self, row: Any) -> Event:
|
|
1805
1861
|
"""Convert database row to Event object."""
|
|
1806
1862
|
# Column order: event_id[0], run_id[1], sequence[2], type[3], timestamp[4], data[5], step_id[6]
|
|
@@ -188,6 +188,7 @@ tests/integration/test_child_workflows.py
|
|
|
188
188
|
tests/integration/test_continue_as_new.py
|
|
189
189
|
tests/integration/test_dynamodb_storage.py
|
|
190
190
|
tests/integration/test_fault_tolerance.py
|
|
191
|
+
tests/integration/test_list_runs_pagination.py
|
|
191
192
|
tests/integration/test_schedule_storage.py
|
|
192
193
|
tests/integration/test_schema_migrations.py
|
|
193
194
|
tests/integration/test_singleton.py
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration tests for list_runs keyset pagination and summary projection (issue #482).
|
|
3
|
+
|
|
4
|
+
Run against a real SQLite database so we exercise the actual SQL (row-value cursor comparison,
|
|
5
|
+
ORDER BY created_at DESC, run_id DESC, and the summary column projection) rather than mocks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from pyworkflow.storage.schemas import RunStatus, WorkflowRun
|
|
13
|
+
from pyworkflow.storage.sqlite import SQLiteStorageBackend
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
async def storage(tmp_path):
|
|
18
|
+
"""Real SQLite backend backed by a temp file."""
|
|
19
|
+
backend = SQLiteStorageBackend(db_path=str(tmp_path / "runs.db"))
|
|
20
|
+
await backend.connect()
|
|
21
|
+
yield backend
|
|
22
|
+
await backend.disconnect()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def _make_run(storage, run_id: str, created_at: datetime, **kwargs) -> None:
|
|
26
|
+
run = WorkflowRun(
|
|
27
|
+
run_id=run_id,
|
|
28
|
+
workflow_name="wf",
|
|
29
|
+
status=RunStatus.COMPLETED,
|
|
30
|
+
created_at=created_at,
|
|
31
|
+
updated_at=created_at,
|
|
32
|
+
**kwargs,
|
|
33
|
+
)
|
|
34
|
+
await storage.create_run(run)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
async def test_list_runs_keyset_pagination_handles_tied_created_at(storage):
|
|
39
|
+
"""Rows sharing one created_at must each appear exactly once across cursor pages.
|
|
40
|
+
|
|
41
|
+
The old `created_at < cursor.created_at` predicate skipped/duplicated rows on ties; the keyset
|
|
42
|
+
`(created_at, run_id)` predicate must page through all of them.
|
|
43
|
+
"""
|
|
44
|
+
ts = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
45
|
+
# Five runs, all with the SAME created_at — the pathological tie case.
|
|
46
|
+
run_ids = {f"run_{i}" for i in range(5)}
|
|
47
|
+
for run_id in run_ids:
|
|
48
|
+
await _make_run(storage, run_id, ts)
|
|
49
|
+
|
|
50
|
+
seen: list[str] = []
|
|
51
|
+
cursor = None
|
|
52
|
+
for _ in range(10): # generous upper bound; should terminate well before this
|
|
53
|
+
runs, cursor = await storage.list_runs(limit=2, cursor=cursor)
|
|
54
|
+
seen.extend(r.run_id for r in runs)
|
|
55
|
+
if cursor is None:
|
|
56
|
+
break
|
|
57
|
+
|
|
58
|
+
assert sorted(seen) == sorted(run_ids) # every row exactly once, no skips/dupes
|
|
59
|
+
assert len(seen) == len(set(seen))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.asyncio
|
|
63
|
+
async def test_list_runs_returns_summary_without_heavy_payload(storage):
|
|
64
|
+
"""list_runs omits the heavy payload columns; they come back as defaults, error survives."""
|
|
65
|
+
ts = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
66
|
+
await _make_run(
|
|
67
|
+
storage,
|
|
68
|
+
"run_heavy",
|
|
69
|
+
ts,
|
|
70
|
+
input_args='["big"]',
|
|
71
|
+
input_kwargs='{"k": "v"}',
|
|
72
|
+
result='{"r": 1}',
|
|
73
|
+
error="boom",
|
|
74
|
+
context={"meta": "data"},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
runs, _ = await storage.list_runs(limit=10)
|
|
78
|
+
|
|
79
|
+
assert len(runs) == 1
|
|
80
|
+
summary = runs[0]
|
|
81
|
+
# Heavy payload columns are not fetched -> placeholder defaults.
|
|
82
|
+
assert summary.input_args == "[]"
|
|
83
|
+
assert summary.input_kwargs == "{}"
|
|
84
|
+
assert summary.result is None
|
|
85
|
+
assert summary.context == {}
|
|
86
|
+
# Lightweight fields used by the list view survive.
|
|
87
|
+
assert summary.error == "boom"
|
|
88
|
+
assert summary.status == RunStatus.COMPLETED
|
|
89
|
+
|
|
90
|
+
# get_run still returns the full payload.
|
|
91
|
+
full = await storage.get_run("run_heavy")
|
|
92
|
+
assert full.input_args == '["big"]'
|
|
93
|
+
assert full.result == '{"r": 1}'
|
|
94
|
+
assert full.context == {"meta": "data"}
|
{pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/test_postgres_storage.py
RENAMED
|
@@ -531,6 +531,31 @@ class TestWorkflowRunOperations:
|
|
|
531
531
|
|
|
532
532
|
assert len(runs) == 1
|
|
533
533
|
assert runs[0].run_id == "run_1"
|
|
534
|
+
# list_runs returns summaries: heavy payload columns are not fetched and carry defaults,
|
|
535
|
+
# while lightweight fields (incl. error) survive. See issue #482.
|
|
536
|
+
assert runs[0].input_args == "[]"
|
|
537
|
+
assert runs[0].input_kwargs == "{}"
|
|
538
|
+
assert runs[0].result is None
|
|
539
|
+
assert runs[0].context == {}
|
|
540
|
+
assert runs[0].error is None
|
|
541
|
+
|
|
542
|
+
# The query must not SELECT * (avoids scatter-gathering wide rows across Citus shards) and
|
|
543
|
+
# must order by the keyset tuple (created_at, run_id).
|
|
544
|
+
sql = mock_conn.fetch.call_args[0][0]
|
|
545
|
+
assert "SELECT *" not in sql
|
|
546
|
+
assert "input_args" not in sql
|
|
547
|
+
assert "ORDER BY created_at DESC, run_id DESC" in sql
|
|
548
|
+
|
|
549
|
+
@pytest.mark.asyncio
|
|
550
|
+
async def test_list_runs_cursor_uses_keyset(self, mock_backend):
|
|
551
|
+
"""Cursor pagination compares the (created_at, run_id) tuple, not created_at alone."""
|
|
552
|
+
backend, mock_conn = mock_backend
|
|
553
|
+
mock_conn.fetch.return_value = []
|
|
554
|
+
|
|
555
|
+
await backend.list_runs(limit=10, cursor="run_1")
|
|
556
|
+
|
|
557
|
+
sql = mock_conn.fetch.call_args[0][0]
|
|
558
|
+
assert "(created_at, run_id) <" in sql
|
|
534
559
|
|
|
535
560
|
|
|
536
561
|
class TestEventOperations:
|