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
@@ -5,15 +5,16 @@ import logging
5
5
  import os
6
6
  from contextlib import closing
7
7
  from pathlib import Path
8
- from typing import Iterable, List, Optional, TYPE_CHECKING, Tuple, Union
8
+ from typing import Optional, TYPE_CHECKING, Union
9
+ from collections.abc import Iterable
9
10
 
10
11
  from tableauserverclient.helpers.headers import fix_filename
11
12
 
12
- from .dqw_endpoint import _DataQualityWarningEndpoint
13
- from .endpoint import QuerysetEndpoint, api
14
- from .exceptions import InternalServerError, MissingRequiredFieldError
15
- from .permissions_endpoint import _PermissionsEndpoint
16
- from .resource_tagger import _ResourceTagger
13
+ from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarningEndpoint
14
+ from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
15
+ from tableauserverclient.server.endpoint.exceptions import InternalServerError, MissingRequiredFieldError
16
+ from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
17
+ from tableauserverclient.server.endpoint.resource_tagger import _ResourceTagger, TaggingMixin
17
18
  from tableauserverclient.models import FlowItem, PaginationItem, ConnectionItem, JobItem
18
19
  from tableauserverclient.server import RequestFactory
19
20
  from tableauserverclient.filesys_helpers import (
@@ -22,6 +23,7 @@ from tableauserverclient.filesys_helpers import (
22
23
  get_file_type,
23
24
  get_file_object_size,
24
25
  )
26
+ from tableauserverclient.server.query import QuerySet
25
27
 
26
28
  io_types_r = (io.BytesIO, io.BufferedReader)
27
29
  io_types_w = (io.BytesIO, io.BufferedWriter)
@@ -50,20 +52,20 @@ PathOrFileR = Union[FilePath, FileObjectR]
50
52
  PathOrFileW = Union[FilePath, FileObjectW]
51
53
 
52
54
 
53
- class Flows(QuerysetEndpoint[FlowItem]):
55
+ class Flows(QuerysetEndpoint[FlowItem], TaggingMixin[FlowItem]):
54
56
  def __init__(self, parent_srv):
55
- super(Flows, self).__init__(parent_srv)
57
+ super().__init__(parent_srv)
56
58
  self._resource_tagger = _ResourceTagger(parent_srv)
57
59
  self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
58
60
  self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "flow")
59
61
 
60
62
  @property
61
63
  def baseurl(self) -> str:
62
- return "{0}/sites/{1}/flows".format(self.parent_srv.baseurl, self.parent_srv.site_id)
64
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/flows"
63
65
 
64
66
  # Get all flows
65
67
  @api(version="3.3")
66
- def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[FlowItem], PaginationItem]:
68
+ def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[FlowItem], PaginationItem]:
67
69
  logger.info("Querying all flows on site")
68
70
  url = self.baseurl
69
71
  server_response = self.get_request(url, req_options)
@@ -77,8 +79,8 @@ class Flows(QuerysetEndpoint[FlowItem]):
77
79
  if not flow_id:
78
80
  error = "Flow ID undefined."
79
81
  raise ValueError(error)
80
- logger.info("Querying single flow (ID: {0})".format(flow_id))
81
- url = "{0}/{1}".format(self.baseurl, flow_id)
82
+ logger.info(f"Querying single flow (ID: {flow_id})")
83
+ url = f"{self.baseurl}/{flow_id}"
82
84
  server_response = self.get_request(url)
83
85
  return FlowItem.from_response(server_response.content, self.parent_srv.namespace)[0]
84
86
 
@@ -93,10 +95,10 @@ class Flows(QuerysetEndpoint[FlowItem]):
93
95
  return self._get_flow_connections(flow_item)
94
96
 
95
97
  flow_item._set_connections(connections_fetcher)
96
- logger.info("Populated connections for flow (ID: {0})".format(flow_item.id))
98
+ logger.info(f"Populated connections for flow (ID: {flow_item.id})")
97
99
 
98
- def _get_flow_connections(self, flow_item, req_options: Optional["RequestOptions"] = None) -> List[ConnectionItem]:
99
- url = "{0}/{1}/connections".format(self.baseurl, flow_item.id)
100
+ def _get_flow_connections(self, flow_item, req_options: Optional["RequestOptions"] = None) -> list[ConnectionItem]:
101
+ url = f"{self.baseurl}/{flow_item.id}/connections"
100
102
  server_response = self.get_request(url, req_options)
