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.
- {workly-0.2.1 → workly-0.3.0}/PKG-INFO +25 -4
- {workly-0.2.1 → workly-0.3.0}/README.md +24 -3
- {workly-0.2.1 → workly-0.3.0}/pyproject.toml +1 -1
- {workly-0.2.1 → workly-0.3.0}/pyproject.toml.orig +1 -1
- {workly-0.2.1 → workly-0.3.0}/src/workly/_async_client.py +30 -17
- {workly-0.2.1 → workly-0.3.0}/src/workly/_client.py +28 -17
- {workly-0.2.1 → workly-0.3.0}/src/workly/_models.py +48 -9
- {workly-0.2.1 → workly-0.3.0}/src/workly/_requests.py +14 -9
- {workly-0.2.1 → workly-0.3.0}/LICENSE +0 -0
- {workly-0.2.1 → workly-0.3.0}/src/workly/__init__.py +0 -0
- {workly-0.2.1 → workly-0.3.0}/src/workly/_errors.py +0 -0
- {workly-0.2.1 → workly-0.3.0}/src/workly/_signing.py +0 -0
- {workly-0.2.1 → workly-0.3.0}/src/workly/_transport.py +0 -0
- {workly-0.2.1 → workly-0.3.0}/src/workly/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workly
|
|
3
|
-
Version: 0.
|
|
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://
|
|
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.
|
|
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://
|
|
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://
|
|
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.
|
|
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://
|
|
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:
|
|
@@ -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
|
-
|
|
512
|
-
|
|
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
|
-
"""
|
|
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.
|
|
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
|
|
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 `
|
|
533
|
-
req = r.list_runs(
|
|
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
|
-
|
|
510
|
-
|
|
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
|
-
"""
|
|
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.
|
|
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
|
|
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 `
|
|
530
|
-
req = r.list_runs(
|
|
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
|
|
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
|
-
|
|
522
|
-
|
|
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,
|
|
93
|
+
url, headers = cb.url, cb.headers
|
|
95
94
|
else:
|
|
96
|
-
url,
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|