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
@@ -6,9 +6,11 @@ import os
6
6
 
7
7
  from contextlib import closing
8
8
  from pathlib import Path
9
- from typing import List, Mapping, Optional, Sequence, Tuple, TYPE_CHECKING, Union
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 _ResourceTagger
24
+ from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
23
25
 
24
- from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS, FILESIZE_LIMIT_MB, BYTES_PER_MB, CHUNK_SIZE_MB
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(Datasources, self).__init__(parent_srv)
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 "{0}/sites/{1}/datasources".format(self.parent_srv.baseurl, self.parent_srv.site_id)
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) -> Tuple[List[DatasourceItem], PaginationItem]:
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: {0})".format(datasource_id))
87
- url = "{0}/{1}".format(self.baseurl, datasource_id)
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: {0})".format(datasource_item.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 = "{0}/{1}/connections".format(self.baseurl, datasource_item.id)
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 = "{0}/{1}".format(self.baseurl, datasource_id)
117
+ url = f"{self.baseurl}/{datasource_id}"
117
118
  self.delete_request(url)
118
- logger.info("Deleted single datasource (ID: {0})".format(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
- ) -> str:
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._resource_tagger.update_tags(self.baseurl, datasource_item)
153
+ self.update_tags(datasource_item)
153
154
 
154
155
  # Update the datasource itself
155
- url = "{0}/{1}".format(self.baseurl, datasource_item.id)
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: {0})".format(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 = "{0}/{1}/connections/{2}".format(self.baseurl, datasource_item.id, connection_item.id)
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 ({0})".format(len(connections)))
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 = "{0}/{1}/refresh".format(self.baseurl, id_)
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 = "{0}/{1}/createExtract?encrypt={2}".format(self.baseurl, id_, encrypt)
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 = "{0}/{1}/deleteExtract".format(self.baseurl, id_)
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 IOError(error)
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 `{}`".format(filename, 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 {}".format(file_type)
249
+ error = f"Unsupported file type {file_type}"
251
250
  raise ValueError(error)
252
251
 
253
- filename = "{}.{}".format(datasource_item.name, file_extension)
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 = "{0}?datasourceType={1}".format(self.baseurl, file_extension)
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 += "&{0}=true".format(mode.lower())
265
+ url += f"&{mode.lower()}=true"
267
266
 
268
267
  if as_job:
269
- url += "&{0}=true".format("asJob")
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 = "{0}&uploadSessionId={1}".format(url, upload_session_id)
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 {0} to server".format(filename))
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 {0} (JOB_ID: {1}".format(filename, new_job.id))
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 {0} (ID: {1})".format(filename, new_datasource.id))
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 = "{0}/{1}/data".format(self.baseurl, datasource_id)
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 = "{0}/{1}/connections/{2}/data".format(self.baseurl, datasource_id, connection_id)
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 = "{0}/{1}/data".format(self.baseurl, datasource_or_connection_item)
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 IOError(error)
341
+ raise OSError(error)
343
342
 
344
- logger.info("Uploading {0} to server with chunking method for Update job".format(payload))
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 = "{0}?uploadSessionId={1}".format(url, upload_session_id)
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: List["PermissionsRule"]) -> None:
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: {0})".format(datasource_item.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
- ) -> List[RevisionItem]:
398
- url = "{0}/{1}/revisions".format(self.baseurl, datasource_item.id)
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 = "{0}/{1}/content".format(self.baseurl, datasource_id)
415
+ url = f"{self.baseurl}/{datasource_id}/content"
417
416
  else:
418
- url = "{0}/{1}/revisions/{2}/content".format(self.baseurl, datasource_id, revision_number)
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
- ) -> List["AddResponse"]: # actually should return a task
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, List, Optional, Sequence, Union
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(_DefaultPermissionsEndpoint, self).__init__(parent_srv)
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]>".format(self.owner_baseurl())
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
- ) -> List[PermissionsRule]:
43
- url = "{0}/{1}/default-permissions/{2}".format(self.owner_baseurl(), resource.id, plural_type(content_type))
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 {}".format(content_type, resource.id))
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(self, resource: BaseItem, rule: PermissionsRule, content_type: Resource) -> None:
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 {0} permission for capability {1}".format(mode, capability))
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() -> List[PermissionsRule]:
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 {0} permissions for item (ID: {1})".format(content_type, item.id))
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
- ) -> List[PermissionsRule]:
90
- url = "{0}/{1}/default-permissions/{2}".format(self.owner_baseurl(), item.id, plural_type(content_type))
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(_DataQualityWarningEndpoint, self).__init__(parent_srv)
13
+ super().__init__(parent_srv)
14
14
  self.resource_type = resource_type
15
15
 
16
16
  @property
17
17
  def baseurl(self):
18
- return "{0}/sites/{1}/dataQualityWarnings/{2}".format(
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}/{content_luid}".format(baseurl=self.baseurl, content_luid=resource.id)
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 {0}".format(resource.id))
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}/{content_luid}".format(baseurl=self.baseurl, content_luid=resource.id)
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 {0}".format(resource.id))
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}/{content_luid}".format(baseurl=self.baseurl, content_luid=resource.id)
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: {0})".format(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}/{content_luid}".format(baseurl=self.baseurl, content_luid=item.id)
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