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.
- tableauserverclient/__init__.py +34 -18
- tableauserverclient/_version.py +3 -3
- tableauserverclient/config.py +20 -6
- tableauserverclient/models/__init__.py +12 -0
- tableauserverclient/models/column_item.py +1 -1
- tableauserverclient/models/connection_credentials.py +1 -1
- tableauserverclient/models/connection_item.py +10 -8
- tableauserverclient/models/custom_view_item.py +29 -6
- tableauserverclient/models/data_acceleration_report_item.py +2 -2
- tableauserverclient/models/data_alert_item.py +5 -5
- tableauserverclient/models/data_freshness_policy_item.py +6 -6
- tableauserverclient/models/database_item.py +8 -2
- tableauserverclient/models/datasource_item.py +10 -10
- tableauserverclient/models/dqw_item.py +1 -1
- tableauserverclient/models/favorites_item.py +5 -6
- tableauserverclient/models/fileupload_item.py +1 -1
- tableauserverclient/models/flow_item.py +12 -12
- tableauserverclient/models/flow_run_item.py +3 -3
- tableauserverclient/models/group_item.py +4 -4
- tableauserverclient/models/groupset_item.py +53 -0
- tableauserverclient/models/interval_item.py +36 -23
- tableauserverclient/models/job_item.py +26 -10
- tableauserverclient/models/linked_tasks_item.py +102 -0
- tableauserverclient/models/metric_item.py +5 -5
- tableauserverclient/models/pagination_item.py +1 -1
- tableauserverclient/models/permissions_item.py +19 -14
- tableauserverclient/models/project_item.py +35 -19
- tableauserverclient/models/property_decorators.py +12 -11
- tableauserverclient/models/reference_item.py +2 -2
- tableauserverclient/models/revision_item.py +3 -3
- tableauserverclient/models/schedule_item.py +2 -2
- tableauserverclient/models/server_info_item.py +26 -6
- tableauserverclient/models/site_item.py +69 -3
- tableauserverclient/models/subscription_item.py +3 -3
- tableauserverclient/models/table_item.py +1 -1
- tableauserverclient/models/tableau_auth.py +115 -5
- tableauserverclient/models/tableau_types.py +11 -9
- tableauserverclient/models/tag_item.py +3 -4
- tableauserverclient/models/task_item.py +4 -4
- tableauserverclient/models/user_item.py +47 -17
- tableauserverclient/models/view_item.py +11 -10
- tableauserverclient/models/virtual_connection_item.py +78 -0
- tableauserverclient/models/webhook_item.py +6 -6
- tableauserverclient/models/workbook_item.py +90 -12
- tableauserverclient/namespace.py +1 -1
- tableauserverclient/server/__init__.py +2 -1
- tableauserverclient/server/endpoint/__init__.py +8 -0
- tableauserverclient/server/endpoint/auth_endpoint.py +68 -11
- tableauserverclient/server/endpoint/custom_views_endpoint.py +124 -19
- tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py +2 -2
- tableauserverclient/server/endpoint/data_alert_endpoint.py +14 -14
- tableauserverclient/server/endpoint/databases_endpoint.py +32 -17
- tableauserverclient/server/endpoint/datasources_endpoint.py +150 -59
- tableauserverclient/server/endpoint/default_permissions_endpoint.py +19 -18
- tableauserverclient/server/endpoint/dqw_endpoint.py +9 -9
- tableauserverclient/server/endpoint/endpoint.py +47 -31
- tableauserverclient/server/endpoint/exceptions.py +23 -7
- tableauserverclient/server/endpoint/favorites_endpoint.py +31 -31
- tableauserverclient/server/endpoint/fileuploads_endpoint.py +11 -13
- tableauserverclient/server/endpoint/flow_runs_endpoint.py +59 -17
- tableauserverclient/server/endpoint/flow_task_endpoint.py +2 -2
- tableauserverclient/server/endpoint/flows_endpoint.py +73 -35
- tableauserverclient/server/endpoint/groups_endpoint.py +96 -27
- tableauserverclient/server/endpoint/groupsets_endpoint.py +127 -0
- tableauserverclient/server/endpoint/jobs_endpoint.py +79 -12
- tableauserverclient/server/endpoint/linked_tasks_endpoint.py +45 -0
- tableauserverclient/server/endpoint/metadata_endpoint.py +2 -2
- tableauserverclient/server/endpoint/metrics_endpoint.py +10 -10
- tableauserverclient/server/endpoint/permissions_endpoint.py +13 -15
- tableauserverclient/server/endpoint/projects_endpoint.py +124 -30
- tableauserverclient/server/endpoint/resource_tagger.py +139 -6
- tableauserverclient/server/endpoint/schedules_endpoint.py +17 -18
- tableauserverclient/server/endpoint/server_info_endpoint.py +40 -5
- tableauserverclient/server/endpoint/sites_endpoint.py +282 -17
- tableauserverclient/server/endpoint/subscriptions_endpoint.py +10 -10
- tableauserverclient/server/endpoint/tables_endpoint.py +33 -19
- tableauserverclient/server/endpoint/tasks_endpoint.py +8 -8
- tableauserverclient/server/endpoint/users_endpoint.py +405 -19
- tableauserverclient/server/endpoint/views_endpoint.py +111 -25
- tableauserverclient/server/endpoint/virtual_connections_endpoint.py +174 -0
- tableauserverclient/server/endpoint/webhooks_endpoint.py +11 -11
- tableauserverclient/server/endpoint/workbooks_endpoint.py +735 -68
- tableauserverclient/server/filter.py +2 -2
- tableauserverclient/server/pager.py +8 -10
- tableauserverclient/server/query.py +70 -20
- tableauserverclient/server/request_factory.py +213 -41
- tableauserverclient/server/request_options.py +125 -145
- tableauserverclient/server/server.py +73 -9
- tableauserverclient/server/sort.py +2 -2
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/METADATA +17 -17
- tableauserverclient-0.34.dist-info/RECORD +106 -0
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/WHEEL +1 -1
- tableauserverclient-0.32.dist-info/RECORD +0 -100
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/LICENSE +0 -0
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/LICENSE.versioneer +0 -0
- {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
|
|
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(
|
|
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 "{
|
|
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) ->
|
|
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: {
|
|
81
|
-
url = "{
|
|
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: {
|
|
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) ->
|
|
99
|
-
url = "{
|
|
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 = "{
|
|
112
|
+
url = f"{self.baseurl}/{flow_id}"
|
|
111
113
|
self.delete_request(url)
|
|
112
|
-
logger.info("Deleted single 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 = "{
|
|
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 {
|
|
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 = "{
|
|
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: {
|
|
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 = "{
|
|
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: {
|
|
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 = "{
|
|
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[
|
|
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
|
|
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 {}!"
|
|
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 = "{}.{}"
|
|
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 = "{
|
|
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 += "&{
|
|
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 {
|
|
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 = "{
|
|
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 {
|
|
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 {
|
|
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
|
-
) ->
|
|
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
|
|
7
|
+
from tableauserverclient.server.pager import Pager
|
|
8
8
|
|
|
9
9
|
from tableauserverclient.helpers.logging import logger
|
|
10
10
|
|
|
11
|
-
from typing import
|
|
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
|
|
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 "{
|
|
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) ->
|
|
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
|
-
) ->
|
|
52
|
-
url = "{
|
|
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: {
|
|
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 = "{
|
|
68
|
+
url = f"{self.baseurl}/{group_id}"
|
|
66
69
|
self.delete_request(url)
|
|
67
|
-
logger.info("Deleted single 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 = "{
|
|
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: {
|
|
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 = "{
|
|
122
|
+
url = f"{self.baseurl}/{group_item.id}/users/{user_id}"
|
|
120
123
|
self.delete_request(url)
|
|
121
|
-
logger.info("Removed user (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 = "{
|
|
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: {
|
|
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)
|