101
103
  connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
102
104
  return connections
@@ -107,9 +109,9 @@ class Flows(QuerysetEndpoint[FlowItem]):
107
109
  if not flow_id:
108
110
  error = "Flow ID undefined."
109
111
  raise ValueError(error)
110
- url = "{0}/{1}".format(self.baseurl, flow_id)
112
+ url = f"{self.baseurl}/{flow_id}"
111
113
  self.delete_request(url)
112
- logger.info("Deleted single flow (ID: {0})".format(flow_id))
114
+ logger.info(f"Deleted single flow (ID: {flow_id})")
113
115
 
114
116
  # Download 1 flow by id
115
117
  @api(version="3.3")
@@ -117,7 +119,7 @@ class Flows(QuerysetEndpoint[FlowItem]):
117
119
  if not flow_id:
118
120
  error = "Flow ID undefined."
119
121
  raise ValueError(error)
120
- url = "{0}/{1}/content".format(self.baseurl, flow_id)
122
+ url = f"{self.baseurl}/{flow_id}/content"
121
123
 
122
124
  with closing(self.get_request(url, parameters={"stream": True})) as server_response:
123
125
  m = Message()
@@ -136,7 +138,7 @@ class Flows(QuerysetEndpoint[FlowItem]):
136
138
  f.write(chunk)
137
139
  return_path = os.path.abspath(download_path)
138
140
 
139
- logger.info("Downloaded flow to {0} (ID: {1})".format(return_path, flow_id))
141
+ logger.info(f"Downloaded flow to {return_path} (ID: {flow_id})")
140
142
  return return_path
141
143
 
142
144
  # Update flow
@@ -149,28 +151,28 @@ class Flows(QuerysetEndpoint[FlowItem]):
149
151
  self._resource_tagger.update_tags(self.baseurl, flow_item)
150
152
 
151
153
  # Update the flow itself
152
- url = "{0}/{1}".format(self.baseurl, flow_item.id)
154
+ url = f"{self.baseurl}/{flow_item.id}"
153
155
  update_req = RequestFactory.Flow.update_req(flow_item)
154
156
  server_response = self.put_request(url, update_req)
155
- logger.info("Updated flow item (ID: {0})".format(flow_item.id))
157
+ logger.info(f"Updated flow item (ID: {flow_item.id})")
156
158
  updated_flow = copy.copy(flow_item)
157
159
  return updated_flow._parse_common_elements(server_response.content, self.parent_srv.namespace)
158
160
 
159
161
  # Update flow connections
160
162
  @api(version="3.3")
161
163
  def update_connection(self, flow_item: FlowItem, connection_item: ConnectionItem) -> ConnectionItem:
162
- url = "{0}/{1}/connections/{2}".format(self.baseurl, flow_item.id, connection_item.id)
164
+ url = f"{self.baseurl}/{flow_item.id}/connections/{connection_item.id}"
163
165
 
164
166
  update_req = RequestFactory.Connection.update_req(connection_item)
165
167
  server_response = self.put_request(url, update_req)
166
168
  connection = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)[0]
167
169
 
168
- logger.info("Updated flow item (ID: {0} & connection item {1}".format(flow_item.id, connection_item.id))
170
+ logger.info(f"Updated flow item (ID: {flow_item.id} & connection item {connection_item.id}")
169
171
  return connection
170
172
 
171
173
  @api(version="3.3")
172
174
  def refresh(self, flow_item: FlowItem) -> JobItem:
173
- url = "{0}/{1}/run".format(self.baseurl, flow_item.id)
175
+ url = f"{self.baseurl}/{flow_item.id}/run"
174
176
  empty_req = RequestFactory.Empty.empty_req()
175
177
  server_response = self.post_request(url, empty_req)
176
178
  new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
@@ -179,7 +181,7 @@ class Flows(QuerysetEndpoint[FlowItem]):
179
181
  # Publish flow
180
182
  @api(version="3.3")
181
183
  def publish(
182
- self, flow_item: FlowItem, file: PathOrFileR, mode: str, connections: Optional[List[ConnectionItem]] = None
184
+ self, flow_item: FlowItem, file: PathOrFileR, mode: str, connections: Optional[list[ConnectionItem]] = None
183
185
  ) -> FlowItem:
