pyworkflow-engine 0.1.22__tar.gz → 0.1.23__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.
Files changed (196) hide show
  1. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/PKG-INFO +1 -1
  2. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyproject.toml +1 -1
  3. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/__init__.py +1 -1
  4. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/celery/app.py +18 -0
  5. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/celery/tasks.py +61 -71
  6. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/storage/base.py +36 -0
  7. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/storage/cassandra.py +34 -0
  8. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/storage/dynamodb.py +34 -0
  9. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/storage/file.py +52 -0
  10. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/storage/memory.py +37 -0
  11. pyworkflow_engine-0.1.23/pyworkflow/storage/migrations/__init__.py +15 -0
  12. pyworkflow_engine-0.1.23/pyworkflow/storage/migrations/base.py +299 -0
  13. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/storage/mysql.py +186 -5
  14. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/storage/postgres.py +194 -6
  15. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/storage/sqlite.py +171 -5
  16. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow_engine.egg-info/SOURCES.txt +6 -1
  17. pyworkflow_engine-0.1.23/tests/integration/test_schema_migrations.py +278 -0
  18. pyworkflow_engine-0.1.23/tests/unit/storage/__init__.py +1 -0
  19. pyworkflow_engine-0.1.23/tests/unit/storage/test_migrations.py +291 -0
  20. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/CLAUDE.md +0 -0
  21. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/DISTRIBUTED.md +0 -0
  22. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/LICENSE +0 -0
  23. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/MANIFEST.in +0 -0
  24. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/README.md +0 -0
  25. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/RELEASING.md +0 -0
  26. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/concepts/cancellation.mdx +0 -0
  27. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/concepts/continue-as-new.mdx +0 -0
  28. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/concepts/events.mdx +0 -0
  29. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/concepts/fault-tolerance.mdx +0 -0
  30. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/concepts/hooks.mdx +0 -0
  31. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/concepts/limitations.mdx +0 -0
  32. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/concepts/schedules.mdx +0 -0
  33. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/concepts/sleep.mdx +0 -0
  34. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/concepts/step-context.mdx +0 -0
  35. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/concepts/steps.mdx +0 -0
  36. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/concepts/workflows.mdx +0 -0
  37. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/guides/brokers.mdx +0 -0
  38. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/guides/cli.mdx +0 -0
  39. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/guides/configuration.mdx +0 -0
  40. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/introduction.mdx +0 -0
  41. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/docs/quickstart.mdx +0 -0
  42. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/__init__.py +0 -0
  43. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/__init__.py +0 -0
  44. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/docker-compose.yml +0 -0
  45. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/pyworkflow.config.yaml +0 -0
  46. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/__init__.py +0 -0
  47. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/basic.py +0 -0
  48. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/batch_processing.py +0 -0
  49. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/cancellation.py +0 -0
  50. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
  51. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/child_workflows.py +0 -0
  52. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/continue_as_new.py +0 -0
  53. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
  54. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/hooks.py +0 -0
  55. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/idempotency.py +0 -0
  56. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/long_running.py +0 -0
  57. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/retries.py +0 -0
  58. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/schedules.py +0 -0
  59. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/durable/workflows/step_context.py +0 -0
  60. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/transient/01_basic_workflow.py +0 -0
  61. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/transient/02_fault_tolerance.py +0 -0
  62. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/transient/__init__.py +0 -0
  63. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/celery/transient/pyworkflow.config.yaml +0 -0
  64. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/__init__.py +0 -0
  65. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/01_basic_workflow.py +0 -0
  66. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/02_file_storage.py +0 -0
  67. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/03_retries.py +0 -0
  68. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/04_long_running.py +0 -0
  69. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/05_event_log.py +0 -0
  70. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/06_idempotency.py +0 -0
  71. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/07_hooks.py +0 -0
  72. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/08_cancellation.py +0 -0
  73. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/09_child_workflows.py +0 -0
  74. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/10_child_workflow_patterns.py +0 -0
  75. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/11_continue_as_new.py +0 -0
  76. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/12_schedules.py +0 -0
  77. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/13_step_context.py +0 -0
  78. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/durable/__init__.py +0 -0
  79. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/transient/01_quick_tasks.py +0 -0
  80. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/transient/02_retries.py +0 -0
  81. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/transient/03_sleep.py +0 -0
  82. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/examples/local/transient/__init__.py +0 -0
  83. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/aws/__init__.py +0 -0
  84. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/aws/context.py +0 -0
  85. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/aws/handler.py +0 -0
  86. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/aws/testing.py +0 -0
  87. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/celery/__init__.py +0 -0
  88. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/celery/loop.py +0 -0
  89. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/celery/scheduler.py +0 -0
  90. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/celery/singleton.py +0 -0
  91. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/__init__.py +0 -0
  92. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/__main__.py +0 -0
  93. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/commands/__init__.py +0 -0
  94. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/commands/hooks.py +0 -0
  95. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/commands/quickstart.py +0 -0
  96. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/commands/runs.py +0 -0
  97. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/commands/scheduler.py +0 -0
  98. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/commands/schedules.py +0 -0
  99. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/commands/setup.py +0 -0
  100. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/commands/worker.py +0 -0
  101. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/commands/workflows.py +0 -0
  102. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/output/__init__.py +0 -0
  103. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/output/formatters.py +0 -0
  104. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/output/styles.py +0 -0
  105. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/utils/__init__.py +0 -0
  106. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/utils/async_helpers.py +0 -0
  107. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/utils/config.py +0 -0
  108. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/utils/config_generator.py +0 -0
  109. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/utils/discovery.py +0 -0
  110. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/utils/docker_manager.py +0 -0
  111. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/utils/interactive.py +0 -0
  112. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/cli/utils/storage.py +0 -0
  113. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/config.py +0 -0
  114. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/context/__init__.py +0 -0
  115. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/context/aws.py +0 -0
  116. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/context/base.py +0 -0
  117. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/context/local.py +0 -0
  118. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/context/mock.py +0 -0
  119. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/context/step_context.py +0 -0
  120. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/core/__init__.py +0 -0
  121. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/core/exceptions.py +0 -0
  122. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/core/registry.py +0 -0
  123. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/core/scheduled.py +0 -0
  124. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/core/step.py +0 -0
  125. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/core/validation.py +0 -0
  126. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/core/workflow.py +0 -0
  127. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/discovery.py +0 -0
  128. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/engine/__init__.py +0 -0
  129. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/engine/events.py +0 -0
  130. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/engine/executor.py +0 -0
  131. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/engine/replay.py +0 -0
  132. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/observability/__init__.py +0 -0
  133. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/observability/logging.py +0 -0
  134. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/primitives/__init__.py +0 -0
  135. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/primitives/child_handle.py +0 -0
  136. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/primitives/child_workflow.py +0 -0
  137. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/primitives/continue_as_new.py +0 -0
  138. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/primitives/define_hook.py +0 -0
  139. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/primitives/hooks.py +0 -0
  140. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/primitives/resume_hook.py +0 -0
  141. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/primitives/schedule.py +0 -0
  142. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/primitives/shield.py +0 -0
  143. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/primitives/sleep.py +0 -0
  144. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/runtime/__init__.py +0 -0
  145. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/runtime/base.py +0 -0
  146. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/runtime/celery.py +0 -0
  147. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/runtime/factory.py +0 -0
  148. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/runtime/local.py +0 -0
  149. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/scheduler/__init__.py +0 -0
  150. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/scheduler/local.py +0 -0
  151. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/serialization/__init__.py +0 -0
  152. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/serialization/decoder.py +0 -0
  153. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/serialization/encoder.py +0 -0
  154. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/storage/__init__.py +0 -0
  155. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/storage/config.py +0 -0
  156. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/storage/schemas.py +0 -0
  157. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/utils/__init__.py +0 -0
  158. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/utils/duration.py +0 -0
  159. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/pyworkflow/utils/schedule.py +0 -0
  160. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/setup.cfg +0 -0
  161. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/integration/__init__.py +0 -0
  162. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/integration/test_cancellation.py +0 -0
  163. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/integration/test_cassandra_storage.py +0 -0
  164. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/integration/test_child_workflows.py +0 -0
  165. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/integration/test_continue_as_new.py +0 -0
  166. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/integration/test_dynamodb_storage.py +0 -0
  167. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/integration/test_fault_tolerance.py +0 -0
  168. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/integration/test_schedule_storage.py +0 -0
  169. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/integration/test_singleton.py +0 -0
  170. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/integration/test_workflow_suspended.py +0 -0
  171. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/__init__.py +0 -0
  172. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/backends/__init__.py +0 -0
  173. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/backends/test_cassandra_storage.py +0 -0
  174. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/backends/test_dynamodb_storage.py +0 -0
  175. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/backends/test_postgres_storage.py +0 -0
  176. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/backends/test_sqlite_storage.py +0 -0
  177. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/conftest.py +0 -0
  178. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_cancellation.py +0 -0
  179. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_child_workflows.py +0 -0
  180. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_cli_worker.py +0 -0
  181. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_continue_as_new.py +0 -0
  182. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_event_limits.py +0 -0
  183. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_executor.py +0 -0
  184. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_fault_tolerance.py +0 -0
  185. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_hooks.py +0 -0
  186. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_registry.py +0 -0
  187. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_replay.py +0 -0
  188. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_schedule_schemas.py +0 -0
  189. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_schedule_utils.py +0 -0
  190. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_scheduled_workflow.py +0 -0
  191. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_singleton.py +0 -0
  192. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_step.py +0 -0
  193. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_step_context.py +0 -0
  194. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_validation.py +0 -0
  195. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/tests/unit/test_workflow.py +0 -0
  196. {pyworkflow_engine-0.1.22 → pyworkflow_engine-0.1.23}/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.1.22
