1
1
from collections .abc import Sized
2
2
from itertools import count
3
+ import sys
3
4
from typing import Iterable , Iterator , List , Optional , Protocol , Tuple , TYPE_CHECKING , TypeVar , overload
4
5
from tableauserverclient .config import config
5
6
from tableauserverclient .models .pagination_item import PaginationItem
7
+ from tableauserverclient .server .endpoint .exceptions import ServerResponseError
6
8
from tableauserverclient .server .filter import Filter
7
9
from tableauserverclient .server .request_options import RequestOptions
8
10
from tableauserverclient .server .sort import Sort
@@ -34,6 +36,31 @@ def to_camel_case(word: str) -> str:
34
36
35
37
36
38
class QuerySet (Iterable [T ], Sized ):
39
+ """
40
+ QuerySet is a class that allows easy filtering, sorting, and iterating over
41
+ many endpoints in TableauServerClient. It is designed to be used in a similar
42
+ way to Django QuerySets, but with a more limited feature set.
43
+
44
+ QuerySet is an iterable, and can be used in for loops, list comprehensions,
45
+ and other places where iterables are expected.
46
+
47
+ QuerySet is also a Sized, and can be used in places where the length of the
48
+ QuerySet is needed. The length of the QuerySet is the total number of items
49
+ available in the QuerySet, not just the number of items that have been
50
+ fetched. If the endpoint does not return a total count of items, the length
51
+ of the QuerySet will be None. If there is no total count, the QuerySet will
52
+ continue to fetch items until there are no more items to fetch.
53
+
54
+ QuerySet is not re-entrant. It is not designed to be used in multiple places
55
+ at the same time. If you need to use a QuerySet in multiple places, you
56
+ should create a new QuerySet for each place you need to use it, convert it
57
+ to a list, or create a deep copy of the QuerySet.
58
+
59
+ QuerySet's are also indexable, and can be sliced. If you try to access an
60
+ item that has not been fetched, the QuerySet will fetch the page that
61
+ contains the item you are looking for.
62
+ """
63
+
37
64
def __init__ (self , model : "QuerysetEndpoint[T]" , page_size : Optional [int ] = None ) -> None :
38
6D47
65
self .model = model
39
66
self .request_options = RequestOptions (pagesize = page_size or config .PAGE_SIZE )
@@ -49,10 +76,20 @@ def __iter__(self: Self) -> Iterator[T]:
49
76
for page in count (1 ):
50
77
self .request_options .pagenumber = page
51
78
self ._result_cache = []
52
- self ._fetch_all ()
79
+ try :
80
+ self ._fetch_all ()
81
+ except ServerResponseError as e :
82
+ if e .code == "400006" :
83
+ # If the endpoint does not support pagination, it will end
84
+ # up overrunning the total number of pages. Catch the
85
+ # error and break out of the loop.
86
+ raise StopIteration
53
87
yield from self ._result_cache
54
- # Set result_cache to empty so the fetch will populate
55
- if (page * self .page_size ) >= len (self ):
88
+ # If the length of the QuerySet is unknown, continue fetching until
89
+ # the result cache is empty.
90
+ if (size := len (self )) == 0 :
91
+ continue
92
+ if (page * self .page_size ) >= size :
56
93
return
57
94
58
95
@overload
@@ -115,10 +152,15 @@ def _fetch_all(self: Self) -> None:
115
152
Retrieve the data and store result and pagination item in cache
116
153
"""
117
154
if not self ._result_cache :
118
- self ._result_cache , self ._pagination_item = self .model .get (self .request_options )
155
+ response = self .model .get (self .request_options )
156
+ if isinstance (response , tuple ):
157
+ self ._result_cache , self ._pagination_item = response
158
+ else :
159
+ self ._result_cache = response
160
+ self ._pagination_item = PaginationItem ()
119
161
120
162
def __len__ (self : Self ) -> int :
121
- return self .total_available
163
+ return self .total_available or 0
122
164
123
165
@property
124
166
def total_available (self : Self ) -> int :
@@ -128,12 +170,16 @@ def total_available(self: Self) -> int:
128
170
@property
129
171
def page_number (self : Self ) -> int :
130
172
self ._fetch_all ()
131
- return self ._pagination_item .page_number
173
+ # If the PaginationItem is not returned from the endpoint, use the
174
+ # pagenumber from the RequestOptions.
175
+ return self ._pagination_item .page_number or self .request_options .pagenumber
132
176
133
177
@property
134
178
def page_size (self : Self ) -> int :
135
179
self ._fetch_all ()
136
- return self ._pagination_item .page_size
180
+ # If the PaginationItem is not returned from the endpoint, use the
181
+ # pagesize from the RequestOptions.
182
+ return self ._pagination_item .page_size or self .request_options .pagesize
137
183
138
184
def filter (self : Self , * invalid , page_size : Optional [int ] = None , ** kwargs ) -> Self :
139
185
if invalid :
0 commit comments