tableauserverclient 0.33__py3-none-any.whl → 0.34__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 (94) hide show
  1. tableauserverclient/__init__.py +28 -22
  2. tableauserverclient/_version.py +3 -3
  3. tableauserverclient/config.py +5 -3
  4. tableauserverclient/models/column_item.py +1 -1
  5. tableauserverclient/models/connection_credentials.py +1 -1
  6. tableauserverclient/models/connection_item.py +6 -6
  7. tableauserverclient/models/custom_view_item.py +29 -6
  8. tableauserverclient/models/data_acceleration_report_item.py +2 -2
  9. tableauserverclient/models/data_alert_item.py +5 -5
  10. tableauserverclient/models/data_freshness_policy_item.py +6 -6
  11. tableauserverclient/models/database_item.py +3 -3
  12. tableauserverclient/models/datasource_item.py +10 -10
  13. tableauserverclient/models/dqw_item.py +1 -1
  14. tableauserverclient/models/favorites_item.py +5 -6
  15. tableauserverclient/models/fileupload_item.py +1 -1
  16. tableauserverclient/models/flow_item.py +6 -6
  17. tableauserverclient/models/flow_run_item.py +3 -3
  18. tableauserverclient/models/group_item.py +4 -4
  19. tableauserverclient/models/groupset_item.py +4 -4
  20. tableauserverclient/models/interval_item.py +9 -9
  21. tableauserverclient/models/job_item.py +8 -8
  22. tableauserverclient/models/linked_tasks_item.py +5 -5
  23. tableauserverclient/models/metric_item.py +5 -5
  24. tableauserverclient/models/pagination_item.py +1 -1
  25. tableauserverclient/models/permissions_item.py +12 -10
  26. tableauserverclient/models/project_item.py +35 -19
  27. tableauserverclient/models/property_decorators.py +12 -11
  28. tableauserverclient/models/reference_item.py +2 -2
  29. tableauserverclient/models/revision_item.py +3 -3
  30. tableauserverclient/models/schedule_item.py +2 -2
  31. tableauserverclient/models/server_info_item.py +26 -6
  32. tableauserverclient/models/site_item.py +69 -3
  33. tableauserverclient/models/subscription_item.py +3 -3
  34. tableauserverclient/models/table_item.py +1 -1
  35. tableauserverclient/models/tableau_auth.py +115 -5
  36. tableauserverclient/models/tableau_types.py +2 -2
  37. tableauserverclient/models/tag_item.py +3 -4
  38. tableauserverclient/models/task_item.py +4 -4
  39. tableauserverclient/models/user_item.py +47 -17
  40. tableauserverclient/models/view_item.py +11 -10
  41. tableauserverclient/models/virtual_connection_item.py +6 -5
  42. tableauserverclient/models/webhook_item.py +6 -6
  43. tableauserverclient/models/workbook_item.py +90 -12
  44. tableauserverclient/namespace.py +1 -1
  45. tableauserverclient/server/__init__.py +2 -1
  46. tableauserverclient/server/endpoint/auth_endpoint.py +65 -8
  47. tableauserverclient/server/endpoint/custom_views_endpoint.py +62 -18
  48. tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py +2 -2
  49. tableauserverclient/server/endpoint/data_alert_endpoint.py +14 -14
  50. tableauserverclient/server/endpoint/databases_endpoint.py +13 -12
  51. tableauserverclient/server/endpoint/datasources_endpoint.py +49 -54
  52. tableauserverclient/server/endpoint/default_permissions_endpoint.py +19 -18
  53. tableauserverclient/server/endpoint/dqw_endpoint.py +9 -9
  54. tableauserverclient/server/endpoint/endpoint.py +19 -21
  55. tableauserverclient/server/endpoint/exceptions.py +23 -7
  56. tableauserverclient/server/endpoint/favorites_endpoint.py +31 -31
  57. tableauserverclient/server/endpoint/fileuploads_endpoint.py +9 -11
  58. tableauserverclient/server/endpoint/flow_runs_endpoint.py +15 -13
  59. tableauserverclient/server/endpoint/flow_task_endpoint.py +2 -2
  60. tableauserverclient/server/endpoint/flows_endpoint.py +30 -29
  61. tableauserverclient/server/endpoint/groups_endpoint.py +18 -17
  62. tableauserverclient/server/endpoint/groupsets_endpoint.py +2 -2
  63. tableauserverclient/server/endpoint/jobs_endpoint.py +7 -7
  64. tableauserverclient/server/endpoint/linked_tasks_endpoint.py +2 -2
  65. tableauserverclient/server/endpoint/metadata_endpoint.py +2 -2
  66. tableauserverclient/server/endpoint/metrics_endpoint.py +10 -10
  67. tableauserverclient/server/endpoint/permissions_endpoint.py +13 -15
  68. tableauserverclient/server/endpoint/projects_endpoint.py +81 -30
  69. tableauserverclient/server/endpoint/resource_tagger.py +14 -13
  70. tableauserverclient/server/endpoint/schedules_endpoint.py +17 -18
  71. tableauserverclient/server/endpoint/server_info_endpoint.py +40 -5
  72. tableauserverclient/server/endpoint/sites_endpoint.py +282 -17
  73. tableauserverclient/server/endpoint/subscriptions_endpoint.py +10 -10
  74. tableauserverclient/server/endpoint/tables_endpoint.py +15 -14
  75. tableauserverclient/server/endpoint/tasks_endpoint.py +8 -8
  76. tableauserverclient/server/endpoint/users_endpoint.py +366 -19
  77. tableauserverclient/server/endpoint/views_endpoint.py +19 -18
  78. tableauserverclient/server/endpoint/virtual_connections_endpoint.py +6 -5
  79. tableauserverclient/server/endpoint/webhooks_endpoint.py +11 -11
  80. tableauserverclient/server/endpoint/workbooks_endpoint.py +647 -61
  81. tableauserverclient/server/filter.py +2 -2
  82. tableauserverclient/server/pager.py +5 -6
  83. tableauserverclient/server/query.py +68 -19
  84. tableauserverclient/server/request_factory.py +37 -36
  85. tableauserverclient/server/request_options.py +123 -145
  86. tableauserverclient/server/server.py +65 -9
  87. tableauserverclient/server/sort.py +2 -2
  88. {tableauserverclient-0.33.dist-info → tableauserverclient-0.34.dist-info}/METADATA +6 -6
  89. tableauserverclient-0.34.dist-info/RECORD +106 -0
  90. {tableauserverclient-0.33.dist-info → tableauserverclient-0.34.dist-info}/WHEEL +1 -1
  91. tableauserverclient-0.33.dist-info/RECORD +0 -106
  92. {tableauserverclient-0.33.dist-info → tableauserverclient-0.34.dist-info}/LICENSE +0 -0
  93. {tableauserverclient-0.33.dist-info → tableauserverclient-0.34.dist-info}/LICENSE.versioneer +0 -0
  94. {tableauserverclient-0.33.dist-info → tableauserverclient-0.34.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
1
  import datetime as dt
2
2
  import json
3
- from typing import Callable, Dict, Iterable, List, Optional
3
+ from typing import Callable, Optional
4
+ from collections.abc import Iterable
4
5
  from xml.etree.ElementTree import Element
5
6
 
6
7
  from defusedxml.ElementTree import fromstring
@@ -23,7 +24,7 @@ class VirtualConnectionItem:
23
24
  self._connections: Optional[Callable[[], Iterable[ConnectionItem]]] = None
24
25
  self.project_id: Optional[str] = None
25
26
  self.owner_id: Optional[str] = None
26
- self.content: Optional[Dict[str, dict]] = None
27
+ self.content: Optional[dict[str, dict]] = None
27
28
  self.certification_note: Optional[str] = None
28
29
 
29
30
  def __str__(self) -> str:
@@ -40,7 +41,7 @@ class VirtualConnectionItem:
40
41
  return self._id
41
42
 
42
43
  @property
43
- def permissions(self) -> List[PermissionsRule]:
44
+ def permissions(self) -> list[PermissionsRule]:
44
45
  if self._permissions is None:
45
46
  error = "Workbook item must be populated with permissions first."
46
47
  raise UnpopulatedPropertyError(error)
@@ -53,12 +54,12 @@ class VirtualConnectionItem:
53
54
  return self._connections()
54
55
 
55
56
  @classmethod
56
- def from_response(cls, response: bytes, ns: Dict[str, str]) -> List["VirtualConnectionItem"]:
57
+ def from_response(cls, response: bytes, ns: dict[str, str]) -> list["VirtualConnectionItem"]:
57
58
  parsed_response = fromstring(response)
58
59
  return [cls.from_xml(xml, ns) for xml in parsed_response.findall(".//t:virtualConnection[@name]", ns)]
59
60
 
60
61
  @classmethod
61
- def from_xml(cls, xml: Element, ns: Dict[str, str]) -> "VirtualConnectionItem":
62
+ def from_xml(cls, xml: Element, ns: dict[str, str]) -> "VirtualConnectionItem":
62
63
  v_conn = cls(xml.get("name", ""))
63
64
  v_conn._id = xml.get("id", None)
64
65
  v_conn.webpage_url = xml.get("webpageUrl", None)
@@ -1,6 +1,6 @@
1
1
  import re
2
2
  import xml.etree.ElementTree as ET
3
- from typing import List, Optional, Tuple, Type
3
+ from typing import Optional
4
4
 
5
5
  from defusedxml.ElementTree import fromstring
6
6
 
@@ -13,7 +13,7 @@ def _parse_event(events):
13
13
  return NAMESPACE_RE.sub("", event.tag)
14
14
 
15
15
 
16
- class WebhookItem(object):
16
+ class WebhookItem:
17
17
  def __init__(self):
18
18
  self._id: Optional[str] = None
19
19
  self.name: Optional[str] = None
@@ -45,10 +45,10 @@ class WebhookItem(object):
45
45
 
46
46
  @event.setter
47
47
  def event(self, value: str) -> None:
48
- self._event = "webhook-source-event-{}".format(value)
48
+ self._event = f"webhook-source-event-{value}"
49
49
 
50
50
  @classmethod
51
- def from_response(cls: Type["WebhookItem"], resp: bytes, ns) -> List["WebhookItem"]:
51
+ def from_response(cls: type["WebhookItem"], resp: bytes, ns) -> list["WebhookItem"]:
52
52
  all_webhooks_items = list()
53
53
  parsed_response = fromstring(resp)
54
54
  all_webhooks_xml = parsed_response.findall(".//t:webhook", namespaces=ns)
@@ -61,7 +61,7 @@ class WebhookItem(object):
61
61
  return all_webhooks_items
62
62
 
63
63
  @staticmethod
64
- def _parse_element(webhook_xml: ET.Element, ns) -> Tuple:
64
+ def _parse_element(webhook_xml: ET.Element, ns) -> tuple:
65
65
  id = webhook_xml.get("id", None)
66
66
  name = webhook_xml.get("name", None)
67
67
 
@@ -82,4 +82,4 @@ class WebhookItem(object):
82
82
  return id, name, url, event, owner_id
83
83
 
84
84
  def __repr__(self) -> str:
85
- return "<Webhook id={} name={} url={} event={}>".format(self.id, self.name, self.url, self.event)
85
+ return f"<Webhook id={self.id} name={self.name} url={self.url} event={self.event}>"
@@ -2,7 +2,7 @@ import copy
2
2
  import datetime
3
3
  import uuid
4
4
  import xml.etree.ElementTree as ET
5
- from typing import Callable, Dict, List, Optional, Set
5
+ from typing import Callable, Optional
6
6
 
7
7
  from defusedxml.ElementTree import fromstring
8
8
 
@@ -20,7 +20,85 @@ from .view_item import ViewItem
20
20
  from .data_freshness_policy_item import DataFreshnessPolicyItem
21
21
 
22
22
 
23
- class WorkbookItem(object):
23
+ class WorkbookItem:
24
+ """
25
+ The workbook resources for Tableau are defined in the WorkbookItem class.
26
+ The class corresponds to the workbook resources you can access using the
27
+ Tableau REST API. Some workbook methods take an instance of the WorkbookItem
28
+ class as arguments. The workbook item specifies the project.
29
+
30
+ Parameters
31
+ ----------
32
+ project_id : Optional[str], optional
33
+ The project ID for the workbook, by default None.
34
+
35
+ name : Optional[str], optional
36
+ The name of the workbook, by default None.
37
+
38
+ show_tabs : bool, optional
39
+ Determines whether the workbook shows tabs for the view.
40
+
41
+ Attributes
42
+ ----------
43
+ connections : list[ConnectionItem]
44
+ The list of data connections (ConnectionItem) for the data sources used
45
+ by the workbook. You must first call the workbooks.populate_connections
46
+ method to access this data. See the ConnectionItem class.
47
+
48
+ content_url : Optional[str]
49
+ The name of the workbook as it appears in the URL.
50
+
51
+ created_at : Optional[datetime.datetime]
52
+ The date and time the workbook was created.
53
+
54
+ description : Optional[str]
55
+ User-defined description of the workbook.
56
+
57
+ id : Optional[str]
58
+ The identifier for the workbook. You need this value to query a specific
59
+ workbook or to delete a workbook with the get_by_id and delete methods.
60
+
61
+ owner_id : Optional[str]
62
+ The identifier for the owner (UserItem) of the workbook.
63
+
64
+ preview_image : bytes
65
+ The thumbnail image for the view. You must first call the
66
+ workbooks.populate_preview_image method to access this data.
67
+
68
+ project_name : Optional[str]
69
+ The name of the project that contains the workbook.
70
+
71
+ size: int
72
+ The size of the workbook in megabytes.
73
+
74
+ hidden_views: Optional[list[str]]
75
+ List of string names of views that need to be hidden when the workbook
76
+ is published.
77
+
78
+ tags: set[str]
79
+ The set of tags associated with the workbook.
80
+
81
+ updated_at : Optional[datetime.datetime]
82
+ The date and time the workbook was last updated.
83
+
84
+ views : list[ViewItem]
85
+ The list of views (ViewItem) for the workbook. You must first call the
86
+ workbooks.populate_views method to access this data. See the ViewItem
87
+ class.
88
+
89
+ web_page_url : Optional[str]
90
+ The full URL for the workbook.
91
+
92
+ Examples
93
+ --------
94
+ # creating a new instance of a WorkbookItem
95
+ >>> import tableauserverclient as TSC
96
+
97
+ >>> # Create new workbook_item with project id '3a8b6148-493c-11e6-a621-6f3499394a39'
98
+
99
+ >>> new_workbook = TSC.WorkbookItem('3a8b6148-493c-11e6-a621-6f3499394a39')
100
+ """
101
+
24
102
  def __init__(self, project_id: Optional[str] = None, name: Optional[str] = None, show_tabs: bool = False) -> None:
25
103
  self._connections = None
26
104
  self._content_url = None
@@ -35,15 +113,15 @@ class WorkbookItem(object):
35
113
  self._revisions = None
36
114
  self._size = None
37
115
  self._updated_at = None
38
- self._views: Optional[Callable[[], List[ViewItem]]] = None
116
+ self._views: Optional[Callable[[], list[ViewItem]]] = None
39
117
  self.name = name
40
118
  self._description = None
41
119
  self.owner_id: Optional[str] = None
42
120
  # workaround for Personal Space workbooks without a project
43
121
  self.project_id: Optional[str] = project_id or uuid.uuid4().__str__()
44
122
  self.show_tabs = show_tabs
45
- self.hidden_views: Optional[List[str]] = None
46
- self.tags: Set[str] = set()
123
+ self.hidden_views: Optional[list[str]] = None
124
+ self.tags: set[str] = set()
47
125
  self.data_acceleration_config = {
48
126
  "acceleration_enabled": None,
49
127
  "accelerate_now": None,
@@ -56,7 +134,7 @@ class WorkbookItem(object):
56
134
  return None
57
135
 
58
136
  def __str__(self):
59
- return "<WorkbookItem {0} '{1}' contentUrl='{2}' project={3}>".format(
137
+ return "<WorkbookItem {} '{}' contentUrl='{}' project={}>".format(
60
138
  self._id, self.name, self.content_url, self.project_id
61
139
  )
62
140
 
@@ -64,14 +142,14 @@ class WorkbookItem(object):
64
142
  return self.__str__() + " { " + ", ".join(" % s: % s" % item for item in vars(self).items()) + "}"
65
143
 
66
144
  @property
67
- def connections(self) -> List[ConnectionItem]:
145
+ def connections(self) -> list[ConnectionItem]:
68
146
  if self._connections is None:
69
147
  error = "Workbook item must be populated with connections first."
70
148
  raise UnpopulatedPropertyError(error)
71
149
  return self._connections()
72
150
 
73
151
  @property
74
- def permissions(self) -> List[PermissionsRule]:
152
+ def permissions(self) -> list[PermissionsRule]:
75
153
  if self._permissions is None:
76
154
  error = "Workbook item must be populated with permissions first."
77
155
  raise UnpopulatedPropertyError(error)
@@ -152,7 +230,7 @@ class WorkbookItem(object):
152
230
  return self._updated_at
153
231
 
154
232
  @property
155
- def views(self) -> List[ViewItem]:
233
+ def views(self) -> list[ViewItem]:
156
234
  # Views can be set in an initial workbook response OR by a call
157
235
  # to Server. Without getting too fancy, I think we can rely on
158
236
  # returning a list from the response, until they call
@@ -191,7 +269,7 @@ class WorkbookItem(object):
191
269
  self._data_freshness_policy = value
192
270
 
193
271
  @property
194
- def revisions(self) -> List[RevisionItem]:
272
+ def revisions(self) -> list[RevisionItem]:
195
273
  if self._revisions is None:
196
274
  error = "Workbook item must be populated with revisions first."
197
275
  raise UnpopulatedPropertyError(error)
@@ -203,7 +281,7 @@ class WorkbookItem(object):
203
281
  def _set_permissions(self, permissions):
204
282
  self._permissions = permissions
205
283
 
206
- def _set_views(self, views: Callable[[], List[ViewItem]]) -> None:
284
+ def _set_views(self, views: Callable[[], list[ViewItem]]) -> None:
207
285
  self._views = views
208
286
 
209
287
  def _set_pdf(self, pdf: Callable[[], bytes]) -> None:
@@ -316,7 +394,7 @@ class WorkbookItem(object):
316
394
  self.data_freshness_policy = data_freshness_policy
317
395
 
318
396
  @classmethod
319
- def from_response(cls, resp: str, ns: Dict[str, str]) -> List["WorkbookItem"]:
397
+ def from_response(cls, resp: str, ns: dict[str, str]) -> list["WorkbookItem"]:
320
398
  all_workbook_items = list()
321
399
  parsed_response = fromstring(resp)
322
400
  all_workbook_xml = parsed_response.findall(".//t:workbook", namespaces=ns)
@@ -11,7 +11,7 @@ class UnknownNamespaceError(Exception):
11
11
  pass
12
12
 
13
13
 
14
- class Namespace(object):
14
+ class Namespace:
15
15
  def __init__(self):
16
16
  self._namespace = {"t": NEW_NAMESPACE}
17
17
  self._detected = False
@@ -11,7 +11,7 @@ from tableauserverclient.server.filter import Filter
11
11
  from tableauserverclient.server.sort import Sort
12
12
  from tableauserverclient.server.server import Server
13
13
  from tableauserverclient.server.pager import Pager
14
- from tableauserverclient.server.endpoint.exceptions import NotSignedInError
14
+ from tableauserverclient.server.endpoint.exceptions import FailedSignInError, NotSignedInError
15
15
 
16
16
  from tableauserverclient.server.endpoint import (
17
17
  Auth,
@@ -57,6 +57,7 @@ __all__ = [
57
57
  "Sort",
58
58
  "Server",
59
59
  "Pager",
60
+ "FailedSignInError",
60
61
  "NotSignedInError",
61
62
  "Auth",
62
63
  "CustomViews",
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
16
16
 
17
17
 
18
18
  class Auth(Endpoint):
19
- class contextmgr(object):
19
+ class contextmgr:
20
20
  def __init__(self, callback):
21
21
  self._callback = callback
22
22
 
@@ -28,7 +28,7 @@ class Auth(Endpoint):
28
28
 
29
29
  @property
30
30
  def baseurl(self) -> str:
31
- return "{0}/auth".format(self.parent_srv.baseurl)
31
+ return f"{self.parent_srv.baseurl}/auth"
32
32
 
33
33
  @api(version="2.0")
34
34
  def sign_in(self, auth_req: "Credentials") -> contextmgr:
@@ -41,8 +41,32 @@ class Auth(Endpoint):
41
41
  optionally a user_id to impersonate.
42
42
 
43
43
  Creates a context manager that will sign out of the server upon exit.
44
+
45
+ Parameters
46
+ ----------
47
+ auth_req : Credentials
48
+ The credentials object to use for signing in. Can be a TableauAuth,
49
+ PersonalAccessTokenAuth, or JWTAuth object.
50
+
51
+ Returns
52
+ -------
53
+ contextmgr
54
+ A context manager that will sign out of the server upon exit.
55
+
56
+ Examples
57
+ --------
58
+ >>> import tableauserverclient as TSC
59
+
60
+ >>> # create an auth object
61
+ >>> tableau_auth = TSC.TableauAuth('USERNAME', 'PASSWORD')
62
+
63
+ >>> # create an instance for your server
64
+ >>> server = TSC.Server('https://SERVER_URL')
65
+
66
+ >>> # call the sign-in method with the auth object
67
+ >>> server.auth.sign_in(tableau_auth)
44
68
  """
45
- url = "{0}/{1}".format(self.baseurl, "signin")
69
+ url = f"{self.baseurl}/signin"
46
70
  signin_req = RequestFactory.Auth.signin_req(auth_req)
47
71
  server_response = self.parent_srv.session.post(
48
72
  url, data=signin_req, **self.parent_srv.http_options, allow_redirects=False
@@ -63,22 +87,25 @@ class Auth(Endpoint):
63
87
  user_id = parsed_response.find(".//t:user", namespaces=self.parent_srv.namespace).get("id", None)
64
88
  auth_token = parsed_response.find("t:credentials", namespaces=self.parent_srv.namespace).get("token", None)
65
89
  self.parent_srv._set_auth(site_id, user_id, auth_token)
66
- logger.info("Signed into {0} as user with id {1}".format(self.parent_srv.server_address, user_id))
90
+ logger.info(f"Signed into {self.parent_srv.server_address} as user with id {user_id}")
67
91
  return Auth.contextmgr(self.sign_out)
68
92
 
69
93
  # We use the same request that username/password login uses for all auth types.
70
94
  # The distinct methods are mostly useful for explicitly showing api version support for each auth type
71
95
  @api(version="3.6")
72
96
  def sign_in_with_personal_access_token(self, auth_req: "Credentials") -> contextmgr:
97
+ """Passthrough to sign_in method"""
73
98
  return self.sign_in(auth_req)
74
99
 
75
100
  @api(version="3.17")
76
101
  def sign_in_with_json_web_token(self, auth_req: "Credentials") -> contextmgr:
102
+ """Passthrough to sign_in method"""
77
103
  return self.sign_in(auth_req)
78
104
 
79
105
  @api(version="2.0")
80
106
  def sign_out(self) -> None:
81
- url = "{0}/{1}".format(self.baseurl, "signout")
107
+ """Sign out of current session."""
108
+ url = f"{self.baseurl}/signout"
82
109
  # If there are no auth tokens you're already signed out. No-op
83
110
  if not self.parent_srv.is_signed_in():
84
111
  return
@@ -88,7 +115,34 @@ class Auth(Endpoint):
88
115
 
89
116
  @api(version="2.6")
90
117
  def switch_site(self, site_item: "SiteItem") -> contextmgr:
91
- url = "{0}/{1}".format(self.baseurl, "switchSite")
118
+ """
119
+ Switch to a different site on the server. This will sign out of the
120
+ current site and sign in to the new site. If used as a context manager,
121
+ will sign out of the new site upon exit.
122
+
123
+ Parameters
124
+ ----------
125
+ site_item : SiteItem
126
+ The site to switch to.
127
+
128
+ Returns
129
+ -------
130
+ contextmgr
131
+ A context manager that will sign out of the new site upon exit.
132
+
133
+ Examples
134
+ --------
135
+ >>> import tableauserverclient as TSC
136
+
137
+ >>> # Find the site you want to switch to
138
+ >>> new_site = server.sites.get_by_id("9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d")
139
+ >>> # switch to the new site
140
+ >>> with server.auth.switch_site(new_site):
141
+ >>> # do something on the new site
142
+ >>> pass
143
+
144
+ """
145
+ url = f"{self.baseurl}/switchSite"
92
146
  switch_req = RequestFactory.Auth.switch_req(site_item.content_url)
93
147
  try:
94
148
  server_response = self.post_request(url, switch_req)
@@ -104,11 +158,14 @@ class Auth(Endpoint):
104
158
  user_id = parsed_response.find(".//t:user", namespaces=self.parent_srv.namespace).get("id", None)
105
159
  auth_token = parsed_response.find("t:credentials", namespaces=self.parent_srv.namespace).get("token", None)
106
160
  self.parent_srv._set_auth(site_id, user_id, auth_token)
107
- logger.info("Signed into {0} as user with id {1}".format(self.parent_srv.server_address, user_id))
161
+ logger.info(f"Signed into {self.parent_srv.server_address} as user with id {user_id}")
108
162
  return Auth.contextmgr(self.sign_out)
109
163
 
110
164
  @api(version="3.10")
111
165
  def revoke_all_server_admin_tokens(self) -> None:
112
- url = "{0}/{1}".format(self.baseurl, "revokeAllServerAdminTokens")
166
+ """
167
+ Revokes all personal access tokens for all server admins on the server.
168
+ """
169
+ url = f"{self.baseurl}/revokeAllServerAdminTokens"
113
170
  self.post_request(url, "")
114
171
  logger.info("Revoked all tokens for all server admins")
@@ -1,15 +1,23 @@
1
1
  import io
2
2
  import logging
3
3
  import os
4
+ from contextlib import closing
4
5
  from pathlib import Path
5
- from typing import List, Optional, Tuple, Union
6
+ from typing import Optional, Union
7
+ from collections.abc import Iterator
6
8
 
7
- from tableauserverclient.config import BYTES_PER_MB, FILESIZE_LIMIT_MB
9
+ from tableauserverclient.config import BYTES_PER_MB, config
8
10
  from tableauserverclient.filesys_helpers import get_file_object_size
9
11
  from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
10
12
  from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
11
13
  from tableauserverclient.models import CustomViewItem, PaginationItem
12
- from tableauserverclient.server import RequestFactory, RequestOptions, ImageRequestOptions
14
+ from tableauserverclient.server import (
15
+ RequestFactory,
16
+ RequestOptions,
17
+ ImageRequestOptions,
18
+ PDFRequestOptions,
19
+ CSVRequestOptions,
20
+ )
13
21
 
14
22
  from tableauserverclient.helpers.logging import logger
15
23
 
@@ -33,11 +41,11 @@ io_types_w = (io.BufferedWriter, io.BytesIO)
33
41
 
34
42
  class CustomViews(QuerysetEndpoint[CustomViewItem]):
35
43
  def __init__(self, parent_srv):
36
- super(CustomViews, self).__init__(parent_srv)
44
+ super().__init__(parent_srv)
37
45
 
38
46
  @property
39
47
  def baseurl(self) -> str:
40
- return "{0}/sites/{1}/customviews".format(self.parent_srv.baseurl, self.parent_srv.site_id)
48
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/customviews"
41
49
 
42
50
  @property
43
51
  def expurl(self) -> str:
@@ -55,7 +63,7 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]):
55
63
  """
56
64
 
57
65
  @api(version="3.18")
58
- def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[CustomViewItem], PaginationItem]:
66
+ def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[CustomViewItem], PaginationItem]:
59
67
  logger.info("Querying all custom views on site")
60
68
  url = self.baseurl
61
69
  server_response = self.get_request(url, req_options)
@@ -68,8 +76,8 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]):
68
76
  if not view_id:
69
77
  error = "Custom view item missing ID."
70
78
  raise MissingRequiredFieldError(error)
71
- logger.info("Querying custom view (ID: {0})".format(view_id))
72
- url = "{0}/{1}".format(self.baseurl, view_id)
79
+ logger.info(f"Querying custom view (ID: {view_id})")
80
+ url = f"{self.baseurl}/{view_id}"
73
81
  server_response = self.get_request(url)
74
82
  return CustomViewItem.from_response(server_response.content, self.parent_srv.namespace)
75
83
 
@@ -83,17 +91,53 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]):
83
91
  return self._get_view_image(view_item, req_options)
