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.
Files changed (138) hide show
  1. {queueflow-0.3.0/queueflow.egg-info → queueflow-0.4.0}/PKG-INFO +57 -10
  2. {queueflow-0.3.0 → queueflow-0.4.0}/README.md +56 -9
  3. {queueflow-0.3.0 → queueflow-0.4.0}/pyproject.toml +1 -1
  4. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/__init__.py +9 -2
  5. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/cron_api.py +35 -1
  6. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/dlq_api.py +314 -1
  7. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/health_api.py +1 -1
  8. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/jobs_api.py +317 -1
  9. queueflow-0.4.0/queueflow/api/system_api.py +1551 -0
  10. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/worker_api.py +1 -1
  11. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/workflows_api.py +35 -1
  12. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api_client.py +2 -2
  13. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/configuration.py +3 -3
  14. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/exceptions.py +1 -1
  15. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/facade.py +94 -3
  16. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/__init__.py +8 -1
  17. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/backoff_strategy.py +1 -1
  18. queueflow-0.4.0/queueflow/models/cancel_jobs_request.py +128 -0
  19. queueflow-0.4.0/queueflow/models/cancel_jobs_response.py +88 -0
  20. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/complete_job_request.py +1 -1
  21. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_batch_jobs_request.py +1 -1
  22. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_batch_jobs_response.py +1 -1
  23. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_cron_request.py +1 -1
  24. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_cron_response.py +1 -1
  25. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_job_request.py +1 -1
  26. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_job_response.py +1 -1
  27. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_workflow_request.py +1 -1
  28. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/create_workflow_response.py +1 -1
  29. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/cron_schedule.py +1 -1
  30. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/dead_letter.py +1 -1
  31. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/error_body.py +1 -1
  32. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/fail_job_request.py +1 -1
  33. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/health_status.py +1 -1
  34. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/heartbeat_request.py +1 -1
  35. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/heartbeat_response.py +1 -1
  36. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/job.py +1 -1
  37. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/job_config.py +1 -1
  38. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/job_config_request.py +1 -1
  39. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/job_status.py +1 -1
  40. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/lease_jobs_request.py +1 -1
  41. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/lease_jobs_response.py +1 -1
  42. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/leased_job.py +1 -1
  43. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/list_crons_response.py +1 -1
  44. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/list_dead_letters_response.py +1 -1
  45. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/list_jobs_response.py +1 -1
  46. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/list_workflows_response.py +1 -1
  47. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/on_failure.py +1 -1
  48. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/on_success.py +1 -1
  49. queueflow-0.4.0/queueflow/models/queue_control_response.py +94 -0
  50. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/queue_stats.py +5 -3
  51. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/queues_response.py +2 -2
  52. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/ready_status.py +1 -1
  53. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/replay_dead_letter_response.py +1 -1
  54. queueflow-0.4.0/queueflow/models/replay_dead_letters_request.py +121 -0
  55. queueflow-0.4.0/queueflow/models/replay_dead_letters_response.py +98 -0
  56. queueflow-0.4.0/queueflow/models/replayed_dead_letter.py +101 -0
  57. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/stats_snapshot.py +1 -1
  58. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/step_status.py +1 -1
  59. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/tasks_response.py +1 -1
  60. queueflow-0.4.0/queueflow/models/who_am_i_response.py +89 -0
  61. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow.py +1 -1
  62. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow_diagram_response.py +1 -1
  63. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow_status.py +1 -1
  64. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow_step.py +1 -1
  65. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow_step_state.py +1 -1
  66. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/models/workflow_step_states_response.py +1 -1
  67. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/rest.py +1 -1
  68. {queueflow-0.3.0 → queueflow-0.4.0/queueflow.egg-info}/PKG-INFO +57 -10
  69. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow.egg-info/SOURCES.txt +14 -0
  70. {queueflow-0.3.0 → queueflow-0.4.0}/setup.py +2 -2
  71. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_backoff_strategy.py +1 -1
  72. queueflow-0.4.0/test/test_cancel_jobs_request.py +56 -0
  73. queueflow-0.4.0/test/test_cancel_jobs_response.py +52 -0
  74. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_complete_job_request.py +1 -1
  75. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_batch_jobs_request.py +1 -1
  76. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_batch_jobs_response.py +1 -1
  77. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_cron_request.py +1 -1
  78. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_cron_response.py +1 -1
  79. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_job_request.py +1 -1
  80. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_job_response.py +1 -1
  81. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_workflow_request.py +1 -1
  82. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_create_workflow_response.py +1 -1
  83. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_cron_api.py +1 -1
  84. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_cron_schedule.py +1 -1
  85. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_dead_letter.py +1 -1
  86. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_dlq_api.py +7 -1
  87. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_error_body.py +1 -1
  88. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_facade.py +158 -0
  89. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_fail_job_request.py +1 -1
  90. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_health_api.py +1 -1
  91. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_health_status.py +1 -1
  92. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_heartbeat_request.py +1 -1
  93. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_heartbeat_response.py +1 -1
  94. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_job.py +1 -1
  95. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_job_config.py +1 -1
  96. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_job_config_request.py +1 -1
  97. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_job_status.py +1 -1
  98. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_jobs_api.py +7 -1
  99. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_lease_jobs_request.py +1 -1
  100. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_lease_jobs_response.py +1 -1
  101. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_leased_job.py +1 -1
  102. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_list_crons_response.py +1 -1
  103. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_list_dead_letters_response.py +1 -1
  104. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_list_jobs_response.py +1 -1
  105. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_list_workflows_response.py +1 -1
  106. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_live.py +148 -5
  107. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_on_failure.py +1 -1
  108. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_on_success.py +1 -1
  109. queueflow-0.4.0/test/test_queue_control_response.py +58 -0
  110. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_queue_stats.py +2 -1
  111. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_queues_response.py +3 -1
  112. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_ready_status.py +1 -1
  113. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_replay_dead_letter_response.py +1 -1
  114. queueflow-0.4.0/test/test_replay_dead_letters_request.py +55 -0
  115. queueflow-0.4.0/test/test_replay_dead_letters_response.py +64 -0
  116. queueflow-0.4.0/test/test_replayed_dead_letter.py +54 -0
  117. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_stats_snapshot.py +1 -1
  118. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_step_status.py +1 -1
  119. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_system_api.py +19 -1
  120. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_tasks_response.py +1 -1
  121. queueflow-0.4.0/test/test_who_am_i_response.py +54 -0
  122. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_worker_api.py +1 -1
  123. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow.py +1 -1
  124. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow_diagram_response.py +1 -1
  125. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow_status.py +1 -1
  126. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow_step.py +1 -1
  127. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow_step_state.py +1 -1
  128. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflow_step_states_response.py +1 -1
  129. {queueflow-0.3.0 → queueflow-0.4.0}/test/test_workflows_api.py +1 -1
  130. queueflow-0.3.0/queueflow/api/system_api.py +0 -767
  131. {queueflow-0.3.0 → queueflow-0.4.0}/LICENSE +0 -0
  132. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api/__init__.py +0 -0
  133. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/api_response.py +0 -0
  134. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow/py.typed +0 -0
  135. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow.egg-info/dependency_links.txt +0 -0
  136. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow.egg-info/requires.txt +0 -0
  137. {queueflow-0.3.0 → queueflow-0.4.0}/queueflow.egg-info/top_level.txt +0 -0
  138. {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.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(...)` / `get_dead_letter(id)` | Inspect terminally-failed jobs. |
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.queues()` (`GET /api/v1/queues`) returns a `QueueStats` per queue that currently has
255
- non-terminal jobs, so you can decide whether to add workers; idle queues are omitted.
256
- `qf.system.list_queues()` is the raw call.
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, and runs `run_worker` against a dedicated queue. The
302
- tests are marked `live` and skip themselves unless `QUEUEFLOW_URL` is set, so plain `pytest` and
303
- CI stay offline.
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(...)` / `get_dead_letter(id)` | Inspect terminally-failed jobs. |
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.queues()` (`GET /api/v1/queues`) returns a `QueueStats` per queue that currently has
214
- non-terminal jobs, so you can decide whether to add workers; idle queues are omitted.
215
- `qf.system.list_queues()` is the raw call.
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, and runs `run_worker` against a dedicated queue. The
261
- tests are marked `live` and skip themselves unless `QUEUEFLOW_URL` is set, so plain `pytest` and
262
- CI stay offline.
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 \
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "queueflow"
7
- version = "0.3.0"
7
+ version = "0.4.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.2.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.3.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.2.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))