3
+ Version: 0.1.23
4
4
  Summary: A Python implementation of durable, event-sourced workflows inspired by Vercel Workflow
5
5
  Author: PyWorkflow Contributors
6
6
  License: MIT
@@ -7,7 +7,7 @@ packages = [{include = "pyworkflow"}]
7
7
 
8
8
  [project]
9
9
  name = "pyworkflow-engine"
10
- version = "0.1.22"
10
+ version = "0.1.23"
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"
@@ -29,7 +29,7 @@ Quick Start:
29
29
  >>> run_id = await start(my_workflow, "Alice")
30
30
  """
31
31
 
32
- __version__ = "0.1.22"
32
+ __version__ = "0.1.23"
33
33
 
34
34
  # Configuration
35
35
  from pyworkflow.config import (
@@ -151,6 +151,8 @@ def create_celery_app(
151
151
  sentinel_master_name: str | None = None,
152
152
  broker_transport_options: dict[str, Any] | None = None,
153
153
  result_backend_transport_options: dict[str, Any] | None = None,
154
+ worker_max_memory_per_child: int | None = None,
155
+ worker_max_tasks_per_child: int | None = None,
154
156
  ) -> Celery:
155
157
  """
156
158
  Create and configure a Celery application for PyWorkflow.
@@ -162,6 +164,8 @@ def create_celery_app(
162
164
  sentinel_master_name: Redis Sentinel master name. Priority: parameter > PYWORKFLOW_CELERY_SENTINEL_MASTER env var > "mymaster"
163
165
  broker_transport_options: Additional transport options for the broker (merged with defaults)
164
166
  result_backend_transport_options: Additional transport options for the result backend (merged with defaults)
167
+ worker_max_memory_per_child: Max memory per worker child process (KB). Priority: parameter > PYWORKFLOW_WORKER_MAX_MEMORY env var > None (no limit)
168
+ worker_max_tasks_per_child: Max tasks per worker child before recycling. Priority: parameter > PYWORKFLOW_WORKER_MAX_TASKS env var > None (no limit)
165
169
 
166
170
  Returns:
167
171
  Configured Celery application
@@ -170,6 +174,8 @@ def create_celery_app(
170
174
  PYWORKFLOW_CELERY_BROKER: Celery broker URL (used if broker_url param not provided)
171
175
  PYWORKFLOW_CELERY_RESULT_BACKEND: Result backend URL (used if result_backend param not provided)
172
176
  PYWORKFLOW_CELERY_SENTINEL_MASTER: Sentinel master name (used if sentinel_master_name param not provided)
177
+ PYWORKFLOW_WORKER_MAX_MEMORY: Max memory per worker child (KB) (used if worker_max_memory_per_child param not provided)
178
+ PYWORKFLOW_WORKER_MAX_TASKS: Max tasks per worker child (used if worker_max_tasks_per_child param not provided)
173
179
 
174
180
  Examples:
175
181
  # Default configuration (uses env vars if set, otherwise localhost Redis)
@@ -202,6 +208,14 @@ def create_celery_app(
202
208
  or "redis://localhost:6379/1"
203
209
  )
204
210
 
211
+ # Worker memory limits (KB) - prevents memory leaks from accumulating
212
+ # Priority: parameter > env var > None (no limit by default)
213
+ max_memory_env = os.getenv("PYWORKFLOW_WORKER_MAX_MEMORY")
214
+ max_memory = worker_max_memory_per_child or (int(max_memory_env) if max_memory_env else None)
215
+
216
+ max_tasks_env = os.getenv("PYWORKFLOW_WORKER_MAX_TASKS")
217
+ max_tasks = worker_max_tasks_per_child or (int(max_tasks_env) if max_tasks_env else None)
218
+
205
219
  # Detect broker and backend types
206
220
  is_sentinel_broker = is_sentinel_url(broker_url)
207
221
  is_sentinel_backend = is_sentinel_url(result_backend)
@@ -310,6 +324,10 @@ def create_celery_app(
310
324
  # Logging
311
325
  worker_log_format="[%(asctime)s: %(levelname)s/%(processName)s] %(message)s",
312
326
  worker_task_log_format="[%(asctime)s: %(levelname)s/%(processName)s] [%(task_name)s(%(task_id)s)] %(message)s",
327
+ # Worker memory management - prevents memory leaks from accumulating
328
+ # When set, workers are recycled after exceeding these limits
329
+ worker_max_memory_per_child=max_memory, # KB, None = no limit
330
+ worker_max_tasks_per_child=max_tasks, # None = no limit
313
331
  )
314
332
 
315
333
  # Configure singleton locking for Redis or Sentinel brokers
@@ -11,7 +11,6 @@ These tasks enable:
11
11
 
12
12
  import asyncio
13
13
  import random
14
- import traceback
15
14
  import uuid
16
15
  from collections.abc import Callable
17
16
  from datetime import UTC, datetime
@@ -172,10 +171,9 @@ def execute_step_task(
172
171
  raise FatalError(f"Step '{step_name}' not found in registry")
173
172
 
174
173
  # Ignore processing step if already completed (idempotency)
175
- events = run_async(storage.get_events(run_id))
176
- already_completed = any(
177
- evt.type == EventType.STEP_COMPLETED and evt.data.get("step_id") == step_id
178
- for evt in events
174
+ # Use has_event() for efficient EXISTS check instead of loading all events
175
+ already_completed = run_async(
176
+ storage.has_event(run_id, EventType.STEP_COMPLETED.value, step_id=step_id)
179
177
  )
180
178
  if already_completed:
181
179
  logger.warning(
@@ -398,10 +396,9 @@ async def _record_step_completion_and_resume(
398
396
  await storage.connect()
399
397
 
400
398
  # Idempotency check: skip if step already completed
401
- events = await storage.get_events(run_id)
402
- already_completed = any(
403
- evt.type == EventType.STEP_COMPLETED and evt.data.get("step_id") == step_id
404
- for evt in events
399
+ # Use has_event() for efficient EXISTS check instead of loading all events
400
+ already_completed = await storage.has_event(
401
+ run_id, EventType.STEP_COMPLETED.value, step_id=step_id
405
402
  )
406
403
  if already_completed:
407
404
  logger.info(
@@ -414,26 +411,23 @@ async def _record_step_completion_and_resume(
414
411
 
415
412
  # Wait for WORKFLOW_SUSPENDED event before recording STEP_COMPLETED
416
413
  # This prevents race conditions where both events get the same sequence number
414
+ # Use has_event() for memory-efficient polling instead of loading all events
417
415
  max_wait_attempts = 50 # 50 * 10ms = 500ms max wait
418
416
  wait_interval = 0.01 # 10ms between checks
419
417
 
420
- for attempt in range(max_wait_attempts):
421
- has_suspended = any(
422
- evt.type == EventType.WORKFLOW_SUSPENDED
423
- and evt.data.get("step_id") == step_id
424
- for evt in events
418
+ for _attempt in range(max_wait_attempts):
419
+ has_suspended = await storage.has_event(
420
+ run_id, EventType.WORKFLOW_SUSPENDED.value, step_id=step_id
425
421
  )
426
422
  if has_suspended:
427
423
  break
428
424
 
429
- # Wait and refresh events
425
+ # Wait and check again
430
426
  await asyncio.sleep(wait_interval)
431
- events = await storage.get_events(run_id)
432
427
 
433
428
  # Also check if step was already completed by another task during wait
434
- already_completed = any(
435
- evt.type == EventType.STEP_COMPLETED and evt.data.get("step_id") == step_id
436
- for evt in events
429
+ already_completed = await storage.has_event(
430
+ run_id, EventType.STEP_COMPLETED.value, step_id=step_id
437
431
  )
438
432
  if already_completed:
439
433
  logger.info(
@@ -506,17 +500,18 @@ async def _record_step_failure_and_resume(
506
500
  await storage.connect()
507
501
 
508
502
  # Idempotency check: skip if step already completed or terminally failed
509
- events = await storage.get_events(run_id)
510
- already_handled = any(
511
- (evt.type == EventType.STEP_COMPLETED and evt.data.get("step_id") == step_id)
512
- or (
513
- evt.type == EventType.STEP_FAILED
514
- and evt.data.get("step_id") == step_id
515
- and not evt.data.get("is_retryable", True)
516
- )
517
- for evt in events
503
+ # Use has_event() for efficient EXISTS check instead of loading all events
504
+ # Note: For STEP_FAILED with is_retryable check, we use has_event for STEP_COMPLETED
505
+ # and separately check STEP_FAILED (non-retryable failures are rare, so this is still efficient)
506
+ already_completed = await storage.has_event(
507
+ run_id, EventType.STEP_COMPLETED.value, step_id=step_id
508
+ )
509
+ # For terminal failures, we check separately (is_retryable=false in data)
510
+ # This is less common, so checking completion first is the fast path
511
+ already_failed_terminal = await storage.has_event(
512
+ run_id, EventType.STEP_FAILED.value, step_id=step_id, is_retryable="False"
518
513
  )
519
- if already_handled:
514
+ if already_completed or already_failed_terminal:
520
515
  logger.info(
521
516
  "Step already completed/failed by another task, skipping",
522
517
  run_id=run_id,
@@ -527,33 +522,28 @@ async def _record_step_failure_and_resume(
527
522
 
528
523
  # Wait for WORKFLOW_SUSPENDED event before recording STEP_FAILED
529
524
  # This prevents race conditions where both events get the same sequence number
525
+ # Use has_event() for memory-efficient polling instead of loading all events
530
526
  max_wait_attempts = 50 # 50 * 10ms = 500ms max wait
531
527
  wait_interval = 0.01 # 10ms between checks
532
528
 
533
- for attempt in range(max_wait_attempts):
534
- has_suspended = any(
535
- evt.type == EventType.WORKFLOW_SUSPENDED
536
- and evt.data.get("step_id") == step_id
537
- for evt in events
529
+ for _attempt in range(max_wait_attempts):
530
+ has_suspended = await storage.has_event(
531
+ run_id, EventType.WORKFLOW_SUSPENDED.value, step_id=step_id
538
532
  )
539
533
  if has_suspended:
540
534
  break
541
535
 
542
- # Wait and refresh events
536
+ # Wait and check again
543
537
  await asyncio.sleep(wait_interval)
544
- events = await storage.get_events(run_id)
545
538
 
546
539
  # Also check if step was already handled by another task during wait
547
- already_handled = any(
548
- (evt.type == EventType.STEP_COMPLETED and evt.data.get("step_id") == step_id)
549
- or (
550
- evt.type == EventType.STEP_FAILED
551
- and evt.data.get("step_id") == step_id
552
- and not evt.data.get("is_retryable", True)
553
- )
554
- for evt in events
540
+ already_completed = await storage.has_event(
541
+ run_id, EventType.STEP_COMPLETED.value, step_id=step_id
542
+ )
543
+ already_failed_terminal = await storage.has_event(
544
+ run_id, EventType.STEP_FAILED.value, step_id=step_id, is_retryable="False"
555
545
  )
556
- if already_handled:
546
+ if already_completed or already_failed_terminal:
557
547
  logger.info(
558
548
  "Step already completed/failed by another task during wait, skipping",
559
549
  run_id=run_id,
@@ -891,13 +881,13 @@ async def _execute_child_workflow_on_worker(
891
881
 
892
882
  # For step dispatch suspensions, check if step already completed/failed
893
883
  if step_id and e.reason.startswith("step_dispatch:"):
894
- events = await storage.get_events(child_run_id)
895
- step_finished = any(
896
- evt.type in (EventType.STEP_COMPLETED, EventType.STEP_FAILED)
897
- and evt.data.get("step_id") == step_id
898
- for evt in events
884
+ step_completed = await storage.has_event(
885
+ child_run_id, EventType.STEP_COMPLETED.value, step_id=step_id
899
886
  )
900
- if step_finished:
887
+ step_failed = await storage.has_event(
888
+ child_run_id, EventType.STEP_FAILED.value, step_id=step_id
889
+ )
890
+ if step_completed or step_failed:
901
891
  logger.info(
902
892
  "Child step finished before suspension completed, scheduling resume",
903
893
  child_run_id=child_run_id,
@@ -1144,8 +1134,8 @@ async def _handle_workflow_recovery(
1144
1134
  return False
1145
1135
 
1146
1136
  # Get last event sequence
1147
- events = await storage.get_events(run.run_id)
1148
- last_event_sequence = max((e.sequence or 0 for e in events), default=0) if events else None
1137
+ latest_event = await storage.get_latest_event(run.run_id)
1138
+ last_event_sequence = latest_event.sequence if latest_event else None
1149
1139
 
1150
1140
  # Record interruption event
1151
1141
  interrupted_event = create_workflow_interrupted_event(
@@ -1287,13 +1277,13 @@ async def _recover_workflow_on_worker(
1287
1277
 
1288
1278
  # For step dispatch suspensions, check if step already completed/failed
1289
1279
  if step_id and e.reason.startswith("step_dispatch:"):
1290
- events = await storage.get_events(run_id)
1291
- step_finished = any(
1292
- evt.type in (EventType.STEP_COMPLETED, EventType.STEP_FAILED)
1293
- and evt.data.get("step_id") == step_id
1294
- for evt in events
1280
+ step_completed = await storage.has_event(
1281
+ run_id, EventType.STEP_COMPLETED.value, step_id=step_id
1295
1282
  )
1296
- if step_finished:
1283
+ step_failed = await storage.has_event(
1284
+ run_id, EventType.STEP_FAILED.value, step_id=step_id
1285
+ )
1286
+ if step_completed or step_failed:
1297
1287
  logger.info(
1298
1288
  "Step finished before recovery suspension completed, scheduling resume",
1299
1289
  run_id=run_id,
@@ -1679,13 +1669,13 @@ async def _start_workflow_on_worker(
1679
1669
  # For step dispatch suspensions, check if step already completed/failed (race condition)
1680
1670
  # If so, schedule resume immediately
1681
1671
  if step_id and e.reason.startswith("step_dispatch:"):
1682
- events = await storage.get_events(run_id)
1683
- step_finished = any(
1684
- evt.type in (EventType.STEP_COMPLETED, EventType.STEP_FAILED)
1685
- and evt.data.get("step_id") == step_id
1686
- for evt in events
1672
+ step_completed = await storage.has_event(
1673
+ run_id, EventType.STEP_COMPLETED.value, step_id=step_id
1687
1674
  )
1688
- if step_finished:
1675
+ step_failed = await storage.has_event(
1676
+ run_id, EventType.STEP_FAILED.value, step_id=step_id
1677
+ )
1678
+ if step_completed or step_failed:
1689
1679
  logger.info(
1690
1680
  "Step finished before suspension completed, scheduling resume",
1691
1681
  run_id=run_id,
@@ -2269,13 +2259,13 @@ async def _resume_workflow_on_worker(
2269
2259
 
2270
2260
  # For step dispatch suspensions, check if step already completed/failed
2271
2261
  if step_id and e.reason.startswith("step_dispatch:"):
2272
- events = await storage.get_events(run_id)
2273
- step_finished = any(
2274
- evt.type in (EventType.STEP_COMPLETED, EventType.STEP_FAILED)
2275
- and evt.data.get("step_id") == step_id
2276
- for evt in events
2262
+ step_completed = await storage.has_event(
2263
+ run_id, EventType.STEP_COMPLETED.value, step_id=step_id
2264
+ )
2265
+ step_failed = await storage.has_event(
2266
+ run_id, EventType.STEP_FAILED.value, step_id=step_id
2277
2267
  )
2278
- if step_finished:
2268
+ if step_completed or step_failed:
2279
2269
  logger.info(
2280
2270
  "Step finished before resume suspension completed, scheduling resume",
2281
2271
  run_id=run_id,
@@ -203,6 +203,42 @@ class StorageBackend(ABC):
203
203
  """
204
204
  pass
205
205
 
206
+ @abstractmethod
207
+ async def has_event(
208
+ self,
209
+ run_id: str,
210
+ event_type: str,
211
+ **filters: str,
212
+ ) -> bool:
213
+ """
214
+ Check if an event exists matching the criteria.
215
+
216
+ This is a memory-efficient alternative to get_events() when you only
217
+ need to check for existence. Uses SQL EXISTS queries in SQL backends
218
+ for O(1) memory usage instead of loading all events.
219
+
220
+ Args:
221
+ run_id: Workflow run identifier
222
+ event_type: Event type to check for (e.g., "step_completed")
223
+ **filters: Additional filters to match against event data fields.
224
+ For example, step_id="abc" will check data->>'step_id' = 'abc'
225
+
226
+ Returns:
227
+ True if a matching event exists, False otherwise
228
+
229
+ Example:
230
+ # Check if step completed
231
+ exists = await storage.has_event(
232
+ run_id, "step_completed", step_id="step_123"
233
+ )
234
+
235
+ # Check if workflow suspended for a specific step
236
+ exists = await storage.has_event(
237
+ run_id, "workflow_suspended", step_id="step_123"
238
+ )
239
+ """
240
+ pass
241
+
206
242
  @abstractmethod
207
243
  async def get_latest_event(
208
244
  self,
@@ -896,6 +896,40 @@ class CassandraStorageBackend(StorageBackend):
896
896
 
897
897
  return None
898
898
 
899
+ async def has_event(
900
+ self,
901
+ run_id: str,
902
+ event_type: str,
903
+ **filters: str,
904
+ ) -> bool:
905
+ """
906
+ Check if an event exists matching the criteria.
907
+
908
+ Loads events of the specified type and filters in Python for efficiency.
909
+
910
+ Args:
911
+ run_id: Workflow run identifier
912
+ event_type: Event type to check for
913
+ **filters: Additional filters for event data fields
914
+
915
+ Returns:
916
+ True if a matching event exists, False otherwise
917
+ """
918
+ # Load only events of the specific type
919
+ events = await self.get_events(run_id, event_types=[event_type])
920
+
921
+ # Filter in Python
922
+ for event in events:
923
+ match = True
924
+ for key, value in filters.items():
925
+ if str(event.data.get(key)) != str(value):
926
+ match = False
927
+ break
928
+ if match:
929
+ return True
930
+
931
+ return False
932
+
899
933
  # Step Operations
900
934
 
901
935
  async def create_step(self, step: StepExecution) -> None:
@@ -588,6 +588,40 @@ class DynamoDBStorageBackend(StorageBackend):
588
588
 
589
589
  return self._item_to_event(self._item_to_dict(items[0]))
590
590
 
591
+ async def has_event(
592
+ self,
593
+ run_id: str,
594
+ event_type: str,
595
+ **filters: str,
596
+ ) -> bool:
597
+ """
598
+ Check if an event exists matching the criteria.
599
+
600
+ Loads events of the specified type and filters in Python for efficiency.
601
+
602
+ Args:
603
+ run_id: Workflow run identifier
604
+ event_type: Event type to check for
605
+ **filters: Additional filters for event data fields
606
+
607
+ Returns:
608
+ True if a matching event exists, False otherwise
609
+ """
610
+ # Load only events of the specific type
611
+ events = await self.get_events(run_id, event_types=[event_type])
612
+
613
+ # Filter in Python
614
+ for event in events:
615
+ match = True
616
+ for key, value in filters.items():
617
+ if str(event.data.get(key)) != str(value):
618
+ match = False
619
+ break
620
+ if match:
621
+ return True
622
+
623
+ return False
624
+
591
625
  # Step Operations
592
626
 
593
627
  async def create_step(self, step: StepExecution) -> None:
@@ -373,6 +373,58 @@ class FileStorageBackend(StorageBackend):
373
373
  events = await self.get_events(run_id, event_types=[event_type] if event_type else None)
374
374
  return events[-1] if events else None
375
375
 
376
+ async def has_event(
377
+ self,
378
+ run_id: str,
379
+ event_type: str,
380
+ **filters: str,
381
+ ) -> bool:
382
+ """
383
+ Check if an event exists using file-based iteration with early termination.
384
+
385
+ Reads the events file line by line and returns as soon as a match is found,
386
+ avoiding loading the entire event log into memory.
387
+
388
+ Args:
389
+ run_id: Workflow run identifier
390
+ event_type: Event type to check for
391
+ **filters: Additional filters for event data fields
392
+
393
+ Returns:
394
+ True if a matching event exists, False otherwise
395
+ """
396
+ events_file = self.events_dir / f"{run_id}.jsonl"
397
+
398
+ if not events_file.exists():
399
+ return False
400
+
401
+ def _check() -> bool:
402
+ with events_file.open("r") as f:
403
+ for line in f:
404
+ if not line.strip():
405
+ continue
406
+
407
+ data = json.loads(line)
408
+
409
+ # Check event type
410
+ if data["type"] != event_type:
411
+ continue
412
+
413
+ # Check all data filters
414
+ match = True
415
+ event_data = data.get("data", {})
416
+ for key, value in filters.items():
417
+ if str(event_data.get(key)) != str(value):
418
+ match = False
419
+ break
420
+
421
+ if match:
422
+ return True
423
+
424
+ return False
425
+
426
+ return await asyncio.to_thread(_check)
427
+
376
428
  # Step Operations
377
429
 
378
430
  async def create_step(self, step: StepExecution) -> None:
@@ -250,6 +250,43 @@ class InMemoryStorageBackend(StorageBackend):
250
250
  # Return event with highest sequence
251
251
  return max(events, key=lambda e: e.sequence or 0)
252
252
 
253
+ async def has_event(
254
+ self,
255
+ run_id: str,
256
+ event_type: str,
257
+ **filters: str,
258
+ ) -> bool:
259
+ """
260
+ Check if an event exists by loading events of the specific type and filtering.
261
+
262
+ This approach:
263
+ 1. Uses the event_types filter to load only events of the target type
264
+ 2. Filters in Python on the loaded data
265
+ 3. Significantly reduces memory vs loading ALL events
266
+
267
+ Args:
268
+ run_id: Workflow run identifier
269
+ event_type: Event type to check for
270
+ **filters: Additional filters for event data fields
271
+
272
+ Returns:
273
+ True if a matching event exists, False otherwise
274
+ """
275
+ # Load only events of the specific type
276
+ events = await self.get_events(run_id, event_types=[event_type])
277
+
278
+ # Filter in Python
279
+ for event in events:
280
+ match = True
281
+ for key, value in filters.items():
282
+ if str(event.data.get(key)) != str(value):
283
+ match = False
284
+ break
285
+ if match:
286
+ return True
287
+
288
+ return False
289
+
253
290
  # Step Operations
254
291
 
255
292
  async def create_step(self, step: StepExecution) -> None:
@@ -0,0 +1,15 @@
1
+ """
2
+ Database schema migration framework for PyWorkflow storage backends.
3
+
4
+ This module provides a migration framework that allows storage backends to
5
+ evolve their schema over time while maintaining backward compatibility with
6
+ existing databases.
7
+ """
8
+
9
+ from pyworkflow.storage.migrations.base import (
10
+ Migration,
11
+ MigrationRegistry,
12
+ MigrationRunner,
13
+ )
14
+
15
+ __all__ = ["Migration", "MigrationRegistry", "MigrationRunner"]