workly 0.2.1__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workly
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Python SDK for Workly, the open-source HTTP task queue
5
5
  Keywords: task queue,background jobs,webhooks,http
6
6
  License-Expression: Apache-2.0
@@ -156,7 +156,7 @@ client.queues.delete("emails") # delete the queue and all its tasks
156
156
 
157
157
  ## Schedules
158
158
 
159
- A schedule calls a URL on a cron timetable; each call is a run, retried and recorded like a task. Schedules are separate from queues. `put` is declarative, like `queues.put`. See [Schedules](https://docs.workly.run/schedules/).
159
+ A schedule calls a URL on a cron timetable; each call is a run, retried and recorded like a task. Schedules are separate from queues. `put` is declarative, like `queues.put`. See [Schedules](https://workly.run/docs/schedules/).
160
160
 
161
161
  ```python
162
162
  schedule = client.schedules.put(
@@ -171,7 +171,7 @@ schedule = client.schedules.put(
171
171
  print(schedule.next_runs) # the next five times
172
172
 
173
173
  run = client.schedules.trigger("nightly-report") # run it now
174
- for run in client.schedules.runs("nightly-report", state="failed"):
174
+ for run in client.runs.list(schedule="nightly-report", state="failed"):
175
175
  print(run.id, run.scheduled_for, run.last_error)
176
176
  client.runs.attempts(run.id)
177
177
  client.runs.retry(run.id) # deliver it again, e.g. a failed or skipped run
@@ -184,7 +184,7 @@ Runs are delivered with `Workly-Run-Id`, `Workly-Schedule` and `Workly-Scheduled
184
184
 
185
185
  ## Task groups
186
186
 
187
- A group tracks a set of tasks and enqueues a callback task once it's closed and they've all finished: a `POST` of the group's status and counts. See [Task groups](https://docs.workly.run/groups/). The callback goes to Workly's `_callbacks` queue unless it names a `queue`.
187
+ A group tracks a set of tasks and enqueues a callback task once it's closed and they've all finished: a `POST` of the group's status and counts. See [Task groups](https://workly.run/docs/groups/). Like every callback, it goes to Workly's `_callbacks` queue.
188
188
 
189
189
  ```python
190
190
  from workly import Callback
@@ -212,6 +212,27 @@ for group in client.groups.list(name="daily-stock-prices", status="failed"):
212
212
  print(group.id, group.finished_at)
213
213
  ```
214
214
 
215
+ ## Callbacks
216
+
217
+ Tasks and schedules take the same `on_complete`: each time the task, or a run of the schedule, succeeds, fails or is cancelled, Workly POSTs a summary of it to the URL. See [Callbacks](https://workly.run/docs/callbacks/).
218
+
219
+ ```python
220
+ client.tasks.enqueue(
221
+ "exports",
222
+ url="https://api.example.com/jobs/export",
223
+ payload={"export_id": 42},
224
+ on_complete=Callback(url="https://api.example.com/jobs/export-done"),
225
+ )
226
+ client.schedules.put(
227
+ "nightly-report",
228
+ cron="0 3 * * *",
229
+ url="https://api.example.com/reports/nightly",
230
+ on_complete={"url": "https://api.example.com/reports/nightly-done"},
231
+ )
232
+ ```
233
+
234
+ The summary arrives as the callback task's JSON body: `task_id`, `queue`, `state`, `attempt_count`, `last_status`, `last_error` and `finished_at` for a task; `run_id`, `schedule` and the same outcome fields for a run.
235
+
215
236
  ## Async
216
237
 
217
238
  `AsyncWorkly` has the same methods, as coroutines:
@@ -136,7 +136,7 @@ client.queues.delete("emails") # delete the queue and all its tasks
136
136
 
137
137
  ## Schedules
138
138
 
139
- A schedule calls a URL on a cron timetable; each call is a run, retried and recorded like a task. Schedules are separate from queues. `put` is declarative, like `queues.put`. See [Schedules](https://docs.workly.run/schedules/).
139
+ A schedule calls a URL on a cron timetable; each call is a run, retried and recorded like a task. Schedules are separate from queues. `put` is declarative, like `queues.put`. See [Schedules](https://workly.run/docs/schedules/).
140
140
 
141
141
  ```python
142
142
  schedule = client.schedules.put(
@@ -151,7 +151,7 @@ schedule = client.schedules.put(
151
151
  print(schedule.next_runs) # the next five times
152
152
 
153
153
  run = client.schedules.trigger("nightly-report") # run it now
154
- for run in client.schedules.runs("nightly-report", state="failed"):
154
+ for run in client.runs.list(schedule="nightly-report", state="failed"):
155
155
  print(run.id, run.scheduled_for, run.last_error)
156
156
  client.runs.attempts(run.id)
157
157
  client.runs.retry(run.id) # deliver it again, e.g. a failed or skipped run
@@ -164,7 +164,7 @@ Runs are delivered with `Workly-Run-Id`, `Workly-Schedule` and `Workly-Scheduled
164
164
 
165
165
  ## Task groups
166
166
 
167
- A group tracks a set of tasks and enqueues a callback task once it's closed and they've all finished: a `POST` of the group's status and counts. See [Task groups](https://docs.workly.run/groups/). The callback goes to Workly's `_callbacks` queue unless it names a `queue`.
167
+ A group tracks a set of tasks and enqueues a callback task once it's closed and they've all finished: a `POST` of the group's status and counts. See [Task groups](https://workly.run/docs/groups/). Like every callback, it goes to Workly's `_callbacks` queue.
168
168
 
169
169
  ```python
170
170
  from workly import Callback
@@ -192,6 +192,27 @@ for group in client.groups.list(name="daily-stock-prices", status="failed"):
192
192
  print(group.id, group.finished_at)
193
193
  ```
194
194
 
195
+ ## Callbacks
196
+
197
+ Tasks and schedules take the same `on_complete`: each time the task, or a run of the schedule, succeeds, fails or is cancelled, Workly POSTs a summary of it to the URL. See [Callbacks](https://workly.run/docs/callbacks/).
198
+
199
+ ```python
200
+ client.tasks.enqueue(
201
+ "exports",
202
+ url="https://api.example.com/jobs/export",
203
+ payload={"export_id": 42},
204
+ on_complete=Callback(url="https://api.example.com/jobs/export-done"),
205
+ )
206
+ client.schedules.put(
207
+ "nightly-report",
208
+ cron="0 3 * * *",
209
+ url="https://api.example.com/reports/nightly",
210
+ on_complete={"url": "https://api.example.com/reports/nightly-done"},
211
+ )
212
+ ```
213
+
214
+ The summary arrives as the callback task's JSON body: `task_id`, `queue`, `state`, `attempt_count`, `last_status`, `last_error` and `finished_at` for a task; `run_id`, `schedule` and the same outcome fields for a run.
215
+
195
216
  ## Async
196
217
 
197
218
  `AsyncWorkly` has the same methods, as coroutines:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "workly"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "Python SDK for Workly, the open-source HTTP task queue"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "workly"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "Python SDK for Workly, the open-source HTTP task queue"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -216,6 +216,7 @@ class AsyncTasks:
216
216
  max_attempts: int | None = None,
217
217
  timeout_seconds: float | None = None,
218
218
  group_id: str | None = None,
219
+ on_complete: Callback | r.CallbackDict | None = None,
219
220
  traceparent: str | None = None,
220
221
  ) -> Task:
221
222
  """Enqueue a task to deliver to `url`.
@@ -224,7 +225,8 @@ class AsyncTasks:
224
225
  created with default settings if it doesn't exist. If
225
226
  `idempotency_key` matches a task already in the queue, that task is
226
227
  returned and nothing is enqueued. With `group_id`, the task joins
227
- that group, which must be open.
228
+ that group, which must be open. `on_complete` is called each time the
229
+ task succeeds, fails or is cancelled, with a `TaskSummary`.
228
230
  """
229
231
  spec = {
230
232
  "url": url,
@@ -239,6 +241,7 @@ class AsyncTasks:
239
241
  "max_attempts": max_attempts,
240
242
  "timeout_seconds": timeout_seconds,
241
243
  "group_id": group_id,
244
+ "on_complete": on_complete,
242
245
  }
243
246
  return Task._from_json(await self._client._send(r.enqueue(queue, spec, traceparent)))
244
247
 
@@ -458,12 +461,15 @@ class AsyncSchedules:
458
461
  retry_on_4xx: bool | None = None,
459
462
  overlap: OverlapPolicy | None = None,
460
463
  missed: MissedPolicy | None = None,
464
+ on_complete: Callback | r.CallbackDict | None = None,
461
465
  ) -> Schedule:
462
466
  """Create or update a schedule that calls `url` on `cron` (read in `timezone`, default UTC).
463
467
 
464
468
  Declarative: the schedule ends up with exactly these settings, and
465
469
  omitted ones are reset to their defaults. It doesn't change whether
466
- the schedule is paused. `next_runs` on the result shows when it runs.
470
+ the schedule is paused. `next_runs` on the result shows when it runs
471
+ (`next_runs_if_resumed` while it is paused). `on_complete` is called
472
+ each time a run succeeds, fails or is cancelled, with a `RunSummary`.
467
473
  """
468
474
  config = dict(
469
475
  cron=cron,
@@ -482,6 +488,7 @@ class AsyncSchedules:
482
488
  retry_on_4xx=retry_on_4xx,
483
489
  overlap=overlap,
484
490
  missed=missed,
491
+ on_complete=on_complete,
485
492
  )
486
493
  return Schedule._from_json(await self._client._send(r.put_schedule(name, config)))
487
494
 
@@ -508,38 +515,44 @@ class AsyncSchedules:
508
515
  """Run the schedule now, even if paused. Doesn't change when it next fires."""
509
516
  return Run._from_json(await self._client._send(r.trigger_schedule(name)))
510
517
 
511
- async def runs(
512
- self, name: str, *, state: RunState | None = None, page_size: int | None = None
518
+
519
+ class AsyncRuns:
520
+ """Runs of schedules: one per firing, with its own attempts."""
521
+
522
+ def __init__(self, client: AsyncWorkly) -> None:
523
+ self._client = client
524
+
525
+ async def list(
526
+ self,
527
+ *,
528
+ schedule: str | None = None,
529
+ state: RunState | None = None,
530
+ page_size: int | None = None,
513
531
  ) -> AsyncIterator[Run]:
514
- """The schedule's runs, newest first, fetching pages as needed."""
532
+ """Runs of every schedule, or of one, newest first, fetching pages as needed."""
515
533
  cursor: str | None = None
516
534
  while True:
517
- page = await self.runs_page(name, state=state, cursor=cursor, limit=page_size)
535
+ page = await self.list_page(
536
+ schedule=schedule, state=state, cursor=cursor, limit=page_size
537
+ )
518
538
  for run in page.runs:
519
539
  yield run
520
540
  if page.next_cursor is None:
521
541
  return
522
542
  cursor = page.next_cursor
523
543
 
524
- async def runs_page(
544
+ async def list_page(
525
545
  self,
526
- name: str,
527
546
  *,
547
+ schedule: str | None = None,
528
548
  state: RunState | None = None,
529
549
  cursor: str | None = None,
530
550
  limit: int | None = None,
531
551
  ) -> RunPage:
532
- """One page of `runs`. Pass the previous page's `next_cursor` as `cursor`."""
533
- req = r.list_runs(name, state=state, cursor=cursor, limit=limit)
552
+ """One page of `list`. Pass the previous page's `next_cursor` as `cursor`."""
553
+ req = r.list_runs(schedule=schedule, state=state, cursor=cursor, limit=limit)
534
554
  return RunPage._from_json(await self._client._send(req))
535
555
 
536
-
537
- class AsyncRuns:
538
- """Runs of schedules: one per firing, with its own attempts."""
539
-
540
- def __init__(self, client: AsyncWorkly) -> None:
541
- self._client = client
542
-
543
556
  async def get(self, run_id: str) -> Run:
544
557
  return Run._from_json(await self._client._send(r.get_run(run_id)))
545
558
 
@@ -216,6 +216,7 @@ class Tasks:
216
216
  max_attempts: int | None = None,
217
217
  timeout_seconds: float | None = None,
218
218
  group_id: str | None = None,
219
+ on_complete: Callback | r.CallbackDict | None = None,
219
220
  traceparent: str | None = None,
220
221
  ) -> Task:
221
222
  """Enqueue a task to deliver to `url`.
@@ -224,7 +225,8 @@ class Tasks:
224
225
  created with default settings if it doesn't exist. If
225
226
  `idempotency_key` matches a task already in the queue, that task is
226
227
  returned and nothing is enqueued. With `group_id`, the task joins
227
- that group, which must be open.
228
+ that group, which must be open. `on_complete` is called each time the
229
+ task succeeds, fails or is cancelled, with a `TaskSummary`.
228
230
  """
229
231
  spec = {
230
232
  "url": url,
@@ -239,6 +241,7 @@ class Tasks:
239
241
  "max_attempts": max_attempts,
240
242
  "timeout_seconds": timeout_seconds,
241
243
  "group_id": group_id,
244
+ "on_complete": on_complete,
242
245
  }
243
246
  return Task._from_json(self._client._send(r.enqueue(queue, spec, traceparent)))
244
247
 
@@ -456,12 +459,15 @@ class Schedules:
456
459
  retry_on_4xx: bool | None = None,
457
460
  overlap: OverlapPolicy | None = None,
458
461
  missed: MissedPolicy | None = None,
462
+ on_complete: Callback | r.CallbackDict | None = None,
459
463
  ) -> Schedule:
460
464
  """Create or update a schedule that calls `url` on `cron` (read in `timezone`, default UTC).
461
465
 
462
466
  Declarative: the schedule ends up with exactly these settings, and
463
467
  omitted ones are reset to their defaults. It doesn't change whether
464
- the schedule is paused. `next_runs` on the result shows when it runs.
468
+ the schedule is paused. `next_runs` on the result shows when it runs
469
+ (`next_runs_if_resumed` while it is paused). `on_complete` is called
470
+ each time a run succeeds, fails or is cancelled, with a `RunSummary`.
465
471
  """
466
472
  config = dict(
467
473
  cron=cron,
@@ -480,6 +486,7 @@ class Schedules:
480
486
  retry_on_4xx=retry_on_4xx,
481
487
  overlap=overlap,
482
488
  missed=missed,
489
+ on_complete=on_complete,
483
490
  )
484
491
  return Schedule._from_json(self._client._send(r.put_schedule(name, config)))
485
492
 
@@ -506,37 +513,41 @@ class Schedules:
506
513
  """Run the schedule now, even if paused. Doesn't change when it next fires."""
507
514
  return Run._from_json(self._client._send(r.trigger_schedule(name)))
508
515
 
509
- def runs(
510
- self, name: str, *, state: RunState | None = None, page_size: int | None = None
516
+
517
+ class Runs:
518
+ """Runs of schedules: one per firing, with its own attempts."""
519
+
520
+ def __init__(self, client: Workly) -> None:
521
+ self._client = client
522
+
523
+ def list(
524
+ self,
525
+ *,
526
+ schedule: str | None = None,
527
+ state: RunState | None = None,
528
+ page_size: int | None = None,
511
529
  ) -> Iterator[Run]:
512
- """The schedule's runs, newest first, fetching pages as needed."""
530
+ """Runs of every schedule, or of one, newest first, fetching pages as needed."""
513
531
  cursor: str | None = None
514
532
  while True:
515
- page = self.runs_page(name, state=state, cursor=cursor, limit=page_size)
533
+ page = self.list_page(schedule=schedule, state=state, cursor=cursor, limit=page_size)
516
534
  yield from page.runs
517
535
  if page.next_cursor is None:
518
536
  return
519
537
  cursor = page.next_cursor
520
538
 
521
- def runs_page(
539
+ def list_page(
522
540
  self,
523
- name: str,
524
541
  *,
542
+ schedule: str | None = None,
525
543
  state: RunState | None = None,
526
544
  cursor: str | None = None,
527
545
  limit: int | None = None,
528
546
  ) -> RunPage:
529
- """One page of `runs`. Pass the previous page's `next_cursor` as `cursor`."""
530
- req = r.list_runs(name, state=state, cursor=cursor, limit=limit)
547
+ """One page of `list`. Pass the previous page's `next_cursor` as `cursor`."""
548
+ req = r.list_runs(schedule=schedule, state=state, cursor=cursor, limit=limit)
531
549
  return RunPage._from_json(self._client._send(req))
532
550
 
533
-
534
- class Runs:
535
- """Runs of schedules: one per firing, with its own attempts."""
536
-
537
- def __init__(self, client: Workly) -> None:
538
- self._client = client
539
-
540
551
  def get(self, run_id: str) -> Run:
541
552
  return Run._from_json(self._client._send(r.get_run(run_id)))
542
553
 
@@ -66,6 +66,10 @@ class QueueActivity:
66
66
  cancelled: int
67
67
  retention_seconds: float | None
68
68
  """How long the server keeps finished tasks; None if forever. Older ones aren't counted."""
69
+ p50_duration_ms: int | None = None
70
+ """Median duration of the latest attempt of the succeeded and failed tasks counted."""
71
+ p95_duration_ms: int | None = None
72
+ """95th percentile of the same durations."""
69
73
 
70
74
  @classmethod
71
75
  def _from_json(cls, d: dict[str, Any]) -> QueueActivity:
@@ -75,6 +79,8 @@ class QueueActivity:
75
79
  failed=d["failed"],
76
80
  cancelled=d["cancelled"],
77
81
  retention_seconds=d.get("retention_seconds"),
82
+ p50_duration_ms=d.get("p50_duration_ms"),
83
+ p95_duration_ms=d.get("p95_duration_ms"),
78
84
  )
79
85
 
80
86
 
@@ -226,9 +232,18 @@ class Task:
226
232
  """When the task was last retried by hand."""
227
233
  group_id: str | None = None
228
234
  """The group the task belongs to, if any."""
235
+ started_at: datetime | None = None
236
+ """When the first delivery attempt started; minus `created_at`, the time spent waiting."""
237
+ last_duration_ms: int | None = None
238
+ """How long the latest finished attempt took."""
239
+ on_complete: Callback | None = None
240
+ """Called each time the task finishes."""
241
+ callback_task_id: str | None = None
242
+ """The callback task enqueued when the task last finished, if it has `on_complete`."""
229
243
 
230
244
  @classmethod
231
245
  def _from_json(cls, d: dict[str, Any]) -> Task:
246
+ cb = d.get("on_complete")
232
247
  return cls(
233
248
  id=d["id"],
234
249
  queue=d["queue"],
@@ -252,6 +267,10 @@ class Task:
252
267
  last_attempt_at=_opt_time(d.get("last_attempt_at")),
253
268
  retried_at=_opt_time(d.get("retried_at")),
254
269
  group_id=d.get("group_id"),
270
+ started_at=_opt_time(d.get("started_at")),
271
+ last_duration_ms=d.get("last_duration_ms"),
272
+ on_complete=None if cb is None else Callback._from_json(cb),
273
+ callback_task_id=d.get("callback_task_id"),
255
274
  )
256
275
 
257
276
 
@@ -376,14 +395,21 @@ class Schedule:
376
395
  next_run_at: datetime | None
377
396
  """When the schedule next fires; None while paused."""
378
397
  next_runs: list[datetime]
379
- """The next five slots (as if resumed now, while paused)."""
398
+ """The next five slots. Empty while paused; see `next_runs_if_resumed`."""
380
399
  last_run: LastRun | None
381
400
  created_at: datetime
382
401
  updated_at: datetime
402
+ paused_at: datetime | None = None
403
+ """When a paused schedule was paused; None while it runs."""
404
+ next_runs_if_resumed: list[datetime] | None = None
405
+ """While paused, the five slots it would run at if resumed now; None while it runs."""
406
+ on_complete: Callback | None = None
407
+ """Called each time a run finishes, except skipped runs."""
383
408
 
384
409
  @classmethod
385
410
  def _from_json(cls, d: dict[str, Any]) -> Schedule:
386
411
  last = d.get("last_run")
412
+ cb = d.get("on_complete")
387
413
  next_runs: list[str] = d.get("next_runs") or []
388
414
  return cls(
389
415
  name=d["name"],
@@ -408,6 +434,13 @@ class Schedule:
408
434
  last_run=None if last is None else LastRun._from_json(last),
409
435
  created_at=_time(d["created_at"]),
410
436
  updated_at=_time(d["updated_at"]),
437
+ paused_at=_opt_time(d.get("paused_at")),
438
+ next_runs_if_resumed=(
439
+ None
440
+ if (resumed := d.get("next_runs_if_resumed")) is None
441
+ else [_time(t) for t in resumed]
442
+ ),
443
+ on_complete=None if cb is None else Callback._from_json(cb),
411
444
  )
412
445
 
413
446
 
@@ -438,6 +471,12 @@ class Run:
438
471
  finished_at: datetime | None
439
472
  last_attempt_at: datetime | None
440
473
  retried_at: datetime | None
474
+ started_at: datetime | None = None
475
+ """When the first delivery attempt started."""
476
+ last_duration_ms: int | None = None
477
+ """How long the latest finished attempt took."""
478
+ callback_task_id: str | None = None
479
+ """The callback task enqueued when the run last finished, if its schedule has `on_complete`."""
441
480
 
442
481
  @classmethod
443
482
  def _from_json(cls, d: dict[str, Any]) -> Run:
@@ -463,6 +502,9 @@ class Run:
463
502
  finished_at=_opt_time(d.get("finished_at")),
464
503
  last_attempt_at=_opt_time(d.get("last_attempt_at")),
465
504
  retried_at=_opt_time(d.get("retried_at")),
505
+ started_at=_opt_time(d.get("started_at")),
506
+ last_duration_ms=d.get("last_duration_ms"),
507
+ callback_task_id=d.get("callback_task_id"),
466
508
  )
467
509
 
468
510
 
@@ -518,22 +560,19 @@ CALLBACK_QUEUE = "_callbacks"
518
560
  class Callback:
519
561
  """A task Workly enqueues when something completes: a POST to `url` saying what completed.
520
562
 
521
- A group's callback has the group's summary as its JSON body. It is
522
- delivered like any task in its queue, with that queue's retries.
563
+ Its JSON body is a `TaskSummary` for a task, a `RunSummary` for a
564
+ schedule's run and a `GroupSummary` for a group. It is delivered as a task
565
+ in the `_callbacks` queue, with that queue's retries: configure it like
566
+ any queue to change them.
523
567
  """
524
568
 
525
569
  url: str
526
- queue: str = CALLBACK_QUEUE
527
- """The queue to enqueue the callback into. `_callbacks`, the default, is
528
- created by Workly; configure it like any queue to change its retries."""
529
570
  headers: Mapping[str, str] = field(default_factory=dict[str, str])
530
571
  """Headers sent with the callback. `Workly-*` headers are reserved."""
531
572
 
532
573
  @classmethod
533
574
  def _from_json(cls, d: dict[str, Any]) -> Callback:
534
- return cls(
535
- url=d["url"], queue=d.get("queue") or CALLBACK_QUEUE, headers=d.get("headers") or {}
536
- )
575
+ return cls(url=d["url"], headers=d.get("headers") or {})
537
576
 
538
577
 
539
578
  GroupCallback = Callback
@@ -15,7 +15,6 @@ from typing import Any, Required, TypedDict
15
15
  from urllib.parse import quote
16
16
 
17
17
  from ._models import (
18
- CALLBACK_QUEUE,
19
18
  AlertChannelType,
20
19
  Callback,
21
20
  GroupStatus,
@@ -75,13 +74,13 @@ class TaskSpec(TypedDict, total=False):
75
74
  max_attempts: int
76
75
  timeout_seconds: float
77
76
  group_id: str
77
+ on_complete: Callback | CallbackDict
78
78
 
79
79
 
80
80
  class CallbackDict(TypedDict, total=False):
81
81
  """A `Callback` as a dict, with the same fields."""
82
82
 
83
83
  url: Required[str]
84
- queue: str
85
84
  headers: Mapping[str, str]
86
85
 
87
86
 
@@ -91,12 +90,10 @@ GroupCallbackSpec = CallbackDict
91
90
 
92
91
  def callback_body(cb: Callback | CallbackDict) -> dict[str, Any]:
93
92
  if isinstance(cb, Callback):
94
- url, queue, headers = cb.url, cb.queue, cb.headers
93
+ url, headers = cb.url, cb.headers
95
94
  else:
96
- url, queue, headers = cb["url"], cb.get("queue", CALLBACK_QUEUE), cb.get("headers", {})
95
+ url, headers = cb["url"], cb.get("headers", {})
97
96
  body: dict[str, Any] = {"url": url}
98
- if queue != CALLBACK_QUEUE:
99
- body["queue"] = queue
100
97
  if headers:
101
98
  body["headers"] = dict(headers)
102
99
  return body
@@ -111,6 +108,8 @@ def task_body(spec: Mapping[str, Any]) -> dict[str, Any]:
111
108
  value = _timestamp(value, "run_at")
112
109
  elif key == "headers":
113
110
  value = dict(value)
111
+ elif key == "on_complete":
112
+ value = callback_body(value)
114
113
  body[key] = value
115
114
  if "payload" in body and "body" in body:
116
115
  raise ValueError("pass payload or body, not both")
@@ -326,7 +325,11 @@ def put_schedule(name: str, config: Mapping[str, Any]) -> Request:
326
325
  for key, value in config.items():
327
326
  if value is None:
328
327
  continue
329
- body[key] = dict(value) if key == "headers" else value
328
+ if key == "headers":
329
+ value = dict(value)
330
+ elif key == "on_complete":
331
+ value = callback_body(value)
332
+ body[key] = value
330
333
  if "payload" in body and "body" in body:
331
334
  raise ValueError("pass payload or body, not both")
332
335
  return Request("PUT", f"/v1/schedules/{_q(name)}", json=body, idempotent=True)
@@ -355,16 +358,18 @@ def trigger_schedule(name: str) -> Request:
355
358
 
356
359
 
357
360
  def list_runs(
358
- name: str, *, state: RunState | None, cursor: str | None, limit: int | None
361
+ *, schedule: str | None, state: RunState | None, cursor: str | None, limit: int | None
359
362
  ) -> Request:
360
363
  params: dict[str, str] = {}
364
+ if schedule is not None:
365
+ params["schedule"] = schedule
361
366
  if state is not None:
362
367
  params["state"] = state
363
368
  if cursor is not None:
364
369
  params["cursor"] = cursor
365
370
  if limit is not None:
366
371
  params["limit"] = str(limit)
367
- return Request("GET", f"/v1/schedules/{_q(name)}/runs", params=params, idempotent=True)
372
+ return Request("GET", "/v1/runs", params=params, idempotent=True)
368
373
 
369
374
 
370
375
  def get_run(run_id: str) -> Request:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes