queueflow 0.3.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.
- {queueflow-0.3.0/queueflow.egg-info → queueflow-0.5.0}/PKG-INFO +138 -21
- {queueflow-0.3.0 → queueflow-0.5.0}/README.md +137 -20
- {queueflow-0.3.0 → queueflow-0.5.0}/pyproject.toml +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/__init__.py +14 -2
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/api/cron_api.py +35 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/api/dlq_api.py +314 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/api/health_api.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/api/jobs_api.py +317 -1
- queueflow-0.5.0/queueflow/api/system_api.py +1551 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/api/worker_api.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/api/workflows_api.py +574 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/api_client.py +2 -2
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/configuration.py +3 -3
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/exceptions.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/facade.py +297 -10
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/__init__.py +13 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/backoff_strategy.py +1 -1
- queueflow-0.5.0/queueflow/models/cancel_jobs_request.py +128 -0
- queueflow-0.5.0/queueflow/models/cancel_jobs_response.py +88 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/complete_job_request.py +1 -1
- queueflow-0.5.0/queueflow/models/condition_op.py +44 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/create_batch_jobs_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/create_batch_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/create_cron_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/create_cron_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/create_job_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/create_job_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/create_workflow_request.py +15 -5
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/create_workflow_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/cron_schedule.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/dead_letter.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/error_body.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/fail_job_request.py +1 -1
- queueflow-0.5.0/queueflow/models/fan_out.py +102 -0
- queueflow-0.5.0/queueflow/models/fan_out_progress.py +92 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/health_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/heartbeat_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/heartbeat_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/job.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/job_config.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/job_config_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/job_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/lease_jobs_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/lease_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/leased_job.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/list_crons_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/list_dead_letters_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/list_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/list_workflows_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/on_failure.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/on_success.py +1 -1
- queueflow-0.5.0/queueflow/models/queue_control_response.py +94 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/queue_stats.py +5 -3
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/queues_response.py +2 -2
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/ready_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/replay_dead_letter_response.py +1 -1
- queueflow-0.5.0/queueflow/models/replay_dead_letters_request.py +121 -0
- queueflow-0.5.0/queueflow/models/replay_dead_letters_response.py +98 -0
- queueflow-0.3.0/queueflow/models/workflow_step_state.py → queueflow-0.5.0/queueflow/models/replayed_dead_letter.py +18 -14
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/stats_snapshot.py +1 -1
- queueflow-0.5.0/queueflow/models/step_condition.py +147 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/step_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/tasks_response.py +1 -1
- queueflow-0.5.0/queueflow/models/who_am_i_response.py +89 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/workflow.py +41 -3
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/workflow_diagram_response.py +1 -1
- queueflow-0.5.0/queueflow/models/workflow_event.py +107 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/workflow_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/workflow_step.py +48 -6
- queueflow-0.5.0/queueflow/models/workflow_step_state.py +137 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/models/workflow_step_states_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/rest.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0/queueflow.egg-info}/PKG-INFO +138 -21
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow.egg-info/SOURCES.txt +24 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/setup.py +2 -2
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_backoff_strategy.py +1 -1
- queueflow-0.5.0/test/test_cancel_jobs_request.py +56 -0
- queueflow-0.5.0/test/test_cancel_jobs_response.py +52 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_complete_job_request.py +1 -1
- queueflow-0.5.0/test/test_condition_op.py +33 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_create_batch_jobs_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_create_batch_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_create_cron_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_create_cron_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_create_job_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_create_job_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_create_workflow_request.py +75 -4
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_create_workflow_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_cron_api.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_cron_schedule.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_dead_letter.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_dlq_api.py +7 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_error_body.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_facade.py +394 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_fail_job_request.py +1 -1
- queueflow-0.5.0/test/test_fan_out.py +54 -0
- queueflow-0.5.0/test/test_fan_out_progress.py +56 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_health_api.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_health_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_heartbeat_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_heartbeat_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_job.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_job_config.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_job_config_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_job_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_jobs_api.py +7 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_lease_jobs_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_lease_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_leased_job.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_list_crons_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_list_dead_letters_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_list_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_list_workflows_response.py +81 -5
- queueflow-0.5.0/test/test_live.py +593 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_on_failure.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_on_success.py +1 -1
- queueflow-0.5.0/test/test_queue_control_response.py +58 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_queue_stats.py +2 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_queues_response.py +3 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_ready_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_replay_dead_letter_response.py +1 -1
- queueflow-0.5.0/test/test_replay_dead_letters_request.py +55 -0
- queueflow-0.5.0/test/test_replay_dead_letters_response.py +64 -0
- queueflow-0.5.0/test/test_replayed_dead_letter.py +54 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_stats_snapshot.py +1 -1
- queueflow-0.5.0/test/test_step_condition.py +97 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_step_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_system_api.py +19 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_tasks_response.py +1 -1
- queueflow-0.5.0/test/test_who_am_i_response.py +54 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_worker_api.py +1 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_workflow.py +80 -4
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_workflow_diagram_response.py +1 -1
- queueflow-0.5.0/test/test_workflow_event.py +83 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_workflow_status.py +1 -1
- queueflow-0.5.0/test/test_workflow_step.py +151 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_workflow_step_state.py +9 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_workflow_step_states_response.py +17 -1
- {queueflow-0.3.0 → queueflow-0.5.0}/test/test_workflows_api.py +14 -1
- queueflow-0.3.0/queueflow/api/system_api.py +0 -767
- queueflow-0.3.0/test/test_live.py +0 -229
- queueflow-0.3.0/test/test_workflow_step.py +0 -73
- {queueflow-0.3.0 → queueflow-0.5.0}/LICENSE +0 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/api/__init__.py +0 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/api_response.py +0 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow/py.typed +0 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow.egg-info/dependency_links.txt +0 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow.egg-info/requires.txt +0 -0
- {queueflow-0.3.0 → queueflow-0.5.0}/queueflow.egg-info/top_level.txt +0 -0
- {queueflow-0.3.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.
|
|
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
|
|
@@ -109,8 +109,9 @@ The generated per-tag clients hang off the facade for anything the helpers do no
|
|
|
109
109
|
| `qf.create_job(task, payload=None, priority=None, max_retries=None, timeout=None, queue=None, retry_backoff=None, retry_delay_secs=None, retry_max_delay_secs=None, jitter_factor=None, idempotency_key=None, run_at=None)` | Enqueue a job and return the created `Job`. |
|
|
110
110
|
| `qf.wait_for_job(job_id, timeout=60.0, interval=0.5)` | Poll until `completed` / `failed` / `cancelled`; raises `WaitTimeout`. |
|
|
111
111
|
| `qf.jobs.get_job(id)` | Fetch a job. |
|
|
112
|
-
| `qf.jobs.list_jobs(status=..., queue=..., limit=..., offset=..., order_by=..., include_total=..., cursor=..., created_after=..., created_before=...)` | List jobs. |
|
|
112
|
+
| `qf.jobs.list_jobs(status=..., queue=..., task_name=..., limit=..., offset=..., order_by=..., include_total=..., cursor=..., created_after=..., created_before=...)` | List jobs. |
|
|
113
113
|
| `qf.jobs.cancel_job(id)` | Cancel a job. |
|
|
114
|
+
| `qf.cancel_jobs(queue=None, task_name=None, status=None, created_after=None, created_before=None, limit=None)` | Cancel every matching non-terminal job of your tenant, oldest first, up to `limit` (default 100); returns the count. |
|
|
114
115
|
| `qf.jobs.create_batch_jobs(CreateBatchJobsRequest(...))` | Enqueue up to 1000 jobs at once. |
|
|
115
116
|
|
|
116
117
|
`idempotency_key` is sent as the `Idempotency-Key` header: re-submitting the same key returns the
|
|
@@ -119,6 +120,16 @@ original job instead of creating a duplicate. `run_at` (a `datetime`) delays the
|
|
|
119
120
|
List responses carry `next_cursor` when there are more pages; pass it back as `cursor` for keyset
|
|
120
121
|
pagination (cheaper than deep `offset`).
|
|
121
122
|
|
|
123
|
+
`cancel_jobs` takes optional filters that combine: `queue`, `task_name`, `status` (`pending`,
|
|
124
|
+
`retrying` or `running`; all three when omitted, a terminal status is a 400) and the half-open
|
|
125
|
+
`datetime` window `[created_after, created_before)`. A count below `limit` means the matching set is
|
|
126
|
+
exhausted, so loop until then to drain a large backlog:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
while qf.cancel_jobs(queue="orders", task_name="send_email", limit=500) == 500:
|
|
130
|
+
pass
|
|
131
|
+
```
|
|
132
|
+
|
|
122
133
|
### Workflows
|
|
123
134
|
|
|
124
135
|
| Method | Description |
|
|
@@ -126,26 +137,92 @@ pagination (cheaper than deep `offset`).
|
|
|
126
137
|
| `qf.create_workflow(builder_or_request)` | Create a workflow from a `wf()` builder or a raw `CreateWorkflowRequest`. |
|
|
127
138
|
| `qf.wait_for_workflow(workflow_id, timeout=60.0, interval=0.5)` | Poll until a terminal workflow state. |
|
|
128
139
|
| `qf.workflows.get_workflow(id)` / `list_workflows(...)` / `cancel_workflow(id)` | Fetch / list / cancel. |
|
|
129
|
-
| `qf.
|
|
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`. |
|
|
130
143
|
| `qf.workflows.get_workflow_diagram(id)` | Mermaid (`graph TD`) diagram of the DAG. |
|
|
131
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
|
+
|
|
132
156
|
### Workflow builder
|
|
133
157
|
|
|
134
158
|
```python
|
|
135
159
|
from queueflow.facade import wf
|
|
136
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
|
+
|
|
137
168
|
dag = (
|
|
138
169
|
wf("order_123")
|
|
139
|
-
.step("validate", "validate_order")
|
|
140
|
-
.step("
|
|
141
|
-
.step("
|
|
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")
|
|
142
179
|
.context(source="web")
|
|
180
|
+
.metadata(app="shop")
|
|
181
|
+
.timeout_secs(3600)
|
|
143
182
|
)
|
|
144
|
-
# .build() runs locally first: duplicate names, dangling deps, and
|
|
145
|
-
# 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.
|
|
146
185
|
```
|
|
147
186
|
|
|
148
|
-
|
|
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.
|
|
149
226
|
|
|
150
227
|
### Worker
|
|
151
228
|
|
|
@@ -238,22 +315,57 @@ Rules the runtime enforces, and that any hand-rolled loop over `qf.worker` must
|
|
|
238
315
|
|
|
239
316
|
| Method | Description |
|
|
240
317
|
| --- | --- |
|
|
241
|
-
| `qf.dlq.list_dead_letters(
|
|
318
|
+
| `qf.dlq.list_dead_letters(task_name=..., replayed=..., limit=..., offset=..., order_by=..., include_total=..., cursor=..., created_after=..., created_before=...)` / `get_dead_letter(id)` | Inspect terminally-failed jobs. `replayed=False` lists only entries not yet replayed, `True` only the replayed ones. |
|
|
242
319
|
| `qf.replay_dead_letter(id)` | Re-run one as a fresh job and return it (at most once; a second replay is a 409). |
|
|
320
|
+
| `qf.replay_dead_letters(queue=None, task_name=None, created_after=None, created_before=None, limit=None)` | Replay every unreplayed entry matching the filter, oldest first, up to `limit` (default 100). Returns a `ReplayedDeadLetter` per entry: `job_id` on success, otherwise `error`. |
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
for r in qf.replay_dead_letters(queue="orders", task_name="send_email"):
|
|
324
|
+
if r.error:
|
|
325
|
+
print("dead letter", r.dead_letter_id, "not replayed:", r.error)
|
|
326
|
+
else:
|
|
327
|
+
print("dead letter", r.dead_letter_id, "replayed as", r.job_id)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Already-replayed entries are never considered again, so repeating the call walks a large backlog;
|
|
331
|
+
an empty list means nothing was left to replay.
|
|
243
332
|
|
|
244
333
|
### System
|
|
245
334
|
|
|
246
335
|
```python
|
|
336
|
+
qf.whoami() # the tenant this token is scoped to and its access level
|
|
247
337
|
qf.system.get_stats() # engine counters
|
|
248
338
|
qf.system.list_tasks() # registered task handler names
|
|
249
|
-
qf.queues() # live per-queue backlog: pending, scheduled, running, oldest pending age
|
|
339
|
+
qf.queues() # live per-queue backlog: pending, scheduled, running, oldest pending age, paused
|
|
340
|
+
qf.pause_queue(name) # stop claims of your jobs on a queue; running jobs finish
|
|
341
|
+
qf.resume_queue(name) # let claims flow again
|
|
250
342
|
qf.health_api.get_health()
|
|
251
343
|
qf.health_api.get_ready()
|
|
252
344
|
```
|
|
253
345
|
|
|
254
|
-
`qf.
|
|
255
|
-
|
|
256
|
-
|
|
346
|
+
`qf.whoami()` (`GET /api/v1/whoami`) returns a `WhoAmIResponse` with `tenant` and `access`
|
|
347
|
+
(`read_write`, or `read_only` for a read-only key), handy for validating a credential at startup.
|
|
348
|
+
|
|
349
|
+
`qf.queues()` (`GET /api/v1/queues`) returns a `QueueStats` per queue so you can decide whether to
|
|
350
|
+
add workers. A 0.5.0 or later server lists every queue your tenant has enqueued to (drained ones
|
|
351
|
+
with zero counts); older servers list only queues with non-terminal jobs. Its `paused` flag says
|
|
352
|
+
whether claims are paused for your tenant. `qf.system.list_queues()` is the raw call.
|
|
353
|
+
|
|
354
|
+
`qf.pause_queue(name)` / `qf.resume_queue(name)` (`POST /api/v1/queues/{queue}/pause` and
|
|
355
|
+
`/resume`) gate workers' claims of your tenant's jobs on that queue. Pausing lets running jobs
|
|
356
|
+
finish while pending ones wait, so the queue is drained once the returned `running` count reaches
|
|
357
|
+
zero; resuming wakes parked workers immediately. Both are idempotent and return a
|
|
358
|
+
`QueueControlResponse` (`queue`, `paused`, `running`, `pending`).
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
import time
|
|
362
|
+
|
|
363
|
+
qf.pause_queue("orders")
|
|
364
|
+
while qf.pause_queue("orders").running > 0: # idempotent, so it doubles as a status poll
|
|
365
|
+
time.sleep(1)
|
|
366
|
+
deploy_new_handlers()
|
|
367
|
+
qf.resume_queue("orders")
|
|
368
|
+
```
|
|
257
369
|
|
|
258
370
|
## Authentication
|
|
259
371
|
|
|
@@ -287,20 +399,25 @@ except ApiException as err:
|
|
|
287
399
|
print(err.status, err.body)
|
|
288
400
|
```
|
|
289
401
|
|
|
290
|
-
## Known
|
|
402
|
+
## Known limitations
|
|
291
403
|
|
|
292
|
-
`JobsApi.stream_job_events`
|
|
293
|
-
|
|
294
|
-
|
|
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.
|
|
295
408
|
|
|
296
409
|
## Conformance tests
|
|
297
410
|
|
|
298
411
|
`test/test_live.py` runs the facade against a real QueueFlow server (started with the default
|
|
299
412
|
`queueflow serve`, i.e. `--mode all`, so the built-in `echo` handler is registered). It creates
|
|
300
413
|
and waits on an `echo` job, checks idempotent re-creation, runs a two-step workflow, exercises the
|
|
301
|
-
cron and dead-letter endpoints, reads stats,
|
|
302
|
-
|
|
303
|
-
|
|
414
|
+
cron and dead-letter endpoints, reads stats, runs `run_worker` against a dedicated queue, and
|
|
415
|
+
exercises `whoami`, bulk cancel, queue pause/resume and bulk dead-letter replay, each scoped to a
|
|
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.
|
|
304
421
|
|
|
305
422
|
```bash
|
|
306
423
|
QUEUEFLOW_URL=http://localhost:8000 \
|
|
@@ -68,8 +68,9 @@ The generated per-tag clients hang off the facade for anything the helpers do no
|
|
|
68
68
|
| `qf.create_job(task, payload=None, priority=None, max_retries=None, timeout=None, queue=None, retry_backoff=None, retry_delay_secs=None, retry_max_delay_secs=None, jitter_factor=None, idempotency_key=None, run_at=None)` | Enqueue a job and return the created `Job`. |
|
|
69
69
|
| `qf.wait_for_job(job_id, timeout=60.0, interval=0.5)` | Poll until `completed` / `failed` / `cancelled`; raises `WaitTimeout`. |
|
|
70
70
|
| `qf.jobs.get_job(id)` | Fetch a job. |
|
|
71
|
-
| `qf.jobs.list_jobs(status=..., queue=..., limit=..., offset=..., order_by=..., include_total=..., cursor=..., created_after=..., created_before=...)` | List jobs. |
|
|
71
|
+
| `qf.jobs.list_jobs(status=..., queue=..., task_name=..., limit=..., offset=..., order_by=..., include_total=..., cursor=..., created_after=..., created_before=...)` | List jobs. |
|
|
72
72
|
| `qf.jobs.cancel_job(id)` | Cancel a job. |
|
|
73
|
+
| `qf.cancel_jobs(queue=None, task_name=None, status=None, created_after=None, created_before=None, limit=None)` | Cancel every matching non-terminal job of your tenant, oldest first, up to `limit` (default 100); returns the count. |
|
|
73
74
|
| `qf.jobs.create_batch_jobs(CreateBatchJobsRequest(...))` | Enqueue up to 1000 jobs at once. |
|
|
74
75
|
|
|
75
76
|
`idempotency_key` is sent as the `Idempotency-Key` header: re-submitting the same key returns the
|
|
@@ -78,6 +79,16 @@ original job instead of creating a duplicate. `run_at` (a `datetime`) delays the
|
|
|
78
79
|
List responses carry `next_cursor` when there are more pages; pass it back as `cursor` for keyset
|
|
79
80
|
pagination (cheaper than deep `offset`).
|
|
80
81
|
|
|
82
|
+
`cancel_jobs` takes optional filters that combine: `queue`, `task_name`, `status` (`pending`,
|
|
83
|
+
`retrying` or `running`; all three when omitted, a terminal status is a 400) and the half-open
|
|
84
|
+
`datetime` window `[created_after, created_before)`. A count below `limit` means the matching set is
|
|
85
|
+
exhausted, so loop until then to drain a large backlog:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
while qf.cancel_jobs(queue="orders", task_name="send_email", limit=500) == 500:
|
|
89
|
+
pass
|
|
90
|
+
```
|
|
91
|
+
|
|
81
92
|
### Workflows
|
|
82
93
|
|
|
83
94
|
| Method | Description |
|
|
@@ -85,26 +96,92 @@ pagination (cheaper than deep `offset`).
|
|
|
85
96
|
| `qf.create_workflow(builder_or_request)` | Create a workflow from a `wf()` builder or a raw `CreateWorkflowRequest`. |
|
|
86
97
|
| `qf.wait_for_workflow(workflow_id, timeout=60.0, interval=0.5)` | Poll until a terminal workflow state. |
|
|
87
98
|
| `qf.workflows.get_workflow(id)` / `list_workflows(...)` / `cancel_workflow(id)` | Fetch / list / cancel. |
|
|
88
|
-
| `qf.
|
|
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`. |
|
|
89
102
|
| `qf.workflows.get_workflow_diagram(id)` | Mermaid (`graph TD`) diagram of the DAG. |
|
|
90
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
|
+
|
|
91
115
|
### Workflow builder
|
|
92
116
|
|
|
93
117
|
```python
|
|
94
118
|
from queueflow.facade import wf
|
|
95
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
|
+
|
|
96
127
|
dag = (
|
|
97
128
|
wf("order_123")
|
|
98
|
-
.step("validate", "validate_order")
|
|
99
|
-
.step("
|
|
100
|
-
.step("
|
|
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")
|
|
101
138
|
.context(source="web")
|
|
139
|
+
.metadata(app="shop")
|
|
140
|
+
.timeout_secs(3600)
|
|
102
141
|
)
|
|
103
|
-
# .build() runs locally first: duplicate names, dangling deps, and
|
|
104
|
-
# 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.
|
|
105
144
|
```
|
|
106
145
|
|
|
107
|
-
|
|
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.
|
|
108
185
|
|
|
109
186
|
### Worker
|
|
110
187
|
|
|
@@ -197,22 +274,57 @@ Rules the runtime enforces, and that any hand-rolled loop over `qf.worker` must
|
|
|
197
274
|
|
|
198
275
|
| Method | Description |
|
|
199
276
|
| --- | --- |
|
|
200
|
-
| `qf.dlq.list_dead_letters(
|
|
277
|
+
| `qf.dlq.list_dead_letters(task_name=..., replayed=..., limit=..., offset=..., order_by=..., include_total=..., cursor=..., created_after=..., created_before=...)` / `get_dead_letter(id)` | Inspect terminally-failed jobs. `replayed=False` lists only entries not yet replayed, `True` only the replayed ones. |
|
|
201
278
|
| `qf.replay_dead_letter(id)` | Re-run one as a fresh job and return it (at most once; a second replay is a 409). |
|
|
279
|
+
| `qf.replay_dead_letters(queue=None, task_name=None, created_after=None, created_before=None, limit=None)` | Replay every unreplayed entry matching the filter, oldest first, up to `limit` (default 100). Returns a `ReplayedDeadLetter` per entry: `job_id` on success, otherwise `error`. |
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
for r in qf.replay_dead_letters(queue="orders", task_name="send_email"):
|
|
283
|
+
if r.error:
|
|
284
|
+
print("dead letter", r.dead_letter_id, "not replayed:", r.error)
|
|
285
|
+
else:
|
|
286
|
+
print("dead letter", r.dead_letter_id, "replayed as", r.job_id)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Already-replayed entries are never considered again, so repeating the call walks a large backlog;
|
|
290
|
+
an empty list means nothing was left to replay.
|
|
202
291
|
|
|
203
292
|
### System
|
|
204
293
|
|
|
205
294
|
```python
|
|
295
|
+
qf.whoami() # the tenant this token is scoped to and its access level
|
|
206
296
|
qf.system.get_stats() # engine counters
|
|
207
297
|
qf.system.list_tasks() # registered task handler names
|
|
208
|
-
qf.queues() # live per-queue backlog: pending, scheduled, running, oldest pending age
|
|
298
|
+
qf.queues() # live per-queue backlog: pending, scheduled, running, oldest pending age, paused
|
|
299
|
+
qf.pause_queue(name) # stop claims of your jobs on a queue; running jobs finish
|
|
300
|
+
qf.resume_queue(name) # let claims flow again
|
|
209
301
|
qf.health_api.get_health()
|
|
210
302
|
qf.health_api.get_ready()
|
|
211
303
|
```
|
|
212
304
|
|
|
213
|
-
`qf.
|
|
214
|
-
|
|
215
|
-
|
|
305
|
+
`qf.whoami()` (`GET /api/v1/whoami`) returns a `WhoAmIResponse` with `tenant` and `access`
|
|
306
|
+
(`read_write`, or `read_only` for a read-only key), handy for validating a credential at startup.
|
|
307
|
+
|
|
308
|
+
`qf.queues()` (`GET /api/v1/queues`) returns a `QueueStats` per queue so you can decide whether to
|
|
309
|
+
add workers. A 0.5.0 or later server lists every queue your tenant has enqueued to (drained ones
|
|
310
|
+
with zero counts); older servers list only queues with non-terminal jobs. Its `paused` flag says
|
|
311
|
+
whether claims are paused for your tenant. `qf.system.list_queues()` is the raw call.
|
|
312
|
+
|
|
313
|
+
`qf.pause_queue(name)` / `qf.resume_queue(name)` (`POST /api/v1/queues/{queue}/pause` and
|
|
314
|
+
`/resume`) gate workers' claims of your tenant's jobs on that queue. Pausing lets running jobs
|
|
315
|
+
finish while pending ones wait, so the queue is drained once the returned `running` count reaches
|
|
316
|
+
zero; resuming wakes parked workers immediately. Both are idempotent and return a
|
|
317
|
+
`QueueControlResponse` (`queue`, `paused`, `running`, `pending`).
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
import time
|
|
321
|
+
|
|
322
|
+
qf.pause_queue("orders")
|
|
323
|
+
while qf.pause_queue("orders").running > 0: # idempotent, so it doubles as a status poll
|
|
324
|
+
time.sleep(1)
|
|
325
|
+
deploy_new_handlers()
|
|
326
|
+
qf.resume_queue("orders")
|
|
327
|
+
```
|
|
216
328
|
|
|
217
329
|
## Authentication
|
|
218
330
|
|
|
@@ -246,20 +358,25 @@ except ApiException as err:
|
|
|
246
358
|
print(err.status, err.body)
|
|
247
359
|
```
|
|
248
360
|
|
|
249
|
-
## Known
|
|
361
|
+
## Known limitations
|
|
250
362
|
|
|
251
|
-
`JobsApi.stream_job_events`
|
|
252
|
-
|
|
253
|
-
|
|
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.
|
|
254
367
|
|
|
255
368
|
## Conformance tests
|
|
256
369
|
|
|
257
370
|
`test/test_live.py` runs the facade against a real QueueFlow server (started with the default
|
|
258
371
|
`queueflow serve`, i.e. `--mode all`, so the built-in `echo` handler is registered). It creates
|
|
259
372
|
and waits on an `echo` job, checks idempotent re-creation, runs a two-step workflow, exercises the
|
|
260
|
-
cron and dead-letter endpoints, reads stats,
|
|
261
|
-
|
|
262
|
-
|
|
373
|
+
cron and dead-letter endpoints, reads stats, runs `run_worker` against a dedicated queue, and
|
|
374
|
+
exercises `whoami`, bulk cancel, queue pause/resume and bulk dead-letter replay, each scoped to a
|
|
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.
|
|
263
380
|
|
|
264
381
|
```bash
|
|
265
382
|
QUEUEFLOW_URL=http://localhost:8000 \
|
|
@@ -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.
|
|
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.
|
|
17
|
+
__version__ = "0.5.0"
|
|
18
18
|
|
|
19
19
|
# import apis into sdk package
|
|
20
20
|
from queueflow.api.cron_api import CronApi
|
|
@@ -38,7 +38,10 @@ from queueflow.exceptions import ApiException
|
|
|
38
38
|
|
|
39
39
|
# import models into sdk package
|
|
40
40
|
from queueflow.models.backoff_strategy import BackoffStrategy
|
|
41
|
+
from queueflow.models.cancel_jobs_request import CancelJobsRequest
|
|
42
|
+
from queueflow.models.cancel_jobs_response import CancelJobsResponse
|
|
41
43
|
from queueflow.models.complete_job_request import CompleteJobRequest
|
|
44
|
+
from queueflow.models.condition_op import ConditionOp
|
|
42
45
|
from queueflow.models.create_batch_jobs_request import CreateBatchJobsRequest
|
|
43
46
|
from queueflow.models.create_batch_jobs_response import CreateBatchJobsResponse
|
|
44
47
|
from queueflow.models.create_cron_request import CreateCronRequest
|
|
@@ -51,6 +54,8 @@ from queueflow.models.cron_schedule import CronSchedule
|
|
|
51
54
|
from queueflow.models.dead_letter import DeadLetter
|
|
52
55
|
from queueflow.models.error_body import ErrorBody
|
|
53
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
|
|
54
59
|
from queueflow.models.health_status import HealthStatus
|
|
55
60
|
from queueflow.models.heartbeat_request import HeartbeatRequest
|
|
56
61
|
from queueflow.models.heartbeat_response import HeartbeatResponse
|
|
@@ -67,15 +72,22 @@ from queueflow.models.list_jobs_response import ListJobsResponse
|
|
|
67
72
|
from queueflow.models.list_workflows_response import ListWorkflowsResponse
|
|
68
73
|
from queueflow.models.on_failure import OnFailure
|
|
69
74
|
from queueflow.models.on_success import OnSuccess
|
|
75
|
+
from queueflow.models.queue_control_response import QueueControlResponse
|
|
70
76
|
from queueflow.models.queue_stats import QueueStats
|
|
71
77
|
from queueflow.models.queues_response import QueuesResponse
|
|
72
78
|
from queueflow.models.ready_status import ReadyStatus
|
|
73
79
|
from queueflow.models.replay_dead_letter_response import ReplayDeadLetterResponse
|
|
80
|
+
from queueflow.models.replay_dead_letters_request import ReplayDeadLettersRequest
|
|
81
|
+
from queueflow.models.replay_dead_letters_response import ReplayDeadLettersResponse
|
|
82
|
+
from queueflow.models.replayed_dead_letter import ReplayedDeadLetter
|
|
74
83
|
from queueflow.models.stats_snapshot import StatsSnapshot
|
|
84
|
+
from queueflow.models.step_condition import StepCondition
|
|
75
85
|
from queueflow.models.step_status import StepStatus
|
|
76
86
|
from queueflow.models.tasks_response import TasksResponse
|
|
87
|
+
from queueflow.models.who_am_i_response import WhoAmIResponse
|
|
77
88
|
from queueflow.models.workflow import Workflow
|
|
78
89
|
from queueflow.models.workflow_diagram_response import WorkflowDiagramResponse
|
|
90
|
+
from queueflow.models.workflow_event import WorkflowEvent
|
|
79
91
|
from queueflow.models.workflow_status import WorkflowStatus
|
|
80
92
|
from queueflow.models.workflow_step import WorkflowStep
|
|
81
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.
|
|
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.
|
|
@@ -862,6 +862,8 @@ class CronApi:
|
|
|
862
862
|
self,
|
|
863
863
|
status: Annotated[Optional[StrictStr], Field(description="Filter by status (e.g. `pending`, `completed`).")] = None,
|
|
864
864
|
queue: Annotated[Optional[StrictStr], Field(description="Filter by queue name (jobs only).")] = None,
|
|
865
|
+
task_name: Annotated[Optional[StrictStr], Field(description="Filter by task name (jobs and dead letters).")] = None,
|
|
866
|
+
replayed: Annotated[Optional[StrictBool], Field(description="Dead letters only: `false` lists entries not yet replayed, `true` the replayed ones; both when omitted.")] = None,
|
|
865
867
|
limit: Annotated[Optional[StrictInt], Field(description="Page size, 1..=100 (default 50).")] = None,
|
|
866
868
|
offset: Annotated[Optional[StrictInt], Field(description="Number of records to skip (default 0).")] = None,
|
|
867
869
|
order_by: Annotated[Optional[StrictStr], Field(description="`created_at ASC` or `created_at DESC` (default DESC).")] = None,
|
|
@@ -889,6 +891,10 @@ class CronApi:
|
|
|
889
891
|
:type status: str
|
|
890
892
|
:param queue: Filter by queue name (jobs only).
|
|
891
893
|
:type queue: str
|
|
894
|
+
:param task_name: Filter by task name (jobs and dead letters).
|
|
895
|
+
:type task_name: str
|
|
896
|
+
:param replayed: Dead letters only: `false` lists entries not yet replayed, `true` the replayed ones; both when omitted.
|
|
897
|
+
:type replayed: bool
|
|
892
898
|
:param limit: Page size, 1..=100 (default 50).
|
|
893
899
|
:type limit: int
|
|
894
900
|
:param offset: Number of records to skip (default 0).
|
|
@@ -928,6 +934,8 @@ class CronApi:
|
|
|
928
934
|
_param = self._list_crons_serialize(
|
|
929
935
|
status=status,
|
|
930
936
|
queue=queue,
|
|
937
|
+
task_name=task_name,
|
|
938
|
+
replayed=replayed,
|
|
931
939
|
limit=limit,
|
|
932
940
|
offset=offset,
|
|
933
941
|
order_by=order_by,
|
|
@@ -961,6 +969,8 @@ class CronApi:
|
|
|
961
969
|
self,
|
|
962
970
|
status: Annotated[Optional[StrictStr], Field(description="Filter by status (e.g. `pending`, `completed`).")] = None,
|
|
963
971
|
queue: Annotated[Optional[StrictStr], Field(description="Filter by queue name (jobs only).")] = None,
|
|
972
|
+
task_name: Annotated[Optional[StrictStr], Field(description="Filter by task name (jobs and dead letters).")] = None,
|
|
973
|
+
replayed: Annotated[Optional[StrictBool], Field(description="Dead letters only: `false` lists entries not yet replayed, `true` the replayed ones; both when omitted.")] = None,
|
|
964
974
|
limit: Annotated[Optional[StrictInt], Field(description="Page size, 1..=100 (default 50).")] = None,
|
|
965
975
|
offset: Annotated[Optional[StrictInt], Field(description="Number of records to skip (default 0).")] = None,
|
|
966
976
|
order_by: Annotated[Optional[StrictStr], Field(description="`created_at ASC` or `created_at DESC` (default DESC).")] = None,
|
|
@@ -988,6 +998,10 @@ class CronApi:
|
|
|
988
998
|
:type status: str
|
|
989
999
|
:param queue: Filter by queue name (jobs only).
|
|
990
1000
|
:type queue: str
|
|
1001
|
+
:param task_name: Filter by task name (jobs and dead letters).
|
|
1002
|
+
:type task_name: str
|
|
1003
|
+
:param replayed: Dead letters only: `false` lists entries not yet replayed, `true` the replayed ones; both when omitted.
|
|
1004
|
+
:type replayed: bool
|
|
991
1005
|
:param limit: Page size, 1..=100 (default 50).
|
|
992
1006
|
:type limit: int
|
|
993
1007
|
:param offset: Number of records to skip (default 0).
|
|
@@ -1027,6 +1041,8 @@ class CronApi:
|
|
|
1027
1041
|
_param = self._list_crons_serialize(
|
|
1028
1042
|
status=status,
|
|
1029
1043
|
queue=queue,
|
|
1044
|
+
task_name=task_name,
|
|
1045
|
+
replayed=replayed,
|
|
1030
1046
|
limit=limit,
|
|
1031
1047
|
offset=offset,
|
|
1032
1048
|
order_by=order_by,
|
|
@@ -1060,6 +1076,8 @@ class CronApi:
|
|
|
1060
1076
|
self,
|
|
1061
1077
|
status: Annotated[Optional[StrictStr], Field(description="Filter by status (e.g. `pending`, `completed`).")] = None,
|
|
1062
1078
|
queue: Annotated[Optional[StrictStr], Field(description="Filter by queue name (jobs only).")] = None,
|
|
1079
|
+
task_name: Annotated[Optional[StrictStr], Field(description="Filter by task name (jobs and dead letters).")] = None,
|
|
1080
|
+
replayed: Annotated[Optional[StrictBool], Field(description="Dead letters only: `false` lists entries not yet replayed, `true` the replayed ones; both when omitted.")] = None,
|
|
1063
1081
|
limit: Annotated[Optional[StrictInt], Field(description="Page size, 1..=100 (default 50).")] = None,
|
|
1064
1082
|
offset: Annotated[Optional[StrictInt], Field(description="Number of records to skip (default 0).")] = None,
|
|
1065
1083
|
order_by: Annotated[Optional[StrictStr], Field(description="`created_at ASC` or `created_at DESC` (default DESC).")] = None,
|
|
@@ -1087,6 +1105,10 @@ class CronApi:
|
|
|
1087
1105
|
:type status: str
|
|
1088
1106
|
:param queue: Filter by queue name (jobs only).
|
|
1089
1107
|
:type queue: str
|
|
1108
|
+
:param task_name: Filter by task name (jobs and dead letters).
|
|
1109
|
+
:type task_name: str
|
|
1110
|
+
:param replayed: Dead letters only: `false` lists entries not yet replayed, `true` the replayed ones; both when omitted.
|
|
1111
|
+
:type replayed: bool
|
|
1090
1112
|
:param limit: Page size, 1..=100 (default 50).
|
|
1091
1113
|
:type limit: int
|
|
1092
1114
|
:param offset: Number of records to skip (default 0).
|
|
@@ -1126,6 +1148,8 @@ class CronApi:
|
|
|
1126
1148
|
_param = self._list_crons_serialize(
|
|
1127
1149
|
status=status,
|
|
1128
1150
|
queue=queue,
|
|
1151
|
+
task_name=task_name,
|
|
1152
|
+
replayed=replayed,
|
|
1129
1153
|
limit=limit,
|
|
1130
1154
|
offset=offset,
|
|
1131
1155
|
order_by=order_by,
|
|
@@ -1154,6 +1178,8 @@ class CronApi:
|
|
|
1154
1178
|
self,
|
|
1155
1179
|
status,
|
|
1156
1180
|
queue,
|
|
1181
|
+
task_name,
|
|
1182
|
+
replayed,
|
|
1157
1183
|
limit,
|
|
1158
1184
|
offset,
|
|
1159
1185
|
order_by,
|
|
@@ -1191,6 +1217,14 @@ class CronApi:
|
|
|
1191
1217
|
|
|
1192
1218
|
_query_params.append(('queue', queue))
|
|
1193
1219
|
|
|
1220
|
+
if task_name is not None:
|
|
1221
|
+
|
|
1222
|
+
_query_params.append(('task_name', task_name))
|
|
1223
|
+
|
|
1224
|
+
if replayed is not None:
|
|
1225
|
+
|
|
1226
|
+
_query_params.append(('replayed', replayed))
|
|
1227
|
+
|
|
1194
1228
|
if limit is not None:
|
|
1195
1229
|
|
|
1196
1230
|
_query_params.append(('limit', limit))
|