84
92
 
85
93
  view_item._set_image(image_fetcher)
86
- logger.info("Populated image for custom view (ID: {0})".format(view_item.id))
94
+ logger.info(f"Populated image for custom view (ID: {view_item.id})")
87
95
 
88
96
  def _get_view_image(self, view_item: CustomViewItem, req_options: Optional["ImageRequestOptions"]) -> bytes:
89
- url = "{0}/{1}/image".format(self.baseurl, view_item.id)
97
+ url = f"{self.baseurl}/{view_item.id}/image"
90
98
  server_response = self.get_request(url, req_options)
91
99
  image = server_response.content
92
100
  return image
93
101
 
94
- """
95
- Not yet implemented: pdf or csv exports
96
- """
102
+ @api(version="3.23")
103
+ def populate_pdf(self, custom_view_item: CustomViewItem, req_options: Optional["PDFRequestOptions"] = None) -> None:
104
+ if not custom_view_item.id:
105
+ error = "Custom View item missing ID."
106
+ raise MissingRequiredFieldError(error)
107
+
108
+ def pdf_fetcher():
109
+ return self._get_custom_view_pdf(custom_view_item, req_options)
110
+
111
+ custom_view_item._set_pdf(pdf_fetcher)
112
+ logger.info(f"Populated pdf for custom view (ID: {custom_view_item.id})")
113
+
114
+ def _get_custom_view_pdf(
115
+ self, custom_view_item: CustomViewItem, req_options: Optional["PDFRequestOptions"]
116
+ ) -> bytes:
117
+ url = f"{self.baseurl}/{custom_view_item.id}/pdf"
118
+ server_response = self.get_request(url, req_options)
119
+ pdf = server_response.content
120
+ return pdf
121
+
122
+ @api(version="3.23")
123
+ def populate_csv(self, custom_view_item: CustomViewItem, req_options: Optional["CSVRequestOptions"] = None) -> None:
124
+ if not custom_view_item.id:
125
+ error = "Custom View item missing ID."
126
+ raise MissingRequiredFieldError(error)
127
+
128
+ def csv_fetcher():
129
+ return self._get_custom_view_csv(custom_view_item, req_options)
130
+
131
+ custom_view_item._set_csv(csv_fetcher)
132
+ logger.info(f"Populated csv for custom view (ID: {custom_view_item.id})")
133
+
134
+ def _get_custom_view_csv(
135
+ self, custom_view_item: CustomViewItem, req_options: Optional["CSVRequestOptions"]
136
+ ) -> Iterator[bytes]:
137
+ url = f"{self.baseurl}/{custom_view_item.id}/data"
138
+
139
+ with closing(self.get_request(url, request_object=req_options, parameters={"stream": True})) as server_response:
140
+ yield from server_response.iter_content(1024)
97
141
 
