queueflow 0.4.0__tar.gz → 0.5.0__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 (148) hide show
  1. {queueflow-0.4.0/queueflow.egg-info → queueflow-0.5.0}/PKG-INFO +84 -14
  2. {queueflow-0.4.0 → queueflow-0.5.0}/README.md +83 -13
  3. {queueflow-0.4.0 → queueflow-0.5.0}/pyproject.toml +1 -1
  4. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/__init__.py +7 -2
  5. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/api/cron_api.py +1 -1
  6. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/api/dlq_api.py +1 -1
  7. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/api/health_api.py +1 -1
  8. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/api/jobs_api.py +1 -1
  9. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/api/system_api.py +1 -1
  10. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/api/worker_api.py +1 -1
  11. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/api/workflows_api.py +540 -1
  12. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/api_client.py +2 -2
  13. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/configuration.py +3 -3
  14. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/exceptions.py +1 -1
  15. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/facade.py +203 -7
  16. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/__init__.py +6 -1
  17. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/backoff_strategy.py +1 -1
  18. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/cancel_jobs_request.py +1 -1
  19. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/cancel_jobs_response.py +1 -1
  20. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/complete_job_request.py +1 -1
  21. queueflow-0.5.0/queueflow/models/condition_op.py +44 -0
  22. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/create_batch_jobs_request.py +1 -1
  23. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/create_batch_jobs_response.py +1 -1
  24. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/create_cron_request.py +1 -1
  25. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/create_cron_response.py +1 -1
  26. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/create_job_request.py +1 -1
  27. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/create_job_response.py +1 -1
  28. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/create_workflow_request.py +15 -5
  29. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/create_workflow_response.py +1 -1
  30. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/cron_schedule.py +1 -1
  31. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/dead_letter.py +1 -1
  32. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/error_body.py +1 -1
  33. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/fail_job_request.py +1 -1
  34. queueflow-0.4.0/queueflow/models/workflow_step_state.py → queueflow-0.5.0/queueflow/models/fan_out.py +21 -16
  35. queueflow-0.5.0/queueflow/models/fan_out_progress.py +92 -0
  36. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/health_status.py +1 -1
  37. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/heartbeat_request.py +1 -1
  38. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/heartbeat_response.py +1 -1
  39. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/job.py +1 -1
  40. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/job_config.py +1 -1
  41. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/job_config_request.py +1 -1
  42. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/job_status.py +1 -1
  43. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/lease_jobs_request.py +1 -1
  44. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/lease_jobs_response.py +1 -1
  45. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/leased_job.py +1 -1
  46. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/list_crons_response.py +1 -1
  47. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/list_dead_letters_response.py +1 -1
  48. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/list_jobs_response.py +1 -1
  49. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/list_workflows_response.py +1 -1
  50. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/on_failure.py +1 -1
  51. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/on_success.py +1 -1
  52. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/queue_control_response.py +1 -1
  53. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/queue_stats.py +1 -1
  54. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/queues_response.py +1 -1
  55. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/ready_status.py +1 -1
  56. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/replay_dead_letter_response.py +1 -1
  57. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/replay_dead_letters_request.py +1 -1
  58. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/replay_dead_letters_response.py +1 -1
  59. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/replayed_dead_letter.py +1 -1
  60. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/stats_snapshot.py +1 -1
  61. queueflow-0.5.0/queueflow/models/step_condition.py +147 -0
  62. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/step_status.py +1 -1
  63. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/tasks_response.py +1 -1
  64. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/who_am_i_response.py +1 -1
  65. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/workflow.py +41 -3
  66. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/workflow_diagram_response.py +1 -1
  67. queueflow-0.5.0/queueflow/models/workflow_event.py +107 -0
  68. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/workflow_status.py +1 -1
  69. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/workflow_step.py +48 -6
  70. queueflow-0.5.0/queueflow/models/workflow_step_state.py +137 -0
  71. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/models/workflow_step_states_response.py +1 -1
  72. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/rest.py +1 -1
  73. {queueflow-0.4.0 → queueflow-0.5.0/queueflow.egg-info}/PKG-INFO +84 -14
  74. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow.egg-info/SOURCES.txt +10 -0
  75. {queueflow-0.4.0 → queueflow-0.5.0}/setup.py +2 -2
  76. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_backoff_strategy.py +1 -1
  77. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_cancel_jobs_request.py +1 -1
  78. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_cancel_jobs_response.py +1 -1
  79. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_complete_job_request.py +1 -1
  80. queueflow-0.5.0/test/test_condition_op.py +33 -0
  81. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_create_batch_jobs_request.py +1 -1
  82. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_create_batch_jobs_response.py +1 -1
  83. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_create_cron_request.py +1 -1
  84. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_create_cron_response.py +1 -1
  85. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_create_job_request.py +1 -1
  86. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_create_job_response.py +1 -1
  87. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_create_workflow_request.py +75 -4
  88. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_create_workflow_response.py +1 -1
  89. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_cron_api.py +1 -1
  90. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_cron_schedule.py +1 -1
  91. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_dead_letter.py +1 -1
  92. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_dlq_api.py +1 -1
  93. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_error_body.py +1 -1
  94. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_facade.py +236 -0
  95. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_fail_job_request.py +1 -1
  96. queueflow-0.5.0/test/test_fan_out.py +54 -0
  97. queueflow-0.5.0/test/test_fan_out_progress.py +56 -0
  98. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_health_api.py +1 -1
  99. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_health_status.py +1 -1
  100. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_heartbeat_request.py +1 -1
  101. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_heartbeat_response.py +1 -1
  102. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_job.py +1 -1
  103. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_job_config.py +1 -1
  104. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_job_config_request.py +1 -1
  105. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_job_status.py +1 -1
  106. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_jobs_api.py +1 -1
  107. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_lease_jobs_request.py +1 -1
  108. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_lease_jobs_response.py +1 -1
  109. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_leased_job.py +1 -1
  110. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_list_crons_response.py +1 -1
  111. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_list_dead_letters_response.py +1 -1
  112. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_list_jobs_response.py +1 -1
  113. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_list_workflows_response.py +81 -5
  114. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_live.py +221 -0
  115. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_on_failure.py +1 -1
  116. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_on_success.py +1 -1
  117. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_queue_control_response.py +1 -1
  118. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_queue_stats.py +1 -1
  119. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_queues_response.py +1 -1
  120. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_ready_status.py +1 -1
  121. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_replay_dead_letter_response.py +1 -1
  122. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_replay_dead_letters_request.py +1 -1
  123. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_replay_dead_letters_response.py +1 -1
  124. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_replayed_dead_letter.py +1 -1
  125. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_stats_snapshot.py +1 -1
  126. queueflow-0.5.0/test/test_step_condition.py +97 -0
  127. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_step_status.py +1 -1
  128. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_system_api.py +1 -1
  129. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_tasks_response.py +1 -1
  130. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_who_am_i_response.py +1 -1
  131. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_worker_api.py +1 -1
  132. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_workflow.py +80 -4
  133. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_workflow_diagram_response.py +1 -1
  134. queueflow-0.5.0/test/test_workflow_event.py +83 -0
  135. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_workflow_status.py +1 -1
  136. queueflow-0.5.0/test/test_workflow_step.py +151 -0
  137. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_workflow_step_state.py +9 -1
  138. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_workflow_step_states_response.py +17 -1
  139. {queueflow-0.4.0 → queueflow-0.5.0}/test/test_workflows_api.py +14 -1
  140. queueflow-0.4.0/test/test_workflow_step.py +0 -73
  141. {queueflow-0.4.0 → queueflow-0.5.0}/LICENSE +0 -0
  142. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/api/__init__.py +0 -0
  143. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/api_response.py +0 -0
  144. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow/py.typed +0 -0
  145. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow.egg-info/dependency_links.txt +0 -0
  146. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow.egg-info/requires.txt +0 -0
  147. {queueflow-0.4.0 → queueflow-0.5.0}/queueflow.egg-info/top_level.txt +0 -0
  148. {queueflow-0.4.0 → queueflow-0.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: queueflow
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Python client for QueueFlow, a PostgreSQL-native distributed job queue and workflow engine.
5
5
  Home-page: https://github.com/elision-labs/queueflow-sdk-python
6
6
  Author: QueueFlow
@@ -137,26 +137,92 @@ while qf.cancel_jobs(queue="orders", task_name="send_email", limit=500) == 500:
137
137
  | `qf.create_workflow(builder_or_request)` | Create a workflow from a `wf()` builder or a raw `CreateWorkflowRequest`. |
138
138
  | `qf.wait_for_workflow(workflow_id, timeout=60.0, interval=0.5)` | Poll until a terminal workflow state. |
139
139
  | `qf.workflows.get_workflow(id)` / `list_workflows(...)` / `cancel_workflow(id)` | Fetch / list / cancel. |
140
- | `qf.workflows.get_workflow_step_states(id)` | Runtime status of every step, in declaration order. |
140
+ | `qf.workflow_steps(workflow_id)` | Live progress of every step, in declaration order: `name`, `status`, `job_id` (the first child, for a fan-out), `error` (`condition not met` for a step skipped by its `when`), `started_at`, `completed_at`, `child_workflow_id` (sub-workflow steps) and `fan_out` (`total`, `completed`, `failed`). |
141
+ | `qf.retry_workflow(workflow_id)` | Re-run a `failed`, `partially_failed` or `cancelled` workflow from where it stopped and return its refreshed record (server 0.6.0). A running or completed workflow is a 409. |
142
+ | `qf.workflows.get_workflow_step_states(id)` | The raw step-states call behind `qf.workflow_steps`. |
141
143
  | `qf.workflows.get_workflow_diagram(id)` | Mermaid (`graph TD`) diagram of the DAG. |
142
144
 
145
+ The `Workflow` record carries `status`, the accumulated `context` (step results keyed by step name),
146
+ the step definitions and, from server 0.6.0, `error_message`, `timeout_secs` and `deadline_at`,
147
+ `retry_count`, and `parent_workflow_id` / `parent_step_name` on a child spawned by a sub-workflow
148
+ step.
149
+
150
+ `retry_workflow` sends every step that did not complete (failed, cancelled, or skipped by a failure
151
+ policy) back to `pending`; completed steps and condition-skipped steps keep their results and do not
152
+ run again. The workflow becomes `running`, its `error_message` clears, `retry_count` increments and
153
+ the deadline, if any, is re-armed from the original `timeout_secs`. Fix the cause first: a retry
154
+ with the same inputs fails the same way.
155
+
143
156
  ### Workflow builder
144
157
 
145
158
  ```python
146
159
  from queueflow.facade import wf
147
160
 
161
+ fulfilment = (
162
+ wf("fulfilment")
163
+ .step("pick", "pick_items", queue="warehouse")
164
+ .step("pack", "pack_parcel", after=["pick"], queue="warehouse")
165
+ .timeout_secs(1800)
166
+ )
167
+
148
168
  dag = (
149
169
  wf("order_123")
150
- .step("validate", "validate_order")
151
- .step("pay", "process_payment", after=["validate"])
152
- .step("ship", "create_shipment", after=["pay"], on_failure="continue")
170
+ .step("validate", "validate_order", payload={"order_id": 123})
171
+ .step("charge", "charge_payment", after=["validate"])
172
+ .step("fraud", "fraud_check", after=["validate"], on_failure="halt")
173
+ .step("review", "queue_for_review", after=["fraud"],
174
+ when={"path": "fraud.flags", "op": "eq", "value": {"manual": True}})
175
+ .step("resize", "resize_image", after=["validate"], queue="images", on_failure="continue",
176
+ fan_out={"path": "validate.images", "item_key": "image"})
177
+ .sub_workflow("fulfil", fulfilment, after=["charge", "fraud"], payload={"warehouse": "eu-1"})
178
+ .step("confirm", "send_confirmation", after=["fulfil"], on_failure="skip")
153
179
  .context(source="web")
180
+ .metadata(app="shop")
181
+ .timeout_secs(3600)
154
182
  )
155
- # .build() runs locally first: duplicate names, dangling deps, and cycles raise
156
- # WorkflowValidationError before any network round-trip.
183
+ # .build() runs locally first: duplicate names, dangling deps, cycles, and malformed
184
+ # when / fan_out shapes raise WorkflowValidationError before any network round-trip.
157
185
  ```
158
186
 
159
- `step()` also accepts `payload=`, `on_success=`, and `config=` (a `models.JobConfig` override).
187
+ | Builder method | Description |
188
+ | --- | --- |
189
+ | `step(name, task, after=None, payload=None, on_failure=None, on_success=None, config=None, queue=None, when=None, fan_out=None)` | A task step. `config` is a `models.JobConfig` override; `on_failure` is `halt` (default), `skip` or `continue`. |
190
+ | `sub_workflow(name, child, after=None, payload=None, on_failure=None, on_success=None, when=None)` | A step that runs `child` (a `wf()` builder, built and validated here, or a `CreateWorkflowRequest`) as an inline child workflow instead of a task. |
191
+ | `context(**kwargs)` / `metadata(**kwargs)` | Seed the shared context / store bookkeeping JSON on the workflow. Both accumulate across calls. |
192
+ | `timeout_secs(secs)` | Whole-workflow deadline. When it passes, every unfinished step is cancelled and the workflow ends `failed` with `error_message` `workflow timed out after <secs>s`. |
193
+ | `build()` | Validate locally and return the `CreateWorkflowRequest`; `qf.create_workflow` calls it for you. |
194
+
195
+ `queue`, `when`, `fan_out`, `sub_workflow` and `timeout_secs` need server 0.6.0 or later.
196
+
197
+ **Queues.** `queue=` puts the step's job(s) on that queue (the server's default queue when
198
+ omitted), so a step can be routed to the workers that know its task.
199
+
200
+ **Conditions.** `when=` gates a step on the workflow context at the moment it becomes ready. It
201
+ takes a dict in the wire shape or a `models.StepCondition`. A leaf names a dotted `path` (step
202
+ results live under their step name, so `validate.ok` is the `ok` key of `validate`'s result) and
203
+ compares the value there with `value` using `op`: `eq` (default with a value), `ne`, `gt`, `gte`,
204
+ `lt`, `lte`, `exists` (default without a value), `contains`, `in`. `{"all": [...]}`,
205
+ `{"any": [...]}` and `{"not": {...}}` combine leaves. A false condition marks the step `skipped`
206
+ with the error `condition not met`; unlike a step skipped by a failure policy, its dependents still
207
+ run, simply without a result for it in `_context`. With this release a leaf's `value` must be a JSON
208
+ object (see [Known limitations](#known-limitations)); `exists` needs no value.
209
+
210
+ **Fan-out.** `fan_out=` (`{"path": ..., "item_key": "item", "max_items": 10000}`, or a
211
+ `models.FanOut`) runs the task once per element of the array at `path`, all children created in one
212
+ atomic claim and run concurrently across the queue's workers, each with its own retries. A child's
213
+ payload is the step payload plus the element under `item_key` and its position under `_index`; its
214
+ metadata carries `fan_out_index` and `fan_out_total`. The step completes with the result
215
+ `{"results": [r0, r1, ...]}` in element order (`_context.<step>.results` downstream); a failed child
216
+ follows the step's `on_failure`. The step state's `fan_out` reports `total`, `completed` and
217
+ `failed`, and its `job_id` is the first child.
218
+
219
+ **Sub-workflows.** `sub_workflow()` runs the child as a workflow of its own, in the same tenant,
220
+ with `parent_workflow_id` and `parent_step_name` set; the parent's step state carries its
221
+ `child_workflow_id`. The child's context is its declared `context`, then the step's `payload`, then
222
+ the parent's whole context under `_parent`. A completed child completes the step with the child's
223
+ final context as the step result; a failed, partially failed or cancelled child fails the step under
224
+ its `on_failure`. Cancelling or timing out the parent cancels a running child. Children nest up to
225
+ 8 levels and carry their own step `queue`s.
160
226
 
161
227
  ### Worker
162
228
 
@@ -333,11 +399,12 @@ except ApiException as err:
333
399
  print(err.status, err.body)
334
400
  ```
335
401
 
336
- ## Known limitation: `stream_job_events`
402
+ ## Known limitations
337
403
 
338
- `JobsApi.stream_job_events` cannot stream: it buffers the whole SSE response until the server
339
- closes it (terminal status or the 15-minute cap). Use `wait_for_job` (polling) instead, or the
340
- `stream_job_events_without_preload_content` variant with a hand-rolled SSE parser.
404
+ **SSE streams buffer.** `JobsApi.stream_job_events` and `WorkflowsApi.stream_workflow_events`
405
+ cannot stream: the generated client buffers the whole SSE response until the server closes it
406
+ (terminal status or the 15-minute cap). Use `wait_for_job` / `wait_for_workflow` (polling) instead,
407
+ or the `*_without_preload_content` variants with a hand-rolled SSE parser.
341
408
 
342
409
  ## Conformance tests
343
410
 
@@ -346,8 +413,11 @@ closes it (terminal status or the 15-minute cap). Use `wait_for_job` (polling) i
346
413
  and waits on an `echo` job, checks idempotent re-creation, runs a two-step workflow, exercises the
347
414
  cron and dead-letter endpoints, reads stats, runs `run_worker` against a dedicated queue, and
348
415
  exercises `whoami`, bulk cancel, queue pause/resume and bulk dead-letter replay, each scoped to a
349
- queue unique to the run. The tests are marked `live` and skip themselves unless `QUEUEFLOW_URL` is
350
- set, so plain `pytest` and CI stay offline.
416
+ queue unique to the run. Against a 0.6.0 server it also runs its own worker on a run-unique queue
417
+ to cover a condition-skipped step whose dependents still run, a fan-out with ordered results and
418
+ progress counts, a sub-workflow child linked to its parent, and a retry after a non-retryable
419
+ failure. The tests are marked `live` and skip themselves unless `QUEUEFLOW_URL` is set, so plain
420
+ `pytest` and CI stay offline.
351
421
 
352
422
  ```bash
353
423
  QUEUEFLOW_URL=http://localhost:8000 \
@@ -96,26 +96,92 @@ while qf.cancel_jobs(queue="orders", task_name="send_email", limit=500) == 500:
96
96
  | `qf.create_workflow(builder_or_request)` | Create a workflow from a `wf()` builder or a raw `CreateWorkflowRequest`. |
97
97
  | `qf.wait_for_workflow(workflow_id, timeout=60.0, interval=0.5)` | Poll until a terminal workflow state. |
98
98
  | `qf.workflows.get_workflow(id)` / `list_workflows(...)` / `cancel_workflow(id)` | Fetch / list / cancel. |
99
- | `qf.workflows.get_workflow_step_states(id)` | Runtime status of every step, in declaration order. |
99
+ | `qf.workflow_steps(workflow_id)` | Live progress of every step, in declaration order: `name`, `status`, `job_id` (the first child, for a fan-out), `error` (`condition not met` for a step skipped by its `when`), `started_at`, `completed_at`, `child_workflow_id` (sub-workflow steps) and `fan_out` (`total`, `completed`, `failed`). |
100
+ | `qf.retry_workflow(workflow_id)` | Re-run a `failed`, `partially_failed` or `cancelled` workflow from where it stopped and return its refreshed record (server 0.6.0). A running or completed workflow is a 409. |
101
+ | `qf.workflows.get_workflow_step_states(id)` | The raw step-states call behind `qf.workflow_steps`. |
100
102
  | `qf.workflows.get_workflow_diagram(id)` | Mermaid (`graph TD`) diagram of the DAG. |
101
103
 
104
+ The `Workflow` record carries `status`, the accumulated `context` (step results keyed by step name),
105
+ the step definitions and, from server 0.6.0, `error_message`, `timeout_secs` and `deadline_at`,
106
+ `retry_count`, and `parent_workflow_id` / `parent_step_name` on a child spawned by a sub-workflow
107
+ step.
108
+
109
+ `retry_workflow` sends every step that did not complete (failed, cancelled, or skipped by a failure
110
+ policy) back to `pending`; completed steps and condition-skipped steps keep their results and do not
111
+ run again. The workflow becomes `running`, its `error_message` clears, `retry_count` increments and
112
+ the deadline, if any, is re-armed from the original `timeout_secs`. Fix the cause first: a retry
113
+ with the same inputs fails the same way.
114
+
102
115
  ### Workflow builder
103
116
 
104
117
  ```python
105
118
  from queueflow.facade import wf
106
119
 
120
+ fulfilment = (
121
+ wf("fulfilment")
122
+ .step("pick", "pick_items", queue="warehouse")
123
+ .step("pack", "pack_parcel", after=["pick"], queue="warehouse")
124
+ .timeout_secs(1800)
125
+ )
126
+
107
127
  dag = (
108
128
  wf("order_123")
109
- .step("validate", "validate_order")
110
- .step("pay", "process_payment", after=["validate"])
111
- .step("ship", "create_shipment", after=["pay"], on_failure="continue")
129
+ .step("validate", "validate_order", payload={"order_id": 123})
130
+ .step("charge", "charge_payment", after=["validate"])
131
+ .step("fraud", "fraud_check", after=["validate"], on_failure="halt")
132
+ .step("review", "queue_for_review", after=["fraud"],
133
+ when={"path": "fraud.flags", "op": "eq", "value": {"manual": True}})
134
+ .step("resize", "resize_image", after=["validate"], queue="images", on_failure="continue",
135
+ fan_out={"path": "validate.images", "item_key": "image"})
136
+ .sub_workflow("fulfil", fulfilment, after=["charge", "fraud"], payload={"warehouse": "eu-1"})
137
+ .step("confirm", "send_confirmation", after=["fulfil"], on_failure="skip")
112
138
  .context(source="web")
139
+ .metadata(app="shop")
140
+ .timeout_secs(3600)
113
141
  )
114
- # .build() runs locally first: duplicate names, dangling deps, and cycles raise
115
- # WorkflowValidationError before any network round-trip.
142
+ # .build() runs locally first: duplicate names, dangling deps, cycles, and malformed
143
+ # when / fan_out shapes raise WorkflowValidationError before any network round-trip.
116
144
  ```
117
145
 
118
- `step()` also accepts `payload=`, `on_success=`, and `config=` (a `models.JobConfig` override).
146
+ | Builder method | Description |
147
+ | --- | --- |
148
+ | `step(name, task, after=None, payload=None, on_failure=None, on_success=None, config=None, queue=None, when=None, fan_out=None)` | A task step. `config` is a `models.JobConfig` override; `on_failure` is `halt` (default), `skip` or `continue`. |
149
+ | `sub_workflow(name, child, after=None, payload=None, on_failure=None, on_success=None, when=None)` | A step that runs `child` (a `wf()` builder, built and validated here, or a `CreateWorkflowRequest`) as an inline child workflow instead of a task. |
150
+ | `context(**kwargs)` / `metadata(**kwargs)` | Seed the shared context / store bookkeeping JSON on the workflow. Both accumulate across calls. |
151
+ | `timeout_secs(secs)` | Whole-workflow deadline. When it passes, every unfinished step is cancelled and the workflow ends `failed` with `error_message` `workflow timed out after <secs>s`. |
152
+ | `build()` | Validate locally and return the `CreateWorkflowRequest`; `qf.create_workflow` calls it for you. |
153
+
154
+ `queue`, `when`, `fan_out`, `sub_workflow` and `timeout_secs` need server 0.6.0 or later.
155
+
156
+ **Queues.** `queue=` puts the step's job(s) on that queue (the server's default queue when
157
+ omitted), so a step can be routed to the workers that know its task.
158
+
159
+ **Conditions.** `when=` gates a step on the workflow context at the moment it becomes ready. It
160
+ takes a dict in the wire shape or a `models.StepCondition`. A leaf names a dotted `path` (step
161
+ results live under their step name, so `validate.ok` is the `ok` key of `validate`'s result) and
162
+ compares the value there with `value` using `op`: `eq` (default with a value), `ne`, `gt`, `gte`,
163
+ `lt`, `lte`, `exists` (default without a value), `contains`, `in`. `{"all": [...]}`,
164
+ `{"any": [...]}` and `{"not": {...}}` combine leaves. A false condition marks the step `skipped`
165
+ with the error `condition not met`; unlike a step skipped by a failure policy, its dependents still
166
+ run, simply without a result for it in `_context`. With this release a leaf's `value` must be a JSON
167
+ object (see [Known limitations](#known-limitations)); `exists` needs no value.
168
+
169
+ **Fan-out.** `fan_out=` (`{"path": ..., "item_key": "item", "max_items": 10000}`, or a
170
+ `models.FanOut`) runs the task once per element of the array at `path`, all children created in one
171
+ atomic claim and run concurrently across the queue's workers, each with its own retries. A child's
172
+ payload is the step payload plus the element under `item_key` and its position under `_index`; its
173
+ metadata carries `fan_out_index` and `fan_out_total`. The step completes with the result
174
+ `{"results": [r0, r1, ...]}` in element order (`_context.<step>.results` downstream); a failed child
175
+ follows the step's `on_failure`. The step state's `fan_out` reports `total`, `completed` and
176
+ `failed`, and its `job_id` is the first child.
177
+
178
+ **Sub-workflows.** `sub_workflow()` runs the child as a workflow of its own, in the same tenant,
179
+ with `parent_workflow_id` and `parent_step_name` set; the parent's step state carries its
180
+ `child_workflow_id`. The child's context is its declared `context`, then the step's `payload`, then
181
+ the parent's whole context under `_parent`. A completed child completes the step with the child's
182
+ final context as the step result; a failed, partially failed or cancelled child fails the step under
183
+ its `on_failure`. Cancelling or timing out the parent cancels a running child. Children nest up to
184
+ 8 levels and carry their own step `queue`s.
119
185
 
120
186
  ### Worker
121
187
 
@@ -292,11 +358,12 @@ except ApiException as err:
292
358
  print(err.status, err.body)
293
359
  ```
294
360
 
295
- ## Known limitation: `stream_job_events`
361
+ ## Known limitations
296
362
 
297
- `JobsApi.stream_job_events` cannot stream: it buffers the whole SSE response until the server
298
- closes it (terminal status or the 15-minute cap). Use `wait_for_job` (polling) instead, or the
299
- `stream_job_events_without_preload_content` variant with a hand-rolled SSE parser.
363
+ **SSE streams buffer.** `JobsApi.stream_job_events` and `WorkflowsApi.stream_workflow_events`
364
+ cannot stream: the generated client buffers the whole SSE response until the server closes it
365
+ (terminal status or the 15-minute cap). Use `wait_for_job` / `wait_for_workflow` (polling) instead,
366
+ or the `*_without_preload_content` variants with a hand-rolled SSE parser.
300
367
 
301
368
  ## Conformance tests
302
369
 
@@ -305,8 +372,11 @@ closes it (terminal status or the 15-minute cap). Use `wait_for_job` (polling) i
305
372
  and waits on an `echo` job, checks idempotent re-creation, runs a two-step workflow, exercises the
306
373
  cron and dead-letter endpoints, reads stats, runs `run_worker` against a dedicated queue, and
307
374
  exercises `whoami`, bulk cancel, queue pause/resume and bulk dead-letter replay, each scoped to a
308
- queue unique to the run. The tests are marked `live` and skip themselves unless `QUEUEFLOW_URL` is
309
- set, so plain `pytest` and CI stay offline.
375
+ queue unique to the run. Against a 0.6.0 server it also runs its own worker on a run-unique queue
376
+ to cover a condition-skipped step whose dependents still run, a fan-out with ordered results and
377
+ progress counts, a sub-workflow child linked to its parent, and a retry after a non-retryable
378
+ failure. The tests are marked `live` and skip themselves unless `QUEUEFLOW_URL` is set, so plain
379
+ `pytest` and CI stay offline.
310
380
 
311
381
  ```bash
312
382
  QUEUEFLOW_URL=http://localhost:8000 \
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "queueflow"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  description = "Python client for QueueFlow, a PostgreSQL-native distributed job queue and workflow engine."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -7,14 +7,14 @@
7
7
 
8
8
  REST API for QueueFlow, a PostgreSQL-native distributed job queue and workflow engine.
9
9
 
10
- The version of the OpenAPI document: 0.5.0
10
+ The version of the OpenAPI document: 0.6.0
11
11
  Generated by OpenAPI Generator (https://openapi-generator.tech)
12
12
 
13
13
  Do not edit the class manually.
14
14
  """ # noqa: E501
15
15
 
16
16
 
17
- __version__ = "0.4.0"
17
+ __version__ = "0.5.0"
18
18
 
19
19
  # import apis into sdk package
20
20
  from queueflow.api.cron_api import CronApi
@@ -41,6 +41,7 @@ from queueflow.models.backoff_strategy import BackoffStrategy
41
41
  from queueflow.models.cancel_jobs_request import CancelJobsRequest
42
42
  from queueflow.models.cancel_jobs_response import CancelJobsResponse
43
43
  from queueflow.models.complete_job_request import CompleteJobRequest
44
+ from queueflow.models.condition_op import ConditionOp
44
45
  from queueflow.models.create_batch_jobs_request import CreateBatchJobsRequest
45
46
  from queueflow.models.create_batch_jobs_response import CreateBatchJobsResponse
46
47
  from queueflow.models.create_cron_request import CreateCronRequest
@@ -53,6 +54,8 @@ from queueflow.models.cron_schedule import CronSchedule
53
54
  from queueflow.models.dead_letter import DeadLetter
54
55
  from queueflow.models.error_body import ErrorBody
55
56
  from queueflow.models.fail_job_request import FailJobRequest
57
+ from queueflow.models.fan_out import FanOut
58
+ from queueflow.models.fan_out_progress import FanOutProgress
56
59
  from queueflow.models.health_status import HealthStatus
57
60
  from queueflow.models.heartbeat_request import HeartbeatRequest
58
61
  from queueflow.models.heartbeat_response import HeartbeatResponse
@@ -78,11 +81,13 @@ from queueflow.models.replay_dead_letters_request import ReplayDeadLettersReques
78
81
  from queueflow.models.replay_dead_letters_response import ReplayDeadLettersResponse
79
82
  from queueflow.models.replayed_dead_letter import ReplayedDeadLetter
80
83
  from queueflow.models.stats_snapshot import StatsSnapshot
84
+ from queueflow.models.step_condition import StepCondition
81
85
  from queueflow.models.step_status import StepStatus
82
86
  from queueflow.models.tasks_response import TasksResponse
83
87
  from queueflow.models.who_am_i_response import WhoAmIResponse
84
88
  from queueflow.models.workflow import Workflow
85
89
  from queueflow.models.workflow_diagram_response import WorkflowDiagramResponse
90
+ from queueflow.models.workflow_event import WorkflowEvent
86
91
  from queueflow.models.workflow_status import WorkflowStatus
87
92
  from queueflow.models.workflow_step import WorkflowStep
88
93
  from queueflow.models.workflow_step_state import WorkflowStepState
@@ -5,7 +5,7 @@
5
5
 
6
6
  REST API for QueueFlow, a PostgreSQL-native distributed job queue and workflow engine.
7
7
 
8
- The version of the OpenAPI document: 0.5.0
8
+ The version of the OpenAPI document: 0.6.0
9
9
  Generated by OpenAPI Generator (https://openapi-generator.tech)
10
10
 
11
11
  Do not edit the class manually.
@@ -5,7 +5,7 @@
5
5
 
6
6
  REST API for QueueFlow, a PostgreSQL-native distributed job queue and workflow engine.
7
7
 
8
- The version of the OpenAPI document: 0.5.0
8
+ The version of the OpenAPI document: 0.6.0
9
9
  Generated by OpenAPI Generator (https://openapi-generator.tech)
10
10
 
11
11
  Do not edit the class manually.
@@ -5,7 +5,7 @@
5
5
 
6
6
  REST API for QueueFlow, a PostgreSQL-native distributed job queue and workflow engine.
7
7
 
8
- The version of the OpenAPI document: 0.5.0
8
+ The version of the OpenAPI document: 0.6.0
9
9
  Generated by OpenAPI Generator (https://openapi-generator.tech)
10
10
 
11
11
  Do not edit the class manually.
@@ -5,7 +5,7 @@
5
5
 
6
6
  REST API for QueueFlow, a PostgreSQL-native distributed job queue and workflow engine.
7
7
 
8
- The version of the OpenAPI document: 0.5.0
8
+ The version of the OpenAPI document: 0.6.0
9
9
  Generated by OpenAPI Generator (https://openapi-generator.tech)
10
10
 
11
11
  Do not edit the class manually.
@@ -5,7 +5,7 @@
5
5
 
6
6
  REST API for QueueFlow, a PostgreSQL-native distributed job queue and workflow engine.
7
7
 
8
- The version of the OpenAPI document: 0.5.0
8
+ The version of the OpenAPI document: 0.6.0
9
9
  Generated by OpenAPI Generator (https://openapi-generator.tech)
10
10
 
11
11
  Do not edit the class manually.
@@ -5,7 +5,7 @@
5
5
 
6
6
  REST API for QueueFlow, a PostgreSQL-native distributed job queue and workflow engine.
7
7
 
8
- The version of the OpenAPI document: 0.5.0
8
+ The version of the OpenAPI document: 0.6.0
9
9
  Generated by OpenAPI Generator (https://openapi-generator.tech)
10
10
 
11
11
  Do not edit the class manually.