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.
@@ -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
+ ]