184
186
  if not mode or not hasattr(self.parent_srv.PublishMode, mode):
185
187
  error = "Invalid mode defined."
@@ -188,7 +190,7 @@ class Flows(QuerysetEndpoint[FlowItem]):
188
190
  if isinstance(file, (str, os.PathLike)):
189
191
  if not os.path.isfile(file):
190
192
  error = "File path does not lead to an existing file."
191
- raise IOError(error)
193
+ raise OSError(error)
192
194
 
193
195
  filename = os.path.basename(file)
194
196
  file_extension = os.path.splitext(filename)[1][1:]
@@ -212,30 +214,30 @@ class Flows(QuerysetEndpoint[FlowItem]):
212
214
  elif file_type == "xml":
213
215
  file_extension = "tfl"
214
216
  else:
215
- error = "Unsupported file type {}!".format(file_type)
217
+ error = f"Unsupported file type {file_type}!"
216
218
  raise ValueError(error)
217
219
 
218
220
  # Generate filename for file object.
219
221
  # This is needed when publishing the flow in a single request
220
- filename = "{}.{}".format(flow_item.name, file_extension)
222
+ filename = f"{flow_item.name}.{file_extension}"
221
223
  file_size = get_file_object_size(file)
222
224
 
223
225
  else:
224
226
  raise TypeError("file should be a filepath or file object.")
225
227
 
226
228
  # Construct the url with the defined mode
227
- url = "{0}?flowType={1}".format(self.baseurl, file_extension)
229
+ url = f"{self.baseurl}?flowType={file_extension}"
228
230
  if mode == self.parent_srv.PublishMode.Overwrite or mode == self.parent_srv.PublishMode.Append:
229
- url += "&{0}=true".format(mode.lower())
231
+ url += f"&{mode.lower()}=true"
230
232
 
231
233
  # Determine if chunking is required (64MB is the limit for single upload method)
232
234
  if file_size >= FILESIZE_LIMIT:
233
- logger.info("Publishing {0} to server with chunking method (flow over 64MB)".format(filename))
235
+ logger.info(f"Publishing {filename} to server with chunking method (flow over 64MB)")
234
236
  upload_session_id = self.parent_srv.fileuploads.upload(file)
235
- url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
237
+ url = f"{url}&uploadSessionId={upload_session_id}"
236
238
  xml_request, content_type = RequestFactory.Flow.publish_req_chunked(flow_item, connections)
237
239
  else:
238
- logger.info("Publishing {0} to server".format(filename))
240
+ logger.info(f"Publishing {filename} to server")
239
241
 
240
242
  if isinstance(file, (str, Path)):
241
243
  with open(file, "rb") as f:
@@ -258,7 +260,7 @@ class Flows(QuerysetEndpoint[FlowItem]):
258
260
  raise err
259
261
  else:
260
262
  new_flow = FlowItem.from_response(server_response.content, self.parent_srv.namespace)[0]
261
- logger.info("Published {0} (ID: {1})".format(filename, new_flow.id))
263
+ logger.info(f"Published {filename} (ID: {new_flow.id})")
262
264
  return new_flow
263
265
 
264
266
  @api(version="3.3")
@@ -293,5 +295,41 @@ class Flows(QuerysetEndpoint[FlowItem]):
293
295
  @api(version="3.3")
294
296
  def schedule_flow_run(
295
297
  self, schedule_id: str, item: FlowItem
296
- ) -> List["AddResponse"]: # actually should return a task
298
+ ) -> list["AddResponse"]: # actually should return a task
297
299
  return self.parent_srv.schedules.add_to_schedule(schedule_id, flow=item)
300
+
301
+ def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[FlowItem]:
302
+ """
303
+ Queries the Tableau Server for items using the specified filters. Page
304
+ size can be specified to limit the number of items returned in a single
305
+ request. If not specified, the default page size is 100. Page size can
306
+ be an integer between 1 and 1000.
307
+
308
+ No positional arguments are allowed. All filters must be specified as
309
+ keyword arguments. If you use the equality operator, you can specify it
310
+ through <field_name>=<value>. If you want to use a different operator,
311
+ you can specify it through <field_name>__<operator>=<value>. Field
312
+ names can either be in snake_case or camelCase.
313
+
314
+ This endpoint supports the following fields and operators:
315
+
316
+
317
+ created_at=...
318
+ created_at__gt=...
319
+ created_at__gte=...
320
+ created_at__lt=...
321
+ created_at__lte=...
322
+ name=...
323
+ name__in=...
324
+ owner_name=...
325
+ project_id=...
326
+ project_name=...
327
+ project_name__in=...
328
+ updated=...
329
+ updated__gt=...
330
+ updated__gte=...
331
+ updated__lt=...
332
+ updated__lte=...
333
+ """
334
+
335
+ return super().filter(*invalid, page_size=page_size, **kwargs)
@@ -1,27 +1,30 @@
1
1
  import logging
2
2
 
3
- from .endpoint import QuerysetEndpoint, api
4
- from .exceptions import MissingRequiredFieldError
3
+ from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
4
+ from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
5
5
  from tableauserverclient.server import RequestFactory
6
6
  from tableauserverclient.models import GroupItem, UserItem, PaginationItem, JobItem
7
- from ..pager import Pager
7
+ from tableauserverclient.server.pager import Pager
8
8
 
9
9
  from tableauserverclient.helpers.logging import logger
10
10
 
11
- from typing import List, Optional, TYPE_CHECKING, Tuple, Union
11
+ from typing import Optional, TYPE_CHECKING, Union
12
+ from collections.abc import Iterable
13
+
14
+ from tableauserverclient.server.query import QuerySet
12
15
 
13
16
  if TYPE_CHECKING:
14
- from ..request_options import RequestOptions
17
+ from tableauserverclient.server.request_options import RequestOptions
15
18
 
16
19
 
17
20
  class Groups(QuerysetEndpoint[GroupItem]):
18
21
  @property
19
22
  def baseurl(self) -> str:
20
- return "{0}/sites/{1}/groups".format(self.parent_srv.baseurl, self.parent_srv.site_id)
23
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/groups"
21
24
 
22
- # Gets all groups
23
25
  @api(version="2.0")
24
- def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[GroupItem], PaginationItem]:
26
+ def get(self, req_options: Optional["RequestOptions"] = None) -> tuple[list[GroupItem], PaginationItem]:
27
+ """Gets all groups"""
25
28
  logger.info("Querying all groups on site")
26
29
  url = self.baseurl
27
30
  server_response = self.get_request(url, req_options)
@@ -29,9 +32,9 @@ class Groups(QuerysetEndpoint[GroupItem]):
29
32
  all_group_items = GroupItem.from_response(server_response.content, self.parent_srv.namespace)
30
33
  return all_group_items, pagination_item
31
34
 
32
- # Gets all users in a given group
33
35
  @api(version="2.0")
34
- def populate_users(self, group_item, req_options: Optional["RequestOptions"] = None) -> None:
36
+ def populate_users(self, group_item: GroupItem, req_options: Optional["RequestOptions"] = None) -> None:
37
+ """Gets all users in a given group"""
35
38
  if not group_item.id:
36
39
  error = "Group item missing ID. Group must be retrieved from server first."
37
40
  raise MissingRequiredFieldError(error)
@@ -47,28 +50,28 @@ class Groups(QuerysetEndpoint[GroupItem]):
47
50
  group_item._set_users(user_pager)
48
51
 
49
52
  def _get_users_for_group(
50
- self, group_item, req_options: Optional["RequestOptions"] = None
51
- ) -> Tuple[List[UserItem], PaginationItem]:
52
- url = "{0}/{1}/users".format(self.baseurl, group_item.id)
53
+ self, group_item: GroupItem, req_options: Optional["RequestOptions"] = None
54
+ ) -> tuple[list[UserItem], PaginationItem]:
55
+ url = f"{self.baseurl}/{group_item.id}/users"
53
56
  server_response = self.get_request(url, req_options)
54
57
  user_item = UserItem.from_response(server_response.content, self.parent_srv.namespace)
55
58
  pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
56
- logger.info("Populated users for group (ID: {0})".format(group_item.id))
59
+ logger.info(f"Populated users for group (ID: {group_item.id})")
57
60
  return user_item, pagination_item
58
61
 
59
- # Deletes 1 group by id
60
62
  @api(version="2.0")
61
63
  def delete(self, group_id: str) -> None:
64
+ """Deletes 1 group by id"""
62
65
  if not group_id:
63
66
  error = "Group ID undefined."
64
67
  raise ValueError(error)