98
142
  @api(version="3.18")
99
143
  def update(self, view_item: CustomViewItem) -> Optional[CustomViewItem]:
@@ -105,10 +149,10 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]):
105
149
  return view_item
106
150
 
107
151
  # Update the custom view owner or name
108
- url = "{0}/{1}".format(self.baseurl, view_item.id)
152
+ url = f"{self.baseurl}/{view_item.id}"
109
153
  update_req = RequestFactory.CustomView.update_req(view_item)
110
154
  server_response = self.put_request(url, update_req)
111
- logger.info("Updated custom view (ID: {0})".format(view_item.id))
155
+ logger.info(f"Updated custom view (ID: {view_item.id})")
112
156
  return CustomViewItem.from_response(server_response.content, self.parent_srv.namespace)
113
157
 
114
158
  # Delete 1 view by id
@@ -117,9 +161,9 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]):
117
161
  if not view_id:
118
162
  error = "Custom View ID undefined."
119
163
  raise ValueError(error)
120
- url = "{0}/{1}".format(self.baseurl, view_id)
164
+ url = f"{self.baseurl}/{view_id}"
121
165
  self.delete_request(url)
122
- logger.info("Deleted single custom view (ID: {0})".format(view_id))
166
+ logger.info(f"Deleted single custom view (ID: {view_id})")
123
167
 
