pyworkflow-engine 0.3.0__tar.gz → 0.3.1__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.0 → pyworkflow_engine-0.3.1}/PKG-INFO +1 -1
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyproject.toml +1 -1
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/__init__.py +1 -1
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/singleton.py +82 -12
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/tasks.py +5 -1
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_singleton.py +126 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/CLAUDE.md +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/DISTRIBUTED.md +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/LICENSE +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/MANIFEST.in +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/README.md +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/RELEASING.md +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/architecture.md +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/cancellation.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/continue-as-new.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/events.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/fault-tolerance.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/hooks.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/limitations.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/schedules.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/sleep.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/step-context.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/steps.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/stream-steps.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/stream-workflows.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/concepts/workflows.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/conventions.md +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/guides/brokers.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/guides/cli.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/guides/configuration.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/harness-gaps.md +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/introduction.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/layers.md +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/docs/quickstart.mdx +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/docker-compose.yml +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/basic.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/batch_processing.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/cancellation.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/child_workflows.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/hooks.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/idempotency.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/long_running.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/retries.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/schedules.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/step_context.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/transient/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/transient/02_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/transient/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/transient/pyworkflow.config.yaml +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/01_basic_workflow.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/02_file_storage.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/03_retries.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/04_long_running.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/05_event_log.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/06_idempotency.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/07_hooks.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/08_cancellation.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/09_child_workflows.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/10_child_workflow_patterns.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/11_continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/12_schedules.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/13_step_context.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/14_child_workflow_from_step.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/transient/01_quick_tasks.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/transient/02_retries.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/transient/03_sleep.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/transient/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/aws/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/aws/context.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/aws/handler.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/aws/testing.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/app.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/loop.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/celery/scheduler.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/__main__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/hooks.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/quickstart.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/runs.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/scheduler.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/schedules.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/setup.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/worker.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/commands/workflows.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/output/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/output/formatters.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/output/styles.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/async_helpers.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/config.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/config_generator.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/discovery.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/docker_manager.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/interactive.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/storage.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/config.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/aws.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/base.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/local.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/mock.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/context/step_context.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/exceptions.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/registry.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/scheduled.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/step.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/validation.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/core/workflow.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/discovery.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/engine/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/engine/events.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/engine/executor.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/engine/replay.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/observability/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/observability/logging.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/child_handle.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/child_workflow.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/define_hook.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/hooks.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/resume_hook.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/schedule.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/shield.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/sleep.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/step_checkpoint.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/primitives/step_hook.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/base.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/celery.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/factory.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/runtime/local.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/scheduler/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/scheduler/local.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/serialization/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/serialization/decoder.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/serialization/encoder.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/base.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/cassandra.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/citus.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/config.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/dynamodb.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/file.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/memory.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/migrations/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/migrations/base.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/mysql.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/postgres.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/schemas.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/storage/sqlite.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/checkpoint.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/consumer.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/context.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/decorator.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/dispatcher.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/emit.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/registry.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/runtime.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/signal.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/streams/step_context.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/tracing/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/tracing/base.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/tracing/factory.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/tracing/langfuse.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/tracing/types.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/utils/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/utils/duration.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/utils/helpers.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/utils/schedule.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow_engine.egg-info/SOURCES.txt +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/setup.cfg +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_cancellation.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_schedule_storage.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_schema_migrations.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_singleton.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_stream_e2e.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/integration/test_workflow_suspended.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_cassandra_storage.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_citus_storage.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_dynamodb_storage.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_postgres_storage.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/backends/test_sqlite_storage.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/conftest.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/storage/__init__.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/storage/test_migrations.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_cancellation.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_child_workflows.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_cli_worker.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_continue_as_new.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_emit.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_event_limits.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_executor.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_fault_tolerance.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_force_local.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_hooks.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_parent_run_id.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_primitives_from_steps.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_registry.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_replay.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_retention.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_schedule_schemas.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_schedule_utils.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_scheduled_workflow.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_signal.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_step.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_step_checkpoint.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_step_context.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_step_hook.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_stream_storage.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_stream_workflow.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_validation.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/tests/unit/test_workflow.py +0 -0
- {pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/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.3.
|
|
10
|
+
version = "0.3.1"
|
|
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"
|
|
@@ -11,8 +11,9 @@ Based on:
|
|
|
11
11
|
|
|
12
12
|
import inspect
|
|
13
13
|
import json
|
|
14
|
+
from collections.abc import Callable
|
|
14
15
|
from hashlib import md5
|
|
15
|
-
from typing import TYPE_CHECKING, Any
|
|
16
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
16
17
|
from uuid import uuid4
|
|
17
18
|
|
|
18
19
|
from celery import Task
|
|
@@ -20,8 +21,11 @@ from celery.exceptions import WorkerLostError
|
|
|
20
21
|
from loguru import logger
|
|
21
22
|
|
|
22
23
|
if TYPE_CHECKING:
|
|
24
|
+
from redis import Redis
|
|
23
25
|
from redis.sentinel import Sentinel
|
|
24
26
|
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
|
|
25
29
|
|
|
26
30
|
def generate_lock_key(
|
|
27
31
|
task_name: str,
|
|
@@ -74,10 +78,17 @@ class SingletonConfig:
|
|
|
74
78
|
|
|
75
79
|
|
|
76
80
|
class RedisLockBackend:
|
|
77
|
-
"""Redis backend for distributed locking with Sentinel support.
|
|
81
|
+
"""Redis backend for distributed locking with Sentinel support.
|
|
82
|
+
|
|
83
|
+
During Sentinel failover (master rotation), cached connections can raise
|
|
84
|
+
MasterNotFoundError or socket TimeoutError. On those, we refresh the client
|
|
85
|
+
via ``sentinel.master_for()`` and retry the operation once. See
|
|
86
|
+
``_execute_with_refresh``.
|
|
87
|
+
"""
|
|
78
88
|
|
|
79
89
|
_sentinel: "Sentinel | None"
|
|
80
90
|
_master_name: str | None
|
|
91
|
+
redis: "Redis"
|
|
81
92
|
|
|
82
93
|
def __init__(
|
|
83
94
|
self,
|
|
@@ -91,21 +102,62 @@ class RedisLockBackend:
|
|
|
91
102
|
from redis.sentinel import Sentinel
|
|
92
103
|
|
|
93
104
|
sentinels = self._parse_sentinel_url(url)
|
|
105
|
+
# socket_timeout=5 matches FlowHunt's production-tested value.
|
|
106
|
+
# The previous 0.5s was too tight during Sentinel failover windows.
|
|
94
107
|
self._sentinel = Sentinel(
|
|
95
108
|
sentinels,
|
|
96
|
-
socket_timeout=
|
|
109
|
+
socket_timeout=5,
|
|
97
110
|
decode_responses=True,
|
|
98
111
|
)
|
|
99
112
|
self._master_name = sentinel_master or "mymaster"
|
|
100
|
-
self.redis = self.
|
|
101
|
-
self._master_name,
|
|
102
|
-
decode_responses=True,
|
|
103
|
-
)
|
|
113
|
+
self.redis = self._build_master_client()
|
|
104
114
|
else:
|
|
105
115
|
self._sentinel = None
|
|
106
116
|
self._master_name = None
|
|
107
117
|
self.redis = redis.from_url(url, decode_responses=True)
|
|
108
118
|
|
|
119
|
+
def _build_master_client(self) -> "Redis":
|
|
120
|
+
"""Get a fresh master client from Sentinel. Sentinel mode only."""
|
|
121
|
+
assert self._sentinel is not None and self._master_name is not None
|
|
122
|
+
return self._sentinel.master_for(
|
|
123
|
+
self._master_name,
|
|
124
|
+
decode_responses=True,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _refresh_client(self) -> None:
|
|
128
|
+
"""Re-resolve master via Sentinel after a failover-related error."""
|
|
129
|
+
if self._sentinel is None:
|
|
130
|
+
return
|
|
131
|
+
self.redis = self._build_master_client()
|
|
132
|
+
logger.info(
|
|
133
|
+
"Refreshed Redis master connection for sentinel master '{}'",
|
|
134
|
+
self._master_name,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _execute_with_refresh(self, fn: Callable[..., T], *args: Any, **kwargs: Any) -> T:
|
|
138
|
+
"""Run ``fn``; on Sentinel failover errors, refresh the client and retry once.
|
|
139
|
+
|
|
140
|
+
In non-sentinel mode this is a no-op passthrough — refresh has nothing
|
|
141
|
+
to do, so any exception propagates as before.
|
|
142
|
+
"""
|
|
143
|
+
from redis.exceptions import ConnectionError as RedisConnectionError
|
|
144
|
+
from redis.exceptions import TimeoutError as RedisTimeoutError
|
|
145
|
+
from redis.sentinel import MasterNotFoundError
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
return fn(*args, **kwargs)
|
|
149
|
+
except (MasterNotFoundError, RedisConnectionError, RedisTimeoutError) as exc:
|
|
150
|
+
if self._sentinel is None:
|
|
151
|
+
raise
|
|
152
|
+
logger.warning(
|
|
153
|
+
"Redis lock op hit sentinel failover error ({}: {}). "
|
|
154
|
+
"Refreshing master and retrying once.",
|
|
155
|
+
type(exc).__name__,
|
|
156
|
+
exc,
|
|
157
|
+
)
|
|
158
|
+
self._refresh_client()
|
|
159
|
+
return fn(*args, **kwargs)
|
|
160
|
+
|
|
109
161
|
@staticmethod
|
|
110
162
|
def _parse_sentinel_url(url: str) -> list[tuple[str, int]]:
|
|
111
163
|
"""
|
|
@@ -142,15 +194,19 @@ class RedisLockBackend:
|
|
|
142
194
|
|
|
143
195
|
def lock(self, lock_key: str, task_id: str, expiry: int | None = None) -> bool:
|
|
144
196
|
"""Acquire lock atomically. Returns True if acquired."""
|
|
145
|
-
return bool(
|
|
197
|
+
return bool(
|
|
198
|
+
self._execute_with_refresh(
|
|
199
|
+
lambda: self.redis.set(lock_key, task_id, nx=True, ex=expiry)
|
|
200
|
+
)
|
|
201
|
+
)
|
|
146
202
|
|
|
147
203
|
def unlock(self, lock_key: str) -> None:
|
|
148
204
|
"""Release the lock."""
|
|
149
|
-
self.redis.delete(lock_key)
|
|
205
|
+
self._execute_with_refresh(lambda: self.redis.delete(lock_key))
|
|
150
206
|
|
|
151
207
|
def get(self, lock_key: str) -> str | None:
|
|
152
208
|
"""Get the task ID holding the lock."""
|
|
153
|
-
return self.redis.get(lock_key)
|
|
209
|
+
return self._execute_with_refresh(lambda: self.redis.get(lock_key))
|
|
154
210
|
|
|
155
211
|
|
|
156
212
|
class DuplicateTaskError(Exception):
|
|
@@ -364,6 +420,16 @@ class SingletonWorkflowTask(Task):
|
|
|
364
420
|
|
|
365
421
|
# Lock not acquired - check for existing task
|
|
366
422
|
existing_task_id = self.get_existing_task_id(lock_key)
|
|
423
|
+
|
|
424
|
+
# Self-retry: when celery's Task.retry() calls apply_async with the
|
|
425
|
+
# same task_id we already hold, it's the running task re-queueing
|
|
426
|
+
# itself. Allow the queue write through — on_retry will release the
|
|
427
|
+
# lock before the retried message is picked up. Without this branch,
|
|
428
|
+
# apply_async would silently return AsyncResult without enqueueing,
|
|
429
|
+
# and the retry would be lost.
|
|
430
|
+
if existing_task_id == task_id:
|
|
431
|
+
return super().apply_async(args, kwargs, task_id=task_id, **options)
|
|
432
|
+
|
|
367
433
|
if existing_task_id:
|
|
368
434
|
logger.debug(
|
|
369
435
|
"Singleton: duplicate task blocked",
|
|
@@ -427,13 +493,17 @@ class SingletonWorkflowTask(Task):
|
|
|
427
493
|
error=str(exc),
|
|
428
494
|
)
|
|
429
495
|
|
|
430
|
-
# Log appropriately
|
|
496
|
+
# Log appropriately. Keep str(exc) out of the message string: when
|
|
497
|
+
# kwargs are passed, Loguru calls .format() on the message, which
|
|
498
|
+
# parses any `{` from the exception (e.g. JSON in the message) as a
|
|
499
|
+
# format spec and raises KeyError.
|
|
431
500
|
if isinstance(exc, WorkerLostError):
|
|
432
501
|
logger.warning("Task interrupted due to worker loss", task_id=task_id)
|
|
433
502
|
else:
|
|
434
503
|
logger.error(
|
|
435
|
-
f"Task {self.name} failed
|
|
504
|
+
f"Task {self.name} failed",
|
|
436
505
|
task_id=task_id,
|
|
506
|
+
error=str(exc),
|
|
437
507
|
traceback=einfo.traceback if einfo else None,
|
|
438
508
|
)
|
|
439
509
|
|
|
@@ -442,8 +442,12 @@ def execute_step_task(
|
|
|
442
442
|
if self.request.retries < max_retries:
|
|
443
443
|
# Use exponential backoff for unexpected errors
|
|
444
444
|
countdown = _calculate_exponential_backoff(self.request.retries)
|
|
445
|
+
# Don't inline str(e) into the message: Loguru calls .format() on
|
|
446
|
+
# the message when kwargs are present, and any `{` from the
|
|
447
|
+
# exception (e.g. a JSON body) is parsed as a format spec and
|
|
448
|
+
# raises KeyError, which would skip the retry below.
|
|
445
449
|
logger.warning(
|
|
446
|
-
f"Step failed (unexpected): {step_name}, retrying in {countdown:.1f}s
|
|
450
|
+
f"Step failed (unexpected): {step_name}, retrying in {countdown:.1f}s...",
|
|
447
451
|
run_id=run_id,
|
|
448
452
|
step_id=step_id,
|
|
449
453
|
error=str(e),
|
|
@@ -785,6 +785,132 @@ class TestRedisLockBackendSentinel:
|
|
|
785
785
|
assert backend._sentinel is None
|
|
786
786
|
|
|
787
787
|
|
|
788
|
+
class TestRedisLockBackendAutoRefresh:
|
|
789
|
+
"""Sentinel failover recovery in RedisLockBackend.
|
|
790
|
+
|
|
791
|
+
When the Sentinel master rotates, the cached Redis client can raise
|
|
792
|
+
``MasterNotFoundError`` or ``TimeoutError``. The backend should transparently
|
|
793
|
+
refresh the client via ``sentinel.master_for()`` and retry the operation once.
|
|
794
|
+
"""
|
|
795
|
+
|
|
796
|
+
def _make_sentinel_backend(self, mock_sentinel_class, stale_client, fresh_client):
|
|
797
|
+
"""Build a sentinel-backed backend where master_for returns stale then fresh."""
|
|
798
|
+
mock_sentinel = mock_sentinel_class.return_value
|
|
799
|
+
mock_sentinel.master_for.side_effect = [stale_client, fresh_client]
|
|
800
|
+
|
|
801
|
+
return RedisLockBackend(
|
|
802
|
+
"sentinel://host1:26379/0",
|
|
803
|
+
is_sentinel=True,
|
|
804
|
+
sentinel_master="mymaster",
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
def test_lock_recovers_from_master_not_found(self):
|
|
808
|
+
"""MasterNotFoundError on lock() triggers refresh and succeeds on retry."""
|
|
809
|
+
from redis.sentinel import MasterNotFoundError
|
|
810
|
+
|
|
811
|
+
with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
|
|
812
|
+
stale = MagicMock()
|
|
813
|
+
stale.set.side_effect = MasterNotFoundError("No master found for 'mymaster'")
|
|
814
|
+
fresh = MagicMock()
|
|
815
|
+
fresh.set.return_value = True
|
|
816
|
+
|
|
817
|
+
backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
|
|
818
|
+
result = backend.lock("test_key", "task_123", expiry=3600)
|
|
819
|
+
|
|
820
|
+
assert result is True
|
|
821
|
+
stale.set.assert_called_once()
|
|
822
|
+
fresh.set.assert_called_once_with("test_key", "task_123", nx=True, ex=3600)
|
|
823
|
+
# master_for called twice: initial + refresh
|
|
824
|
+
assert mock_sentinel_class.return_value.master_for.call_count == 2
|
|
825
|
+
|
|
826
|
+
def test_lock_recovers_from_timeout_error(self):
|
|
827
|
+
"""Redis TimeoutError on lock() triggers refresh and succeeds on retry."""
|
|
828
|
+
from redis.exceptions import TimeoutError as RedisTimeoutError
|
|
829
|
+
|
|
830
|
+
with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
|
|
831
|
+
stale = MagicMock()
|
|
832
|
+
stale.set.side_effect = RedisTimeoutError("Timeout reading from socket")
|
|
833
|
+
fresh = MagicMock()
|
|
834
|
+
fresh.set.return_value = True
|
|
835
|
+
|
|
836
|
+
backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
|
|
837
|
+
result = backend.lock("test_key", "task_123", expiry=3600)
|
|
838
|
+
|
|
839
|
+
assert result is True
|
|
840
|
+
assert mock_sentinel_class.return_value.master_for.call_count == 2
|
|
841
|
+
|
|
842
|
+
def test_lock_recovers_from_connection_error(self):
|
|
843
|
+
"""Redis ConnectionError on lock() triggers refresh and succeeds on retry."""
|
|
844
|
+
from redis.exceptions import ConnectionError as RedisConnectionError
|
|
845
|
+
|
|
846
|
+
with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
|
|
847
|
+
stale = MagicMock()
|
|
848
|
+
stale.set.side_effect = RedisConnectionError("Connection reset by peer")
|
|
849
|
+
fresh = MagicMock()
|
|
850
|
+
fresh.set.return_value = True
|
|
851
|
+
|
|
852
|
+
backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
|
|
853
|
+
assert backend.lock("test_key", "task_123", expiry=3600) is True
|
|
854
|
+
assert mock_sentinel_class.return_value.master_for.call_count == 2
|
|
855
|
+
|
|
856
|
+
def test_unlock_recovers_from_master_rotation(self):
|
|
857
|
+
"""unlock() refreshes and retries once on Sentinel failover errors."""
|
|
858
|
+
from redis.sentinel import MasterNotFoundError
|
|
859
|
+
|
|
860
|
+
with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
|
|
861
|
+
stale = MagicMock()
|
|
862
|
+
stale.delete.side_effect = MasterNotFoundError("No master found")
|
|
863
|
+
fresh = MagicMock()
|
|
864
|
+
|
|
865
|
+
backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
|
|
866
|
+
backend.unlock("test_key")
|
|
867
|
+
|
|
868
|
+
fresh.delete.assert_called_once_with("test_key")
|
|
869
|
+
|
|
870
|
+
def test_get_recovers_from_master_rotation(self):
|
|
871
|
+
"""get() refreshes and retries once on Sentinel failover errors."""
|
|
872
|
+
from redis.sentinel import MasterNotFoundError
|
|
873
|
+
|
|
874
|
+
with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
|
|
875
|
+
stale = MagicMock()
|
|
876
|
+
stale.get.side_effect = MasterNotFoundError("No master found")
|
|
877
|
+
fresh = MagicMock()
|
|
878
|
+
fresh.get.return_value = "task_123"
|
|
879
|
+
|
|
880
|
+
backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
|
|
881
|
+
assert backend.get("test_key") == "task_123"
|
|
882
|
+
|
|
883
|
+
def test_second_attempt_failure_propagates(self):
|
|
884
|
+
"""If the retry also fails, the exception propagates."""
|
|
885
|
+
from redis.sentinel import MasterNotFoundError
|
|
886
|
+
|
|
887
|
+
with patch("redis.sentinel.Sentinel") as mock_sentinel_class:
|
|
888
|
+
stale = MagicMock()
|
|
889
|
+
stale.set.side_effect = MasterNotFoundError("No master found")
|
|
890
|
+
fresh = MagicMock()
|
|
891
|
+
fresh.set.side_effect = MasterNotFoundError("Still no master")
|
|
892
|
+
|
|
893
|
+
backend = self._make_sentinel_backend(mock_sentinel_class, stale, fresh)
|
|
894
|
+
with pytest.raises(MasterNotFoundError):
|
|
895
|
+
backend.lock("test_key", "task_123", expiry=3600)
|
|
896
|
+
|
|
897
|
+
def test_non_sentinel_mode_does_not_refresh(self):
|
|
898
|
+
"""Non-sentinel backend has no refresh path — exceptions propagate."""
|
|
899
|
+
from redis.sentinel import MasterNotFoundError
|
|
900
|
+
|
|
901
|
+
with patch("redis.from_url") as mock_from_url:
|
|
902
|
+
mock_redis = MagicMock()
|
|
903
|
+
mock_redis.set.side_effect = MasterNotFoundError("No master found")
|
|
904
|
+
mock_from_url.return_value = mock_redis
|
|
905
|
+
|
|
906
|
+
backend = RedisLockBackend("redis://localhost:6379/0")
|
|
907
|
+
with pytest.raises(MasterNotFoundError):
|
|
908
|
+
backend.lock("test_key", "task_123", expiry=3600)
|
|
909
|
+
|
|
910
|
+
# Only the initial resolution — no refresh attempts.
|
|
911
|
+
assert mock_redis.set.call_count == 1
|
|
912
|
+
|
|
913
|
+
|
|
788
914
|
class TestSingletonConfigSentinel:
|
|
789
915
|
"""Test SingletonConfig Sentinel properties."""
|
|
790
916
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/docker-compose.yml
RENAMED
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/pyworkflow.config.yaml
RENAMED
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/__init__.py
RENAMED
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/basic.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/hooks.py
RENAMED
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/idempotency.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/retries.py
RENAMED
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/durable/workflows/schedules.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/transient/01_basic_workflow.py
RENAMED
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/transient/02_fault_tolerance.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/celery/transient/pyworkflow.config.yaml
RENAMED
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/01_basic_workflow.py
RENAMED
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/02_file_storage.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/04_long_running.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/06_idempotency.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/08_cancellation.py
RENAMED
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/09_child_workflows.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/11_continue_as_new.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/durable/13_step_context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/examples/local/transient/01_quick_tasks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyworkflow_engine-0.3.0 → pyworkflow_engine-0.3.1}/pyworkflow/cli/utils/config_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|