Skip to content

Commit 1fc7b74

Browse files
authored
Merge pull request #1390 from jorwoods/jorwoods/typed_pager
chore: add typing to Pager
2 parents 6c4374b + eee71d7 commit 1fc7b74

File tree

1 file changed

+47
-46
lines changed

1 file changed

+47
-46
lines changed

tableauserverclient/server/pager.py

+47-46
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1+
import copy
12
from functools import partial
3+
from typing import Generic, Iterable, Iterator, List, Optional, Protocol, Tuple, TypeVar, Union, runtime_checkable
24

3-
from . import RequestOptions
5+
from tableauserverclient.models.pagination_item import PaginationItem
6+
from tableauserverclient.server.request_options import RequestOptions
47

58

6-
class Pager(object):
9+
T = TypeVar("T")
10+
ReturnType = Tuple[List[T], PaginationItem]
11+
12+
13+
@runtime_checkable
14+
class Endpoint(Protocol):
15+
def get(self, req_options: Optional[RequestOptions], **kwargs) -> ReturnType:
16+
...
17+
18+
19+
@runtime_checkable
20+
class CallableEndpoint(Protocol):
21+
def __call__(self, __req_options: Optional[RequestOptions], **kwargs) -> ReturnType:
22+
...
23+
24+
25+
class Pager(Iterable[T]):
726
"""
827
Generator that takes an endpoint (top level endpoints with `.get)` and lazily loads items from Server.
928
Supports all `RequestOptions` including starting on any page. Also used by models to load sub-models
@@ -12,60 +31,42 @@ class Pager(object):
1231
Will loop over anything that returns (List[ModelItem], PaginationItem).
1332
"""
1433

15-
def __init__(self, endpoint, request_opts=None, **kwargs):
16-
if hasattr(endpoint, "get"):
34+
def __init__(
35+
self,
36+
endpoint: Union[CallableEndpoint, Endpoint],
37+
request_opts: Optional[RequestOptions] = None,
38+
**kwargs,
39+
) -> None:
40+
if isinstance(endpoint, Endpoint):
1741
# The simpliest case is to take an Endpoint and call its get
1842
endpoint = partial(endpoint.get, **kwargs)
1943
self._endpoint = endpoint
20-
elif callable(endpoint):
44+
elif isinstance(endpoint, CallableEndpoint):
2145
# but if they pass a callable then use that instead (used internally)
2246
endpoint = partial(endpoint, **kwargs)
2347
self._endpoint = endpoint
2448
else:
2549
# Didn't get something we can page over
2650
raise ValueError("Pager needs a server endpoint to page through.")
2751

28-
self._options = request_opts
52+
self._options = request_opts or RequestOptions()
2953

30-
# If we have options we could be starting on any page, backfill the count
31-
if self._options:
32-
self._count = (self._options.pagenumber - 1) * self._options.pagesize
33-
else:
34-
self._count = 0
35-
self._options = RequestOptions()
36-
37-
def __iter__(self):
38-
# Fetch the first page
39-
current_item_list, last_pagination_item = self._endpoint(self._options)
40-
41-
if last_pagination_item.total_available is None:
42-
# This endpoint does not support pagination, drain the list and return
43-
while current_item_list:
44-
yield current_item_list.pop(0)
45-
46-
return
47-
48-
# Get the rest on demand as a generator
49-
while self._count < last_pagination_item.total_available:
50-
if (
51-
len(current_item_list) == 0
52-
and (last_pagination_item.page_number * last_pagination_item.page_size)
53-
< last_pagination_item.total_available
54-
):
55-
current_item_list, last_pagination_item = self._load_next_page(last_pagination_item)
56-
57-
try:
58-
yield current_item_list.pop(0)
59-
self._count += 1
60-
61-
except IndexError:
62-
# The total count on Server changed while fetching exit gracefully
54+
def __iter__(self) -> Iterator[T]:
55+
options = copy.deepcopy(self._options)
56+
while True:
57+
# Fetch the first page
58+
current_item_list, pagination_item = self._endpoint(options)
59+
60+
if pagination_item.total_available is None:
61+
# This endpoint does not support pagination, drain the list and return
62+
yield from current_item_list
63+
return
64+
yield from current_item_list
65+
66+
if pagination_item.page_size * pagination_item.page_number >= pagination_item.total_available:
67+
# Last page, exit
6368
return
6469

65-
def _load_next_page(self, last_pagination_item):
66-
next_page = last_pagination_item.page_number + 1
67-
opts = RequestOptions(pagenumber=next_page, pagesize=last_pagination_item.page_size)
68-
if self._options is not None:
69-
opts.sort, opts.filter = self._options.sort, self._options.filter
70-
current_item_list, last_pagination_item = self._endpoint(opts)
71-
return current_item_list, last_pagination_item
70+
# Update the options to fetch the next page
71+
options.pagenumber = pagination_item.page_number + 1
72+
options.pagesize = pagination_item.page_size

0 commit comments

Comments
 (0)