queueflow 0.3.0__tar.gz → 0.4.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.4.0}/PKG-INFO +57 -10
- {queueflow-0.3.0 → queueflow-0.4.0}/README.md +56 -9
- {queueflow-0.3.0 → queueflow-0.4.0}/pyproject.toml +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/__init__.py +9 -2
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/cron_api.py +35 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/dlq_api.py +314 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/health_api.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/jobs_api.py +317 -1
- queueflow-0.4.0/queueflow/api/system_api.py +1551 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/worker_api.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/workflows_api.py +35 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api_client.py +2 -2
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/configuration.py +3 -3
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/exceptions.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/facade.py +94 -3
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/__init__.py +8 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/backoff_strategy.py +1 -1
- queueflow-0.4.0/queueflow/models/cancel_jobs_request.py +128 -0
- queueflow-0.4.0/queueflow/models/cancel_jobs_response.py +88 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/complete_job_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_batch_jobs_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_batch_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_cron_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_cron_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_job_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_job_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_workflow_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_workflow_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/cron_schedule.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/dead_letter.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/error_body.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/fail_job_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/health_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/heartbeat_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/heartbeat_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/job.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/job_config.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/job_config_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/job_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/lease_jobs_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/lease_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/leased_job.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/list_crons_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/list_dead_letters_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/list_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/list_workflows_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/on_failure.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/on_success.py +1 -1
- queueflow-0.4.0/queueflow/models/queue_control_response.py +94 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/queue_stats.py +5 -3
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/queues_response.py +2 -2
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/ready_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/replay_dead_letter_response.py +1 -1
- queueflow-0.4.0/queueflow/models/replay_dead_letters_request.py +121 -0
- queueflow-0.4.0/queueflow/models/replay_dead_letters_response.py +98 -0
- queueflow-0.4.0/queueflow/models/replayed_dead_letter.py +101 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/stats_snapshot.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/step_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/tasks_response.py +1 -1
- queueflow-0.4.0/queueflow/models/who_am_i_response.py +89 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow_diagram_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow_step.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow_step_state.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow_step_states_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/rest.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0/queueflow.egg-info}/PKG-INFO +57 -10
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow.egg-info/SOURCES.txt +14 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/setup.py +2 -2
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_backoff_strategy.py +1 -1
- queueflow-0.4.0/test/test_cancel_jobs_request.py +56 -0
- queueflow-0.4.0/test/test_cancel_jobs_response.py +52 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_complete_job_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_batch_jobs_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_batch_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_cron_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_cron_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_job_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_job_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_workflow_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_workflow_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_cron_api.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_cron_schedule.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_dead_letter.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_dlq_api.py +7 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_error_body.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_facade.py +158 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_fail_job_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_health_api.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_health_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_heartbeat_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_heartbeat_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_job.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_job_config.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_job_config_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_job_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_jobs_api.py +7 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_lease_jobs_request.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_lease_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_leased_job.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_list_crons_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_list_dead_letters_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_list_jobs_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_list_workflows_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_live.py +148 -5
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_on_failure.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_on_success.py +1 -1
- queueflow-0.4.0/test/test_queue_control_response.py +58 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_queue_stats.py +2 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_queues_response.py +3 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_ready_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_replay_dead_letter_response.py +1 -1
- queueflow-0.4.0/test/test_replay_dead_letters_request.py +55 -0
- queueflow-0.4.0/test/test_replay_dead_letters_response.py +64 -0
- queueflow-0.4.0/test/test_replayed_dead_letter.py +54 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_stats_snapshot.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_step_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_system_api.py +19 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_tasks_response.py +1 -1
- queueflow-0.4.0/test/test_who_am_i_response.py +54 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_worker_api.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow_diagram_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow_status.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow_step.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow_step_state.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow_step_states_response.py +1 -1
- {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflows_api.py +1 -1
- queueflow-0.3.0/queueflow/api/system_api.py +0 -767
- {queueflow-0.3.0 → queueflow-0.4.0}/LICENSE +0 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/__init__.py +0 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api_response.py +0 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/py.typed +0 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow.egg-info/dependency_links.txt +0 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow.egg-info/requires.txt +0 -0
- {queueflow-0.3.0 → queueflow-0.4.0}/queueflow.egg-info/top_level.txt +0 -0
- {queueflow-0.3.0 → queueflow-0.4.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.4.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 |
|
|
@@ -238,22 +249,57 @@ Rules the runtime enforces, and that any hand-rolled loop over `qf.worker` must
|
|
|
238
249
|
|
|
239
250
|
| Method | Description |
|
|
240
251
|
| --- | --- |
|
|
241
|
-
| `qf.dlq.list_dead_letters(
|
|
252
|
+
| `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
253
|
| `qf.replay_dead_letter(id)` | Re-run one as a fresh job and return it (at most once; a second replay is a 409). |
|
|
254
|
+
| `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`. |
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
for r in qf.replay_dead_letters(queue="orders", task_name="send_email"):
|
|
258
|
+
if r.error:
|
|
259
|
+
print("dead letter", r.dead_letter_id, "not replayed:", r.error)
|
|
260
|
+
else:
|
|
261
|
+
print("dead letter", r.dead_letter_id, "replayed as", r.job_id)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Already-replayed entries are never considered again, so repeating the call walks a large backlog;
|
|
265
|
+
an empty list means nothing was left to replay.
|
|
243
266
|
|
|
244
267
|
### System
|
|
245
268
|
|
|
246
269
|
```python
|
|
270
|
+
qf.whoami() # the tenant this token is scoped to and its access level
|
|
247
271
|
qf.system.get_stats() # engine counters
|
|
248
272
|
qf.system.list_tasks() # registered task handler names
|
|
249
|
-
qf.queues() # live per-queue backlog: pending, scheduled, running, oldest pending age
|
|
273
|
+
qf.queues() # live per-queue backlog: pending, scheduled, running, oldest pending age, paused
|
|
274
|
+
qf.pause_queue(name) # stop claims of your jobs on a queue; running jobs finish
|
|
275
|
+
qf.resume_queue(name) # let claims flow again
|
|
250
276
|
qf.health_api.get_health()
|
|
251
277
|
qf.health_api.get_ready()
|
|
252
278
|
```
|
|
253
279
|
|
|
254
|
-
`qf.
|
|
255
|
-
|
|
256
|
-
|
|
280
|
+
`qf.whoami()` (`GET /api/v1/whoami`) returns a `WhoAmIResponse` with `tenant` and `access`
|
|
281
|
+
(`read_write`, or `read_only` for a read-only key), handy for validating a credential at startup.
|
|
282
|
+
|
|
283
|
+
`qf.queues()` (`GET /api/v1/queues`) returns a `QueueStats` per queue so you can decide whether to
|
|
284
|
+
add workers. A 0.5.0 or later server lists every queue your tenant has enqueued to (drained ones
|
|
285
|
+
with zero counts); older servers list only queues with non-terminal jobs. Its `paused` flag says
|
|
286
|
+
whether claims are paused for your tenant. `qf.system.list_queues()` is the raw call.
|
|
287
|
+
|
|
288
|
+
`qf.pause_queue(name)` / `qf.resume_queue(name)` (`POST /api/v1/queues/{queue}/pause` and
|
|
289
|
+
`/resume`) gate workers' claims of your tenant's jobs on that queue. Pausing lets running jobs
|
|
290
|
+
finish while pending ones wait, so the queue is drained once the returned `running` count reaches
|
|
291
|
+
zero; resuming wakes parked workers immediately. Both are idempotent and return a
|
|
292
|
+
`QueueControlResponse` (`queue`, `paused`, `running`, `pending`).
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
import time
|
|
296
|
+
|
|
297
|
+
qf.pause_queue("orders")
|
|
298
|
+
while qf.pause_queue("orders").running > 0: # idempotent, so it doubles as a status poll
|
|
299
|
+
time.sleep(1)
|
|
300
|
+
deploy_new_handlers()
|
|
301
|
+
qf.resume_queue("orders")
|
|
302
|
+
```
|
|
257
303
|
|
|
258
304
|
## Authentication
|
|
259
305
|
|
|
@@ -298,9 +344,10 @@ closes it (terminal status or the 15-minute cap). Use `wait_for_job` (polling) i
|
|
|
298
344
|
`test/test_live.py` runs the facade against a real QueueFlow server (started with the default
|
|
299
345
|
`queueflow serve`, i.e. `--mode all`, so the built-in `echo` handler is registered). It creates
|
|
300
346
|
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
|
-
|
|
347
|
+
cron and dead-letter endpoints, reads stats, runs `run_worker` against a dedicated queue, and
|
|
348
|
+
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.
|
|
304
351
|
|
|
305
352
|
```bash
|
|
306
353
|
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 |
|
|
@@ -197,22 +208,57 @@ Rules the runtime enforces, and that any hand-rolled loop over `qf.worker` must
|
|
|
197
208
|
|
|
198
209
|
| Method | Description |
|
|
199
210
|
| --- | --- |
|
|
200
|
-
| `qf.dlq.list_dead_letters(
|
|
211
|
+
| `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
212
|
| `qf.replay_dead_letter(id)` | Re-run one as a fresh job and return it (at most once; a second replay is a 409). |
|
|
213
|
+
| `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`. |
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
for r in qf.replay_dead_letters(queue="orders", task_name="send_email"):
|
|
217
|
+
if r.error:
|
|
218
|
+
print("dead letter", r.dead_letter_id, "not replayed:", r.error)
|
|
219
|
+
else:
|
|
220
|
+
print("dead letter", r.dead_letter_id, "replayed as", r.job_id)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Already-replayed entries are never considered again, so repeating the call walks a large backlog;
|
|
224
|
+
an empty list means nothing was left to replay.
|
|
202
225
|
|
|
203
226
|
### System
|
|
204
227
|
|
|
205
228
|
```python
|
|
229
|
+
qf.whoami() # the tenant this token is scoped to and its access level
|
|
206
230
|
qf.system.get_stats() # engine counters
|
|
207
231
|
qf.system.list_tasks() # registered task handler names
|
|
208
|
-
qf.queues() # live per-queue backlog: pending, scheduled, running, oldest pending age
|
|
232
|
+
qf.queues() # live per-queue backlog: pending, scheduled, running, oldest pending age, paused
|
|
233
|
+
qf.pause_queue(name) # stop claims of your jobs on a queue; running jobs finish
|
|
234
|
+
qf.resume_queue(name) # let claims flow again
|
|
209
235
|
qf.health_api.get_health()
|
|
210
236
|
qf.health_api.get_ready()
|
|
211
237
|
```
|
|
212
238
|
|
|
213
|
-
`qf.
|
|
214
|
-
|
|
215
|
-
|
|
239
|
+
`qf.whoami()` (`GET /api/v1/whoami`) returns a `WhoAmIResponse` with `tenant` and `access`
|
|
240
|
+
(`read_write`, or `read_only` for a read-only key), handy for validating a credential at startup.
|
|
241
|
+
|
|
242
|
+
`qf.queues()` (`GET /api/v1/queues`) returns a `QueueStats` per queue so you can decide whether to
|
|
243
|
+
add workers. A 0.5.0 or later server lists every queue your tenant has enqueued to (drained ones
|
|
244
|
+
with zero counts); older servers list only queues with non-terminal jobs. Its `paused` flag says
|
|
245
|
+
whether claims are paused for your tenant. `qf.system.list_queues()` is the raw call.
|
|
246
|
+
|
|
247
|
+
`qf.pause_queue(name)` / `qf.resume_queue(name)` (`POST /api/v1/queues/{queue}/pause` and
|
|
248
|
+
`/resume`) gate workers' claims of your tenant's jobs on that queue. Pausing lets running jobs
|
|
249
|
+
finish while pending ones wait, so the queue is drained once the returned `running` count reaches
|
|
250
|
+
zero; resuming wakes parked workers immediately. Both are idempotent and return a
|
|
251
|
+
`QueueControlResponse` (`queue`, `paused`, `running`, `pending`).
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
import time
|
|
255
|
+
|
|
256
|
+
qf.pause_queue("orders")
|
|
257
|
+
while qf.pause_queue("orders").running > 0: # idempotent, so it doubles as a status poll
|
|
258
|
+
time.sleep(1)
|
|
259
|
+
deploy_new_handlers()
|
|
260
|
+
qf.resume_queue("orders")
|
|
261
|
+
```
|
|
216
262
|
|
|
217
263
|
## Authentication
|
|
218
264
|
|
|
@@ -257,9 +303,10 @@ closes it (terminal status or the 15-minute cap). Use `wait_for_job` (polling) i
|
|
|
257
303
|
`test/test_live.py` runs the facade against a real QueueFlow server (started with the default
|
|
258
304
|
`queueflow serve`, i.e. `--mode all`, so the built-in `echo` handler is registered). It creates
|
|
259
305
|
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
|
-
|
|
306
|
+
cron and dead-letter endpoints, reads stats, runs `run_worker` against a dedicated queue, and
|
|
307
|
+
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.
|
|
263
310
|
|
|
264
311
|
```bash
|
|
265
312
|
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.5.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.4.0"
|
|
18
18
|
|
|
19
19
|
# import apis into sdk package
|
|
20
20
|
from queueflow.api.cron_api import CronApi
|
|
@@ -38,6 +38,8 @@ 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
|
|
42
44
|
from queueflow.models.create_batch_jobs_request import CreateBatchJobsRequest
|
|
43
45
|
from queueflow.models.create_batch_jobs_response import CreateBatchJobsResponse
|
|
@@ -67,13 +69,18 @@ from queueflow.models.list_jobs_response import ListJobsResponse
|
|
|
67
69
|
from queueflow.models.list_workflows_response import ListWorkflowsResponse
|
|
68
70
|
from queueflow.models.on_failure import OnFailure
|
|
69
71
|
from queueflow.models.on_success import OnSuccess
|
|
72
|
+
from queueflow.models.queue_control_response import QueueControlResponse
|
|
70
73
|
from queueflow.models.queue_stats import QueueStats
|
|
71
74
|
from queueflow.models.queues_response import QueuesResponse
|
|
72
75
|
from queueflow.models.ready_status import ReadyStatus
|
|
73
76
|
from queueflow.models.replay_dead_letter_response import ReplayDeadLetterResponse
|
|
77
|
+
from queueflow.models.replay_dead_letters_request import ReplayDeadLettersRequest
|
|
78
|
+
from queueflow.models.replay_dead_letters_response import ReplayDeadLettersResponse
|
|
79
|
+
from queueflow.models.replayed_dead_letter import ReplayedDeadLetter
|
|
74
80
|
from queueflow.models.stats_snapshot import StatsSnapshot
|
|
75
81
|
from queueflow.models.step_status import StepStatus
|
|
76
82
|
from queueflow.models.tasks_response import TasksResponse
|
|
83
|
+
from queueflow.models.who_am_i_response import WhoAmIResponse
|
|
77
84
|
from queueflow.models.workflow import Workflow
|
|
78
85
|
from queueflow.models.workflow_diagram_response import WorkflowDiagramResponse
|
|
79
86
|
from queueflow.models.workflow_status import WorkflowStatus
|
|
@@ -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.5.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))
|