tableauserverclient 0.31__py3-none-any.whl → 0.33__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.
- tableauserverclient/__init__.py +74 -4
- tableauserverclient/_version.py +3 -3
- tableauserverclient/config.py +16 -4
- tableauserverclient/models/__init__.py +100 -37
- tableauserverclient/models/connection_item.py +4 -2
- tableauserverclient/models/database_item.py +6 -0
- tableauserverclient/models/datasource_item.py +18 -6
- tableauserverclient/models/favorites_item.py +7 -7
- tableauserverclient/models/flow_item.py +6 -6
- tableauserverclient/models/groupset_item.py +53 -0
- tableauserverclient/models/interval_item.py +27 -14
- tableauserverclient/models/job_item.py +18 -2
- tableauserverclient/models/linked_tasks_item.py +102 -0
- tableauserverclient/models/permissions_item.py +53 -5
- tableauserverclient/models/project_item.py +4 -3
- tableauserverclient/models/reference_item.py +5 -0
- tableauserverclient/models/tableau_auth.py +22 -23
- tableauserverclient/models/tableau_types.py +9 -7
- tableauserverclient/models/virtual_connection_item.py +77 -0
- tableauserverclient/server/__init__.py +83 -8
- tableauserverclient/server/endpoint/__init__.py +69 -28
- tableauserverclient/server/endpoint/auth_endpoint.py +3 -3
- tableauserverclient/server/endpoint/custom_views_endpoint.py +66 -5
- tableauserverclient/server/endpoint/databases_endpoint.py +21 -18
- tableauserverclient/server/endpoint/datasources_endpoint.py +115 -35
- tableauserverclient/server/endpoint/endpoint.py +53 -20
- tableauserverclient/server/endpoint/favorites_endpoint.py +1 -1
- tableauserverclient/server/endpoint/fileuploads_endpoint.py +2 -2
- tableauserverclient/server/endpoint/flow_runs_endpoint.py +45 -5
- tableauserverclient/server/endpoint/flows_endpoint.py +43 -16
- tableauserverclient/server/endpoint/groups_endpoint.py +85 -31
- tableauserverclient/server/endpoint/groupsets_endpoint.py +127 -0
- tableauserverclient/server/endpoint/jobs_endpoint.py +74 -7
- tableauserverclient/server/endpoint/linked_tasks_endpoint.py +45 -0
- tableauserverclient/server/endpoint/metadata_endpoint.py +3 -3
- tableauserverclient/server/endpoint/metrics_endpoint.py +1 -1
- tableauserverclient/server/endpoint/projects_endpoint.py +50 -18
- tableauserverclient/server/endpoint/resource_tagger.py +135 -3
- tableauserverclient/server/endpoint/tables_endpoint.py +19 -16
- tableauserverclient/server/endpoint/users_endpoint.py +40 -1
- tableauserverclient/server/endpoint/views_endpoint.py +97 -10
- tableauserverclient/server/endpoint/virtual_connections_endpoint.py +173 -0
- tableauserverclient/server/endpoint/workbooks_endpoint.py +97 -45
- tableauserverclient/server/pager.py +46 -46
- tableauserverclient/server/query.py +62 -33
- tableauserverclient/server/request_factory.py +192 -49
- tableauserverclient/server/request_options.py +4 -2
- tableauserverclient/server/server.py +13 -6
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/METADATA +15 -16
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/RECORD +54 -48
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/WHEEL +1 -1
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/LICENSE +0 -0
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/LICENSE.versioneer +0 -0
- {tableauserverclient-0.31.dist-info → tableauserverclient-0.33.dist-info}/top_level.txt +0 -0
|
@@ -7,11 +7,12 @@ from contextlib import closing
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from tableauserverclient.helpers.headers import fix_filename
|
|
10
|
+
from tableauserverclient.server.query import QuerySet
|
|
10
11
|
|
|
11
|
-
from .endpoint import QuerysetEndpoint, api, parameter_added_in
|
|
12
|
-
from .exceptions import InternalServerError, MissingRequiredFieldError
|
|
13
|
-
from .permissions_endpoint import _PermissionsEndpoint
|
|
14
|
-
from .resource_tagger import
|
|
12
|
+
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api, parameter_added_in
|
|
13
|
+
from tableauserverclient.server.endpoint.exceptions import InternalServerError, MissingRequiredFieldError
|
|
14
|
+
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
|
|
15
|
+
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
|
|
15
16
|
|
|
16
17
|
from tableauserverclient.filesys_helpers import (
|
|
17
18
|
to_filename,
|
|
@@ -24,9 +25,11 @@ from tableauserverclient.models import WorkbookItem, ConnectionItem, ViewItem, P
|
|
|
24
25
|
from tableauserverclient.server import RequestFactory
|
|
25
26
|
|
|
26
27
|
from typing import (
|
|
28
|
+
Iterable,
|
|
27
29
|
List,
|
|
28
30
|
Optional,
|
|
29
31
|
Sequence,
|
|
32
|
+
Set,
|
|
30
33
|
Tuple,
|
|
31
34
|
TYPE_CHECKING,
|
|
32
35
|
Union,
|
|
@@ -35,8 +38,8 @@ from typing import (
|
|
|
35
38
|
if TYPE_CHECKING:
|
|
36
39
|
from tableauserverclient.server import Server
|
|
37
40
|
from tableauserverclient.server.request_options import RequestOptions
|
|
38
|
-
from tableauserverclient.models import DatasourceItem
|
|
39
|
-
from .schedules_endpoint import AddResponse
|
|
41
|
+
from tableauserverclient.models import DatasourceItem
|
|
42
|
+
from tableauserverclient.server.endpoint.schedules_endpoint import AddResponse
|
|
40
43
|
|
|
41
44
|
io_types_r = (io.BytesIO, io.BufferedReader)
|
|
42
45
|
io_types_w = (io.BytesIO, io.BufferedWriter)
|
|
@@ -56,10 +59,9 @@ PathOrFileR = Union[FilePath, FileObjectR]
|
|
|
56
59
|
PathOrFileW = Union[FilePath, FileObjectW]
|
|
57
60
|
|
|
58
61
|
|
|
59
|
-
class Workbooks(QuerysetEndpoint):
|
|
62
|
+
class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]):
|
|
60
63
|
def __init__(self, parent_srv: "Server") -> None:
|
|
61
64
|
super(Workbooks, self).__init__(parent_srv)
|
|
62
|
-
self._resource_tagger = _ResourceTagger(parent_srv)
|
|
63
65
|
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
|
|
64
66
|
|
|
65
67
|
return None
|
|
@@ -147,7 +149,7 @@ class Workbooks(QuerysetEndpoint):
|
|
|
147
149
|
error = "Workbook item missing ID. Workbook must be retrieved from server first."
|
|
148
150
|
raise MissingRequiredFieldError(error)
|
|
149
151
|
|
|
150
|
-
self.
|
|
152
|
+
self.update_tags(workbook_item)
|
|
151
153
|
|
|
152
154
|
# Update the workbook itself
|
|
153
155
|
url = "{0}/{1}".format(self.baseurl, workbook_item.id)
|
|
@@ -160,13 +162,6 @@ class Workbooks(QuerysetEndpoint):
|
|
|
160
162
|
updated_workbook = copy.copy(workbook_item)
|
|
161
163
|
return updated_workbook._parse_common_tags(server_response.content, self.parent_srv.namespace)
|
|
162
164
|
|
|
163
|
-
@api(version="2.3")
|
|
164
|
-
def update_conn(self, *args, **kwargs):
|
|
165
|
-
import warnings
|
|
166
|
-
|
|
167
|
-
warnings.warn("update_conn is deprecated, please use update_connection instead")
|
|
168
|
-
return self.update_connection(*args, **kwargs)
|
|
169
|
-
|
|
170
165
|
# Update workbook_connection
|
|
171
166
|
@api(version="2.3")
|
|
172
167
|
def update_connection(self, workbook_item: WorkbookItem, connection_item: ConnectionItem) -> ConnectionItem:
|
|
@@ -189,9 +184,13 @@ class Workbooks(QuerysetEndpoint):
|
|
|
189
184
|
workbook_id: str,
|
|
190
185
|
filepath: Optional[PathOrFileW] = None,
|
|
191
186
|
include_extract: bool = True,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
187
|
+
) -> PathOrFileW:
|
|
188
|
+
return self.download_revision(
|
|
189
|
+
workbook_id,
|
|
190
|
+
None,
|
|
191
|
+
filepath,
|
|
192
|
+
include_extract,
|
|
193
|
+
)
|
|
195
194
|
|
|
196
195
|
# Get all views of workbook
|
|
197
196
|
@api(version="2.0")
|
|
@@ -315,21 +314,11 @@ class Workbooks(QuerysetEndpoint):
|
|
|
315
314
|
workbook_item: WorkbookItem,
|
|
316
315
|
file: PathOrFileR,
|
|
317
316
|
mode: str,
|
|
318
|
-
connection_credentials: Optional["ConnectionCredentials"] = None,
|
|
319
317
|
connections: Optional[Sequence[ConnectionItem]] = None,
|
|
320
318
|
as_job: bool = False,
|
|
321
|
-
hidden_views: Optional[Sequence[str]] = None,
|
|
322
319
|
skip_connection_check: bool = False,
|
|
323
320
|
parameters=None,
|
|
324
321
|
):
|
|
325
|
-
if connection_credentials is not None:
|
|
326
|
-
import warnings
|
|
327
|
-
|
|
328
|
-
warnings.warn(
|
|
329
|
-
"connection_credentials is being deprecated. Use connections instead",
|
|
330
|
-
DeprecationWarning,
|
|
331
|
-
)
|
|
332
|
-
|
|
333
322
|
if isinstance(file, (str, os.PathLike)):
|
|
334
323
|
if not os.path.isfile(file):
|
|
335
324
|
error = "File path does not lead to an existing file."
|
|
@@ -391,12 +380,9 @@ class Workbooks(QuerysetEndpoint):
|
|
|
391
380
|
logger.info("Publishing {0} to server with chunking method (workbook over 64MB)".format(workbook_item.name))
|
|
392
381
|
upload_session_id = self.parent_srv.fileuploads.upload(file)
|
|
393
382
|
url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
|
|
394
|
-
conn_creds = connection_credentials
|
|
395
383
|
xml_request, content_type = RequestFactory.Workbook.publish_req_chunked(
|
|
396
384
|
workbook_item,
|
|
397
|
-
connection_credentials=conn_creds,
|
|
398
385
|
connections=connections,
|
|
399
|
-
hidden_views=hidden_views,
|
|
400
386
|
)
|
|
401
387
|
else:
|
|
402
388
|
logger.info("Publishing {0} to server".format(filename))
|
|
@@ -411,14 +397,11 @@ class Workbooks(QuerysetEndpoint):
|
|
|
411
397
|
else:
|
|
412
398
|
raise TypeError("file should be a filepath or file object.")
|
|
413
399
|
|
|
414
|
-
conn_creds = connection_credentials
|
|
415
400
|
xml_request, content_type = RequestFactory.Workbook.publish_req(
|
|
416
401
|
workbook_item,
|
|
417
402
|
filename,
|
|
418
403
|
file_contents,
|
|
419
|
-
connection_credentials=conn_creds,
|
|
420
404
|
connections=connections,
|
|
421
|
-
hidden_views=hidden_views,
|
|
422
405
|
)
|
|
423
406
|
logger.debug("Request xml: {0} ".format(redact_xml(xml_request[:1000])))
|
|
424
407
|
|
|
@@ -468,7 +451,6 @@ class Workbooks(QuerysetEndpoint):
|
|
|
468
451
|
revision_number: Optional[str],
|
|
469
452
|
filepath: Optional[PathOrFileW] = None,
|
|
470
453
|
include_extract: bool = True,
|
|
471
|
-
no_extract: Optional[bool] = None,
|
|
472
454
|
) -> PathOrFileW:
|
|
473
455
|
if not workbook_id:
|
|
474
456
|
error = "Workbook ID undefined."
|
|
@@ -478,15 +460,6 @@ class Workbooks(QuerysetEndpoint):
|
|
|
478
460
|
else:
|
|
479
461
|
url = "{0}/{1}/revisions/{2}/content".format(self.baseurl, workbook_id, revision_number)
|
|
480
462
|
|
|
481
|
-
if no_extract is False or no_extract is True:
|
|
482
|
-
import warnings
|
|
483
|
-
|
|
484
|
-
warnings.warn(
|
|
485
|
-
"no_extract is deprecated, use include_extract instead.",
|
|
486
|
-
DeprecationWarning,
|
|
487
|
-
)
|
|
488
|
-
include_extract = not no_extract
|
|
489
|
-
|
|
490
463
|
if not include_extract:
|
|
491
464
|
url += "?includeExtract=False"
|
|
492
465
|
|
|
@@ -527,3 +500,82 @@ class Workbooks(QuerysetEndpoint):
|
|
|
527
500
|
self, schedule_id: str, item: WorkbookItem
|
|
528
501
|
) -> List["AddResponse"]: # actually should return a task
|
|
529
502
|
return self.parent_srv.schedules.add_to_schedule(schedule_id, workbook=item)
|
|
503
|
+
|
|
504
|
+
@api(version="1.0")
|
|
505
|
+
def add_tags(self, item: Union[WorkbookItem, str], tags: Union[Iterable[str], str]) -> Set[str]:
|
|
506
|
+
return super().add_tags(item, tags)
|
|
507
|
+
|
|
508
|
+
@api(version="1.0")
|
|
509
|
+
def delete_tags(self, item: Union[WorkbookItem, str], tags: Union[Iterable[str], str]) -> None:
|
|
510
|
+
return super().delete_tags(item, tags)
|
|
511
|
+
|
|
512
|
+
@api(version="1.0")
|
|
513
|
+
def update_tags(self, item: WorkbookItem) -> None:
|
|
514
|
+
return super().update_tags(item)
|
|
515
|
+
|
|
516
|
+
def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[WorkbookItem]:
|
|
517
|
+
"""
|
|
518
|
+
Queries the Tableau Server for items using the specified filters. Page
|
|
519
|
+
size can be specified to limit the number of items returned in a single
|
|
520
|
+
request. If not specified, the default page size is 100. Page size can
|
|
521
|
+
be an integer between 1 and 1000.
|
|
522
|
+
|
|
523
|
+
No positional arguments are allowed. All filters must be specified as
|
|
524
|
+
keyword arguments. If you use the equality operator, you can specify it
|
|
525
|
+
through <field_name>=<value>. If you want to use a different operator,
|
|
526
|
+
you can specify it through <field_name>__<operator>=<value>. Field
|
|
527
|
+
names can either be in snake_case or camelCase.
|
|
528
|
+
|
|
529
|
+
This endpoint supports the following fields and operators:
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
created_at=...
|
|
533
|
+
created_at__gt=...
|
|
534
|
+
created_at__gte=...
|
|
535
|
+
created_at__lt=...
|
|
536
|
+
created_at__lte=...
|
|
537
|
+
content_url=...
|
|
538
|
+
content_url__in=...
|
|
539
|
+
display_tabs=...
|
|
540
|
+
favorites_total=...
|
|
541
|
+
favorites_total__gt=...
|
|
542
|
+
favorites_total__gte=...
|
|
543
|
+
favorites_total__lt=...
|
|
544
|
+
favorites_total__lte=...
|
|
545
|
+
has_alerts=...
|
|
546
|
+
has_extracts=...
|
|
547
|
+
name=...
|
|
548
|
+
name__in=...
|
|
549
|
+
owner_domain=...
|
|
550
|
+
owner_domain__in=...
|
|
551
|
+
owner_email=...
|
|
552
|
+
owner_email__in=...
|
|
553
|
+
owner_name=...
|
|
554
|
+
owner_name__in=...
|
|
555
|
+
project_name=...
|
|
556
|
+
project_name__in=...
|
|
557
|
+
sheet_count=...
|
|
558
|
+
sheet_count__gt=...
|
|
559
|
+
sheet_count__gte=...
|
|
560
|
+
sheet_count__lt=...
|
|
561
|
+
sheet_count__lte=...
|
|
562
|
+
size=...
|
|
563
|
+
size__gt=...
|
|
564
|
+
size__gte=...
|
|
565
|
+
size__lt=...
|
|
566
|
+
size__lte=...
|
|
567
|
+
subscriptions_total=...
|
|
568
|
+
subscriptions_total__gt=...
|
|
569
|
+
subscriptions_total__gte=...
|
|
570
|
+
subscriptions_total__lt=...
|
|
571
|
+
subscriptions_total__lte=...
|
|
572
|
+
tags=...
|
|
573
|
+
tags__in=...
|
|
574
|
+
updated_at=...
|
|
575
|
+
updated_at__gt=...
|
|
576
|
+
updated_at__gte=...
|
|
577
|
+
updated_at__lt=...
|
|
578
|
+
updated_at__lte=...
|
|
579
|
+
"""
|
|
580
|
+
|
|
581
|
+
return super().filter(*invalid, page_size=page_size, **kwargs)
|
|
@@ -1,9 +1,27 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
from functools import partial
|
|
3
|
+
from typing import Iterable, Iterator, List, Optional, Protocol, Tuple, TypeVar, Union, runtime_checkable
|
|
2
4
|
|
|
3
|
-
from . import
|
|
5
|
+
from tableauserverclient.models.pagination_item import PaginationItem
|
|
6
|
+
from tableauserverclient.server.request_options import RequestOptions
|
|
4
7
|
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@runtime_checkable
|
|
13
|
+
class Endpoint(Protocol[T]):
|
|
14
|
+
def get(self, req_options: Optional[RequestOptions]) -> Tuple[List[T], PaginationItem]:
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class CallableEndpoint(Protocol[T]):
|
|
20
|
+
def __call__(self, __req_options: Optional[RequestOptions], **kwargs) -> Tuple[List[T], PaginationItem]:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Pager(Iterable[T]):
|
|
7
25
|
"""
|
|
8
26
|
Generator that takes an endpoint (top level endpoints with `.get)` and lazily loads items from Server.
|
|
9
27
|
Supports all `RequestOptions` including starting on any page. Also used by models to load sub-models
|
|
@@ -12,12 +30,17 @@ class Pager(object):
|
|
|
12
30
|
Will loop over anything that returns (List[ModelItem], PaginationItem).
|
|
13
31
|
"""
|
|
14
32
|
|
|
15
|
-
def __init__(
|
|
16
|
-
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
endpoint: Union[CallableEndpoint[T], Endpoint[T]],
|
|
36
|
+
request_opts: Optional[RequestOptions] = None,
|
|
37
|
+
**kwargs,
|
|
38
|
+
) -> None:
|
|
39
|
+
if isinstance(endpoint, Endpoint):
|
|
17
40
|
# The simpliest case is to take an Endpoint and call its get
|
|
18
41
|
endpoint = partial(endpoint.get, **kwargs)
|
|
19
42
|
self._endpoint = endpoint
|
|
20
|
-
elif
|
|
43
|
+
elif isinstance(endpoint, CallableEndpoint):
|
|
21
44
|
# but if they pass a callable then use that instead (used internally)
|
|
22
45
|
endpoint = partial(endpoint, **kwargs)
|
|
23
46
|
self._endpoint = endpoint
|
|
@@ -25,47 +48,24 @@ class Pager(object):
|
|
|
25
48
|
# Didn't get something we can page over
|
|
26
49
|
raise ValueError("Pager needs a server endpoint to page through.")
|
|
27
50
|
|
|
28
|
-
self._options = request_opts
|
|
51
|
+
self._options = request_opts or RequestOptions()
|
|
29
52
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
53
|
+
def __iter__(self) -> Iterator[T]:
|
|
54
|
+
options = copy.deepcopy(self._options)
|
|
55
|
+
while True:
|
|
56
|
+
# Fetch the first page
|
|
57
|
+
current_item_list, pagination_item = self._endpoint(options)
|
|
58
|
+
|
|
59
|
+
if pagination_item.total_available is None:
|
|
60
|
+
# This endpoint does not support pagination, drain the list and return
|
|
61
|
+
yield from current_item_list
|
|
62
|
+
return
|
|
63
|
+
yield from current_item_list
|
|
64
|
+
|
|
65
|
+
if pagination_item.page_size * pagination_item.page_number >= pagination_item.total_available:
|
|
66
|
+
# Last page, exit
|
|
63
67
|
return
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
69
|
+
# Update the options to fetch the next page
|
|
70
|
+
options.pagenumber = pagination_item.page_number + 1
|
|
71
|
+
options.pagesize = pagination_item.page_size
|
|
@@ -1,9 +1,26 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
3
|
-
from
|
|
4
|
-
from .
|
|
1
|
+
from collections.abc import Sized
|
|
2
|
+
from itertools import count
|
|
3
|
+
from typing import Iterable, Iterator, List, Optional, Protocol, Tuple, TYPE_CHECKING, TypeVar, overload
|
|
4
|
+
from tableauserverclient.config import config
|
|
5
|
+
from tableauserverclient.models.pagination_item import PaginationItem
|
|
6
|
+
from tableauserverclient.server.filter import Filter
|
|
7
|
+
from tableauserverclient.server.request_options import RequestOptions
|
|
8
|
+
from tableauserverclient.server.sort import Sort
|
|
5
9
|
import math
|
|
6
10
|
|
|
11
|
+
from typing_extensions import Self
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from tableauserverclient.server.endpoint import QuerysetEndpoint
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Slice(Protocol):
|
|
20
|
+
start: Optional[int]
|
|
21
|
+
step: Optional[int]
|
|
22
|
+
stop: Optional[int]
|
|
23
|
+
|
|
7
24
|
|
|
8
25
|
def to_camel_case(word: str) -> str:
|
|
9
26
|
return word.split("_")[0] + "".join(x.capitalize() or "_" for x in word.split("_")[1:])
|
|
@@ -16,28 +33,35 @@ see pagination_sample
|
|
|
16
33
|
"""
|
|
17
34
|
|
|
18
35
|
|
|
19
|
-
class QuerySet:
|
|
20
|
-
def __init__(self, model):
|
|
36
|
+
class QuerySet(Iterable[T], Sized):
|
|
37
|
+
def __init__(self, model: "QuerysetEndpoint[T]", page_size: Optional[int] = None) -> None:
|
|
21
38
|
self.model = model
|
|
22
|
-
self.request_options = RequestOptions()
|
|
23
|
-
self._result_cache =
|
|
24
|
-
self._pagination_item =
|
|
39
|
+
self.request_options = RequestOptions(pagesize=page_size or config.PAGE_SIZE)
|
|
40
|
+
self._result_cache: List[T] = []
|
|
41
|
+
self._pagination_item = PaginationItem()
|
|
25
42
|
|
|
26
|
-
def __iter__(self):
|
|
43
|
+
def __iter__(self: Self) -> Iterator[T]:
|
|
27
44
|
# Not built to be re-entrant. Starts back at page 1, and empties
|
|
28
|
-
# the result cache.
|
|
29
|
-
|
|
30
|
-
self._result_cache =
|
|
31
|
-
total = self.total_available
|
|
32
|
-
size = self.page_size
|
|
33
|
-
yield from self._result_cache
|
|
45
|
+
# the result cache. Ensure the result_cache is empty to not yield
|
|
46
|
+
# items from prior usage.
|
|
47
|
+
self._result_cache = []
|
|
34
48
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
self.
|
|
38
|
-
self._result_cache = None
|
|
49
|
+
for page in count(1):
|
|
50
|
+
self.request_options.pagenumber = page
|
|
51
|
+
self._result_cache = []
|
|
39
52
|
self._fetch_all()
|
|
40
53
|
yield from self._result_cache
|
|
54
|
+
# Set result_cache to empty so the fetch will populate
|
|
55
|
+
if (page * self.page_size) >= len(self):
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
@overload
|
|
59
|
+
def __getitem__(self: Self, k: Slice) -> List[T]:
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
@overload
|
|
63
|
+
def __getitem__(self: Self, k: int) -> T:
|
|
64
|
+
...
|
|
41
65
|
|
|
42
66
|
def __getitem__(self, k):
|
|
43
67
|
page = self.page_number
|
|
@@ -78,7 +102,7 @@ class QuerySet:
|
|
|
78
102
|
return self._result_cache[k % size]
|
|
79
103
|
elif k in range(self.total_available):
|
|
80
104
|
# Otherwise, check if k is even sensible to return
|
|
81
|
-
self._result_cache =
|
|
105
|
+
self._result_cache = []
|
|
82
106
|
# Add one to k, otherwise it gets stuck at page boundaries, e.g. 100
|
|
83
107
|
self.request_options.pagenumber = max(1, math.ceil((k + 1) / size))
|
|
84
108
|
return self[k]
|
|
@@ -86,53 +110,57 @@ class QuerySet:
|
|
|
86
110
|
# If k is unreasonable, raise an IndexError.
|
|
87
111
|
raise IndexError
|
|
88
112
|
|
|
89
|
-
def _fetch_all(self):
|
|
113
|
+
def _fetch_all(self: Self) -> None:
|
|
90
114
|
"""
|
|
91
115
|
Retrieve the data and store result and pagination item in cache
|
|
92
116
|
"""
|
|
93
|
-
if self._result_cache
|
|
117
|
+
if not self._result_cache:
|
|
94
118
|
self._result_cache, self._pagination_item = self.model.get(self.request_options)
|
|
95
119
|
|
|
96
|
-
def __len__(self) -> int:
|
|
120
|
+
def __len__(self: Self) -> int:
|
|
97
121
|
return self.total_available
|
|
98
122
|
|
|
99
123
|
@property
|
|
100
|
-
def total_available(self) -> int:
|
|
124
|
+
def total_available(self: Self) -> int:
|
|
101
125
|
self._fetch_all()
|
|
102
126
|
return self._pagination_item.total_available
|
|
103
127
|
|
|
104
128
|
@property
|
|
105
|
-
def page_number(self) -> int:
|
|
129
|
+
def page_number(self: Self) -> int:
|
|
106
130
|
self._fetch_all()
|
|
107
131
|
return self._pagination_item.page_number
|
|
108
132
|
|
|
109
133
|
@property
|
|
110
|
-
def page_size(self) -> int:
|
|
134
|
+
def page_size(self: Self) -> int:
|
|
111
135
|
self._fetch_all()
|
|
112
136
|
return self._pagination_item.page_size
|
|
113
137
|
|
|
114
|
-
def filter(self, *invalid, **kwargs):
|
|
138
|
+
def filter(self: Self, *invalid, page_size: Optional[int] = None, **kwargs) -> Self:
|
|
115
139
|
if invalid:
|
|
116
|
-
raise RuntimeError(
|
|
140
|
+
raise RuntimeError("Only accepts keyword arguments.")
|
|
117
141
|
for kwarg_key, value in kwargs.items():
|
|
118
142
|
field_name, operator = self._parse_shorthand_filter(kwarg_key)
|
|
119
143
|
self.request_options.filter.add(Filter(field_name, operator, value))
|
|
144
|
+
|
|
145
|
+
if page_size:
|
|
146
|
+
self.request_options.pagesize = page_size
|
|
120
147
|
return self
|
|
121
148
|
|
|
122
|
-
def order_by(self, *args):
|
|
149
|
+
def order_by(self: Self, *args) -> Self:
|
|
123
150
|
for arg in args:
|
|
124
151
|
field_name, direction = self._parse_shorthand_sort(arg)
|
|
125
152
|
self.request_options.sort.add(Sort(field_name, direction))
|
|
126
153
|
return self
|
|
127
154
|
|
|
128
|
-
def paginate(self, **kwargs):
|
|
155
|
+
def paginate(self: Self, **kwargs) -> Self:
|
|
129
156
|
if "page_number" in kwargs:
|
|
130
157
|
self.request_options.pagenumber = kwargs["page_number"]
|
|
131
158
|
if "page_size" in kwargs:
|
|
132
159
|
self.request_options.pagesize = kwargs["page_size"]
|
|
133
160
|
return self
|
|
134
161
|
|
|
135
|
-
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _parse_shorthand_filter(key: str) -> Tuple[str, str]:
|
|
136
164
|
tokens = key.split("__", 1)
|
|
137
165
|
if len(tokens) == 1:
|
|
138
166
|
operator = RequestOptions.Operator.Equals
|
|
@@ -146,7 +174,8 @@ class QuerySet:
|
|
|
146
174
|
raise ValueError("Field name `{}` is not valid.".format(field))
|
|
147
175
|
return (field, operator)
|
|
148
176
|
|
|
149
|
-
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _parse_shorthand_sort(key: str) -> Tuple[str, str]:
|
|
150
179
|
direction = RequestOptions.Direction.Asc
|
|
151
180
|
if key.startswith("-"):
|
|
152
181
|
direction = RequestOptions.Direction.Desc
|