workhold-admin 1.0.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.
- workhold_admin/__init__.py +140 -0
- workhold_admin/admin.py +573 -0
- workhold_admin/async_client.py +1071 -0
- workhold_admin/async_pagination.py +453 -0
- workhold_admin/break_glass.py +291 -0
- workhold_admin/models.py +1516 -0
- workhold_admin/observer.py +250 -0
- workhold_admin/pagination.py +478 -0
- workhold_admin-1.0.0.dist-info/METADATA +10 -0
- workhold_admin-1.0.0.dist-info/RECORD +11 -0
- workhold_admin-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""Bounded lazy async pagination for Observer/Admin cursor-list operations.
|
|
2
|
+
|
|
3
|
+
Mirrors :mod:`workhold_admin.pagination` with async fetch callables.
|
|
4
|
+
Checks task cancellation before each page request so a cancelled consumer
|
|
5
|
+
issues zero subsequent requests.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Generic, TypeVar
|
|
15
|
+
|
|
16
|
+
from workhold_admin.async_client import AsyncAdminClient, AsyncObserverClient
|
|
17
|
+
from workhold_admin.models import (
|
|
18
|
+
PAGE_LIMIT_DEFAULT,
|
|
19
|
+
AttemptPage,
|
|
20
|
+
AuditPage,
|
|
21
|
+
DeadLetterPage,
|
|
22
|
+
QueuePage,
|
|
23
|
+
TaskPage,
|
|
24
|
+
)
|
|
25
|
+
from workhold_admin.pagination import CursorPage, _effective_max_pages, _validate_bounds
|
|
26
|
+
|
|
27
|
+
P = TypeVar("P", bound=CursorPage)
|
|
28
|
+
T = TypeVar("T")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def _raise_if_cancelled() -> None:
|
|
32
|
+
"""Honor cancellation before issuing another page request."""
|
|
33
|
+
|
|
34
|
+
await asyncio.sleep(0)
|
|
35
|
+
task = asyncio.current_task()
|
|
36
|
+
if task is not None and task.cancelled():
|
|
37
|
+
raise asyncio.CancelledError
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(slots=True)
|
|
41
|
+
class AsyncBoundedPageIterator(Generic[P]):
|
|
42
|
+
"""Async lazy page iterator with explicit page/item ceilings."""
|
|
43
|
+
|
|
44
|
+
_fetch: Callable[[str | None], Awaitable[P]]
|
|
45
|
+
_max_pages: int | None
|
|
46
|
+
_max_items: int | None
|
|
47
|
+
_cursor: str | None = None
|
|
48
|
+
_pages_fetched: int = 0
|
|
49
|
+
_items_seen: int = 0
|
|
50
|
+
_last_cursor: str | None = None
|
|
51
|
+
_stopped: bool = False
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def last_cursor(self) -> str | None:
|
|
55
|
+
return self._last_cursor
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def page_count(self) -> int:
|
|
59
|
+
return self._pages_fetched
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def item_count(self) -> int:
|
|
63
|
+
return self._items_seen
|
|
64
|
+
|
|
65
|
+
def __aiter__(self) -> AsyncIterator[P]:
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
async def __anext__(self) -> P:
|
|
69
|
+
if self._stopped:
|
|
70
|
+
raise StopAsyncIteration
|
|
71
|
+
if self._max_pages is not None and self._pages_fetched >= self._max_pages:
|
|
72
|
+
self._stopped = True
|
|
73
|
+
raise StopAsyncIteration
|
|
74
|
+
if self._max_items is not None and self._items_seen >= self._max_items:
|
|
75
|
+
self._stopped = True
|
|
76
|
+
raise StopAsyncIteration
|
|
77
|
+
|
|
78
|
+
await _raise_if_cancelled()
|
|
79
|
+
page = await self._fetch(self._cursor)
|
|
80
|
+
self._pages_fetched += 1
|
|
81
|
+
self._last_cursor = page.next_cursor
|
|
82
|
+
self._items_seen += len(page.items)
|
|
83
|
+
|
|
84
|
+
if page.next_cursor is None:
|
|
85
|
+
self._stopped = True
|
|
86
|
+
else:
|
|
87
|
+
self._cursor = page.next_cursor
|
|
88
|
+
return page
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(slots=True)
|
|
92
|
+
class AsyncBoundedItemIterator(Generic[T]):
|
|
93
|
+
"""Async lazy item iterator with the same ceilings and cancel checks."""
|
|
94
|
+
|
|
95
|
+
_fetch: Callable[[str | None], Awaitable[CursorPage]]
|
|
96
|
+
_max_pages: int | None
|
|
97
|
+
_max_items: int | None
|
|
98
|
+
_cursor: str | None = None
|
|
99
|
+
_pages_fetched: int = 0
|
|
100
|
+
_items_yielded: int = 0
|
|
101
|
+
_last_cursor: str | None = None
|
|
102
|
+
_pending: list[T] = field(default_factory=list)
|
|
103
|
+
_pages_exhausted: bool = False
|
|
104
|
+
_stopped: bool = False
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def last_cursor(self) -> str | None:
|
|
108
|
+
return self._last_cursor
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def page_count(self) -> int:
|
|
112
|
+
return self._pages_fetched
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def item_count(self) -> int:
|
|
116
|
+
return self._items_yielded
|
|
117
|
+
|
|
118
|
+
def __aiter__(self) -> AsyncIterator[T]:
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
async def __anext__(self) -> T:
|
|
122
|
+
while True:
|
|
123
|
+
if self._pending:
|
|
124
|
+
item = self._pending.pop(0)
|
|
125
|
+
self._items_yielded += 1
|
|
126
|
+
if self._max_items is not None and self._items_yielded >= self._max_items:
|
|
127
|
+
self._pending.clear()
|
|
128
|
+
self._stopped = True
|
|
129
|
+
return item
|
|
130
|
+
|
|
131
|
+
if self._stopped or self._pages_exhausted:
|
|
132
|
+
self._stopped = True
|
|
133
|
+
raise StopAsyncIteration
|
|
134
|
+
if self._max_pages is not None and self._pages_fetched >= self._max_pages:
|
|
135
|
+
self._stopped = True
|
|
136
|
+
raise StopAsyncIteration
|
|
137
|
+
if self._max_items is not None and self._items_yielded >= self._max_items:
|
|
138
|
+
self._stopped = True
|
|
139
|
+
raise StopAsyncIteration
|
|
140
|
+
|
|
141
|
+
await _raise_if_cancelled()
|
|
142
|
+
page = await self._fetch(self._cursor)
|
|
143
|
+
self._pages_fetched += 1
|
|
144
|
+
self._last_cursor = page.next_cursor
|
|
145
|
+
self._pending = list(page.items) # type: ignore[arg-type]
|
|
146
|
+
if page.next_cursor is None:
|
|
147
|
+
self._pages_exhausted = True
|
|
148
|
+
else:
|
|
149
|
+
self._cursor = page.next_cursor
|
|
150
|
+
if not self._pending:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def async_bounded_page_iterator(
|
|
155
|
+
fetch: Callable[[str | None], Awaitable[P]],
|
|
156
|
+
*,
|
|
157
|
+
max_pages: int | None = None,
|
|
158
|
+
max_items: int | None = None,
|
|
159
|
+
cursor: str | None = None,
|
|
160
|
+
) -> AsyncBoundedPageIterator[P]:
|
|
161
|
+
_validate_bounds(max_pages=max_pages, max_items=max_items)
|
|
162
|
+
return AsyncBoundedPageIterator(
|
|
163
|
+
_fetch=fetch,
|
|
164
|
+
_max_pages=_effective_max_pages(max_pages, max_items),
|
|
165
|
+
_max_items=max_items,
|
|
166
|
+
_cursor=cursor,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def async_bounded_item_iterator(
|
|
171
|
+
fetch: Callable[[str | None], Awaitable[CursorPage]],
|
|
172
|
+
*,
|
|
173
|
+
max_pages: int | None = None,
|
|
174
|
+
max_items: int | None = None,
|
|
175
|
+
cursor: str | None = None,
|
|
176
|
+
) -> AsyncBoundedItemIterator[T]:
|
|
177
|
+
_validate_bounds(max_pages=max_pages, max_items=max_items)
|
|
178
|
+
return AsyncBoundedItemIterator(
|
|
179
|
+
_fetch=fetch,
|
|
180
|
+
_max_pages=_effective_max_pages(max_pages, max_items),
|
|
181
|
+
_max_items=max_items,
|
|
182
|
+
_cursor=cursor,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def iter_task_attempt_pages(
|
|
187
|
+
client: AsyncObserverClient,
|
|
188
|
+
task_id: str,
|
|
189
|
+
*,
|
|
190
|
+
max_pages: int | None = None,
|
|
191
|
+
max_items: int | None = None,
|
|
192
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
193
|
+
cursor: str | None = None,
|
|
194
|
+
) -> AsyncBoundedPageIterator[AttemptPage]:
|
|
195
|
+
return async_bounded_page_iterator(
|
|
196
|
+
lambda cur: client.list_task_attempts(task_id, cursor=cur, limit=limit),
|
|
197
|
+
max_pages=max_pages,
|
|
198
|
+
max_items=max_items,
|
|
199
|
+
cursor=cursor,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def iter_task_attempts(
|
|
204
|
+
client: AsyncObserverClient,
|
|
205
|
+
task_id: str,
|
|
206
|
+
*,
|
|
207
|
+
max_pages: int | None = None,
|
|
208
|
+
max_items: int | None = None,
|
|
209
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
210
|
+
cursor: str | None = None,
|
|
211
|
+
) -> AsyncBoundedItemIterator[object]:
|
|
212
|
+
return async_bounded_item_iterator(
|
|
213
|
+
lambda cur: client.list_task_attempts(task_id, cursor=cur, limit=limit),
|
|
214
|
+
max_pages=max_pages,
|
|
215
|
+
max_items=max_items,
|
|
216
|
+
cursor=cursor,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def iter_inspection_task_pages(
|
|
221
|
+
client: AsyncObserverClient,
|
|
222
|
+
queue_name: str,
|
|
223
|
+
*,
|
|
224
|
+
max_pages: int | None = None,
|
|
225
|
+
max_items: int | None = None,
|
|
226
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
227
|
+
cursor: str | None = None,
|
|
228
|
+
) -> AsyncBoundedPageIterator[TaskPage]:
|
|
229
|
+
return async_bounded_page_iterator(
|
|
230
|
+
lambda cur: client.list_inspection_tasks(queue_name, cursor=cur, limit=limit),
|
|
231
|
+
max_pages=max_pages,
|
|
232
|
+
max_items=max_items,
|
|
233
|
+
cursor=cursor,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def iter_inspection_tasks(
|
|
238
|
+
client: AsyncObserverClient,
|
|
239
|
+
queue_name: str,
|
|
240
|
+
*,
|
|
241
|
+
max_pages: int | None = None,
|
|
242
|
+
max_items: int | None = None,
|
|
243
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
244
|
+
cursor: str | None = None,
|
|
245
|
+
) -> AsyncBoundedItemIterator[object]:
|
|
246
|
+
return async_bounded_item_iterator(
|
|
247
|
+
lambda cur: client.list_inspection_tasks(queue_name, cursor=cur, limit=limit),
|
|
248
|
+
max_pages=max_pages,
|
|
249
|
+
max_items=max_items,
|
|
250
|
+
cursor=cursor,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def iter_inspection_attempt_pages(
|
|
255
|
+
client: AsyncObserverClient,
|
|
256
|
+
task_id: str,
|
|
257
|
+
*,
|
|
258
|
+
time_from: datetime,
|
|
259
|
+
time_to: datetime,
|
|
260
|
+
max_pages: int | None = None,
|
|
261
|
+
max_items: int | None = None,
|
|
262
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
263
|
+
cursor: str | None = None,
|
|
264
|
+
) -> AsyncBoundedPageIterator[AttemptPage]:
|
|
265
|
+
return async_bounded_page_iterator(
|
|
266
|
+
lambda cur: client.list_inspection_attempts(
|
|
267
|
+
task_id,
|
|
268
|
+
time_from=time_from,
|
|
269
|
+
time_to=time_to,
|
|
270
|
+
cursor=cur,
|
|
271
|
+
limit=limit,
|
|
272
|
+
),
|
|
273
|
+
max_pages=max_pages,
|
|
274
|
+
max_items=max_items,
|
|
275
|
+
cursor=cursor,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def iter_inspection_attempts(
|
|
280
|
+
client: AsyncObserverClient,
|
|
281
|
+
task_id: str,
|
|
282
|
+
*,
|
|
283
|
+
time_from: datetime,
|
|
284
|
+
time_to: datetime,
|
|
285
|
+
max_pages: int | None = None,
|
|
286
|
+
max_items: int | None = None,
|
|
287
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
288
|
+
cursor: str | None = None,
|
|
289
|
+
) -> AsyncBoundedItemIterator[object]:
|
|
290
|
+
return async_bounded_item_iterator(
|
|
291
|
+
lambda cur: client.list_inspection_attempts(
|
|
292
|
+
task_id,
|
|
293
|
+
time_from=time_from,
|
|
294
|
+
time_to=time_to,
|
|
295
|
+
cursor=cur,
|
|
296
|
+
limit=limit,
|
|
297
|
+
),
|
|
298
|
+
max_pages=max_pages,
|
|
299
|
+
max_items=max_items,
|
|
300
|
+
cursor=cursor,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def iter_dead_letter_pages(
|
|
305
|
+
client: AsyncObserverClient,
|
|
306
|
+
queue_name: str,
|
|
307
|
+
*,
|
|
308
|
+
time_from: datetime,
|
|
309
|
+
time_to: datetime,
|
|
310
|
+
max_pages: int | None = None,
|
|
311
|
+
max_items: int | None = None,
|
|
312
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
313
|
+
cursor: str | None = None,
|
|
314
|
+
) -> AsyncBoundedPageIterator[DeadLetterPage]:
|
|
315
|
+
return async_bounded_page_iterator(
|
|
316
|
+
lambda cur: client.list_dead_letters(
|
|
317
|
+
queue_name,
|
|
318
|
+
time_from=time_from,
|
|
319
|
+
time_to=time_to,
|
|
320
|
+
cursor=cur,
|
|
321
|
+
limit=limit,
|
|
322
|
+
),
|
|
323
|
+
max_pages=max_pages,
|
|
324
|
+
max_items=max_items,
|
|
325
|
+
cursor=cursor,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def iter_dead_letters(
|
|
330
|
+
client: AsyncObserverClient,
|
|
331
|
+
queue_name: str,
|
|
332
|
+
*,
|
|
333
|
+
time_from: datetime,
|
|
334
|
+
time_to: datetime,
|
|
335
|
+
max_pages: int | None = None,
|
|
336
|
+
max_items: int | None = None,
|
|
337
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
338
|
+
cursor: str | None = None,
|
|
339
|
+
) -> AsyncBoundedItemIterator[object]:
|
|
340
|
+
return async_bounded_item_iterator(
|
|
341
|
+
lambda cur: client.list_dead_letters(
|
|
342
|
+
queue_name,
|
|
343
|
+
time_from=time_from,
|
|
344
|
+
time_to=time_to,
|
|
345
|
+
cursor=cur,
|
|
346
|
+
limit=limit,
|
|
347
|
+
),
|
|
348
|
+
max_pages=max_pages,
|
|
349
|
+
max_items=max_items,
|
|
350
|
+
cursor=cursor,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def iter_queue_pages(
|
|
355
|
+
client: AsyncAdminClient,
|
|
356
|
+
*,
|
|
357
|
+
max_pages: int | None = None,
|
|
358
|
+
max_items: int | None = None,
|
|
359
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
360
|
+
cursor: str | None = None,
|
|
361
|
+
) -> AsyncBoundedPageIterator[QueuePage]:
|
|
362
|
+
return async_bounded_page_iterator(
|
|
363
|
+
lambda cur: client.list_queues(cursor=cur, limit=limit),
|
|
364
|
+
max_pages=max_pages,
|
|
365
|
+
max_items=max_items,
|
|
366
|
+
cursor=cursor,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def iter_queues(
|
|
371
|
+
client: AsyncAdminClient,
|
|
372
|
+
*,
|
|
373
|
+
max_pages: int | None = None,
|
|
374
|
+
max_items: int | None = None,
|
|
375
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
376
|
+
cursor: str | None = None,
|
|
377
|
+
) -> AsyncBoundedItemIterator[object]:
|
|
378
|
+
return async_bounded_item_iterator(
|
|
379
|
+
lambda cur: client.list_queues(cursor=cur, limit=limit),
|
|
380
|
+
max_pages=max_pages,
|
|
381
|
+
max_items=max_items,
|
|
382
|
+
cursor=cursor,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def iter_admin_audit_pages(
|
|
387
|
+
client: AsyncAdminClient,
|
|
388
|
+
*,
|
|
389
|
+
time_from: datetime,
|
|
390
|
+
time_to: datetime,
|
|
391
|
+
queue_name: str | None = None,
|
|
392
|
+
max_pages: int | None = None,
|
|
393
|
+
max_items: int | None = None,
|
|
394
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
395
|
+
cursor: str | None = None,
|
|
396
|
+
) -> AsyncBoundedPageIterator[AuditPage]:
|
|
397
|
+
return async_bounded_page_iterator(
|
|
398
|
+
lambda cur: client.list_admin_audit(
|
|
399
|
+
time_from=time_from,
|
|
400
|
+
time_to=time_to,
|
|
401
|
+
queue_name=queue_name,
|
|
402
|
+
cursor=cur,
|
|
403
|
+
limit=limit,
|
|
404
|
+
),
|
|
405
|
+
max_pages=max_pages,
|
|
406
|
+
max_items=max_items,
|
|
407
|
+
cursor=cursor,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def iter_admin_audit(
|
|
412
|
+
client: AsyncAdminClient,
|
|
413
|
+
*,
|
|
414
|
+
time_from: datetime,
|
|
415
|
+
time_to: datetime,
|
|
416
|
+
queue_name: str | None = None,
|
|
417
|
+
max_pages: int | None = None,
|
|
418
|
+
max_items: int | None = None,
|
|
419
|
+
limit: int = PAGE_LIMIT_DEFAULT,
|
|
420
|
+
cursor: str | None = None,
|
|
421
|
+
) -> AsyncBoundedItemIterator[object]:
|
|
422
|
+
return async_bounded_item_iterator(
|
|
423
|
+
lambda cur: client.list_admin_audit(
|
|
424
|
+
time_from=time_from,
|
|
425
|
+
time_to=time_to,
|
|
426
|
+
queue_name=queue_name,
|
|
427
|
+
cursor=cur,
|
|
428
|
+
limit=limit,
|
|
429
|
+
),
|
|
430
|
+
max_pages=max_pages,
|
|
431
|
+
max_items=max_items,
|
|
432
|
+
cursor=cursor,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
__all__ = [
|
|
437
|
+
"AsyncBoundedItemIterator",
|
|
438
|
+
"AsyncBoundedPageIterator",
|
|
439
|
+
"async_bounded_item_iterator",
|
|
440
|
+
"async_bounded_page_iterator",
|
|
441
|
+
"iter_admin_audit",
|
|
442
|
+
"iter_admin_audit_pages",
|
|
443
|
+
"iter_dead_letter_pages",
|
|
444
|
+
"iter_dead_letters",
|
|
445
|
+
"iter_inspection_attempt_pages",
|
|
446
|
+
"iter_inspection_attempts",
|
|
447
|
+
"iter_inspection_task_pages",
|
|
448
|
+
"iter_inspection_tasks",
|
|
449
|
+
"iter_queue_pages",
|
|
450
|
+
"iter_queues",
|
|
451
|
+
"iter_task_attempt_pages",
|
|
452
|
+
"iter_task_attempts",
|
|
453
|
+
]
|