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.
Files changed (45) hide show
  1. tableauserverclient/__init__.py +6 -0
  2. tableauserverclient/bin/_version.py +3 -3
  3. tableauserverclient/helpers/strings.py +25 -1
  4. tableauserverclient/models/__init__.py +6 -1
  5. tableauserverclient/models/connection_item.py +3 -3
  6. tableauserverclient/models/datasource_item.py +218 -23
  7. tableauserverclient/models/extract_item.py +82 -0
  8. tableauserverclient/models/flow_item.py +2 -2
  9. tableauserverclient/models/group_item.py +11 -0
  10. tableauserverclient/models/interval_item.py +40 -0
  11. tableauserverclient/models/job_item.py +1 -0
  12. tableauserverclient/models/location_item.py +53 -0
  13. tableauserverclient/models/project_item.py +138 -27
  14. tableauserverclient/models/schedule_item.py +57 -0
  15. tableauserverclient/models/site_item.py +28 -0
  16. tableauserverclient/models/table_item.py +7 -3
  17. tableauserverclient/models/tableau_types.py +13 -1
  18. tableauserverclient/models/user_item.py +101 -1
  19. tableauserverclient/models/view_item.py +79 -5
  20. tableauserverclient/models/workbook_item.py +151 -1
  21. tableauserverclient/server/__init__.py +2 -0
  22. tableauserverclient/server/endpoint/databases_endpoint.py +101 -18
  23. tableauserverclient/server/endpoint/datasources_endpoint.py +562 -7
  24. tableauserverclient/server/endpoint/dqw_endpoint.py +16 -6
  25. tableauserverclient/server/endpoint/endpoint.py +39 -0
  26. tableauserverclient/server/endpoint/exceptions.py +4 -0
  27. tableauserverclient/server/endpoint/fileuploads_endpoint.py +1 -1
  28. tableauserverclient/server/endpoint/groupsets_endpoint.py +2 -2
  29. tableauserverclient/server/endpoint/jobs_endpoint.py +1 -1
  30. tableauserverclient/server/endpoint/schedules_endpoint.py +132 -2
  31. tableauserverclient/server/endpoint/sites_endpoint.py +18 -1
  32. tableauserverclient/server/endpoint/tables_endpoint.py +140 -17
  33. tableauserverclient/server/endpoint/users_endpoint.py +22 -5
  34. tableauserverclient/server/endpoint/views_endpoint.py +5 -1
  35. tableauserverclient/server/endpoint/workbooks_endpoint.py +24 -10
  36. tableauserverclient/server/query.py +36 -0
  37. tableauserverclient/server/request_factory.py +16 -5
  38. tableauserverclient/server/request_options.py +162 -2
  39. tableauserverclient/server/server.py +42 -0
  40. {tableauserverclient-0.36.dist-info → tableauserverclient-0.38.dist-info}/METADATA +3 -2
  41. {tableauserverclient-0.36.dist-info → tableauserverclient-0.38.dist-info}/RECORD +45 -43
  42. {tableauserverclient-0.36.dist-info → tableauserverclient-0.38.dist-info}/WHEEL +1 -1
  43. {tableauserverclient-0.36.dist-info → tableauserverclient-0.38.dist-info/licenses}/LICENSE +0 -0
  44. {tableauserverclient-0.36.dist-info → tableauserverclient-0.38.dist-info/licenses}/LICENSE.versioneer +0 -0
  45. {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._all_fields = True
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(self, user_item: UserItem, req_options: Optional[RequestOptions] = None) -> None:
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
- and has Read (view) permissions for.
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
- return Pager(lambda options: self._get_wbs_for_user(user_item, options), req_options)
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, user_item: UserItem, req_options: Optional[RequestOptions] = None
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 InternalServerError, MissingRequiredFieldError
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["RequestOptions"] = None) -> None:
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 : RequestOptions, optional
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["RequestOptions"]) -> bytes:
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(self, workbook_item: WorkbookItem, req_options: Optional["RequestOptions"] = None) -> None:
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["RequestOptions"]) -> bytes:
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(self, xml_request: ET.Element, incremental: bool = False) -> bytes:
1122
- task_element = ET.SubElement(xml_request, "extractRefresh")
1123
- if incremental:
1124
- task_element.attrib["incremental"] = "true"
1125
- return ET.tostring(xml_request)
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._all_fields = False
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._all_fields:
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.2
1
+ Metadata-Version: 2.4
2
2
  Name: tableauserverclient
3
- Version: 0.36
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