65
- url = "{0}/{1}".format(self.baseurl, group_id)
68
+ url = f"{self.baseurl}/{group_id}"
66
69
  self.delete_request(url)
67
- logger.info("Deleted single group (ID: {0})".format(group_id))
70
+ logger.info(f"Deleted single group (ID: {group_id})")
68
71
 
69
72
  @api(version="2.0")
70
73
  def update(self, group_item: GroupItem, as_job: bool = False) -> Union[GroupItem, JobItem]:
71
- url = "{0}/{1}".format(self.baseurl, group_item.id)
74
+ url = f"{self.baseurl}/{group_item.id}"
72
75
 
73
76
  if not group_item.id:
74
77
  error = "Group item missing ID."
@@ -81,23 +84,23 @@ class Groups(QuerysetEndpoint[GroupItem]):
81
84
 
82
85
  update_req = RequestFactory.Group.update_req(group_item)
83
86
  server_response = self.put_request(url, update_req)
84
- logger.info("Updated group item (ID: {0})".format(group_item.id))
87
+ logger.info(f"Updated group item (ID: {group_item.id})")
85
88
  if as_job:
86
89
  return JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
87
90
  else:
88
91
  return GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0]
89
92
 
90
- # Create a 'local' Tableau group
91
93
  @api(version="2.0")
92
94
  def create(self, group_item: GroupItem) -> GroupItem:
95
+ """Create a 'local' Tableau group"""
93
96
  url = self.baseurl
94
97
  create_req = RequestFactory.Group.create_local_req(group_item)
95
98
  server_response = self.post_request(url, create_req)
96
99
  return GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0]
97
100
 
98
- # Create a group based on Active Directory
99
101
  @api(version="2.0")
100
102
  def create_AD_group(self, group_item: GroupItem, asJob: bool = False) -> Union[GroupItem, JobItem]:
103
+ """Create a group based on Active Directory"""
101
104
  asJobparameter = "?asJob=true" if asJob else ""
102
105
  url = self.baseurl + asJobparameter
103
106
  create_req = RequestFactory.Group.create_ad_req(group_item)
@@ -107,31 +110,97 @@ class Groups(QuerysetEndpoint[GroupItem]):
107
110
  else:
108
111
  return GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0]
109
112
 
110
- # Removes 1 user from 1 group
111
113
  @api(version="2.0")
112
114
  def remove_user(self, group_item: GroupItem, user_id: str) -> None:
115
+ """Removes 1 user from 1 group"""
113
116
  if not group_item.id:
114
117
  error = "Group item missing ID."
115
118
  raise MissingRequiredFieldError(error)
116
119
  if not user_id:
117
120
  error = "User ID undefined."
118
121
  raise ValueError(error)
119
- url = "{0}/{1}/users/{2}".format(self.baseurl, group_item.id, user_id)
122
+ url = f"{self.baseurl}/{group_item.id}/users/{user_id}"
120
123
  self.delete_request(url)
121
- logger.info("Removed user (id: {0}) from group (ID: {1})".format(user_id, group_item.id))
124
+ logger.info(f"Removed user (id: {user_id}) from group (ID: {group_item.id})")
125
+
126
+ @api(version="3.21")
127
+ def remove_users(self, group_item: GroupItem, users: Iterable[Union[str, UserItem]]) -> None:
128
+ """Removes multiple users from 1 group"""
129
+ group_id = group_item.id if hasattr(group_item, "id") else group_item
130
+ if not isinstance(group_id, str):
131
+ raise ValueError(f"Invalid group provided: {group_item}")
132
+
133
+ url = f"{self.baseurl}/{group_id}/users/remove"
134
+ add_req = RequestFactory.Group.remove_users_req(users)
135
+ _ = self.put_request(url, add_req)
136
+ logger.info(f"Removed users to group (ID: {group_item.id})")
137
+ return None
122
138
 
123
- # Adds 1 user to 1 group
124
139
  @api(version="2.0")
125
140
  def add_user(self, group_item: GroupItem, user_id: str) -> UserItem:
141
+ """Adds 1 user to 1 group"""
126
142
  if not group_item.id:
127
143
  error = "Group item missing ID."
128
144
  raise MissingRequiredFieldError(error)
129
145
  if not user_id:
130
146
  error = "User ID undefined."
131
147
  raise ValueError(error)
