tableauserverclient 0.32__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 (96) hide show
  1. tableauserverclient/__init__.py +34 -18
  2. tableauserverclient/_version.py +3 -3
  3. tableauserverclient/config.py +20 -6
  4. tableauserverclient/models/__init__.py +12 -0
  5. tableauserverclient/models/column_item.py +1 -1
  6. tableauserverclient/models/connection_credentials.py +1 -1
  7. tableauserverclient/models/connection_item.py +10 -8
  8. tableauserverclient/models/custom_view_item.py +29 -6
  9. tableauserverclient/models/data_acceleration_report_item.py +2 -2
  10. tableauserverclient/models/data_alert_item.py +5 -5
  11. tableauserverclient/models/data_freshness_policy_item.py +6 -6
  12. tableauserverclient/models/database_item.py +8 -2
  13. tableauserverclient/models/datasource_item.py +10 -10
  14. tableauserverclient/models/dqw_item.py +1 -1
  15. tableauserverclient/models/favorites_item.py +5 -6
  16. tableauserverclient/models/fileupload_item.py +1 -1
  17. tableauserverclient/models/flow_item.py +12 -12
  18. tableauserverclient/models/flow_run_item.py +3 -3
  19. tableauserverclient/models/group_item.py +4 -4
  20. tableauserverclient/models/groupset_item.py +53 -0
  21. tableauserverclient/models/interval_item.py +36 -23
  22. tableauserverclient/models/job_item.py +26 -10
  23. tableauserverclient/models/linked_tasks_item.py +102 -0
  24. tableauserverclient/models/metric_item.py +5 -5
  25. tableauserverclient/models/pagination_item.py +1 -1
  26. tableauserverclient/models/permissions_item.py +19 -14
  27. tableauserverclient/models/project_item.py +35 -19
  28. tableauserverclient/models/property_decorators.py +12 -11
  29. tableauserverclient/models/reference_item.py +2 -2
  30. tableauserverclient/models/revision_item.py +3 -3
  31. tableauserverclient/models/schedule_item.py +2 -2
  32. tableauserverclient/models/server_info_item.py +26 -6
  33. tableauserverclient/models/site_item.py +69 -3
  34. tableauserverclient/models/subscription_item.py +3 -3
  35. tableauserverclient/models/table_item.py +1 -1
  36. tableauserverclient/models/tableau_auth.py +115 -5
  37. tableauserverclient/models/tableau_types.py +11 -9
  38. tableauserverclient/models/tag_item.py +3 -4
  39. tableauserverclient/models/task_item.py +4 -4
  40. tableauserverclient/models/user_item.py +47 -17
  41. tableauserverclient/models/view_item.py +11 -10
  42. tableauserverclient/models/virtual_connection_item.py +78 -0
  43. tableauserverclient/models/webhook_item.py +6 -6
  44. tableauserverclient/models/workbook_item.py +90 -12
  45. tableauserverclient/namespace.py +1 -1
  46. tableauserverclient/server/__init__.py +2 -1
  47. tableauserverclient/server/endpoint/__init__.py +8 -0
  48. tableauserverclient/server/endpoint/auth_endpoint.py +68 -11
  49. tableauserverclient/server/endpoint/custom_views_endpoint.py +124 -19
  50. tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py +2 -2
  51. tableauserverclient/server/endpoint/data_alert_endpoint.py +14 -14
  52. tableauserverclient/server/endpoint/databases_endpoint.py +32 -17
  53. tableauserverclient/server/endpoint/datasources_endpoint.py +150 -59
  54. tableauserverclient/server/endpoint/default_permissions_endpoint.py +19 -18
  55. tableauserverclient/server/endpoint/dqw_endpoint.py +9 -9
  56. tableauserverclient/server/endpoint/endpoint.py +47 -31
  57. tableauserverclient/server/endpoint/exceptions.py +23 -7
  58. tableauserverclient/server/endpoint/favorites_endpoint.py +31 -31
  59. tableauserverclient/server/endpoint/fileuploads_endpoint.py +11 -13
  60. tableauserverclient/server/endpoint/flow_runs_endpoint.py +59 -17
  61. tableauserverclient/server/endpoint/flow_task_endpoint.py +2 -2
  62. tableauserverclient/server/endpoint/flows_endpoint.py +73 -35
  63. tableauserverclient/server/endpoint/groups_endpoint.py +96 -27
  64. tableauserverclient/server/endpoint/groupsets_endpoint.py +127 -0
  65. tableauserverclient/server/endpoint/jobs_endpoint.py +79 -12
  66. tableauserverclient/server/endpoint/linked_tasks_endpoint.py +45 -0
  67. tableauserverclient/server/endpoint/metadata_endpoint.py +2 -2
  68. tableauserverclient/server/endpoint/metrics_endpoint.py +10 -10
  69. tableauserverclient/server/endpoint/permissions_endpoint.py +13 -15
  70. tableauserverclient/server/endpoint/projects_endpoint.py +124 -30
  71. tableauserverclient/server/endpoint/resource_tagger.py +139 -6
  72. tableauserverclient/server/endpoint/schedules_endpoint.py +17 -18
  73. tableauserverclient/server/endpoint/server_info_endpoint.py +40 -5
  74. tableauserverclient/server/endpoint/sites_endpoint.py +282 -17
  75. tableauserverclient/server/endpoint/subscriptions_endpoint.py +10 -10
  76. tableauserverclient/server/endpoint/tables_endpoint.py +33 -19
  77. tableauserverclient/server/endpoint/tasks_endpoint.py +8 -8
  78. tableauserverclient/server/endpoint/users_endpoint.py +405 -19
  79. tableauserverclient/server/endpoint/views_endpoint.py +111 -25
  80. tableauserverclient/server/endpoint/virtual_connections_endpoint.py +174 -0
  81. tableauserverclient/server/endpoint/webhooks_endpoint.py +11 -11
  82. tableauserverclient/server/endpoint/workbooks_endpoint.py +735 -68
  83. tableauserverclient/server/filter.py +2 -2
  84. tableauserverclient/server/pager.py +8 -10
  85. tableauserverclient/server/query.py +70 -20
  86. tableauserverclient/server/request_factory.py +213 -41
  87. tableauserverclient/server/request_options.py +125 -145
  88. tableauserverclient/server/server.py +73 -9
  89. tableauserverclient/server/sort.py +2 -2
  90. {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/METADATA +17 -17
  91. tableauserverclient-0.34.dist-info/RECORD +106 -0
  92. {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/WHEEL +1 -1
  93. tableauserverclient-0.32.dist-info/RECORD +0 -100
  94. {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/LICENSE +0 -0
  95. {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/LICENSE.versioneer +0 -0
  96. {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/top_level.txt +0 -0
@@ -4,9 +4,9 @@ import warnings
4
4
 
5
5
  from defusedxml.ElementTree import fromstring
6
6
 
7
- from .endpoint import Endpoint, api
8
- from .exceptions import ServerResponseError
9
- from ..request_factory import RequestFactory
7
+ from tableauserverclient.server.endpoint.endpoint import Endpoint, api
8
+ from tableauserverclient.server.endpoint.exceptions import ServerResponseError
9
+ from tableauserverclient.server.request_factory import RequestFactory
10
10
 
11
11
  from tableauserverclient.helpers.logging import logger
12
12
 
@@ -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,10 +1,23 @@
1
+ import io
1
2
  import logging
2
- from typing import List, Optional, Tuple
3
-
4
- from .endpoint import QuerysetEndpoint, api
5
- from .exceptions import MissingRequiredFieldError
3
+ import os
4
+ from contextlib import closing
5
+ from pathlib import Path
6
+ from typing import Optional, Union
7
+ from collections.abc import Iterator
8
+
9
+ from tableauserverclient.config import BYTES_PER_MB, config
10
+ from tableauserverclient.filesys_helpers import get_file_object_size
11
+ from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
12
+ from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
6
13
  from tableauserverclient.models import CustomViewItem, PaginationItem
7
- from tableauserverclient.server import RequestFactory, RequestOptions, ImageRequestOptions
14
+ from tableauserverclient.server import (
15
+ RequestFactory,
16
+ RequestOptions,
17
+ ImageRequestOptions,
18
+ PDFRequestOptions,
19
+ CSVRequestOptions,
20
+ )
8
21
 
9
22
  from tableauserverclient.helpers.logging import logger
10
23
 
@@ -16,14 +29,27 @@ Delete a custom view
16
29
  update the name or owner of a custom view.
17
30
  """
18
31
 
32
+ FilePath = Union[str, os.PathLike]
33
+ FileObject = Union[io.BufferedReader, io.BytesIO]
34
+ FileObjectR = Union[io.BufferedReader, io.BytesIO]
35
+ FileObjectW = Union[io.BufferedWriter, io.BytesIO]
36
+ PathOrFileR = Union[FilePath, FileObjectR]
37
+ PathOrFileW = Union[FilePath, FileObjectW]
38
+ io_types_r = (io.BufferedReader, io.BytesIO)
39
+ io_types_w = (io.BufferedWriter, io.BytesIO)
40
+
19
41
 
20
42
  class CustomViews(QuerysetEndpoint[CustomViewItem]):
21
43
  def __init__(self, parent_srv):
22
- super(CustomViews, self).__init__(parent_srv)
44
+ super().__init__(parent_srv)
23
45
 
24
46
  @property
25
47
  def baseurl(self) -> str:
26
- 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"
49
+
50
+ @property
51
+ def expurl(self) -> str:
52
+ return f"{self.parent_srv._server_address}/api/exp/sites/{self.parent_srv.site_id}/customviews"
27
53
 
28
54
  """
29
55
  If the request has no filter parameters: Administrators will see all custom views.
@@ -37,7 +63,7 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]):
37
63
  """
38
64
 
39
65
  @api(version="3.18")
40
- 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]:
41
67
  logger.info("Querying all custom views on site")
42
68
  url = self.baseurl
43
69
  server_response = self.get_request(url, req_options)
@@ -50,8 +76,8 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]):
50
76
  if not view_id:
51
77
  error = "Custom view item missing ID."
52
78
  raise MissingRequiredFieldError(error)
53
- logger.info("Querying custom view (ID: {0})".format(view_id))
54
- 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}"
55
81
  server_response = self.get_request(url)
56
82
  return CustomViewItem.from_response(server_response.content, self.parent_srv.namespace)
57
83
 
@@ -65,17 +91,53 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]):
65
91
  return self._get_view_image(view_item, req_options)
66
92
 
67
93
  view_item._set_image(image_fetcher)
68
- 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})")
69
95
 
70
96
  def _get_view_image(self, view_item: CustomViewItem, req_options: Optional["ImageRequestOptions"]) -> bytes:
71
- url = "{0}/{1}/image".format(self.baseurl, view_item.id)
97
+ url = f"{self.baseurl}/{view_item.id}/image"
72
98
  server_response = self.get_request(url, req_options)
73
99
  image = server_response.content
74
100
  return image
75
101
 
76
- """
77
- Not yet implemented: pdf or csv exports
78
- """
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)
79
141
 
80
142
  @api(version="3.18")
81
143
  def update(self, view_item: CustomViewItem) -> Optional[CustomViewItem]:
@@ -87,10 +149,10 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]):
87
149
  return view_item
88
150
 
89
151
  # Update the custom view owner or name
90
- url = "{0}/{1}".format(self.baseurl, view_item.id)
152
+ url = f"{self.baseurl}/{view_item.id}"
91
153
  update_req = RequestFactory.CustomView.update_req(view_item)
92
154
  server_response = self.put_request(url, update_req)
93
- logger.info("Updated custom view (ID: {0})".format(view_item.id))
155
+ logger.info(f"Updated custom view (ID: {view_item.id})")
94
156
  return CustomViewItem.from_response(server_response.content, self.parent_srv.namespace)
95
157
 
96
158
  # Delete 1 view by id
@@ -99,6 +161,49 @@ class CustomViews(QuerysetEndpoint[CustomViewItem]):
99
161
  if not view_id:
100
162
  error = "Custom View ID undefined."
101
163
  raise ValueError(error)
102
- url = "{0}/{1}".format(self.baseurl, view_id)
164
+ url = f"{self.baseurl}/{view_id}"
103
165
  self.delete_request(url)
104
- logger.info("Deleted single custom view (ID: {0})".format(view_id))
166
+ logger.info(f"Deleted single custom view (ID: {view_id})")
167
+
168
+ @api(version="3.21")
169
+ def download(self, view_item: CustomViewItem, file: PathOrFileW) -> PathOrFileW:
170
+ url = f"{self.expurl}/{view_item.id}/content"
171
+ server_response = self.get_request(url)
172
+ if isinstance(file, io_types_w):
173
+ file.write(server_response.content)
174
+ return file
175
+
176
+ with open(file, "wb") as f:
177
+ f.write(server_response.content)
178
+
179
+ return file
180
+
181
+ @api(version="3.21")
182
+ def publish(self, view_item: CustomViewItem, file: PathOrFileR) -> Optional[CustomViewItem]:
183
+ url = self.expurl
184
+ if isinstance(file, io_types_r):
185
+ size = get_file_object_size(file)
186
+ elif isinstance(file, (str, Path)) and (p := Path(file)).is_file():
187
+ size = p.stat().st_size
188
+ else:
189
+ raise ValueError("File path or file object required for publishing custom view.")
190
+
191
+ if size >= config.FILESIZE_LIMIT_MB * BYTES_PER_MB:
192
+ upload_session_id = self.parent_srv.fileuploads.upload(file)
193
+ url = f"{url}?uploadSessionId={upload_session_id}"
194
+ xml_request, content_type = RequestFactory.CustomView.publish_req_chunked(view_item)
195
+ else:
196
+ if isinstance(file, io_types_r):
197
+ file.seek(0)
198
+ contents = file.read()
199
+ if view_item.name is None:
200
+ raise MissingRequiredFieldError("Custom view item missing name.")
201
+ filename = view_item.name
202
+ elif isinstance(file, (str, Path)):
203
+ filename = Path(file).name
204
+ contents = Path(file).read_bytes()
205
+
206
+ xml_request, content_type = RequestFactory.CustomView.publish_req(view_item, filename, contents)
207
+
208
+ server_response = self.post_request(url, xml_request, content_type)
209
+ return CustomViewItem.from_response(server_response.content, self.parent_srv.namespace)
@@ -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):
@@ -7,7 +7,7 @@ from tableauserverclient.models import DataAlertItem, PaginationItem, UserItem
7
7
 
8
8
  from tableauserverclient.helpers.logging import logger
9
9
 
10
- from typing import List, Optional, TYPE_CHECKING, Tuple, Union
10
+ from typing import Optional, TYPE_CHECKING, Union
11
11
 
12
12
 
13
13
  if TYPE_CHECKING:
@@ -17,14 +17,14 @@ if TYPE_CHECKING:
17
17
 
18
18
  class DataAlerts(Endpoint):
19
19
  def __init__(self, parent_srv: "Server") -> None:
20
- super(DataAlerts, self).__init__(parent_srv)
20
+ super().__init__(parent_srv)
21
21
 
22
22
  @property
23
23
  def baseurl(self) -> str:
24
- return "{0}/sites/{1}/dataAlerts".format(self.parent_srv.baseurl, self.parent_srv.site_id)
24
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/dataAlerts"
25
25
 
26
26
  @api(version="3.2")
27
- def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[DataAlertItem], PaginationItem]:
27
+ def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[DataAlertItem], PaginationItem]:
28
28
  logger.info("Querying all dataAlerts on site")
29
29
  url = self.baseurl
30
30
  server_response = self.get_request(url, req_options)
@@ -38,8 +38,8 @@ class DataAlerts(Endpoint):
38
38
  if not dataAlert_id:
39
39
  error = "dataAlert ID undefined."
40
40
  raise ValueError(error)
41
- logger.info("Querying single dataAlert (ID: {0})".format(dataAlert_id))
42
- url = "{0}/{1}".format(self.baseurl, dataAlert_id)
41
+ logger.info(f"Querying single dataAlert (ID: {dataAlert_id})")
42
+ url = f"{self.baseurl}/{dataAlert_id}"
43
43
  server_response = self.get_request(url)
44
44
  return DataAlertItem.from_response(server_response.content, self.parent_srv.namespace)[0]
45
45
 
@@ -55,9 +55,9 @@ class DataAlerts(Endpoint):
55
55
  error = "Dataalert ID undefined."
56
56
  raise ValueError(error)
57
57
  # DELETE /api/api-version/sites/site-id/dataAlerts/data-alert-id/users/user-id
58
- url = "{0}/{1}".format(self.baseurl, dataAlert_id)
58
+ url = f"{self.baseurl}/{dataAlert_id}"
59
59
  self.delete_request(url)
60
- logger.info("Deleted single dataAlert (ID: {0})".format(dataAlert_id))
60
+ logger.info(f"Deleted single dataAlert (ID: {dataAlert_id})")
61
61
 
62
62
  @api(version="3.2")
63
63
  def delete_user_from_alert(self, dataAlert: Union[DataAlertItem, str], user: Union[UserItem, str]) -> None:
@@ -80,9 +80,9 @@ class DataAlerts(Endpoint):
80
80
  error = "User ID undefined."
81
81
  raise ValueError(error)
82
82
  # DELETE /api/api-version/sites/site-id/dataAlerts/data-alert-id/users/user-id
83
- url = "{0}/{1}/users/{2}".format(self.baseurl, dataAlert_id, user_id)
83
+ url = f"{self.baseurl}/{dataAlert_id}/users/{user_id}"
84
84
  self.delete_request(url)
85
- logger.info("Deleted User (ID {0}) from dataAlert (ID: {1})".format(user_id, dataAlert_id))
85
+ logger.info(f"Deleted User (ID {user_id}) from dataAlert (ID: {dataAlert_id})")
86
86
 
87
87
  @api(version="3.2")
88
88
  def add_user_to_alert(self, dataAlert_item: DataAlertItem, user: Union[UserItem, str]) -> UserItem:
@@ -98,10 +98,10 @@ class DataAlerts(Endpoint):
98
98
  if not user_id:
99
99
  error = "User ID undefined."
100
100
  raise ValueError(error)
101
- url = "{0}/{1}/users".format(self.baseurl, dataAlert_item.id)
101
+ url = f"{self.baseurl}/{dataAlert_item.id}/users"
102
102
  update_req = RequestFactory.DataAlert.add_user_to_alert(dataAlert_item, user_id)
103
103
  server_response = self.post_request(url, update_req)
104
- logger.info("Added user (ID {0}) to dataAlert item (ID: {1})".format(user_id, dataAlert_item.id))
104
+ logger.info(f"Added user (ID {user_id}) to dataAlert item (ID: {dataAlert_item.id})")
105
105
  added_user = UserItem.from_response(server_response.content, self.parent_srv.namespace)[0]
106
106
  return added_user
107
107
 
@@ -111,9 +111,9 @@ class DataAlerts(Endpoint):
111
111
  error = "Dataalert item missing ID."
112
112
  raise MissingRequiredFieldError(error)
113
113
 
114
- url = "{0}/{1}".format(self.baseurl, dataAlert_item.id)
114
+ url = f"{self.baseurl}/{dataAlert_item.id}"
115
115
  update_req = RequestFactory.DataAlert.update_req(dataAlert_item)
116
116
  server_response = self.put_request(url, update_req)
117
- logger.info("Updated dataAlert item (ID: {0})".format(dataAlert_item.id))
117
+ logger.info(f"Updated dataAlert item (ID: {dataAlert_item.id})")
118
118
  updated_dataAlert = DataAlertItem.from_response(server_response.content, self.parent_srv.namespace)[0]
119
119
  return updated_dataAlert
@@ -1,19 +1,22 @@
1
1
  import logging
2
-
3
- from .default_permissions_endpoint import _DefaultPermissionsEndpoint
4
- from .dqw_endpoint import _DataQualityWarningEndpoint
5
- from .endpoint import api, Endpoint
6
- from .exceptions import MissingRequiredFieldError
7
- from .permissions_endpoint import _PermissionsEndpoint
2
+ from typing import Union
3
+ from collections.abc import Iterable
4
+
5
+ from tableauserverclient.server.endpoint.default_permissions_endpoint import _DefaultPermissionsEndpoint
6
+ from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarningEndpoint
7
+ from tableauserverclient.server.endpoint.endpoint import api, Endpoint
8
+ from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
9
+ from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
10
+ from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
8
11
  from tableauserverclient.server import RequestFactory
9
12
  from tableauserverclient.models import DatabaseItem, TableItem, PaginationItem, Resource
10
13
 
11
14
  from tableauserverclient.helpers.logging import logger
12
15
 
13
16
 
14
- class Databases(Endpoint):
17
+ class Databases(Endpoint, TaggingMixin):
15
18
  def __init__(self, parent_srv):
16
- super(Databases, self).__init__(parent_srv)
19
+ super().__init__(parent_srv)
17
20
 
18
21
  self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
19
22
  self._default_permissions = _DefaultPermissionsEndpoint(parent_srv, lambda: self.baseurl)
@@ -21,7 +24,7 @@ class Databases(Endpoint):
21
24
 
22
25
  @property
23
26
  def baseurl(self):
24
- return "{0}/sites/{1}/databases".format(self.parent_srv.baseurl, self.parent_srv.site_id)
27
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/databases"
25
28
 
26
29
  @api(version="3.5")
27
30
  def get(self, req_options=None):
@@ -38,8 +41,8 @@ class Databases(Endpoint):
38
41
  if not database_id:
39
42
  error = "database ID undefined."
40
43
  raise ValueError(error)
41
- logger.info("Querying single database (ID: {0})".format(database_id))
42
- url = "{0}/{1}".format(self.baseurl, database_id)
44
+ logger.info(f"Querying single database (ID: {database_id})")
45
+ url = f"{self.baseurl}/{database_id}"
43
46
  server_response = self.get_request(url)
44
47
  return DatabaseItem.from_response(server_response.content, self.parent_srv.namespace)[0]
45
48
 
@@ -48,9 +51,9 @@ class Databases(Endpoint):
48
51
  if not database_id:
49
52
  error = "Database ID undefined."
50
53
  raise ValueError(error)
51
- url = "{0}/{1}".format(self.baseurl, database_id)
54
+ url = f"{self.baseurl}/{database_id}"
52
55
  self.delete_request(url)
53
- logger.info("Deleted single database (ID: {0})".format(database_id))
56
+ logger.info(f"Deleted single database (ID: {database_id})")
54
57
 
55
58
  @api(version="3.5")
56
59
  def update(self, database_item):
@@ -58,10 +61,10 @@ class Databases(Endpoint):
58
61
  error = "Database item missing ID."
59
62
  raise MissingRequiredFieldError(error)
60
63
 
61
- url = "{0}/{1}".format(self.baseurl, database_item.id)
64
+ url = f"{self.baseurl}/{database_item.id}"
62
65
  update_req = RequestFactory.Database.update_req(database_item)
63
66
  server_response = self.put_request(url, update_req)
64
- logger.info("Updated database item (ID: {0})".format(database_item.id))
67
+ logger.info(f"Updated database item (ID: {database_item.id})")
65
68
  updated_database = DatabaseItem.from_response(server_response.content, self.parent_srv.namespace)[0]
66
69
  return updated_database
67
70
 
@@ -76,10 +79,10 @@ class Databases(Endpoint):
76
79
  return self._get_tables_for_database(database_item)
77
80
 
78
81
  database_item._set_tables(column_fetcher)
79
- logger.info("Populated tables for database (ID: {0}".format(database_item.id))
82
+ logger.info(f"Populated tables for database (ID: {database_item.id}")
80
83
 
81
84
  def _get_tables_for_database(self, database_item):
82
- url = "{0}/{1}/tables".format(self.baseurl, database_item.id)
85
+ url = f"{self.baseurl}/{database_item.id}/tables"
83
86
  server_response = self.get_request(url)
84
87
  tables = TableItem.from_response(server_response.content, self.parent_srv.namespace)
85
88
  return tables
@@ -123,3 +126,15 @@ class Databases(Endpoint):
123
126
  @api(version="3.5")
124
127
  def delete_dqw(self, item):
125
128
  self._data_quality_warnings.clear(item)
129
+
130
+ @api(version="3.9")
131
+ def add_tags(self, item: Union[DatabaseItem, str], tags: Iterable[str]) -> set[str]:
132
+ return super().add_tags(item, tags)
133
+
134
+ @api(version="3.9")
135
+ def delete_tags(self, item: Union[DatabaseItem, str], tags: Iterable[str]) -> None:
136
+ super().delete_tags(item, tags)
137
+
138
+ @api(version="3.9")
139
+ def update_tags(self, item: DatabaseItem) -> None:
140
+ raise NotImplementedError("Update tags is not supported for databases.")