tableauserverclient 0.36__py3-none-any.whl → 0.38__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 +6 -0
- tableauserverclient/bin/_version.py +3 -3
- tableauserverclient/helpers/strings.py +25 -1
- tableauserverclient/models/__init__.py +6 -1
- tableauserverclient/models/connection_item.py +3 -3
- tableauserverclient/models/datasource_item.py +218 -23
- tableauserverclient/models/extract_item.py +82 -0
- tableauserverclient/models/flow_item.py +2 -2
- tableauserverclient/models/group_item.py +11 -0
- tableauserverclient/models/interval_item.py +40 -0
- tableauserverclient/models/job_item.py +1 -0
- tableauserverclient/models/location_item.py +53 -0
- tableauserverclient/models/project_item.py +138 -27
- tableauserverclient/models/schedule_item.py +57 -0
- tableauserverclient/models/site_item.py +28 -0
- tableauserverclient/models/table_item.py +7 -3
- tableauserverclient/models/tableau_types.py +13 -1
- tableauserverclient/models/user_item.py +101 -1
- tableauserverclient/models/view_item.py +79 -5
- tableauserverclient/models/workbook_item.py +151 -1
- tableauserverclient/server/__init__.py +2 -0
- tableauserverclient/server/endpoint/databases_endpoint.py +101 -18
- tableauserverclient/server/endpoint/datasources_endpoint.py +562 -7
- tableauserverclient/server/endpoint/dqw_endpoint.py +16 -6
- tableauserverclient/server/endpoint/endpoint.py +39 -0
- tableauserverclient/server/endpoint/exceptions.py +4 -0
- tableauserverclient/server/endpoint/fileuploads_endpoint.py +1 -1
- tableauserverclient/server/endpoint/groupsets_endpoint.py +2 -2
- tableauserverclient/server/endpoint/jobs_endpoint.py +1 -1
- tableauserverclient/server/endpoint/schedules_endpoint.py +132 -2
- tableauserverclient/server/endpoint/sites_endpoint.py +18 -1
- tableauserverclient/server/endpoint/tables_endpoint.py +140 -17
- tableauserverclient/server/endpoint/users_endpoint.py +22 -5
- tableauserverclient/server/endpoint/views_endpoint.py +5 -1
- tableauserverclient/server/endpoint/workbooks_endpoint.py +24 -10
- tableauserverclient/server/query.py +36 -0
- tableauserverclient/server/request_factory.py +16 -5
- tableauserverclient/server/request_options.py +162 -2
- tableauserverclient/server/server.py +42 -0
- {tableauserverclient-0.36.dist-info → tableauserverclient-0.38.dist-info}/METADATA +3 -2
- {tableauserverclient-0.36.dist-info → tableauserverclient-0.38.dist-info}/RECORD +45 -43
- {tableauserverclient-0.36.dist-info → tableauserverclient-0.38.dist-info}/WHEEL +1 -1
- {tableauserverclient-0.36.dist-info → tableauserverclient-0.38.dist-info/licenses}/LICENSE +0 -0
- {tableauserverclient-0.36.dist-info → tableauserverclient-0.38.dist-info/licenses}/LICENSE.versioneer +0 -0
- {tableauserverclient-0.36.dist-info → tableauserverclient-0.38.dist-info}/top_level.txt +0 -0
|
@@ -87,7 +87,7 @@ class Users(QuerysetEndpoint[UserItem]):
|
|
|
87
87
|
|
|
88
88
|
if req_options is None:
|
|
89
89
|
req_options = RequestOptions()
|
|
90
|
-
req_options.
|
|
90
|
+
req_options.all_fields = True
|
|
91
91
|
|
|
92
92
|
url = self.baseurl
|
|
93
93
|
server_response = self.get_request(url, req_options)
|
|
@@ -381,10 +381,15 @@ class Users(QuerysetEndpoint[UserItem]):
|
|
|
381
381
|
|
|
382
382
|
# Get workbooks for user
|
|
383
383
|
@api(version="2.0")
|
|
384
|
-
def populate_workbooks(
|
|
384
|
+
def populate_workbooks(
|
|
385
|
+
self, user_item: UserItem, req_options: Optional[RequestOptions] = None, owned_only: bool = False
|
|
386
|
+
) -> None:
|
|
385
387
|
"""
|
|
386
388
|
Returns information about the workbooks that the specified user owns
|
|
387
|
-
|
|
389
|
+
or has Read (view) permissions for. If owned_only is set to True,
|
|
390
|
+
only the workbooks that the user owns are returned. If owned_only is
|
|
391
|
+
set to False, all workbooks that the user has Read (view) permissions
|
|
392
|
+
for are returned.
|
|
388
393
|
|
|
389
394
|
This method retrieves the workbook information for the specified user.
|
|
390
395
|
The REST API is designed to return only the information you ask for
|
|
@@ -402,6 +407,10 @@ class Users(QuerysetEndpoint[UserItem]):
|
|
|
402
407
|
req_options : Optional[RequestOptions]
|
|
403
408
|
Optional request options to filter and sort the results.
|
|
404
409
|
|
|
410
|
+
owned_only : bool, default=False
|
|
411
|
+
If True, only the workbooks that the user owns are returned.
|
|
412
|
+
If False, all workbooks that the user has Read (view) permissions
|
|
413
|
+
|
|
405
414
|
Returns
|
|
406
415
|
-------
|
|
407
416
|
None
|
|
@@ -423,14 +432,22 @@ class Users(QuerysetEndpoint[UserItem]):
|
|
|
423
432
|
raise MissingRequiredFieldError(error)
|
|
424
433
|
|
|
425
434
|
def wb_pager():
|
|
426
|
-
|
|
435
|
+
def func(req_options):
|
|
436
|
+
return self._get_wbs_for_user(user_item, req_options, owned_only=owned_only)
|
|
437
|
+
|
|
438
|
+
return Pager(func, req_options)
|
|
427
439
|
|
|
428
440
|
user_item._set_workbooks(wb_pager)
|
|
429
441
|
|
|
430
442
|
def _get_wbs_for_user(
|
|
431
|
-
self,
|
|
443
|
+
self,
|
|
444
|
+
user_item: UserItem,
|
|
445
|
+
req_options: Optional[RequestOptions] = None,
|
|
446
|
+
owned_only: bool = False,
|
|
432
447
|
) -> tuple[list[WorkbookItem], PaginationItem]:
|
|
433
448
|
url = f"{self.baseurl}/{user_item.id}/workbooks"
|
|
449
|
+
if owned_only:
|
|
450
|
+
url += "?ownedBy=true"
|
|
434
451
|
server_response = self.get_request(url, req_options)
|
|
435
452
|
logger.info(f"Populated workbooks for user (ID: {user_item.id})")
|
|
436
453
|
workbook_item = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
@@ -3,7 +3,7 @@ from contextlib import closing
|
|
|
3
3
|
|
|
4
4
|
from tableauserverclient.models.permissions_item import PermissionsRule
|
|
5
5
|
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
|
|
6
|
-
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
|
|
6
|
+
from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError, UnsupportedAttributeError
|
|
7
7
|
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
|
|
8
8
|
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
|
|
9
9
|
from tableauserverclient.server.query import QuerySet
|
|
@@ -171,6 +171,10 @@ class Views(QuerysetEndpoint[ViewItem], TaggingMixin[ViewItem]):
|
|
|
171
171
|
def image_fetcher():
|
|
172
172
|
return self._get_view_image(view_item, req_options)
|
|
173
173
|
|
|
174
|
+
if not self.parent_srv.check_at_least_version("3.23") and req_options is not None:
|
|
175
|
+
if req_options.viz_height or req_options.viz_width:
|
|
176
|
+
raise UnsupportedAttributeError("viz_height and viz_width are only supported in 3.23+")
|
|
177
|
+
|
|
174
178
|
view_item._set_image(image_fetcher)
|
|
175
179
|
logger.info(f"Populated image for view (ID: {view_item.id})")
|
|
176
180
|
|
|
@@ -11,7 +11,11 @@ from tableauserverclient.models.permissions_item import PermissionsRule
|
|
|
11
11
|
from tableauserverclient.server.query import QuerySet
|
|
12
12
|
|
|
13
13
|
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api, parameter_added_in
|
|
14
|
-
from tableauserverclient.server.endpoint.exceptions import
|
|
14
|
+
from tableauserverclient.server.endpoint.exceptions import (
|
|
15
|
+
InternalServerError,
|
|
16
|
+
MissingRequiredFieldError,
|
|
17
|
+
UnsupportedAttributeError,
|
|
18
|
+
)
|
|
15
19
|
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
|
|
16
20
|
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
|
|
17
21
|
|
|
@@ -34,7 +38,7 @@ from collections.abc import Iterable, Sequence
|
|
|
34
38
|
|
|
35
39
|
if TYPE_CHECKING:
|
|
36
40
|
from tableauserverclient.server import Server
|
|
37
|
-
from tableauserverclient.server.request_options import RequestOptions
|
|
41
|
+
from tableauserverclient.server.request_options import RequestOptions, PDFRequestOptions, PPTXRequestOptions
|
|
38
42
|
from tableauserverclient.models import DatasourceItem
|
|
39
43
|
from tableauserverclient.server.endpoint.schedules_endpoint import AddResponse
|
|
40
44
|
|
|
@@ -136,7 +140,7 @@ class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]):
|
|
|
136
140
|
"""
|
|
137
141
|
id_ = getattr(workbook_item, "id", workbook_item)
|
|
138
142
|
url = f"{self.baseurl}/{id_}/refresh"
|
|
139
|
-
refresh_req = RequestFactory.Task.refresh_req(incremental)
|
|
143
|
+
refresh_req = RequestFactory.Task.refresh_req(incremental, self.parent_srv)
|
|
140
144
|
server_response = self.post_request(url, refresh_req)
|
|
141
145
|
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
142
146
|
return new_job
|
|
@@ -472,11 +476,12 @@ class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]):
|
|
|
472
476
|
connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
473
477
|
return connections
|
|
474
478
|
|
|
475
|
-
# Get the pdf of the entire workbook if its tabs are enabled, pdf of the default view if its tabs are disabled
|
|
476
479
|
@api(version="3.4")
|
|
477
|
-
def populate_pdf(self, workbook_item: WorkbookItem, req_options: Optional["
|
|
480
|
+
def populate_pdf(self, workbook_item: WorkbookItem, req_options: Optional["PDFRequestOptions"] = None) -> None:
|
|
478
481
|
"""
|
|
479
|
-
Populates the PDF for the specified workbook item.
|
|
482
|
+
Populates the PDF for the specified workbook item. Get the pdf of the
|
|
483
|
+
entire workbook if its tabs are enabled, pdf of the default view if its
|
|
484
|
+
tabs are disabled.
|
|
480
485
|
|
|
481
486
|
This method populates a PDF with image(s) of the workbook view(s) you
|
|
482
487
|
specify.
|
|
@@ -488,7 +493,7 @@ class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]):
|
|
|
488
493
|
workbook_item : WorkbookItem
|
|
489
494
|
The workbook item to populate the PDF for.
|
|
490
495
|
|
|
491
|
-
req_options :
|
|
496
|
+
req_options : PDFRequestOptions, optional
|
|
492
497
|
(Optional) You can pass in request options to specify the page type
|
|
493
498
|
and orientation of the PDF content, as well as the maximum age of
|
|
494
499
|
the PDF rendered on the server. See PDFRequestOptions class for more
|
|
@@ -510,17 +515,26 @@ class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]):
|
|
|
510
515
|
def pdf_fetcher() -> bytes:
|
|
511
516
|
return self._get_wb_pdf(workbook_item, req_options)
|
|
512
517
|
|
|
518
|
+
if not self.parent_srv.check_at_least_version("3.23") and req_options is not None:
|
|
519
|
+
if req_options.view_filters or req_options.view_parameters:
|
|
520
|
+
raise UnsupportedAttributeError("view_filters and view_parameters are only supported in 3.23+")
|
|
521
|
+
|
|
522
|
+
if req_options.viz_height or req_options.viz_width:
|
|
523
|
+
raise UnsupportedAttributeError("viz_height and viz_width are only supported in 3.23+")
|
|
524
|
+
|
|
513
525
|
workbook_item._set_pdf(pdf_fetcher)
|
|
514
526
|
logger.info(f"Populated pdf for workbook (ID: {workbook_item.id})")
|
|
515
527
|
|
|
516
|
-
def _get_wb_pdf(self, workbook_item: WorkbookItem, req_options: Optional["
|
|
528
|
+
def _get_wb_pdf(self, workbook_item: WorkbookItem, req_options: Optional["PDFRequestOptions"]) -> bytes:
|
|
517
529
|
url = f"{self.baseurl}/{workbook_item.id}/pdf"
|
|
518
530
|
server_response = self.get_request(url, req_options)
|
|
519
531
|
pdf = server_response.content
|
|
520
532
|
return pdf
|
|
521
533
|
|
|
522
534
|
@api(version="3.8")
|
|
523
|
-
def populate_powerpoint(
|
|
535
|
+
def populate_powerpoint(
|
|
536
|
+
self, workbook_item: WorkbookItem, req_options: Optional["PPTXRequestOptions"] = None
|
|
537
|
+
) -> None:
|
|
524
538
|
"""
|
|
525
539
|
Populates the PowerPoint for the specified workbook item.
|
|
526
540
|
|
|
@@ -561,7 +575,7 @@ class Workbooks(QuerysetEndpoint[WorkbookItem], TaggingMixin[WorkbookItem]):
|
|
|
561
575
|
workbook_item._set_powerpoint(pptx_fetcher)
|
|
562
576
|
logger.info(f"Populated powerpoint for workbook (ID: {workbook_item.id})")
|
|
563
577
|
|
|
564
|
-
def _get_wb_pptx(self, workbook_item: WorkbookItem, req_options: Optional["
|
|
578
|
+
def _get_wb_pptx(self, workbook_item: WorkbookItem, req_options: Optional["PPTXRequestOptions"]) -> bytes:
|
|
565
579
|
url = f"{self.baseurl}/{workbook_item.id}/powerpoint"
|
|
566
580
|
server_response = self.get_request(url, req_options)
|
|
567
581
|
pptx = server_response.content
|
|
@@ -208,6 +208,42 @@ class QuerySet(Iterable[T], Sized):
|
|
|
208
208
|
self.request_options.pagesize = kwargs["page_size"]
|
|
209
209
|
return self
|
|
210
210
|
|
|
211
|
+
def fields(self: Self, *fields: str) -> Self:
|
|
212
|
+
"""
|
|
213
|
+
Add fields to the request options. If no fields are provided, the
|
|
214
|
+
default fields will be used. If fields are provided, the default fields
|
|
215
|
+
will be used in addition to the provided fields.
|
|
216
|
+
|
|
217
|
+
Parameters
|
|
218
|
+
----------
|
|
219
|
+
fields : str
|
|
220
|
+
The fields to include in the request options.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
QuerySet
|
|
225
|
+
"""
|
|
226
|
+
self.request_options.fields |= set(fields) | set(("_default_"))
|
|
227
|
+
return self
|
|
228
|
+
|
|
229
|
+
def only_fields(self: Self, *fields: str) -> Self:
|
|
230
|
+
"""
|
|
231
|
+
Add fields to the request options. If no fields are provided, the
|
|
232
|
+
default fields will be used. If fields are provided, the default fields
|
|
233
|
+
will be replaced by the provided fields.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
fields : str
|
|
238
|
+
The fields to include in the request options.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
QuerySet
|
|
243
|
+
"""
|
|
244
|
+
self.request_options.fields |= set(fields)
|
|
245
|
+
return self
|
|
246
|
+
|
|
211
247
|
@staticmethod
|
|
212
248
|
def _parse_shorthand_filter(key: str) -> tuple[str, str]:
|
|
213
249
|
tokens = key.split("__", 1)
|
|
@@ -913,6 +913,8 @@ class UserRequest:
|
|
|
913
913
|
user_element.attrib["authSetting"] = user_item.auth_setting
|
|
914
914
|
if password:
|
|
915
915
|
user_element.attrib["password"] = password
|
|
916
|
+
if user_item.idp_configuration_id is not None:
|
|
917
|
+
user_element.attrib["idpConfigurationId"] = user_item.idp_configuration_id
|
|
916
918
|
return ET.tostring(xml_request)
|
|
917
919
|
|
|
918
920
|
def add_req(self, user_item: UserItem) -> bytes:
|
|
@@ -929,6 +931,9 @@ class UserRequest:
|
|
|
929
931
|
|
|
930
932
|
if user_item.auth_setting:
|
|
931
933
|
user_element.attrib["authSetting"] = user_item.auth_setting
|
|
934
|
+
|
|
935
|
+
if user_item.idp_configuration_id is not None:
|
|
936
|
+
user_element.attrib["idpConfigurationId"] = user_item.idp_configuration_id
|
|
932
937
|
return ET.tostring(xml_request)
|
|
933
938
|
|
|
934
939
|
|
|
@@ -1118,11 +1123,17 @@ class TaskRequest:
|
|
|
1118
1123
|
pass
|
|
1119
1124
|
|
|
1120
1125
|
@_tsrequest_wrapped
|
|
1121
|
-
def refresh_req(
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
+
def refresh_req(
|
|
1127
|
+
self, xml_request: ET.Element, incremental: bool = False, parent_srv: Optional["Server"] = None
|
|
1128
|
+
) -> Optional[bytes]:
|
|
1129
|
+
if parent_srv is not None and parent_srv.check_at_least_version("3.25"):
|
|
1130
|
+
task_element = ET.SubElement(xml_request, "extractRefresh")
|
|
1131
|
+
if incremental:
|
|
1132
|
+
task_element.attrib["incremental"] = "true"
|
|
1133
|
+
return ET.tostring(xml_request)
|
|
1134
|
+
elif incremental:
|
|
1135
|
+
raise ValueError("Incremental refresh is only supported in 3.25+")
|
|
1136
|
+
return None
|
|
1126
1137
|
|
|
1127
1138
|
@_tsrequest_wrapped
|
|
1128
1139
|
def create_extract_req(self, xml_request: ET.Element, extract_item: "TaskItem") -> bytes:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
from typing import Optional
|
|
3
|
+
import warnings
|
|
3
4
|
|
|
4
5
|
from typing_extensions import Self
|
|
5
6
|
|
|
@@ -62,8 +63,21 @@ class RequestOptions(RequestOptionsBase):
|
|
|
62
63
|
self.pagesize = pagesize or config.PAGE_SIZE
|
|
63
64
|
self.sort = set()
|
|
64
65
|
self.filter = set()
|
|
66
|
+
self.fields = set()
|
|
65
67
|
# This is private until we expand all of our parsers to handle the extra fields
|
|
66
|
-
self.
|
|
68
|
+
self.all_fields = False
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def _all_fields(self) -> bool:
|
|
72
|
+
return self.all_fields
|
|
73
|
+
|
|
74
|
+
@_all_fields.setter
|
|
75
|
+
def _all_fields(self, value):
|
|
76
|
+
warnings.warn(
|
|
77
|
+
"Directly setting _all_fields is deprecated, please use the all_fields property instead.",
|
|
78
|
+
DeprecationWarning,
|
|
79
|
+
)
|
|
80
|
+
self.all_fields = value
|
|
67
81
|
|
|
68
82
|
def get_query_params(self) -> dict:
|
|
69
83
|
params = {}
|
|
@@ -75,12 +89,14 @@ class RequestOptions(RequestOptionsBase):
|
|
|
75
89
|
filter_options = (str(filter_item) for filter_item in self.filter)
|
|
76
90
|
ordered_filter_options = sorted(filter_options)
|
|
77
91
|
params["filter"] = ",".join(ordered_filter_options)
|
|
78
|
-
if self.
|
|
92
|
+
if self.all_fields:
|
|
79
93
|
params["fields"] = "_all_"
|
|
80
94
|
if self.pagenumber:
|
|
81
95
|
params["pageNumber"] = self.pagenumber
|
|
82
96
|
if self.pagesize:
|
|
83
97
|
params["pageSize"] = self.pagesize
|
|
98
|
+
if self.fields:
|
|
99
|
+
params["fields"] = ",".join(self.fields)
|
|
84
100
|
return params
|
|
85
101
|
|
|
86
102
|
def page_size(self, page_size):
|
|
@@ -181,6 +197,116 @@ class RequestOptions(RequestOptionsBase):
|
|
|
181
197
|
Desc = "desc"
|
|
182
198
|
Asc = "asc"
|
|
183
199
|
|
|
200
|
+
class SelectFields:
|
|
201
|
+
class Common:
|
|
202
|
+
All = "_all_"
|
|
203
|
+
Default = "_default_"
|
|
204
|
+
|
|
205
|
+
class ContentsCounts:
|
|
206
|
+
ProjectCount = "contentsCounts.projectCount"
|
|
207
|
+
ViewCount = "contentsCounts.viewCount"
|
|
208
|
+
DatasourceCount = "contentsCounts.datasourceCount"
|
|
209
|
+
WorkbookCount = "contentsCounts.workbookCount"
|
|
210
|
+
|
|
211
|
+
class Datasource:
|
|
212
|
+
ContentUrl = "datasource.contentUrl"
|
|
213
|
+
ID = "datasource.id"
|
|
214
|
+
Name = "datasource.name"
|
|
215
|
+
Type = "datasource.type"
|
|
216
|
+
Description = "datasource.description"
|
|
217
|
+
CreatedAt = "datasource.createdAt"
|
|
218
|
+
UpdatedAt = "datasource.updatedAt"
|
|
219
|
+
EncryptExtracts = "datasource.encryptExtracts"
|
|
220
|
+
IsCertified = "datasource.isCertified"
|
|
221
|
+
UseRemoteQueryAgent = "datasource.useRemoteQueryAgent"
|
|
222
|
+
WebPageURL = "datasource.webpageUrl"
|
|
223
|
+
Size = "datasource.size"
|
|
224
|
+
Tag = "datasource.tag"
|
|
225
|
+
FavoritesTotal = "datasource.favoritesTotal"
|
|
226
|
+
DatabaseName = "datasource.databaseName"
|
|
227
|
+
ConnectedWorkbooksCount = "datasource.connectedWorkbooksCount"
|
|
228
|
+
HasAlert = "datasource.hasAlert"
|
|
229
|
+
HasExtracts = "datasource.hasExtracts"
|
|
230
|
+
IsPublished = "datasource.isPublished"
|
|
231
|
+
ServerName = "datasource.serverName"
|
|
232
|
+
|
|
233
|
+
class Favorite:
|
|
234
|
+
Label = "favorite.label"
|
|
235
|
+
ParentProjectName = "favorite.parentProjectName"
|
|
236
|
+
TargetOwnerName = "favorite.targetOwnerName"
|
|
237
|
+
|
|
238
|
+
class Group:
|
|
239
|
+
ID = "group.id"
|
|
240
|
+
Name = "group.name"
|
|
241
|
+
DomainName = "group.domainName"
|
|
242
|
+
UserCount = "group.userCount"
|
|
243
|
+
MinimumSiteRole = "group.minimumSiteRole"
|
|
244
|
+
|
|
245
|
+
class Job:
|
|
246
|
+
ID = "job.id"
|
|
247
|
+
Status = "job.status"
|
|
248
|
+
CreatedAt = "job.createdAt"
|
|
249
|
+
StartedAt = "job.startedAt"
|
|
250
|
+
EndedAt = "job.endedAt"
|
|
251
|
+
Priority = "job.priority"
|
|
252
|
+
JobType = "job.jobType"
|
|
253
|
+
Title = "job.title"
|
|
254
|
+
Subtitle = "job.subtitle"
|
|
255
|
+
|
|
256
|
+
class Owner:
|
|
257
|
+
ID = "owner.id"
|
|
258
|
+
Name = "owner.name"
|
|
259
|
+
FullName = "owner.fullName"
|
|
260
|
+
SiteRole = "owner.siteRole"
|
|
261
|
+
LastLogin = "owner.lastLogin"
|
|
262
|
+
Email = "owner.email"
|
|
263
|
+
|
|
264
|
+
class Project:
|
|
265
|
+
ID = "project.id"
|
|
266
|
+
Name = "project.name"
|
|
267
|
+
Description = "project.description"
|
|
268
|
+
CreatedAt = "project.createdAt"
|
|
269
|
+
UpdatedAt = "project.updatedAt"
|
|
270
|
+
ContentPermissions = "project.contentPermissions"
|
|
271
|
+
ParentProjectID = "project.parentProjectId"
|
|
272
|
+
TopLevelProject = "project.topLevelProject"
|
|
273
|
+
Writeable = "project.writeable"
|
|
274
|
+
|
|
275
|
+
class User:
|
|
276
|
+
ExternalAuthUserId = "user.externalAuthUserId"
|
|
277
|
+
ID = "user.id"
|
|
278
|
+
Name = "user.name"
|
|
279
|
+
SiteRole = "user.siteRole"
|
|
280
|
+
LastLogin = "user.lastLogin"
|
|
281
|
+
FullName = "user.fullName"
|
|
282
|
+
Email = "user.email"
|
|
283
|
+
AuthSetting = "user.authSetting"
|
|
284
|
+
|
|
285
|
+
class View:
|
|
286
|
+
ID = "view.id"
|
|
287
|
+
Name = "view.name"
|
|
288
|
+
ContentUrl = "view.contentUrl"
|
|
289
|
+
CreatedAt = "view.createdAt"
|
|
290
|
+
UpdatedAt = "view.updatedAt"
|
|
291
|
+
Tags = "view.tags"
|
|
292
|
+
SheetType = "view.sheetType"
|
|
293
|
+
Usage = "view.usage"
|
|
294
|
+
|
|
295
|
+
class Workbook:
|
|
296
|
+
ID = "workbook.id"
|
|
297
|
+
Description = "workbook.description"
|
|
298
|
+
Name = "workbook.name"
|
|
299
|
+
ContentUrl = "workbook.contentUrl"
|
|
300
|
+
ShowTabs = "workbook.showTabs"
|
|
301
|
+
Size = "workbook.size"
|
|
302
|
+
CreatedAt = "workbook.createdAt"
|
|
303
|
+
UpdatedAt = "workbook.updatedAt"
|
|
304
|
+
SheetCount = "workbook.sheetCount"
|
|
305
|
+
HasExtracts = "workbook.hasExtracts"
|
|
306
|
+
Tags = "workbook.tags"
|
|
307
|
+
WebpageUrl = "workbook.webpageUrl"
|
|
308
|
+
DefaultViewId = "workbook.defaultViewId"
|
|
309
|
+
|
|
184
310
|
|
|
185
311
|
"""
|
|
186
312
|
These options can be used by methods that are fetching data exported from a specific content item
|
|
@@ -385,6 +511,8 @@ class PDFRequestOptions(_ImagePDFCommonExportOptions):
|
|
|
385
511
|
Options that can be used when exporting a view to PDF. Set the maxage to control the age of the data exported.
|
|
386
512
|
Filters to the underlying data can be applied using the `vf` and `parameter` methods.
|
|
387
513
|
|
|
514
|
+
vf and parameter filters are only supported in API version 3.23 and later.
|
|
515
|
+
|
|
388
516
|
Parameters
|
|
389
517
|
----------
|
|
390
518
|
page_type: str, optional
|
|
@@ -438,3 +566,35 @@ class PDFRequestOptions(_ImagePDFCommonExportOptions):
|
|
|
438
566
|
params["orientation"] = self.orientation
|
|
439
567
|
|
|
440
568
|
return params
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
class PPTXRequestOptions(RequestOptionsBase):
|
|
572
|
+
"""
|
|
573
|
+
Options that can be used when exporting a view to PPTX. Set the maxage to control the age of the data exported.
|
|
574
|
+
|
|
575
|
+
Parameters
|
|
576
|
+
----------
|
|
577
|
+
maxage: int, optional
|
|
578
|
+
The maximum age of the data to export. Shortest possible duration is 1
|
|
579
|
+
minute. No upper limit. Default is -1, which means no limit.
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
def __init__(self, maxage=-1):
|
|
583
|
+
super().__init__()
|
|
584
|
+
self.max_age = maxage
|
|
585
|
+
|
|
586
|
+
@property
|
|
587
|
+
def max_age(self) -> int:
|
|
588
|
+
return self._max_age
|
|
589
|
+
|
|
590
|
+
@max_age.setter
|
|
591
|
+
@property_is_int(range=(0, 240), allowed=[-1])
|
|
592
|
+
def max_age(self, value):
|
|
593
|
+
self._max_age = value
|
|
594
|
+
|
|
595
|
+
def get_query_params(self):
|
|
596
|
+
params = {}
|
|
597
|
+
if self.max_age != -1:
|
|
598
|
+
params["maxAge"] = self.max_age
|
|
599
|
+
|
|
600
|
+
return params
|
|
@@ -2,6 +2,7 @@ from tableauserverclient.helpers.logging import logger
|
|
|
2
2
|
|
|
3
3
|
import requests
|
|
4
4
|
import urllib3
|
|
5
|
+
import ssl
|
|
5
6
|
|
|
6
7
|
from defusedxml.ElementTree import fromstring, ParseError
|
|
7
8
|
from packaging.version import Version
|
|
@@ -91,6 +92,13 @@ class Server:
|
|
|
91
92
|
and a later version of the REST API. For more information, see REST API
|
|
92
93
|
Versions.
|
|
93
94
|
|
|
95
|
+
http_options : dict, optional
|
|
96
|
+
Additional options to pass to the requests library when making HTTP requests.
|
|
97
|
+
|
|
98
|
+
session_factory : callable, optional
|
|
99
|
+
A factory function that returns a requests.Session object. If not provided,
|
|
100
|
+
requests.session is used.
|
|
101
|
+
|
|
94
102
|
Examples
|
|
95
103
|
--------
|
|
96
104
|
>>> import tableauserverclient as TSC
|
|
@@ -107,6 +115,16 @@ class Server:
|
|
|
107
115
|
>>> # for example, 2.8
|
|
108
116
|
>>> # server.version = '2.8'
|
|
109
117
|
|
|
118
|
+
>>> # if connecting to an older Tableau Server with weak DH keys (Python 3.12+ only)
|
|
119
|
+
>>> server.configure_ssl(allow_weak_dh=True) # Note: reduces security
|
|
120
|
+
|
|
121
|
+
Notes
|
|
122
|
+
-----
|
|
123
|
+
When using Python 3.12 or later with older versions of Tableau Server, you may encounter
|
|
124
|
+
SSL errors related to weak Diffie-Hellman keys. This is because newer Python versions
|
|
125
|
+
enforce stronger security requirements. You can temporarily work around this using
|
|
126
|
+
configure_ssl(allow_weak_dh=True), but this reduces security and should only be used
|
|
127
|
+
as a temporary measure until the server can be upgraded.
|
|
110
128
|
"""
|
|
111
129
|
|
|
112
130
|
class PublishMode:
|
|
@@ -125,6 +143,7 @@ class Server:
|
|
|
125
143
|
self._auth_token = None
|
|
126
144
|
self._site_id = None
|
|
127
145
|
self._user_id = None
|
|
146
|
+
self._ssl_context = None
|
|
128
147
|
|
|
129
148
|
# TODO: this needs to change to default to https, but without breaking existing code
|
|
130
149
|
if not server_address.startswith("http://") and not server_address.startswith("https://"):
|
|
@@ -313,3 +332,26 @@ class Server:
|
|
|
313
332
|
|
|
314
333
|
def is_signed_in(self):
|
|
315
334
|
return self._auth_token is not None
|
|
335
|
+
|
|
336
|
+
def configure_ssl(self, *, allow_weak_dh=False):
|
|
337
|
+
"""Configure SSL/TLS settings for the server connection.
|
|
338
|
+
|
|
339
|
+
Parameters
|
|
340
|
+
----------
|
|
341
|
+
allow_weak_dh : bool, optional
|
|
342
|
+
If True, allows connections to servers with DH keys that are considered too small by modern Python versions.
|
|
343
|
+
WARNING: This reduces security and should only be used as a temporary workaround.
|
|
344
|
+
"""
|
|
345
|
+
if allow_weak_dh:
|
|
346
|
+
logger.warning(
|
|
347
|
+
"WARNING: Allowing weak Diffie-Hellman keys. This reduces security and should only be used temporarily."
|
|
348
|
+
)
|
|
349
|
+
self._ssl_context = ssl.create_default_context()
|
|
350
|
+
# Allow weak DH keys by setting minimum key size to 512 bits (default is 1024 in Python 3.12+)
|
|
351
|
+
self._ssl_context.set_dh_parameters(min_key_bits=512)
|
|
352
|
+
self.add_http_options({"verify": self._ssl_context})
|
|
353
|
+
else:
|
|
354
|
+
self._ssl_context = None
|
|
355
|
+
# Remove any custom SSL context if we're reverting to default settings
|
|
356
|
+
if "verify" in self._http_options:
|
|
357
|
+
del self._http_options["verify"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: tableauserverclient
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.38
|
|
4
4
|
Summary: A Python module for working with the Tableau Server REST API.
|
|
5
5
|
Author-email: Tableau <github@tableau.com>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -50,6 +50,7 @@ Requires-Dist: pytest>=7.0; extra == "test"
|
|
|
50
50
|
Requires-Dist: pytest-cov; extra == "test"
|
|
51
51
|
Requires-Dist: pytest-subtests; extra == "test"
|
|
52
52
|
Requires-Dist: requests-mock<2.0,>=1.0; extra == "test"
|
|
53
|
+
Dynamic: license-file
|
|
53
54
|
|
|
54
55
|
# Tableau Server Client (Python)
|
|
55
56
|
|