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.
Files changed (238) hide show
  1. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/PKG-INFO +8 -8
  2. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyproject.toml +8 -8
  3. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/__init__.py +1 -1
  4. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/interactive.py +18 -24
  5. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/citus.py +8 -0
  6. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/mysql.py +53 -5
  7. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/postgres.py +61 -5
  8. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/sqlite.py +61 -5
  9. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow_engine.egg-info/SOURCES.txt +1 -0
  10. pyworkflow_engine-0.3.4/tests/integration/test_list_runs_pagination.py +94 -0
  11. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/test_postgres_storage.py +25 -0
  12. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/test_sqlite_storage.py +28 -22
  13. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/CLAUDE.md +0 -0
  14. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/DISTRIBUTED.md +0 -0
  15. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/LICENSE +0 -0
  16. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/MANIFEST.in +0 -0
  17. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/README.md +0 -0
  18. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/RELEASING.md +0 -0
  19. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/architecture.md +0 -0
  20. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/cancellation.mdx +0 -0
  21. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/continue-as-new.mdx +0 -0
  22. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/events.mdx +0 -0
  23. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/fault-tolerance.mdx +0 -0
  24. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/hooks.mdx +0 -0
  25. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/limitations.mdx +0 -0
  26. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/schedules.mdx +0 -0
  27. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/sleep.mdx +0 -0
  28. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/step-context.mdx +0 -0
  29. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/steps.mdx +0 -0
  30. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/stream-steps.mdx +0 -0
  31. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/stream-workflows.mdx +0 -0
  32. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/concepts/workflows.mdx +0 -0
  33. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/conventions.md +0 -0
  34. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/guides/brokers.mdx +0 -0
  35. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/guides/cli.mdx +0 -0
  36. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/guides/configuration.mdx +0 -0
  37. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/harness-gaps.md +0 -0
  38. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/introduction.mdx +0 -0
  39. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/layers.md +0 -0
  40. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/docs/quickstart.mdx +0 -0
  41. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/__init__.py +0 -0
  42. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/__init__.py +0 -0
  43. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/docker-compose.yml +0 -0
  44. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/pyworkflow.config.yaml +0 -0
  45. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/__init__.py +0 -0
  46. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/basic.py +0 -0
  47. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/batch_processing.py +0 -0
  48. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/cancellation.py +0 -0
  49. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/child_workflow_from_step.py +0 -0
  50. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/child_workflow_patterns.py +0 -0
  51. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/child_workflows.py +0 -0
  52. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/continue_as_new.py +0 -0
  53. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/fault_tolerance.py +0 -0
  54. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/hooks.py +0 -0
  55. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/idempotency.py +0 -0
  56. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/long_running.py +0 -0
  57. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/retries.py +0 -0
  58. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/schedules.py +0 -0
  59. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/sleep_in_step.py +0 -0
  60. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/durable/workflows/step_context.py +0 -0
  61. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/transient/01_basic_workflow.py +0 -0
  62. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/transient/02_fault_tolerance.py +0 -0
  63. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/transient/__init__.py +0 -0
  64. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/celery/transient/pyworkflow.config.yaml +0 -0
  65. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/__init__.py +0 -0
  66. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/01_basic_workflow.py +0 -0
  67. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/02_file_storage.py +0 -0
  68. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/03_retries.py +0 -0
  69. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/04_long_running.py +0 -0
  70. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/05_event_log.py +0 -0
  71. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/06_idempotency.py +0 -0
  72. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/07_hooks.py +0 -0
  73. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/08_cancellation.py +0 -0
  74. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/09_child_workflows.py +0 -0
  75. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/10_child_workflow_patterns.py +0 -0
  76. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/11_continue_as_new.py +0 -0
  77. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/12_schedules.py +0 -0
  78. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/13_step_context.py +0 -0
  79. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/14_child_workflow_from_step.py +0 -0
  80. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/durable/__init__.py +0 -0
  81. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/transient/01_quick_tasks.py +0 -0
  82. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/transient/02_retries.py +0 -0
  83. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/transient/03_sleep.py +0 -0
  84. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/examples/local/transient/__init__.py +0 -0
  85. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/aws/__init__.py +0 -0
  86. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/aws/context.py +0 -0
  87. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/aws/handler.py +0 -0
  88. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/aws/testing.py +0 -0
  89. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/__init__.py +0 -0
  90. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/app.py +0 -0
  91. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/loop.py +0 -0
  92. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/scheduler.py +0 -0
  93. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/singleton.py +0 -0
  94. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/celery/tasks.py +0 -0
  95. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/__init__.py +0 -0
  96. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/__main__.py +0 -0
  97. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/__init__.py +0 -0
  98. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/hooks.py +0 -0
  99. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/quickstart.py +0 -0
  100. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/runs.py +0 -0
  101. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/scheduler.py +0 -0
  102. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/schedules.py +0 -0
  103. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/setup.py +0 -0
  104. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/worker.py +0 -0
  105. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/commands/workflows.py +0 -0
  106. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/output/__init__.py +0 -0
  107. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/output/formatters.py +0 -0
  108. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/output/styles.py +0 -0
  109. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/__init__.py +0 -0
  110. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/async_helpers.py +0 -0
  111. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/config.py +0 -0
  112. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/config_generator.py +0 -0
  113. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/discovery.py +0 -0
  114. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/docker_manager.py +0 -0
  115. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/cli/utils/storage.py +0 -0
  116. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/config.py +0 -0
  117. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/__init__.py +0 -0
  118. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/aws.py +0 -0
  119. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/base.py +0 -0
  120. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/local.py +0 -0
  121. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/mock.py +0 -0
  122. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/context/step_context.py +0 -0
  123. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/__init__.py +0 -0
  124. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/exceptions.py +0 -0
  125. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/registry.py +0 -0
  126. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/scheduled.py +0 -0
  127. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/step.py +0 -0
  128. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/validation.py +0 -0
  129. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/core/workflow.py +0 -0
  130. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/discovery.py +0 -0
  131. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/engine/__init__.py +0 -0
  132. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/engine/events.py +0 -0
  133. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/engine/executor.py +0 -0
  134. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/engine/replay.py +0 -0
  135. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/observability/__init__.py +0 -0
  136. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/observability/logging.py +0 -0
  137. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/__init__.py +0 -0
  138. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/child_handle.py +0 -0
  139. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/child_workflow.py +0 -0
  140. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/continue_as_new.py +0 -0
  141. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/define_hook.py +0 -0
  142. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/hooks.py +0 -0
  143. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/resume_hook.py +0 -0
  144. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/schedule.py +0 -0
  145. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/shield.py +0 -0
  146. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/sleep.py +0 -0
  147. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/step_checkpoint.py +0 -0
  148. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/primitives/step_hook.py +0 -0
  149. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/runtime/__init__.py +0 -0
  150. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/runtime/base.py +0 -0
  151. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/runtime/celery.py +0 -0
  152. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/runtime/factory.py +0 -0
  153. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/runtime/local.py +0 -0
  154. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/scheduler/__init__.py +0 -0
  155. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/scheduler/local.py +0 -0
  156. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/serialization/__init__.py +0 -0
  157. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/serialization/decoder.py +0 -0
  158. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/serialization/encoder.py +0 -0
  159. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/__init__.py +0 -0
  160. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/base.py +0 -0
  161. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/cassandra.py +0 -0
  162. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/config.py +0 -0
  163. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/dynamodb.py +0 -0
  164. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/file.py +0 -0
  165. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/memory.py +0 -0
  166. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/migrations/__init__.py +0 -0
  167. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/migrations/base.py +0 -0
  168. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/storage/schemas.py +0 -0
  169. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/__init__.py +0 -0
  170. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/checkpoint.py +0 -0
  171. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/consumer.py +0 -0
  172. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/context.py +0 -0
  173. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/decorator.py +0 -0
  174. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/dispatcher.py +0 -0
  175. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/emit.py +0 -0
  176. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/registry.py +0 -0
  177. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/runtime.py +0 -0
  178. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/signal.py +0 -0
  179. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/streams/step_context.py +0 -0
  180. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/tracing/__init__.py +0 -0
  181. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/tracing/base.py +0 -0
  182. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/tracing/factory.py +0 -0
  183. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/tracing/langfuse.py +0 -0
  184. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/tracing/types.py +0 -0
  185. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/utils/__init__.py +0 -0
  186. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/utils/duration.py +0 -0
  187. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/utils/helpers.py +0 -0
  188. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/pyworkflow/utils/schedule.py +0 -0
  189. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/setup.cfg +0 -0
  190. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/__init__.py +0 -0
  191. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_cancellation.py +0 -0
  192. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_cassandra_storage.py +0 -0
  193. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_child_workflows.py +0 -0
  194. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_continue_as_new.py +0 -0
  195. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_dynamodb_storage.py +0 -0
  196. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_fault_tolerance.py +0 -0
  197. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_schedule_storage.py +0 -0
  198. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_schema_migrations.py +0 -0
  199. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_singleton.py +0 -0
  200. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_stream_e2e.py +0 -0
  201. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/integration/test_workflow_suspended.py +0 -0
  202. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/__init__.py +0 -0
  203. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/__init__.py +0 -0
  204. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/test_cassandra_storage.py +0 -0
  205. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/test_citus_storage.py +0 -0
  206. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/backends/test_dynamodb_storage.py +0 -0
  207. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/conftest.py +0 -0
  208. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/storage/__init__.py +0 -0
  209. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/storage/test_migrations.py +0 -0
  210. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_cancellation.py +0 -0
  211. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_child_workflows.py +0 -0
  212. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_cli_worker.py +0 -0
  213. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_continue_as_new.py +0 -0
  214. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_emit.py +0 -0
  215. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_event_limits.py +0 -0
  216. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_executor.py +0 -0
  217. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_fault_tolerance.py +0 -0
  218. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_force_local.py +0 -0
  219. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_hooks.py +0 -0
  220. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_parent_run_id.py +0 -0
  221. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_primitives_from_steps.py +0 -0
  222. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_registry.py +0 -0
  223. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_replay.py +0 -0
  224. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_retention.py +0 -0
  225. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_schedule_schemas.py +0 -0
  226. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_schedule_utils.py +0 -0
  227. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_scheduled_workflow.py +0 -0
  228. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_signal.py +0 -0
  229. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_singleton.py +0 -0
  230. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_step.py +0 -0
  231. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_step_checkpoint.py +0 -0
  232. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_step_context.py +0 -0
  233. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_step_hook.py +0 -0
  234. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_stream_storage.py +0 -0
  235. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_stream_workflow.py +0 -0
  236. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_validation.py +0 -0
  237. {pyworkflow_engine-0.3.2 → pyworkflow_engine-0.3.4}/tests/unit/test_workflow.py +0 -0
  238. {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.2
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.3.3
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.12.0
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.6.0; extra == "dynamodb"
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.0; extra == "dev"
64
- Requires-Dist: black>=23.0.0; extra == "dev"
65
- Requires-Dist: ruff>=0.15.12; extra == "dev"
66
- Requires-Dist: mypy>=1.7.0; extra == "dev"
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.2"
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.3.3",
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.12.0",
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.6.0",
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.0", # AWS mocking for DynamoDB tests
99
+ "moto[dynamodb]>=5.2.2", # AWS mocking for DynamoDB tests
100
100
 
101
101
  # Code quality
102
- "black>=23.0.0",
103
- "ruff>=0.15.12",
104
- "mypy>=1.7.0",
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
@@ -29,7 +29,7 @@ Quick Start:
29
29
  >>> run_id = await start(my_workflow, "Alice")
30
30
  """
31
31
 
32
- __version__ = "0.3.2"
32
+ __version__ = "0.3.4"
33
33
 
34
34
  # Configuration
35
35
  from pyworkflow.config import (
@@ -76,15 +76,12 @@ def select(
76
76
  try:
77
77
  # Convert string list to Choice objects
78
78
  choice_objects: list[Choice] = []
79
- if choices and isinstance(choices[0], str):
80
- for c in choices:
81
- if isinstance(c, str):
82
- choice_objects.append(Choice(value=c, name=c))
83
- else:
84
- # Dict format: {"name": "...", "value": "..."}
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
- if choices and isinstance(choices[0], str):
226
- for c in choices:
227
- if isinstance(c, str):
228
- choice_objects.append(Choice(value=c, name=c, enabled=(c in (default or []))))
229
- else:
230
- # Dict format: {"name": "...", "value": "..."}
231
- for c in choices:
232
- if isinstance(c, dict):
233
- value = c.get("value", c["name"])
234
- choice_objects.append(
235
- Choice(
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
- "created_at < (SELECT created_at FROM workflow_runs WHERE run_id = %s)"
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 * FROM workflow_runs
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._row_to_workflow_run(row) for row in rows]
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"created_at < (SELECT created_at FROM workflow_runs WHERE run_id = ${param_idx})"
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 * FROM workflow_runs
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._row_to_workflow_run(row) for row in rows]
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
- "created_at < (SELECT created_at FROM workflow_runs WHERE run_id = ?)"
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 * FROM workflow_runs
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._row_to_workflow_run(row) for row in rows]
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"}
@@ -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: