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
|
@@ -6,9 +6,11 @@ import os
|
|
|
6
6
|
|
|
7
7
|
from contextlib import closing
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import Optional, TYPE_CHECKING, Union
|
|
10
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
10
11
|
|
|
11
12
|
from tableauserverclient.helpers.headers import fix_filename
|
|
13
|
+
from tableauserverclient.server.query import QuerySet
|
|
12
14
|
|
|
13
15
|
if TYPE_CHECKING:
|
|
14
16
|
from tableauserverclient.server import Server
|
|
@@ -19,9 +21,9 @@ from tableauserverclient.server.endpoint.dqw_endpoint import _DataQualityWarning
|
|
|
19
21
|
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api, parameter_added_in
|
|
20
22
|
from tableauserverclient.server.endpoint.exceptions import InternalServerError, MissingRequiredFieldError
|
|
21
23
|
from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
|
|
22
|
-
from tableauserverclient.server.endpoint.resource_tagger import
|
|
24
|
+
from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
|
|
23
25
|
|
|
24
|
-
from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS,
|
|
26
|
+
from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS, BYTES_PER_MB, config
|
|
25
27
|
from tableauserverclient.filesys_helpers import (
|
|
26
28
|
make_download_path,
|
|
27
29
|
get_file_type,
|
|
@@ -54,10 +56,9 @@ PathOrFileR = Union[FilePath, FileObjectR]
|
|
|
54
56
|
PathOrFileW = Union[FilePath, FileObjectW]
|
|
55
57
|
|
|
56
58
|
|
|
57
|
-
class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
59
|
+
class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]):
|
|
58
60
|
def __init__(self, parent_srv: "Server") -> None:
|
|
59
|
-
super(
|
|
60
|
-
self._resource_tagger = _ResourceTagger(parent_srv)
|
|
61
|
+
super().__init__(parent_srv)
|
|
61
62
|
self._permissions = _PermissionsEndpoint(parent_srv, lambda: self.baseurl)
|
|
62
63
|
self._data_quality_warnings = _DataQualityWarningEndpoint(self.parent_srv, "datasource")
|
|
63
64
|
|
|
@@ -65,11 +66,11 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
65
66
|
|
|
66
67
|
@property
|
|
67
68
|
def baseurl(self) -> str:
|
|
68
|
-
return "{
|
|
69
|
+
return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/datasources"
|
|
69
70
|
|
|
70
71
|
# Get all datasources
|
|
71
72
|
@api(version="2.0")
|
|
72
|
-
def get(self, req_options: Optional[RequestOptions] = None) ->
|
|
73
|
+
def get(self, req_options: Optional[RequestOptions] = None) -> tuple[list[DatasourceItem], PaginationItem]:
|
|
73
74
|
logger.info("Querying all datasources on site")
|
|
74
75
|
url = self.baseurl
|
|
75
76
|
server_response = self.get_request(url, req_options)
|
|
@@ -83,8 +84,8 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
83
84
|
if not datasource_id:
|
|
84
85
|
error = "Datasource ID undefined."
|
|
85
86
|
raise ValueError(error)
|
|
86
|
-
logger.info("Querying single datasource (ID: {
|
|
87
|
-
url = "{
|
|
87
|
+
logger.info(f"Querying single datasource (ID: {datasource_id})")
|
|
88
|
+
url = f"{self.baseurl}/{datasource_id}"
|
|
88
89
|
server_response = self.get_request(url)
|
|
89
90
|
return DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
90
91
|
|
|
@@ -99,10 +100,10 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
99
100
|
return self._get_datasource_connections(datasource_item)
|
|
100
101
|
|
|
101
102
|
datasource_item._set_connections(connections_fetcher)
|
|
102
|
-
logger.info("Populated connections for datasource (ID: {
|
|
103
|
+
logger.info(f"Populated connections for datasource (ID: {datasource_item.id})")
|
|
103
104
|
|
|
104
105
|
def _get_datasource_connections(self, datasource_item, req_options=None):
|
|
105
|
-
url = "{
|
|
106
|
+
url = f"{self.baseurl}/{datasource_item.id}/connections"
|
|
106
107
|
server_response = self.get_request(url, req_options)
|
|
107
108
|
connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
108
109
|
return connections
|
|
@@ -113,9 +114,9 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
113
114
|
if not datasource_id:
|
|
114
115
|
error = "Datasource ID undefined."
|
|
115
116
|
raise ValueError(error)
|
|
116
|
-
url = "{
|
|
117
|
+
url = f"{self.baseurl}/{datasource_id}"
|
|
117
118
|
self.delete_request(url)
|
|
118
|
-
logger.info("Deleted single datasource (ID: {
|
|
119
|
+
logger.info(f"Deleted single datasource (ID: {datasource_id})")
|
|
119
120
|
|
|
120
121
|
# Download 1 datasource by id
|
|
121
122
|
@api(version="2.0")
|
|
@@ -126,7 +127,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
126
127
|
datasource_id: str,
|
|
127
128
|
filepath: Optional[PathOrFileW] = None,
|
|
128
129
|
include_extract: bool = True,
|
|
129
|
-
) ->
|
|
130
|
+
) -> PathOrFileW:
|
|
130
131
|
return self.download_revision(
|
|
131
132
|
datasource_id,
|
|
132
133
|
None,
|
|
@@ -149,14 +150,14 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
149
150
|
)
|
|
150
151
|
raise MissingRequiredFieldError(error)
|
|
151
152
|
|
|
152
|
-
self.
|
|
153
|
+
self.update_tags(datasource_item)
|
|
153
154
|
|
|
154
155
|
# Update the datasource itself
|
|
155
|
-
url = "{
|
|
156
|
+
url = f"{self.baseurl}/{datasource_item.id}"
|
|
156
157
|
|
|
157
158
|
update_req = RequestFactory.Datasource.update_req(datasource_item)
|
|
158
159
|
server_response = self.put_request(url, update_req)
|
|
159
|
-
logger.info("Updated datasource item (ID: {
|
|
160
|
+
logger.info(f"Updated datasource item (ID: {datasource_item.id})")
|
|
160
161
|
updated_datasource = copy.copy(datasource_item)
|
|
161
162
|
return updated_datasource._parse_common_elements(server_response.content, self.parent_srv.namespace)
|
|
162
163
|
|
|
@@ -165,7 +166,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
165
166
|
def update_connection(
|
|
166
167
|
self, datasource_item: DatasourceItem, connection_item: ConnectionItem
|
|
167
168
|
) -> Optional[ConnectionItem]:
|
|
168
|
-
url = "{
|
|
169
|
+
url = f"{self.baseurl}/{datasource_item.id}/connections/{connection_item.id}"
|
|
169
170
|
|
|
170
171
|
update_req = RequestFactory.Connection.update_req(connection_item)
|
|
171
172
|
server_response = self.put_request(url, update_req)
|
|
@@ -174,18 +175,16 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
174
175
|
return None
|
|
175
176
|
|
|
176
177
|
if len(connections) > 1:
|
|
177
|
-
logger.debug("Multiple connections returned ({
|
|
178
|
+
logger.debug(f"Multiple connections returned ({len(connections)})")
|
|
178
179
|
connection = list(filter(lambda x: x.id == connection_item.id, connections))[0]
|
|
179
180
|
|
|
180
|
-
logger.info(
|
|
181
|
-
"Updated datasource item (ID: {0} & connection item {1}".format(datasource_item.id, connection_item.id)
|
|
182
|
-
)
|
|
181
|
+
logger.info(f"Updated datasource item (ID: {datasource_item.id} & connection item {connection_item.id}")
|
|
183
182
|
return connection
|
|
184
183
|
|
|
185
184
|
@api(version="2.8")
|
|
186
185
|
def refresh(self, datasource_item: DatasourceItem) -> JobItem:
|
|
187
186
|
id_ = getattr(datasource_item, "id", datasource_item)
|
|
188
|
-
url = "{
|
|
187
|
+
url = f"{self.baseurl}/{id_}/refresh"
|
|
189
188
|
empty_req = RequestFactory.Empty.empty_req()
|
|
190
189
|
server_response = self.post_request(url, empty_req)
|
|
191
190
|
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
@@ -194,7 +193,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
194
193
|
@api(version="3.5")
|
|
195
194
|
def create_extract(self, datasource_item: DatasourceItem, encrypt: bool = False) -> JobItem:
|
|
196
195
|
id_ = getattr(datasource_item, "id", datasource_item)
|
|
197
|
-
url = "{
|
|
196
|
+
url = f"{self.baseurl}/{id_}/createExtract?encrypt={encrypt}"
|
|
198
197
|
empty_req = RequestFactory.Empty.empty_req()
|
|
199
198
|
server_response = self.post_request(url, empty_req)
|
|
200
199
|
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
@@ -203,7 +202,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
203
202
|
@api(version="3.5")
|
|
204
203
|
def delete_extract(self, datasource_item: DatasourceItem) -> None:
|
|
205
204
|
id_ = getattr(datasource_item, "id", datasource_item)
|
|
206
|
-
url = "{
|
|
205
|
+
url = f"{self.baseurl}/{id_}/deleteExtract"
|
|
207
206
|
empty_req = RequestFactory.Empty.empty_req()
|
|
208
207
|
self.post_request(url, empty_req)
|
|
209
208
|
|
|
@@ -223,12 +222,12 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
223
222
|
if isinstance(file, (os.PathLike, str)):
|
|
224
223
|
if not os.path.isfile(file):
|
|
225
224
|
error = "File path does not lead to an existing file."
|
|
226
|
-
raise
|
|
225
|
+
raise OSError(error)
|
|
227
226
|
|
|
228
227
|
filename = os.path.basename(file)
|
|
229
228
|
file_extension = os.path.splitext(filename)[1][1:]
|
|
230
229
|
file_size = os.path.getsize(file)
|
|
231
|
-
logger.debug("Publishing file `{}`, size `{}`"
|
|
230
|
+
logger.debug(f"Publishing file `{filename}`, size `{file_size}`")
|
|
232
231
|
# If name is not defined, grab the name from the file to publish
|
|
233
232
|
if not datasource_item.name:
|
|
234
233
|
datasource_item.name = os.path.splitext(filename)[0]
|
|
@@ -247,10 +246,10 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
247
246
|
elif file_type == "xml":
|
|
248
247
|
file_extension = "tds"
|
|
249
248
|
else:
|
|
250
|
-
error = "Unsupported file type {}"
|
|
249
|
+
error = f"Unsupported file type {file_type}"
|
|
251
250
|
raise ValueError(error)
|
|
252
251
|
|
|
253
|
-
filename = "{}.{}"
|
|
252
|
+
filename = f"{datasource_item.name}.{file_extension}"
|
|
254
253
|
file_size = get_file_object_size(file)
|
|
255
254
|
|
|
256
255
|
else:
|
|
@@ -261,27 +260,27 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
261
260
|
raise ValueError(error)
|
|
262
261
|
|
|
263
262
|
# Construct the url with the defined mode
|
|
264
|
-
url = "{
|
|
263
|
+
url = f"{self.baseurl}?datasourceType={file_extension}"
|
|
265
264
|
if mode == self.parent_srv.PublishMode.Overwrite or mode == self.parent_srv.PublishMode.Append:
|
|
266
|
-
url += "&{
|
|
265
|
+
url += f"&{mode.lower()}=true"
|
|
267
266
|
|
|
268
267
|
if as_job:
|
|
269
|
-
url += "&{
|
|
268
|
+
url += "&{}=true".format("asJob")
|
|
270
269
|
|
|
271
270
|
# Determine if chunking is required (64MB is the limit for single upload method)
|
|
272
|
-
if file_size >= FILESIZE_LIMIT_MB * BYTES_PER_MB:
|
|
271
|
+
if file_size >= config.FILESIZE_LIMIT_MB * BYTES_PER_MB:
|
|
273
272
|
logger.info(
|
|
274
273
|
"Publishing {} to server with chunking method (datasource over {}MB, chunk size {}MB)".format(
|
|
275
|
-
filename, FILESIZE_LIMIT_MB, CHUNK_SIZE_MB
|
|
274
|
+
filename, config.FILESIZE_LIMIT_MB, config.CHUNK_SIZE_MB
|
|
276
275
|
)
|
|
277
276
|
)
|
|
278
277
|
upload_session_id = self.parent_srv.fileuploads.upload(file)
|
|
279
|
-
url = "{
|
|
278
|
+
url = f"{url}&uploadSessionId={upload_session_id}"
|
|
280
279
|
xml_request, content_type = RequestFactory.Datasource.publish_req_chunked(
|
|
281
280
|
datasource_item, connection_credentials, connections
|
|
282
281
|
)
|
|
283
282
|
else:
|
|
284
|
-
logger.info("Publishing {
|
|
283
|
+
logger.info(f"Publishing {filename} to server")
|
|
285
284
|
|
|
286
285
|
if isinstance(file, (Path, str)):
|
|
287
286
|
with open(file, "rb") as f:
|
|
@@ -309,11 +308,11 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
309
308
|
|
|
310
309
|
if as_job:
|
|
311
310
|
new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
312
|
-
logger.info("Published {
|
|
311
|
+
logger.info(f"Published {filename} (JOB_ID: {new_job.id}")
|
|
313
312
|
return new_job
|
|
314
313
|
else:
|
|
315
314
|
new_datasource = DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
316
|
-
logger.info("Published {
|
|
315
|
+
logger.info(f"Published {filename} (ID: {new_datasource.id})")
|
|
317
316
|
return new_datasource
|
|
318
317
|
|
|
319
318
|
@api(version="3.13")
|
|
@@ -327,23 +326,23 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
327
326
|
) -> JobItem:
|
|
328
327
|
if isinstance(datasource_or_connection_item, DatasourceItem):
|
|
329
328
|
datasource_id = datasource_or_connection_item.id
|
|
330
|
-
url = "{
|
|
329
|
+
url = f"{self.baseurl}/{datasource_id}/data"
|
|
331
330
|
elif isinstance(datasource_or_connection_item, ConnectionItem):
|
|
332
331
|
datasource_id = datasource_or_connection_item.datasource_id
|
|
333
332
|
connection_id = datasource_or_connection_item.id
|
|
334
|
-
url = "{
|
|
333
|
+
url = f"{self.baseurl}/{datasource_id}/connections/{connection_id}/data"
|
|
335
334
|
else:
|
|
336
335
|
assert isinstance(datasource_or_connection_item, str)
|
|
337
|
-
url = "{
|
|
336
|
+
url = f"{self.baseurl}/{datasource_or_connection_item}/data"
|
|
338
337
|
|
|
339
338
|
if payload is not None:
|
|
340
339
|
if not os.path.isfile(payload):
|
|
341
340
|
error = "File path does not lead to an existing file."
|
|
342
|
-
raise
|
|
341
|
+
raise OSError(error)
|
|
343
342
|
|
|
344
|
-
logger.info("Uploading {
|
|
343
|
+
logger.info(f"Uploading {payload} to server with chunking method for Update job")
|
|
345
344
|
upload_session_id = self.parent_srv.fileuploads.upload(payload)
|
|
346
|
-
url = "{
|
|
345
|
+
url = f"{url}?uploadSessionId={upload_session_id}"
|
|
347
346
|
|
|
348
347
|
json_request = json.dumps({"actions": actions})
|
|
349
348
|
parameters = {"headers": {"requestid": request_id}}
|
|
@@ -356,7 +355,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
356
355
|
self._permissions.populate(item)
|
|
357
356
|
|
|
358
357
|
@api(version="2.0")
|
|
359
|
-
def update_permissions(self, item: DatasourceItem, permission_item:
|
|
358
|
+
def update_permissions(self, item: DatasourceItem, permission_item: list["PermissionsRule"]) -> None:
|
|
360
359
|
self._permissions.update(item, permission_item)
|
|
361
360
|
|
|
362
361
|
@api(version="2.0")
|
|
@@ -390,12 +389,12 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
390
389
|
return self._get_datasource_revisions(datasource_item)
|
|
391
390
|
|
|
392
391
|
datasource_item._set_revisions(revisions_fetcher)
|
|
393
|
-
logger.info("Populated revisions for datasource (ID: {
|
|
392
|
+
logger.info(f"Populated revisions for datasource (ID: {datasource_item.id})")
|
|
394
393
|
|
|
395
394
|
def _get_datasource_revisions(
|
|
396
395
|
self, datasource_item: DatasourceItem, req_options: Optional["RequestOptions"] = None
|
|
397
|
-
) ->
|
|
398
|
-
url = "{
|
|
396
|
+
) -> list[RevisionItem]:
|
|
397
|
+
url = f"{self.baseurl}/{datasource_item.id}/revisions"
|
|
399
398
|
server_response = self.get_request(url, req_options)
|
|
400
399
|
revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, datasource_item)
|
|
401
400
|
return revisions
|
|
@@ -405,7 +404,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
405
404
|
def download_revision(
|
|
406
405
|
self,
|
|
407
406
|
datasource_id: str,
|
|
408
|
-
revision_number: str,
|
|
407
|
+
revision_number: Optional[str],
|
|
409
408
|
filepath: Optional[PathOrFileW] = None,
|
|
410
409
|
include_extract: bool = True,
|
|
411
410
|
) -> PathOrFileW:
|
|
@@ -413,9 +412,9 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
413
412
|
error = "Datasource ID undefined."
|
|
414
413
|
raise ValueError(error)
|
|
415
414
|
if revision_number is None:
|
|
416
|
-
url = "{
|
|
415
|
+
url = f"{self.baseurl}/{datasource_id}/content"
|
|
417
416
|
else:
|
|
418
|
-
url = "{
|
|
417
|
+
url = f"{self.baseurl}/{datasource_id}/revisions/{revision_number}/content"
|
|
419
418
|
|
|
420
419
|
if not include_extract:
|
|
421
420
|
url += "?includeExtract=False"
|
|
@@ -437,9 +436,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
437
436
|
f.write(chunk)
|
|
438
437
|
return_path = os.path.abspath(download_path)
|
|
439
438
|
|
|
440
|
-
logger.info(
|
|
441
|
-
"Downloaded datasource revision {0} to {1} (ID: {2})".format(revision_number, return_path, datasource_id)
|
|
442
|
-
)
|
|
439
|
+
logger.info(f"Downloaded datasource revision {revision_number} to {return_path} (ID: {datasource_id})")
|
|
443
440
|
return return_path
|
|
444
441
|
|
|
445
442
|
@api(version="2.3")
|
|
@@ -449,13 +446,107 @@ class Datasources(QuerysetEndpoint[DatasourceItem]):
|
|
|
449
446
|
url = "/".join([self.baseurl, datasource_id, "revisions", revision_number])
|
|
450
447
|
|
|
451
448
|
self.delete_request(url)
|
|
452
|
-
logger.info(
|
|
453
|
-
"Deleted single datasource revision (ID: {0}) (Revision: {1})".format(datasource_id, revision_number)
|
|
454
|
-
)
|
|
449
|
+
logger.info(f"Deleted single datasource revision (ID: {datasource_id}) (Revision: {revision_number})")
|
|
455
450
|
|
|
456
451
|
# a convenience method
|
|
457
452
|
@api(version="2.8")
|
|
458
453
|
def schedule_extract_refresh(
|
|
459
454
|
self, schedule_id: str, item: DatasourceItem
|
|
460
|
-
) ->
|
|
455
|
+
) -> list["AddResponse"]: # actually should return a task
|
|
461
456
|
return self.parent_srv.schedules.add_to_schedule(schedule_id, datasource=item)
|
|
457
|
+
|
|
458
|
+
@api(version="1.0")
|
|
459
|
+
def add_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> set[str]:
|
|
460
|
+
return super().add_tags(item, tags)
|
|
461
|
+
|
|
462
|
+
@api(version="1.0")
|
|
463
|
+
def delete_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> None:
|
|
464
|
+
return super().delete_tags(item, tags)
|
|
465
|
+
|
|
466
|
+
@api(version="1.0")
|
|
467
|
+
def update_tags(self, item: DatasourceItem) -> None:
|
|
468
|
+
return super().update_tags(item)
|
|
469
|
+
|
|
470
|
+
def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[DatasourceItem]:
|
|
471
|
+
"""
|
|
472
|
+
Queries the Tableau Server for items using the specified filters. Page
|
|
473
|
+
size can be specified to limit the number of items returned in a single
|
|
474
|
+
request. If not specified, the default page size is 100. Page size can
|
|
475
|
+
be an integer between 1 and 1000.
|
|
476
|
+
|
|
477
|
+
No positional arguments are allowed. All filters must be specified as
|
|
478
|
+
keyword arguments. If you use the equality operator, you can specify it
|
|
479
|
+
through <field_name>=<value>. If you want to use a different operator,
|
|
480
|
+
you can specify it through <field_name>__<operator>=<value>. Field
|
|
481
|
+
names can either be in snake_case or camelCase.
|
|
482
|
+
|
|
483
|
+
This endpoint supports the following fields and operators:
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
authentication_type=...
|
|
487
|
+
authentication_type__in=...
|
|
488
|
+
connected_workbook_type=...
|
|
489
|
+
connected_workbook_type__gt=...
|
|
490
|
+
connected_workbook_type__gte=...
|
|
491
|
+
connected_workbook_type__lt=...
|
|
492
|
+
connected_workbook_type__lte=...
|
|
493
|
+
connection_to=...
|
|
494
|
+
connection_to__in=...
|
|
495
|
+
connection_type=...
|
|
496
|
+
connection_type__in=...
|
|
497
|
+
content_url=...
|
|
498
|
+
content_url__in=...
|
|
499
|
+
created_at=...
|
|
500
|
+
created_at__gt=...
|
|
501
|
+
created_at__gte=...
|
|
502
|
+
created_at__lt=...
|
|
503
|
+
created_at__lte=...
|
|
504
|
+
database_name=...
|
|
505
|
+
database_name__in=...
|
|
506
|
+
database_user_name=...
|
|
507
|
+
database_user_name__in=...
|
|
508
|
+
description=...
|
|
509
|
+
description__in=...
|
|
510
|
+
favorites_total=...
|
|
511
|
+
favorites_total__gt=...
|
|
512
|
+
favorites_total__gte=...
|
|
513
|
+
favorites_total__lt=...
|
|
514
|
+
favorites_total__lte=...
|
|
515
|
+
has_alert=...
|
|
516
|
+
has_embedded_password=...
|
|
517
|
+
has_extracts=...
|
|
518
|
+
is_certified=...
|
|
519
|
+
is_connectable=...
|
|
520
|
+
is_default_port=...
|
|
521
|
+
is_hierarchical=...
|
|
522
|
+
is_published=...
|
|
523
|
+
name=...
|
|
524
|
+
name__in=...
|
|
525
|
+
owner_domain=...
|
|
526
|
+
owner_domain__in=...
|
|
527
|
+
owner_email=...
|
|
528
|
+
owner_name=...
|
|
529
|
+
owner_name__in=...
|
|
530
|
+
project_name=...
|
|
531
|
+
project_name__in=...
|
|
532
|
+
server_name=...
|
|
533
|
+
server_name__in=...
|
|
534
|
+
server_port=...
|
|
535
|
+
size=...
|
|
536
|
+
size__gt=...
|
|
537
|
+
size__gte=...
|
|
538
|
+
size__lt=...
|
|
539
|
+
size__lte=...
|
|
540
|
+
table_name=...
|
|
541
|
+
table_name__in=...
|
|
542
|
+
tags=...
|
|
543
|
+
tags__in=...
|
|
544
|
+
type=...
|
|
545
|
+
updated_at=...
|
|
546
|
+
updated_at__gt=...
|
|
547
|
+
updated_at__gte=...
|
|
548
|
+
updated_at__lt=...
|
|
549
|
+
updated_at__lte=...
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
return super().filter(*invalid, page_size=page_size, **kwargs)
|
|
@@ -4,7 +4,8 @@ from .endpoint import Endpoint
|
|
|
4
4
|
from .exceptions import MissingRequiredFieldError
|
|
5
5
|
from tableauserverclient.server import RequestFactory
|
|
6
6
|
from tableauserverclient.models import DatabaseItem, PermissionsRule, ProjectItem, plural_type, Resource
|
|
7
|
-
from typing import TYPE_CHECKING, Callable,
|
|
7
|
+
from typing import TYPE_CHECKING, Callable, Optional, Union
|
|
8
|
+
from collections.abc import Sequence
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
10
11
|
from ..server import Server
|
|
@@ -25,7 +26,7 @@ class _DefaultPermissionsEndpoint(Endpoint):
|
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
28
|
def __init__(self, parent_srv: "Server", owner_baseurl: Callable[[], str]) -> None:
|
|
28
|
-
super(
|
|
29
|
+
super().__init__(parent_srv)
|
|
29
30
|
|
|
30
31
|
# owner_baseurl is the baseurl of the parent, a project or database.
|
|
31
32
|
# It MUST be a lambda since we don't know the full site URL until we sign in.
|
|
@@ -33,23 +34,25 @@ class _DefaultPermissionsEndpoint(Endpoint):
|
|
|
33
34
|
self.owner_baseurl = owner_baseurl
|
|
34
35
|
|
|
35
36
|
def __str__(self):
|
|
36
|
-
return "<DefaultPermissionsEndpoint {} [Flow, Datasource, Workbook, Lens]>"
|
|
37
|
+
return f"<DefaultPermissionsEndpoint {self.owner_baseurl()} [Flow, Datasource, Workbook, Lens]>"
|
|
37
38
|
|
|
38
39
|
__repr__ = __str__
|
|
39
40
|
|
|
40
41
|
def update_default_permissions(
|
|
41
|
-
self, resource: BaseItem, permissions: Sequence[PermissionsRule], content_type: Resource
|
|
42
|
-
) ->
|
|
43
|
-
url = "{
|
|
42
|
+
self, resource: BaseItem, permissions: Sequence[PermissionsRule], content_type: Union[Resource, str]
|
|
43
|
+
) -> list[PermissionsRule]:
|
|
44
|
+
url = f"{self.owner_baseurl()}/{resource.id}/default-permissions/{plural_type(content_type)}"
|
|
44
45
|
update_req = RequestFactory.Permission.add_req(permissions)
|
|
45
46
|
response = self.put_request(url, update_req)
|
|
46
47
|
permissions = PermissionsRule.from_response(response.content, self.parent_srv.namespace)
|
|
47
|
-
logger.info("Updated default {} permissions for resource {
|
|
48
|
+
logger.info(f"Updated default {content_type} permissions for resource {resource.id}")
|
|
48
49
|
logger.info(permissions)
|
|
49
50
|
|
|
50
51
|
return permissions
|
|
51
52
|
|
|
52
|
-
def delete_default_permission(
|
|
53
|
+
def delete_default_permission(
|
|
54
|
+
self, resource: BaseItem, rule: PermissionsRule, content_type: Union[Resource, str]
|
|
55
|
+
) -> None:
|
|
53
56
|
for capability, mode in rule.capabilities.items():
|
|
54
57
|
# Made readability better but line is too long, will make this look better
|
|
55
58
|
url = (
|
|
@@ -65,29 +68,27 @@ class _DefaultPermissionsEndpoint(Endpoint):
|
|
|
65
68
|
)
|
|
66
69
|
)
|
|
67
70
|
|
|
68
|
-
logger.debug("Removing {
|
|
71
|
+
logger.debug(f"Removing {mode} permission for capability {capability}")
|
|
69
72
|
|
|
70
73
|
self.delete_request(url)
|
|
71
74
|
|
|
72
|
-
logger.info(
|
|
73
|
-
"Deleted permission for {0} {1} item {2}".format(rule.grantee.tag_name, rule.grantee.id, resource.id)
|
|
74
|
-
)
|
|
75
|
+
logger.info(f"Deleted permission for {rule.grantee.tag_name} {rule.grantee.id} item {resource.id}")
|
|
75
76
|
|
|
76
|
-
def populate_default_permissions(self, item: BaseItem, content_type: Resource) -> None:
|
|
77
|
+
def populate_default_permissions(self, item: BaseItem, content_type: Union[Resource, str]) -> None:
|
|
77
78
|
if not item.id:
|
|
78
79
|
error = "Server item is missing ID. Item must be retrieved from server first."
|
|
79
80
|
raise MissingRequiredFieldError(error)
|
|
80
81
|
|
|
81
|
-
def permission_fetcher() ->
|
|
82
|
+
def permission_fetcher() -> list[PermissionsRule]:
|
|
82
83
|
return self._get_default_permissions(item, content_type)
|
|
83
84
|
|
|
84
85
|
item._set_default_permissions(permission_fetcher, content_type)
|
|
85
|
-
logger.info("Populated default {
|
|
86
|
+
logger.info(f"Populated default {content_type} permissions for item (ID: {item.id})")
|
|
86
87
|
|
|
87
88
|
def _get_default_permissions(
|
|
88
|
-
self, item: BaseItem, content_type: Resource, req_options: Optional["RequestOptions"] = None
|
|
89
|
-
) ->
|
|
90
|
-
url = "{
|
|
89
|
+
self, item: BaseItem, content_type: Union[Resource, str], req_options: Optional["RequestOptions"] = None
|
|
90
|
+
) -> list[PermissionsRule]:
|
|
91
|
+
url = f"{self.owner_baseurl()}/{item.id}/default-permissions/{plural_type(content_type)}"
|
|
91
92
|
server_response = self.get_request(url, req_options)
|
|
92
93
|
permissions = PermissionsRule.from_response(server_response.content, self.parent_srv.namespace)
|
|
93
94
|
logger.info({"content_type": content_type, "permissions": permissions})
|
|
@@ -10,35 +10,35 @@ from tableauserverclient.helpers.logging import logger
|
|
|
10
10
|
|
|
11
11
|
class _DataQualityWarningEndpoint(Endpoint):
|
|
12
12
|
def __init__(self, parent_srv, resource_type):
|
|
13
|
-
super(
|
|
13
|
+
super().__init__(parent_srv)
|
|
14
14
|
self.resource_type = resource_type
|
|
15
15
|
|
|
16
16
|
@property
|
|
17
17
|
def baseurl(self):
|
|
18
|
-
return "{
|
|
18
|
+
return "{}/sites/{}/dataQualityWarnings/{}".format(
|
|
19
19
|
self.parent_srv.baseurl, self.parent_srv.site_id, self.resource_type
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
def add(self, resource, warning):
|
|
23
|
-
url = "{baseurl}/{
|
|
23
|
+
url = f"{self.baseurl}/{resource.id}"
|
|
24
24
|
add_req = RequestFactory.DQW.add_req(warning)
|
|
25
25
|
response = self.post_request(url, add_req)
|
|
26
26
|
warnings = DQWItem.from_response(response.content, self.parent_srv.namespace)
|
|
27
|
-
logger.info("Added dqw for resource {
|
|
27
|
+
logger.info(f"Added dqw for resource {resource.id}")
|
|
28
28
|
|
|
29
29
|
return warnings
|
|
30
30
|
|
|
31
31
|
def update(self, resource, warning):
|
|
32
|
-
url = "{baseurl}/{
|
|
32
|
+
url = f"{self.baseurl}/{resource.id}"
|
|
33
33
|
add_req = RequestFactory.DQW.update_req(warning)
|
|
34
34
|
response = self.put_request(url, add_req)
|
|
35
35
|
warnings = DQWItem.from_response(response.content, self.parent_srv.namespace)
|
|
36
|
-
logger.info("Added dqw for resource {
|
|
36
|
+
logger.info(f"Added dqw for resource {resource.id}")
|
|
37
37
|
|
|
38
38
|
return warnings
|
|
39
39
|
|
|
40
40
|
def clear(self, resource):
|
|
41
|
-
url = "{baseurl}/{
|
|
41
|
+
url = f"{self.baseurl}/{resource.id}"
|
|
42
42
|
return self.delete_request(url)
|
|
43
43
|
|
|
44
44
|
def populate(self, item):
|
|
@@ -50,10 +50,10 @@ class _DataQualityWarningEndpoint(Endpoint):
|
|
|
50
50
|
return self._get_data_quality_warnings(item)
|
|
51
51
|
|
|
52
52
|
item._set_data_quality_warnings(dqw_fetcher)
|
|
53
|
-
logger.info("Populated permissions for item (ID: {
|
|
53
|
+
logger.info(f"Populated permissions for item (ID: {item.id})")
|
|
54
54
|
|
|
55
55
|
def _get_data_quality_warnings(self, item, req_options=None):
|
|
56
|
-
url = "{baseurl}/{
|
|
56
|
+
url = f"{self.baseurl}/{item.id}"
|
|
57
57
|
server_response = self.get_request(url, req_options)
|
|
58
58
|
dqws = DQWItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
59
59
|
|