124
168
  @api(version="3.21")
125
169
  def download(self, view_item: CustomViewItem, file: PathOrFileW) -> PathOrFileW:
@@ -144,7 +188,7 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]):
144
188
  else:
145
189
  raise ValueError("File path or file object required for publishing custom view.")
146
190
 
147
- if size >= FILESIZE_LIMIT_MB * BYTES_PER_MB:
191
+ if size >= config.FILESIZE_LIMIT_MB * BYTES_PER_MB:
148
192
  upload_session_id = self.parent_srv.fileuploads.upload(file)
149
193
  url = f"{url}?uploadSessionId={upload_session_id}"
150
194
  xml_request, content_type = RequestFactory.CustomView.publish_req_chunked(view_item)
@@ -10,14 +10,14 @@ from tableauserverclient.helpers.logging import logger
10
10
 
11
11
  class DataAccelerationReport(Endpoint):
12
12
  def __init__(self, parent_srv):
13
- super(DataAccelerationReport, self).__init__(parent_srv)
13
+ super().__init__(parent_srv)
14
14
 
15
15
  self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
16
16
  self._default_permissions = _DefaultPermissionsEndpoint(parent_srv, lambda: self.baseurl)
17
17
 
18
18
  @property
19
19
  def baseurl(self):
20
- return "{0}/sites/{1}/dataAccelerationReport".format(self.parent_srv.baseurl, self.parent_srv.site_id)
20
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/dataAccelerationReport"
21
21
 
22
22
  @api(version="3.8")
23
23
  def get(self, req_options=None):