workly 0.1.0__py3-none-any.whl
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/__init__.py +114 -0
- workly/_async_client.py +494 -0
- workly/_client.py +490 -0
- workly/_errors.py +70 -0
- workly/_models.py +502 -0
- workly/_requests.py +322 -0
- workly/_signing.py +99 -0
- workly/_transport.py +59 -0
- workly/py.typed +0 -0
- workly-0.1.0.dist-info/METADATA +216 -0
- workly-0.1.0.dist-info/RECORD +13 -0
- workly-0.1.0.dist-info/WHEEL +4 -0
- workly-0.1.0.dist-info/licenses/LICENSE +202 -0
workly/_models.py
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
"""Response types. They mirror the schemas in api/openapi.yaml.
|
|
2
|
+
|
|
3
|
+
Unknown fields are ignored, so newer servers don't break older SDKs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
TaskState = Literal["pending", "running", "succeeded", "failed", "cancelled"]
|
|
13
|
+
HTTPMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE"]
|
|
14
|
+
AttemptOutcome = Literal["success", "http_error", "timeout", "network_error", "lost"]
|
|
15
|
+
RunState = Literal["pending", "running", "succeeded", "failed", "cancelled", "skipped"]
|
|
16
|
+
RunTrigger = Literal["schedule", "manual"]
|
|
17
|
+
SkipReason = Literal["overlap", "missed"]
|
|
18
|
+
OverlapPolicy = Literal["skip", "allow"]
|
|
19
|
+
MissedPolicy = Literal["skip", "catch_up"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _time(value: str) -> datetime:
|
|
23
|
+
return datetime.fromisoformat(value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _opt_time(value: str | None) -> datetime | None:
|
|
27
|
+
return None if value is None else _time(value)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class QueueStats:
|
|
32
|
+
"""A queue's current work. Counts stop at 10,000."""
|
|
33
|
+
|
|
34
|
+
due: int
|
|
35
|
+
scheduled: int
|
|
36
|
+
running: int
|
|
37
|
+
failed: int
|
|
38
|
+
"""Failed tasks still retained by the server."""
|
|
39
|
+
oldest_due_at: datetime | None
|
|
40
|
+
"""When the longest-waiting due task became due; None when nothing is due."""
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def _from_json(cls, d: dict[str, Any]) -> QueueStats:
|
|
44
|
+
return cls(
|
|
45
|
+
due=d["due"],
|
|
46
|
+
scheduled=d["scheduled"],
|
|
47
|
+
running=d["running"],
|
|
48
|
+
failed=d.get("failed", 0),
|
|
49
|
+
oldest_due_at=_opt_time(d.get("oldest_due_at")),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class QueueActivity:
|
|
55
|
+
"""A queue's finished tasks since `since`, by final state. Counts stop at 10,000."""
|
|
56
|
+
|
|
57
|
+
since: datetime
|
|
58
|
+
succeeded: int
|
|
59
|
+
failed: int
|
|
60
|
+
cancelled: int
|
|
61
|
+
retention_seconds: float | None
|
|
62
|
+
"""How long the server keeps finished tasks; None if forever. Older ones aren't counted."""
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def _from_json(cls, d: dict[str, Any]) -> QueueActivity:
|
|
66
|
+
return cls(
|
|
67
|
+
since=_time(d["since"]),
|
|
68
|
+
succeeded=d["succeeded"],
|
|
69
|
+
failed=d["failed"],
|
|
70
|
+
cancelled=d["cancelled"],
|
|
71
|
+
retention_seconds=d.get("retention_seconds"),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True, slots=True)
|
|
76
|
+
class CronPreview:
|
|
77
|
+
"""When a cron timetable would run, from now."""
|
|
78
|
+
|
|
79
|
+
next_runs: list[datetime]
|
|
80
|
+
interval_seconds: int
|
|
81
|
+
"""The shortest time between two of its runs."""
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def _from_json(cls, d: dict[str, Any]) -> CronPreview:
|
|
85
|
+
return cls(
|
|
86
|
+
next_runs=[_time(t) for t in d["next_runs"]], interval_seconds=d["interval_seconds"]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
ActivityRange = Literal["1h", "24h", "7d"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True, slots=True)
|
|
94
|
+
class ActivityBucket:
|
|
95
|
+
"""Tasks created from `start` until the next bucket, by queue."""
|
|
96
|
+
|
|
97
|
+
start: datetime
|
|
98
|
+
counts: dict[str, int]
|
|
99
|
+
"""Queues with none are left out."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True, slots=True)
|
|
103
|
+
class Activity:
|
|
104
|
+
"""The workspace's tasks created over time: every bucket of the range, oldest first."""
|
|
105
|
+
|
|
106
|
+
range: ActivityRange
|
|
107
|
+
bucket_seconds: int
|
|
108
|
+
buckets: list[ActivityBucket]
|
|
109
|
+
retention_seconds: float | None
|
|
110
|
+
"""How long the server keeps finished tasks; None if forever. Older ones aren't counted."""
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def _from_json(cls, d: dict[str, Any]) -> Activity:
|
|
114
|
+
return cls(
|
|
115
|
+
range=d["range"],
|
|
116
|
+
bucket_seconds=d["bucket_seconds"],
|
|
117
|
+
buckets=[
|
|
118
|
+
ActivityBucket(start=_time(b["start"]), counts=dict(b["counts"]))
|
|
119
|
+
for b in d["buckets"]
|
|
120
|
+
],
|
|
121
|
+
retention_seconds=d.get("retention_seconds"),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True, slots=True)
|
|
126
|
+
class FailureGroup:
|
|
127
|
+
"""Failed tasks in a queue whose latest attempt ended with the same error."""
|
|
128
|
+
|
|
129
|
+
error: str
|
|
130
|
+
"""The tasks' `last_error`. Pass it as `last_error` to list or retry them."""
|
|
131
|
+
count: int
|
|
132
|
+
outcome: AttemptOutcome | None
|
|
133
|
+
status_code: int | None
|
|
134
|
+
"""None if there was no response."""
|
|
135
|
+
last_failed_at: datetime
|
|
136
|
+
latest_task_id: str
|
|
137
|
+
"""The group's most recently failed task."""
|
|
138
|
+
response_body: str | None
|
|
139
|
+
"""The start of the latest task's last response body, if it had one."""
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def _from_json(cls, d: dict[str, Any]) -> FailureGroup:
|
|
143
|
+
return cls(
|
|
144
|
+
error=d["error"],
|
|
145
|
+
count=d["count"],
|
|
146
|
+
outcome=d.get("outcome"),
|
|
147
|
+
status_code=d.get("status_code"),
|
|
148
|
+
last_failed_at=_time(d["last_failed_at"]),
|
|
149
|
+
latest_task_id=d["latest_task_id"],
|
|
150
|
+
response_body=d.get("response_body"),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclass(frozen=True, slots=True)
|
|
155
|
+
class Queue:
|
|
156
|
+
name: str
|
|
157
|
+
paused: bool
|
|
158
|
+
max_attempts: int
|
|
159
|
+
min_backoff_seconds: float
|
|
160
|
+
max_backoff_seconds: float
|
|
161
|
+
backoff_multiplier: float
|
|
162
|
+
timeout_seconds: float
|
|
163
|
+
retry_on_4xx: bool
|
|
164
|
+
rate_per_second: float | None
|
|
165
|
+
max_concurrency: int
|
|
166
|
+
stats: QueueStats | None
|
|
167
|
+
created_at: datetime
|
|
168
|
+
updated_at: datetime
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def _from_json(cls, d: dict[str, Any]) -> Queue:
|
|
172
|
+
stats = d.get("stats")
|
|
173
|
+
return cls(
|
|
174
|
+
name=d["name"],
|
|
175
|
+
paused=d["paused"],
|
|
176
|
+
max_attempts=d["max_attempts"],
|
|
177
|
+
min_backoff_seconds=d["min_backoff_seconds"],
|
|
178
|
+
max_backoff_seconds=d["max_backoff_seconds"],
|
|
179
|
+
backoff_multiplier=d["backoff_multiplier"],
|
|
180
|
+
timeout_seconds=d["timeout_seconds"],
|
|
181
|
+
retry_on_4xx=d["retry_on_4xx"],
|
|
182
|
+
rate_per_second=d.get("rate_per_second"),
|
|
183
|
+
max_concurrency=d["max_concurrency"],
|
|
184
|
+
stats=None if stats is None else QueueStats._from_json(stats),
|
|
185
|
+
created_at=_time(d["created_at"]),
|
|
186
|
+
updated_at=_time(d["updated_at"]),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(frozen=True, slots=True)
|
|
191
|
+
class Task:
|
|
192
|
+
id: str
|
|
193
|
+
queue: str
|
|
194
|
+
state: TaskState
|
|
195
|
+
method: HTTPMethod
|
|
196
|
+
url: str
|
|
197
|
+
headers: dict[str, str]
|
|
198
|
+
payload: Any
|
|
199
|
+
"""The JSON body, decoded. None when the task has a raw `body` (or no body)."""
|
|
200
|
+
body: str | None
|
|
201
|
+
run_at: datetime
|
|
202
|
+
"""When the task is next eligible for delivery."""
|
|
203
|
+
attempt_count: int
|
|
204
|
+
max_attempts: int | None
|
|
205
|
+
"""Per-task override, if set."""
|
|
206
|
+
timeout_seconds: float | None
|
|
207
|
+
"""Per-task override, if set."""
|
|
208
|
+
idempotency_key: str | None
|
|
209
|
+
last_status: int | None
|
|
210
|
+
last_outcome: AttemptOutcome | None
|
|
211
|
+
"""How the latest attempt ended; None before the first attempt."""
|
|
212
|
+
last_error: str | None
|
|
213
|
+
created_at: datetime
|
|
214
|
+
updated_at: datetime
|
|
215
|
+
finished_at: datetime | None
|
|
216
|
+
"""When the task succeeded, failed or was cancelled."""
|
|
217
|
+
last_attempt_at: datetime | None
|
|
218
|
+
"""When the latest attempt finished."""
|
|
219
|
+
retried_at: datetime | None
|
|
220
|
+
"""When the task was last retried by hand."""
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def _from_json(cls, d: dict[str, Any]) -> Task:
|
|
224
|
+
return cls(
|
|
225
|
+
id=d["id"],
|
|
226
|
+
queue=d["queue"],
|
|
227
|
+
state=d["state"],
|
|
228
|
+
method=d["method"],
|
|
229
|
+
url=d["url"],
|
|
230
|
+
headers=d.get("headers") or {},
|
|
231
|
+
payload=d.get("payload"),
|
|
232
|
+
body=d.get("body"),
|
|
233
|
+
run_at=_time(d["run_at"]),
|
|
234
|
+
attempt_count=d["attempt_count"],
|
|
235
|
+
max_attempts=d.get("max_attempts"),
|
|
236
|
+
timeout_seconds=d.get("timeout_seconds"),
|
|
237
|
+
idempotency_key=d.get("idempotency_key"),
|
|
238
|
+
last_status=d.get("last_status"),
|
|
239
|
+
last_outcome=d.get("last_outcome"),
|
|
240
|
+
last_error=d.get("last_error"),
|
|
241
|
+
created_at=_time(d["created_at"]),
|
|
242
|
+
updated_at=_time(d["updated_at"]),
|
|
243
|
+
finished_at=_opt_time(d.get("finished_at")),
|
|
244
|
+
last_attempt_at=_opt_time(d.get("last_attempt_at")),
|
|
245
|
+
retried_at=_opt_time(d.get("retried_at")),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@dataclass(frozen=True, slots=True)
|
|
250
|
+
class Delivery:
|
|
251
|
+
"""The HTTP request that delivers a task or run, freshly signed."""
|
|
252
|
+
|
|
253
|
+
method: HTTPMethod
|
|
254
|
+
url: str
|
|
255
|
+
headers: dict[str, str]
|
|
256
|
+
"""Every header Workly sets, including the task's own and `Workly-Signature`."""
|
|
257
|
+
body: str
|
|
258
|
+
"""The body exactly as sent; the signature covers it."""
|
|
259
|
+
|
|
260
|
+
@classmethod
|
|
261
|
+
def _from_json(cls, d: dict[str, Any]) -> Delivery:
|
|
262
|
+
return cls(method=d["method"], url=d["url"], headers=d.get("headers") or {}, body=d["body"])
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dataclass(frozen=True, slots=True)
|
|
266
|
+
class Attempt:
|
|
267
|
+
id: str
|
|
268
|
+
number: int
|
|
269
|
+
outcome: AttemptOutcome
|
|
270
|
+
"""`lost` means the result is unknown and the task is delivered again."""
|
|
271
|
+
status_code: int | None
|
|
272
|
+
error: str | None
|
|
273
|
+
response_body: str | None
|
|
274
|
+
"""The first 4 KB of the response body."""
|
|
275
|
+
response_headers: dict[str, str]
|
|
276
|
+
started_at: datetime
|
|
277
|
+
finished_at: datetime
|
|
278
|
+
duration_ms: int
|
|
279
|
+
retried_at: datetime | None
|
|
280
|
+
"""Set when a manual retry came just before this attempt: when it happened."""
|
|
281
|
+
|
|
282
|
+
@classmethod
|
|
283
|
+
def _from_json(cls, d: dict[str, Any]) -> Attempt:
|
|
284
|
+
return cls(
|
|
285
|
+
id=d["id"],
|
|
286
|
+
number=d["number"],
|
|
287
|
+
outcome=d["outcome"],
|
|
288
|
+
status_code=d.get("status_code"),
|
|
289
|
+
error=d.get("error"),
|
|
290
|
+
response_body=d.get("response_body"),
|
|
291
|
+
response_headers=d.get("response_headers") or {},
|
|
292
|
+
started_at=_time(d["started_at"]),
|
|
293
|
+
finished_at=_time(d["finished_at"]),
|
|
294
|
+
duration_ms=d["duration_ms"],
|
|
295
|
+
retried_at=_opt_time(d.get("retried_at")),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@dataclass(frozen=True, slots=True)
|
|
300
|
+
class TaskPage:
|
|
301
|
+
tasks: list[Task]
|
|
302
|
+
next_cursor: str | None
|
|
303
|
+
"""Pass as `cursor` to get the next page. None on the last page."""
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def _from_json(cls, d: dict[str, Any]) -> TaskPage:
|
|
307
|
+
return cls(
|
|
308
|
+
tasks=[Task._from_json(t) for t in d["tasks"]],
|
|
309
|
+
next_cursor=d.get("next_cursor"),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@dataclass(frozen=True, slots=True)
|
|
314
|
+
class EnqueueResult:
|
|
315
|
+
created: bool
|
|
316
|
+
"""False if a task with the same idempotency key already existed."""
|
|
317
|
+
task: Task
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@dataclass(frozen=True, slots=True)
|
|
321
|
+
class LastRun:
|
|
322
|
+
"""A schedule's most recent run."""
|
|
323
|
+
|
|
324
|
+
id: str
|
|
325
|
+
state: RunState
|
|
326
|
+
trigger: RunTrigger
|
|
327
|
+
scheduled_for: datetime
|
|
328
|
+
finished_at: datetime | None
|
|
329
|
+
last_status: int | None
|
|
330
|
+
last_error: str | None
|
|
331
|
+
|
|
332
|
+
@classmethod
|
|
333
|
+
def _from_json(cls, d: dict[str, Any]) -> LastRun:
|
|
334
|
+
return cls(
|
|
335
|
+
id=d["id"],
|
|
336
|
+
state=d["state"],
|
|
337
|
+
trigger=d["trigger"],
|
|
338
|
+
scheduled_for=_time(d["scheduled_for"]),
|
|
339
|
+
finished_at=_opt_time(d.get("finished_at")),
|
|
340
|
+
last_status=d.get("last_status"),
|
|
341
|
+
last_error=d.get("last_error"),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@dataclass(frozen=True, slots=True)
|
|
346
|
+
class Schedule:
|
|
347
|
+
"""A cron schedule that calls `url`. Separate from queues: its calls are runs."""
|
|
348
|
+
|
|
349
|
+
name: str
|
|
350
|
+
cron: str
|
|
351
|
+
timezone: str
|
|
352
|
+
method: HTTPMethod
|
|
353
|
+
url: str
|
|
354
|
+
headers: dict[str, str]
|
|
355
|
+
payload: Any
|
|
356
|
+
"""The JSON body, decoded. None when the schedule has a raw `body` (or no body)."""
|
|
357
|
+
body: str | None
|
|
358
|
+
timeout_seconds: float
|
|
359
|
+
max_attempts: int
|
|
360
|
+
min_backoff_seconds: float
|
|
361
|
+
max_backoff_seconds: float
|
|
362
|
+
backoff_multiplier: float
|
|
363
|
+
retry_on_4xx: bool
|
|
364
|
+
overlap: OverlapPolicy
|
|
365
|
+
missed: MissedPolicy
|
|
366
|
+
paused: bool
|
|
367
|
+
next_run_at: datetime | None
|
|
368
|
+
"""When the schedule next fires; None while paused."""
|
|
369
|
+
next_runs: list[datetime]
|
|
370
|
+
"""The next five slots (as if resumed now, while paused)."""
|
|
371
|
+
last_run: LastRun | None
|
|
372
|
+
created_at: datetime
|
|
373
|
+
updated_at: datetime
|
|
374
|
+
|
|
375
|
+
@classmethod
|
|
376
|
+
def _from_json(cls, d: dict[str, Any]) -> Schedule:
|
|
377
|
+
last = d.get("last_run")
|
|
378
|
+
next_runs: list[str] = d.get("next_runs") or []
|
|
379
|
+
return cls(
|
|
380
|
+
name=d["name"],
|
|
381
|
+
cron=d["cron"],
|
|
382
|
+
timezone=d["timezone"],
|
|
383
|
+
method=d["method"],
|
|
384
|
+
url=d["url"],
|
|
385
|
+
headers=d.get("headers") or {},
|
|
386
|
+
payload=d.get("payload"),
|
|
387
|
+
body=d.get("body"),
|
|
388
|
+
timeout_seconds=d["timeout_seconds"],
|
|
389
|
+
max_attempts=d["max_attempts"],
|
|
390
|
+
min_backoff_seconds=d["min_backoff_seconds"],
|
|
391
|
+
max_backoff_seconds=d["max_backoff_seconds"],
|
|
392
|
+
backoff_multiplier=d["backoff_multiplier"],
|
|
393
|
+
retry_on_4xx=d["retry_on_4xx"],
|
|
394
|
+
overlap=d["overlap"],
|
|
395
|
+
missed=d["missed"],
|
|
396
|
+
paused=d["paused"],
|
|
397
|
+
next_run_at=_opt_time(d.get("next_run_at")),
|
|
398
|
+
next_runs=[_time(t) for t in next_runs],
|
|
399
|
+
last_run=None if last is None else LastRun._from_json(last),
|
|
400
|
+
created_at=_time(d["created_at"]),
|
|
401
|
+
updated_at=_time(d["updated_at"]),
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@dataclass(frozen=True, slots=True)
|
|
406
|
+
class Run:
|
|
407
|
+
"""One firing of a schedule, delivered and retried like a task."""
|
|
408
|
+
|
|
409
|
+
id: str
|
|
410
|
+
schedule: str
|
|
411
|
+
state: RunState
|
|
412
|
+
trigger: RunTrigger
|
|
413
|
+
scheduled_for: datetime
|
|
414
|
+
"""The cron slot, or when a manual run was triggered."""
|
|
415
|
+
skip_reason: SkipReason | None
|
|
416
|
+
"""Why a `skipped` run wasn't delivered."""
|
|
417
|
+
method: HTTPMethod
|
|
418
|
+
url: str
|
|
419
|
+
headers: dict[str, str]
|
|
420
|
+
payload: Any
|
|
421
|
+
body: str | None
|
|
422
|
+
run_at: datetime
|
|
423
|
+
attempt_count: int
|
|
424
|
+
last_status: int | None
|
|
425
|
+
last_outcome: AttemptOutcome | None
|
|
426
|
+
last_error: str | None
|
|
427
|
+
created_at: datetime
|
|
428
|
+
updated_at: datetime
|
|
429
|
+
finished_at: datetime | None
|
|
430
|
+
last_attempt_at: datetime | None
|
|
431
|
+
retried_at: datetime | None
|
|
432
|
+
|
|
433
|
+
@classmethod
|
|
434
|
+
def _from_json(cls, d: dict[str, Any]) -> Run:
|
|
435
|
+
return cls(
|
|
436
|
+
id=d["id"],
|
|
437
|
+
schedule=d["schedule"],
|
|
438
|
+
state=d["state"],
|
|
439
|
+
trigger=d["trigger"],
|
|
440
|
+
scheduled_for=_time(d["scheduled_for"]),
|
|
441
|
+
skip_reason=d.get("skip_reason"),
|
|
442
|
+
method=d["method"],
|
|
443
|
+
url=d["url"],
|
|
444
|
+
headers=d.get("headers") or {},
|
|
445
|
+
payload=d.get("payload"),
|
|
446
|
+
body=d.get("body"),
|
|
447
|
+
run_at=_time(d["run_at"]),
|
|
448
|
+
attempt_count=d["attempt_count"],
|
|
449
|
+
last_status=d.get("last_status"),
|
|
450
|
+
last_outcome=d.get("last_outcome"),
|
|
451
|
+
last_error=d.get("last_error"),
|
|
452
|
+
created_at=_time(d["created_at"]),
|
|
453
|
+
updated_at=_time(d["updated_at"]),
|
|
454
|
+
finished_at=_opt_time(d.get("finished_at")),
|
|
455
|
+
last_attempt_at=_opt_time(d.get("last_attempt_at")),
|
|
456
|
+
retried_at=_opt_time(d.get("retried_at")),
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@dataclass(frozen=True, slots=True)
|
|
461
|
+
class RunPage:
|
|
462
|
+
runs: list[Run]
|
|
463
|
+
next_cursor: str | None
|
|
464
|
+
"""Pass as `cursor` to get the next page. None on the last page."""
|
|
465
|
+
|
|
466
|
+
@classmethod
|
|
467
|
+
def _from_json(cls, d: dict[str, Any]) -> RunPage:
|
|
468
|
+
return cls(runs=[Run._from_json(r) for r in d["runs"]], next_cursor=d.get("next_cursor"))
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
AlertChannelType = Literal["email", "slack"]
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
@dataclass(frozen=True, slots=True)
|
|
475
|
+
class AlertChannel:
|
|
476
|
+
"""Where the workspace's alerts about failed tasks and runs go."""
|
|
477
|
+
|
|
478
|
+
id: str
|
|
479
|
+
type: AlertChannelType
|
|
480
|
+
target: str
|
|
481
|
+
"""The email address, or the Slack webhook URL with its secret part hidden."""
|
|
482
|
+
created_at: datetime
|
|
483
|
+
|
|
484
|
+
@classmethod
|
|
485
|
+
def _from_json(cls, d: dict[str, Any]) -> AlertChannel:
|
|
486
|
+
return cls(
|
|
487
|
+
id=d["id"], type=d["type"], target=d["target"], created_at=_time(d["created_at"])
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@dataclass(frozen=True, slots=True)
|
|
492
|
+
class AlertChannelList:
|
|
493
|
+
channels: list[AlertChannel]
|
|
494
|
+
email_enabled: bool
|
|
495
|
+
"""Whether the server can send email, so email channels can be added."""
|
|
496
|
+
|
|
497
|
+
@classmethod
|
|
498
|
+
def _from_json(cls, d: dict[str, Any]) -> AlertChannelList:
|
|
499
|
+
return cls(
|
|
500
|
+
channels=[AlertChannel._from_json(c) for c in d["channels"]],
|
|
501
|
+
email_enabled=d["email_enabled"],
|
|
502
|
+
)
|