132
- url = "{0}/{1}/users".format(self.baseurl, group_item.id)
148
+ url = f"{self.baseurl}/{group_item.id}/users"
133
149
  add_req = RequestFactory.Group.add_user_req(user_id)
134
150
  server_response = self.post_request(url, add_req)
135
151
  user = UserItem.from_response(server_response.content, self.parent_srv.namespace).pop()
136
- logger.info("Added user (id: {0}) to group (ID: {1})".format(user_id, group_item.id))
152
+ logger.info(f"Added user (id: {user_id}) to group (ID: {group_item.id})")
137
153
  return user
154
+
155
+ @api(version="3.21")
156
+ def add_users(self, group_item: GroupItem, users: Iterable[Union[str, UserItem]]) -> list[UserItem]:
157
+ """Adds multiple users to 1 group"""
158
+ group_id = group_item.id if hasattr(group_item, "id") else group_item
159
+ if not isinstance(group_id, str):
160
+ raise ValueError(f"Invalid group provided: {group_item}")
161
+
162
+ url = f"{self.baseurl}/{group_id}/users"
163
+ add_req = RequestFactory.Group.add_users_req(users)
164
+ server_response = self.post_request(url, add_req)
165
+ users = UserItem.from_response(server_response.content, self.parent_srv.namespace)
166
+ logger.info(f"Added users to group (ID: {group_item.id})")
167
+ return users
168
+
169
+ def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[GroupItem]:
170
+ """
171
+ Queries the Tableau Server for items using the specified filters. Page
172
+ size can be specified to limit the number of items returned in a single
173
+ request. If not specified, the default page size is 100. Page size can
174
+ be an integer between 1 and 1000.
175
+
176
+ No positional arguments are allowed. All filters must be specified as
177
+ keyword arguments. If you use the equality operator, you can specify it
178
+ through <field_name>=<value>. If you want to use a different operator,
179
+ you can specify it through <field_name>__<operator>=<value>. Field
180
+ names can either be in snake_case or camelCase.
181
+
182
+ This endpoint supports the following fields and operators:
183
+
184
+
185
+ domain_name=...
186
+ domain_name__in=...
187
+ domain_nickname=...
188
+ domain_nickname__in=...
189
+ is_external_user_enabled=...
190
+ is_local=...
191
+ luid=...
192
+ luid__in=...
193
+ minimum_site_role=...
194
+ minimum_site_role__in=...
195
+ name__cieq=...
196
+ name=...
197
+ name__in=...
198
+ name__like=...
199
+ user_count=...
200
+ user_count__gt=...
201
+ user_count__gte=...
202
+ user_count__lt=...
203
+ user_count__lte=...
204
+ """
205
+
206
+ return super().filter(*invalid, page_size=page_size, **kwargs)
@@ -0,0 +1,127 @@
1
+ from typing import Literal, Optional, TYPE_CHECKING, Union
2
+
3
+ from tableauserverclient.helpers.logging import logger
4
+ from tableauserverclient.models.group_item import GroupItem
5
+ from tableauserverclient.models.groupset_item import GroupSetItem
6
+ from tableauserverclient.models.pagination_item import PaginationItem
7
+ from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint
8
+ from tableauserverclient.server.query import QuerySet
9
+ from tableauserverclient.server.request_options import RequestOptions
10
+ from tableauserverclient.server.request_factory import RequestFactory
11
+ from tableauserverclient.server.endpoint.endpoint import api
12
+
13
+ if TYPE_CHECKING:
14
+ from tableauserverclient.server import Server
15
+
16
+
17
+ class GroupSets(QuerysetEndpoint[GroupSetItem]):
18
+ def __init__(self, parent_srv: "Server") -> None:
19
+ super().__init__(parent_srv)
20
+
21
+ @property
22
+ def baseurl(self) -> str:
23
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/groupsets"
24
+
25
+ @api(version="3.22")
26
+ def get(
27
+ self,
28
+ request_options: Optional[RequestOptions] = None,
29
+ result_level: Optional[Literal["members", "local"]] = None,
30
+ ) -> tuple[list[GroupSetItem], PaginationItem]:
31
+ logger.info("Querying all group sets on site")
32
+ url = self.baseurl
33
+ if result_level:
34
+ url += f"?resultlevel={result_level}"
35
+ server_response = self.get_request(url, request_options)
36
+ pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
37
+ all_group_set_items = GroupSetItem.from_response(server_response.content, self.parent_srv.namespace)
38
+ return all_group_set_items, pagination_item
39
+
40
+ @api(version="3.22")
41
+ def get_by_id(self, groupset_id: str) -> GroupSetItem:
42
+ logger.info(f"Querying group set (ID: {groupset_id})")
43
+ url = f"{self.baseurl}/{groupset_id}"
44
+ server_response = self.get_request(url)
45
+ all_group_set_items = GroupSetItem.from_response(server_response.content, self.parent_srv.namespace)
46
+ return all_group_set_items[0]
47
+
48
+ @api(version="3.22")
49
+ def create(self, groupset_item: GroupSetItem) -> GroupSetItem:
50
+ logger.info(f"Creating group set (name: {groupset_item.name})")
51
+ url = self.baseurl
52
+ request = RequestFactory.GroupSet.create_request(groupset_item)
53
+ server_response = self.post_request(url, request)
54
+ created_groupset = GroupSetItem.from_response(server_response.content, self.parent_srv.namespace)
55
+ return created_groupset[0]
56
+
57
+ @api(version="3.22")
58
+ def add_group(self, groupset_item: GroupSetItem, group: Union[GroupItem, str]) -> None:
59
+ group_id = group.id if isinstance(group, GroupItem) else group
60
+ logger.info(f"Adding group (ID: {group_id}) to group set (ID: {groupset_item.id})")
61
+ url = f"{self.baseurl}/{groupset_item.id}/groups/{group_id}"
62
+ _ = self.put_request(url)
63
+ return None
64
+
65
+ @api(version="3.22")
66
+ def remove_group(self, groupset_item: GroupSetItem, group: Union[GroupItem, str]) -> None:
67
+ group_id = group.id if isinstance(group, GroupItem) else group
68
+ logger.info(f"Removing group (ID: {group_id}) from group set (ID: {groupset_item.id})")
69
+ url = f"{self.baseurl}/{groupset_item.id}/groups/{group_id}"
70
+ _ = self.delete_request(url)
71
+ return None
72
+
73
+ @api(version="3.22")
74
+ def delete(self, groupset: Union[GroupSetItem, str]) -> None:
75
+ groupset_id = groupset.id if isinstance(groupset, GroupSetItem) else groupset
76
+ logger.info(f"Deleting group set (ID: {groupset_id})")
77
+ url = f"{self.baseurl}/{groupset_id}"
78
+ _ = self.delete_request(url)
79
+ return None
80
+
81
+ @api(version="3.22")
82
+ def update(self, groupset: GroupSetItem) -> GroupSetItem:
83
+ logger.info(f"Updating group set (ID: {groupset.id})")
84
+ url = f"{self.baseurl}/{groupset.id}"
85
+ request = RequestFactory.GroupSet.update_request(groupset)
86
+ server_response = self.put_request(url, request)
87
+ updated_groupset = GroupSetItem.from_response(server_response.content, self.parent_srv.namespace)
88
+ return updated_groupset[0]
89
+
90
+ def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[GroupSetItem]:
91
+ """
92
+ Queries the Tableau Server for items using the specified filters. Page
93
+ size can be specified to limit the number of items returned in a single
94
+ request. If not specified, the default page size is 100. Page size can
95
+ be an integer between 1 and 1000.
96
+
97
+ No positional arguments are allowed. All filters must be specified as
98
+ keyword arguments. If you use the equality operator, you can specify it
99
+ through <field_name>=<value>. If you want to use a different operator,
100
+ you can specify it through <field_name>__<operator>=<value>. Field
101
+ names can either be in snake_case or camelCase.
102
+
103
+ This endpoint supports the following fields and operators:
104
+
105
+
106
+ domain_name=...
107
+ domain_name__in=...
108
+ domain_nickname=...
109
+ domain_nickname__in=...
110
+ is_external_user_enabled=...
111
+ is_local=...
112
+ luid=...
113
+ luid__in=...
114
+ minimum_site_role=...
115
+ minimum_site_role__in=...
116
+ name__cieq=...
117
+ name=...
118
+ name__in=...
119
+ name__like=...
120
+ user_count=...
121
+ user_count__gt=...
122
+ user_count__gte=...
123
+ user_count__lt=...
124
+ user_count__lte=...
125
+ """
126
+
127
+ return super().filter(*invalid, page_size=page_size, **kwargs)