tableauserverclient 0.33__py3-none-any.whl → 0.35__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 (94) hide show
  1. tableauserverclient/__init__.py +33 -23
  2. tableauserverclient/{_version.py → bin/_version.py} +3 -3
  3. tableauserverclient/config.py +5 -3
  4. tableauserverclient/models/column_item.py +1 -1
  5. tableauserverclient/models/connection_credentials.py +18 -2
  6. tableauserverclient/models/connection_item.py +44 -6
  7. tableauserverclient/models/custom_view_item.py +78 -11
  8. tableauserverclient/models/data_acceleration_report_item.py +2 -2
  9. tableauserverclient/models/data_alert_item.py +5 -5
  10. tableauserverclient/models/data_freshness_policy_item.py +6 -6
  11. tableauserverclient/models/database_item.py +3 -3
  12. tableauserverclient/models/datasource_item.py +10 -10
  13. tableauserverclient/models/dqw_item.py +1 -1
  14. tableauserverclient/models/favorites_item.py +5 -6
  15. tableauserverclient/models/fileupload_item.py +1 -1
  16. tableauserverclient/models/flow_item.py +54 -9
  17. tableauserverclient/models/flow_run_item.py +3 -3
  18. tableauserverclient/models/group_item.py +44 -4
  19. tableauserverclient/models/groupset_item.py +4 -4
  20. tableauserverclient/models/interval_item.py +9 -9
  21. tableauserverclient/models/job_item.py +73 -8
  22. tableauserverclient/models/linked_tasks_item.py +5 -5
  23. tableauserverclient/models/metric_item.py +5 -5
  24. tableauserverclient/models/pagination_item.py +1 -1
  25. tableauserverclient/models/permissions_item.py +12 -10
  26. tableauserverclient/models/project_item.py +73 -19
  27. tableauserverclient/models/property_decorators.py +12 -11
  28. tableauserverclient/models/reference_item.py +2 -2
  29. tableauserverclient/models/revision_item.py +3 -3
  30. tableauserverclient/models/schedule_item.py +2 -2
  31. tableauserverclient/models/server_info_item.py +26 -6
  32. tableauserverclient/models/site_item.py +69 -3
  33. tableauserverclient/models/subscription_item.py +3 -3
  34. tableauserverclient/models/table_item.py +1 -1
  35. tableauserverclient/models/tableau_auth.py +115 -5
  36. tableauserverclient/models/tableau_types.py +2 -2
  37. tableauserverclient/models/tag_item.py +3 -4
  38. tableauserverclient/models/task_item.py +34 -4
  39. tableauserverclient/models/user_item.py +47 -17
  40. tableauserverclient/models/view_item.py +66 -13
  41. tableauserverclient/models/virtual_connection_item.py +6 -5
  42. tableauserverclient/models/webhook_item.py +39 -6
  43. tableauserverclient/models/workbook_item.py +116 -13
  44. tableauserverclient/namespace.py +1 -1
  45. tableauserverclient/server/__init__.py +2 -1
  46. tableauserverclient/server/endpoint/auth_endpoint.py +69 -10
  47. tableauserverclient/server/endpoint/custom_views_endpoint.py +258 -29
  48. tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py +2 -2
  49. tableauserverclient/server/endpoint/data_alert_endpoint.py +14 -14
  50. tableauserverclient/server/endpoint/databases_endpoint.py +13 -12
  51. tableauserverclient/server/endpoint/datasources_endpoint.py +61 -62
  52. tableauserverclient/server/endpoint/default_permissions_endpoint.py +19 -18
  53. tableauserverclient/server/endpoint/dqw_endpoint.py +9 -9
  54. tableauserverclient/server/endpoint/endpoint.py +19 -21
  55. tableauserverclient/server/endpoint/exceptions.py +23 -7
  56. tableauserverclient/server/endpoint/favorites_endpoint.py +31 -31
  57. tableauserverclient/server/endpoint/fileuploads_endpoint.py +9 -11
  58. tableauserverclient/server/endpoint/flow_runs_endpoint.py +15 -13
  59. tableauserverclient/server/endpoint/flow_task_endpoint.py +2 -2
  60. tableauserverclient/server/endpoint/flows_endpoint.py +344 -29
  61. tableauserverclient/server/endpoint/groups_endpoint.py +342 -27
  62. tableauserverclient/server/endpoint/groupsets_endpoint.py +2 -2
  63. tableauserverclient/server/endpoint/jobs_endpoint.py +116 -7
  64. tableauserverclient/server/endpoint/linked_tasks_endpoint.py +2 -2
  65. tableauserverclient/server/endpoint/metadata_endpoint.py +2 -2
  66. tableauserverclient/server/endpoint/metrics_endpoint.py +10 -10
  67. tableauserverclient/server/endpoint/permissions_endpoint.py +13 -15
  68. tableauserverclient/server/endpoint/projects_endpoint.py +681 -30
  69. tableauserverclient/server/endpoint/resource_tagger.py +14 -13
  70. tableauserverclient/server/endpoint/schedules_endpoint.py +17 -18
  71. tableauserverclient/server/endpoint/server_info_endpoint.py +40 -5
  72. tableauserverclient/server/endpoint/sites_endpoint.py +282 -17
  73. tableauserverclient/server/endpoint/subscriptions_endpoint.py +10 -10
  74. tableauserverclient/server/endpoint/tables_endpoint.py +15 -14
  75. tableauserverclient/server/endpoint/tasks_endpoint.py +86 -8
  76. tableauserverclient/server/endpoint/users_endpoint.py +366 -19
  77. tableauserverclient/server/endpoint/views_endpoint.py +262 -20
  78. tableauserverclient/server/endpoint/virtual_connections_endpoint.py +6 -5
  79. tableauserverclient/server/endpoint/webhooks_endpoint.py +88 -11
  80. tableauserverclient/server/endpoint/workbooks_endpoint.py +653 -65
  81. tableauserverclient/server/filter.py +2 -2
  82. tableauserverclient/server/pager.py +29 -6
  83. tableauserverclient/server/query.py +68 -19
  84. tableauserverclient/server/request_factory.py +57 -37
  85. tableauserverclient/server/request_options.py +243 -141
  86. tableauserverclient/server/server.py +76 -10
  87. tableauserverclient/server/sort.py +16 -2
  88. {tableauserverclient-0.33.dist-info → tableauserverclient-0.35.dist-info}/METADATA +7 -7
  89. tableauserverclient-0.35.dist-info/RECORD +106 -0
  90. {tableauserverclient-0.33.dist-info → tableauserverclient-0.35.dist-info}/WHEEL +1 -1
  91. tableauserverclient-0.33.dist-info/RECORD +0 -106
  92. {tableauserverclient-0.33.dist-info → tableauserverclient-0.35.dist-info}/LICENSE +0 -0
  93. {tableauserverclient-0.33.dist-info → tableauserverclient-0.35.dist-info}/LICENSE.versioneer +0 -0
  94. {tableauserverclient-0.33.dist-info → tableauserverclient-0.35.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,8 @@ import os
6
6
 
7
7
  from contextlib import closing
8
8
  from pathlib import Path
9
- from typing import Iterable, List, Mapping, Optional, Sequence, Set, 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
12
13
  from tableauserverclient.server.query import QuerySet
@@ -22,7 +23,7 @@ from tableauserverclient.server.endpoint.exceptions import InternalServerError,
22
23
  from tableauserverclient.server.endpoint.permissions_endpoint import _PermissionsEndpoint
23
24
  from tableauserverclient.server.endpoint.resource_tagger import TaggingMixin
24
25
 
25
- from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS, FILESIZE_LIMIT_MB, BYTES_PER_MB, config
26
+ from tableauserverclient.config import ALLOWED_FILE_EXTENSIONS, BYTES_PER_MB, config
26
27
  from tableauserverclient.filesys_helpers import (
27
28
  make_download_path,
28
29
  get_file_type,
@@ -57,7 +58,7 @@ PathOrFileW = Union[FilePath, FileObjectW]
57
58
 
58
59
  class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]):
59
60
  def __init__(self, parent_srv: "Server") -> None:
60
- super(Datasources, self).__init__(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], TaggingMixin[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], TaggingMixin[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,12 +100,17 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[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
- def _get_datasource_connections(self, datasource_item, req_options=None):
105
- url = "{0}/{1}/connections".format(self.baseurl, datasource_item.id)
105
+ def _get_datasource_connections(
106
+ self, datasource_item: DatasourceItem, req_options: Optional[RequestOptions] = None
107
+ ) -> list[ConnectionItem]:
108
+ url = f"{self.baseurl}/{datasource_item.id}/connections"
106
109
  server_response = self.get_request(url, req_options)
107
110
  connections = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
111
+ for connection in connections:
112
+ connection._datasource_id = datasource_item.id
113
+ connection._datasource_name = datasource_item.name
108
114
  return connections
109
115
 
110
116
  # Delete 1 datasource by id
@@ -113,9 +119,9 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
113
119
  if not datasource_id:
114
120
  error = "Datasource ID undefined."
115
121
  raise ValueError(error)
116
- url = "{0}/{1}".format(self.baseurl, datasource_id)
122
+ url = f"{self.baseurl}/{datasource_id}"
117
123
  self.delete_request(url)
118
- logger.info("Deleted single datasource (ID: {0})".format(datasource_id))
124
+ logger.info(f"Deleted single datasource (ID: {datasource_id})")
119
125
 
120
126
  # Download 1 datasource by id
121
127
  @api(version="2.0")
@@ -152,11 +158,11 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
152
158
  self.update_tags(datasource_item)
153
159
 
154
160
  # Update the datasource itself
155
- url = "{0}/{1}".format(self.baseurl, datasource_item.id)
161
+ url = f"{self.baseurl}/{datasource_item.id}"
156
162
 
157
163
  update_req = RequestFactory.Datasource.update_req(datasource_item)
158
164
  server_response = self.put_request(url, update_req)
159
- logger.info("Updated datasource item (ID: {0})".format(datasource_item.id))
165
+ logger.info(f"Updated datasource item (ID: {datasource_item.id})")
160
166
  updated_datasource = copy.copy(datasource_item)
161
167
  return updated_datasource._parse_common_elements(server_response.content, self.parent_srv.namespace)
162
168
 
@@ -165,7 +171,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
165
171
  def update_connection(
166
172
  self, datasource_item: DatasourceItem, connection_item: ConnectionItem
167
173
  ) -> Optional[ConnectionItem]:
168
- url = "{0}/{1}/connections/{2}".format(self.baseurl, datasource_item.id, connection_item.id)
174
+ url = f"{self.baseurl}/{datasource_item.id}/connections/{connection_item.id}"
169
175
 
170
176
  update_req = RequestFactory.Connection.update_req(connection_item)
171
177
  server_response = self.put_request(url, update_req)
@@ -174,27 +180,25 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
174
180
  return None
175
181
 
176
182
  if len(connections) > 1:
177
- logger.debug("Multiple connections returned ({0})".format(len(connections)))
183
+ logger.debug(f"Multiple connections returned ({len(connections)})")
178
184
  connection = list(filter(lambda x: x.id == connection_item.id, connections))[0]
179
185
 
180
- logger.info(
181
- "Updated datasource item (ID: {0} & connection item {1}".format(datasource_item.id, connection_item.id)
182
- )
186
+ logger.info(f"Updated datasource item (ID: {datasource_item.id} & connection item {connection_item.id}")
183
187
  return connection
184
188
 
185
189
  @api(version="2.8")
186
- def refresh(self, datasource_item: DatasourceItem) -> JobItem:
190
+ def refresh(self, datasource_item: DatasourceItem, incremental: bool = False) -> JobItem:
187
191
  id_ = getattr(datasource_item, "id", datasource_item)
188
- url = "{0}/{1}/refresh".format(self.baseurl, id_)
189
- empty_req = RequestFactory.Empty.empty_req()
190
- server_response = self.post_request(url, empty_req)
192
+ url = f"{self.baseurl}/{id_}/refresh"
193
+ refresh_req = RequestFactory.Task.refresh_req(incremental)
194
+ server_response = self.post_request(url, refresh_req)
191
195
  new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
192
196
  return new_job
193
197
 
194
198
  @api(version="3.5")
195
199
  def create_extract(self, datasource_item: DatasourceItem, encrypt: bool = False) -> JobItem:
196
200
  id_ = getattr(datasource_item, "id", datasource_item)
197
- url = "{0}/{1}/createExtract?encrypt={2}".format(self.baseurl, id_, encrypt)
201
+ url = f"{self.baseurl}/{id_}/createExtract?encrypt={encrypt}"
198
202
  empty_req = RequestFactory.Empty.empty_req()
199
203
  server_response = self.post_request(url, empty_req)
200
204
  new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0]
@@ -203,7 +207,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
203
207
  @api(version="3.5")
204
208
  def delete_extract(self, datasource_item: DatasourceItem) -> None:
205
209
  id_ = getattr(datasource_item, "id", datasource_item)
206
- url = "{0}/{1}/deleteExtract".format(self.baseurl, id_)
210
+ url = f"{self.baseurl}/{id_}/deleteExtract"
207
211
  empty_req = RequestFactory.Empty.empty_req()
208
212
  self.post_request(url, empty_req)
209
213
 
@@ -223,12 +227,12 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
223
227
  if isinstance(file, (os.PathLike, str)):
224
228
  if not os.path.isfile(file):
225
229
  error = "File path does not lead to an existing file."
226
- raise IOError(error)
230
+ raise OSError(error)
227
231
 
228
232
  filename = os.path.basename(file)
229
233
  file_extension = os.path.splitext(filename)[1][1:]
230
234
  file_size = os.path.getsize(file)
231
- logger.debug("Publishing file `{}`, size `{}`".format(filename, file_size))
235
+ logger.debug(f"Publishing file `{filename}`, size `{file_size}`")
232
236
  # If name is not defined, grab the name from the file to publish
233
237
  if not datasource_item.name:
234
238
  datasource_item.name = os.path.splitext(filename)[0]
@@ -247,41 +251,40 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
247
251
  elif file_type == "xml":
248
252
  file_extension = "tds"
249
253
  else:
250
- error = "Unsupported file type {}".format(file_type)
254
+ error = f"Unsupported file type {file_type}"
251
255
  raise ValueError(error)
252
256
 
253
- filename = "{}.{}".format(datasource_item.name, file_extension)
257
+ filename = f"{datasource_item.name}.{file_extension}"
254
258
  file_size = get_file_object_size(file)
255
259
 
256
260
  else:
257
261
  raise TypeError("file should be a filepath or file object.")
258
262
 
263
+ # Construct the url with the defined mode
264
+ url = f"{self.baseurl}?datasourceType={file_extension}"
259
265
  if not mode or not hasattr(self.parent_srv.PublishMode, mode):
260
- error = "Invalid mode defined."
266
+ error = f"Invalid mode defined: {mode}"
261
267
  raise ValueError(error)
262
-
263
- # Construct the url with the defined mode
264
- url = "{0}?datasourceType={1}".format(self.baseurl, file_extension)
265
- if mode == self.parent_srv.PublishMode.Overwrite or mode == self.parent_srv.PublishMode.Append:
266
- url += "&{0}=true".format(mode.lower())
268
+ else:
269
+ url += f"&{mode.lower()}=true"
267
270
 
268
271
  if as_job:
269
- url += "&{0}=true".format("asJob")
272
+ url += "&{}=true".format("asJob")
270
273
 
271
274
  # Determine if chunking is required (64MB is the limit for single upload method)
272
- if file_size >= FILESIZE_LIMIT_MB * BYTES_PER_MB:
275
+ if file_size >= config.FILESIZE_LIMIT_MB * BYTES_PER_MB:
273
276
  logger.info(
274
277
  "Publishing {} to server with chunking method (datasource over {}MB, chunk size {}MB)".format(
275
- filename, FILESIZE_LIMIT_MB, config.CHUNK_SIZE_MB
278
+ filename, config.FILESIZE_LIMIT_MB, config.CHUNK_SIZE_MB
276
279
  )
277
280
  )
278
281
  upload_session_id = self.parent_srv.fileuploads.upload(file)
279
- url = "{0}&uploadSessionId={1}".format(url, upload_session_id)
282
+ url = f"{url}&uploadSessionId={upload_session_id}"
280
283
  xml_request, content_type = RequestFactory.Datasource.publish_req_chunked(
281
284
  datasource_item, connection_credentials, connections
282
285
  )
283
286
  else:
284
- logger.info("Publishing {0} to server".format(filename))
287
+ logger.info(f"Publishing {filename} to server")
285
288
 
286
289
  if isinstance(file, (Path, str)):
287
290
  with open(file, "rb") as f:
@@ -309,11 +312,11 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
309
312
 
310
313
  if as_job:
311
314
  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))
315
+ logger.info(f"Published {filename} (JOB_ID: {new_job.id}")
313
316
  return new_job
314
317
  else:
315
318
  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))
319
+ logger.info(f"Published {filename} (ID: {new_datasource.id})")
317
320
  return new_datasource
318
321
 
319
322
  @api(version="3.13")
@@ -327,23 +330,23 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
327
330
  ) -> JobItem:
328
331
  if isinstance(datasource_or_connection_item, DatasourceItem):
329
332
  datasource_id = datasource_or_connection_item.id
330
- url = "{0}/{1}/data".format(self.baseurl, datasource_id)
333
+ url = f"{self.baseurl}/{datasource_id}/data"
331
334
  elif isinstance(datasource_or_connection_item, ConnectionItem):
332
335
  datasource_id = datasource_or_connection_item.datasource_id
333
336
  connection_id = datasource_or_connection_item.id
334
- url = "{0}/{1}/connections/{2}/data".format(self.baseurl, datasource_id, connection_id)
337
+ url = f"{self.baseurl}/{datasource_id}/connections/{connection_id}/data"
335
338
  else:
336
339
  assert isinstance(datasource_or_connection_item, str)
337
- url = "{0}/{1}/data".format(self.baseurl, datasource_or_connection_item)
340
+ url = f"{self.baseurl}/{datasource_or_connection_item}/data"
338
341
 
339
342
  if payload is not None:
340
343
  if not os.path.isfile(payload):
341
344
  error = "File path does not lead to an existing file."
342
- raise IOError(error)
345
+ raise OSError(error)
343
346
 
344
- logger.info("Uploading {0} to server with chunking method for Update job".format(payload))
347
+ logger.info(f"Uploading {payload} to server with chunking method for Update job")
345
348
  upload_session_id = self.parent_srv.fileuploads.upload(payload)
346
- url = "{0}?uploadSessionId={1}".format(url, upload_session_id)
349
+ url = f"{url}?uploadSessionId={upload_session_id}"
347
350
 
348
351
  json_request = json.dumps({"actions": actions})
349
352
  parameters = {"headers": {"requestid": request_id}}
@@ -356,7 +359,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
356
359
  self._permissions.populate(item)
357
360
 
358
361
  @api(version="2.0")
359
- def update_permissions(self, item: DatasourceItem, permission_item: List["PermissionsRule"]) -> None:
362
+ def update_permissions(self, item: DatasourceItem, permission_item: list["PermissionsRule"]) -> None:
360
363
  self._permissions.update(item, permission_item)
361
364
 
362
365
  @api(version="2.0")
@@ -390,12 +393,12 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
390
393
  return self._get_datasource_revisions(datasource_item)
391
394
 
392
395
  datasource_item._set_revisions(revisions_fetcher)
393
- logger.info("Populated revisions for datasource (ID: {0})".format(datasource_item.id))
396
+ logger.info(f"Populated revisions for datasource (ID: {datasource_item.id})")
394
397
 
395
398
  def _get_datasource_revisions(
396
399
  self, datasource_item: DatasourceItem, req_options: Optional["RequestOptions"] = None
397
- ) -> List[RevisionItem]:
398
- url = "{0}/{1}/revisions".format(self.baseurl, datasource_item.id)
400
+ ) -> list[RevisionItem]:
401
+ url = f"{self.baseurl}/{datasource_item.id}/revisions"
399
402
  server_response = self.get_request(url, req_options)
400
403
  revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, datasource_item)
401
404
  return revisions
@@ -413,9 +416,9 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
413
416
  error = "Datasource ID undefined."
414
417
  raise ValueError(error)
415
418
  if revision_number is None:
416
- url = "{0}/{1}/content".format(self.baseurl, datasource_id)
419
+ url = f"{self.baseurl}/{datasource_id}/content"
417
420
  else:
418
- url = "{0}/{1}/revisions/{2}/content".format(self.baseurl, datasource_id, revision_number)
421
+ url = f"{self.baseurl}/{datasource_id}/revisions/{revision_number}/content"
419
422
 
420
423
  if not include_extract:
421
424
  url += "?includeExtract=False"
@@ -437,9 +440,7 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
437
440
  f.write(chunk)
438
441
  return_path = os.path.abspath(download_path)
439
442
 
440
- logger.info(
441
- "Downloaded datasource revision {0} to {1} (ID: {2})".format(revision_number, return_path, datasource_id)
442
- )
443
+ logger.info(f"Downloaded datasource revision {revision_number} to {return_path} (ID: {datasource_id})")
443
444
  return return_path
444
445
 
445
446
  @api(version="2.3")
@@ -449,19 +450,17 @@ class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]
449
450
  url = "/".join([self.baseurl, datasource_id, "revisions", revision_number])
450
451
 
451
452
  self.delete_request(url)
452
- logger.info(
453
- "Deleted single datasource revision (ID: {0}) (Revision: {1})".format(datasource_id, revision_number)
454
- )
453
+ logger.info(f"Deleted single datasource revision (ID: {datasource_id}) (Revision: {revision_number})")
455
454
 
456
455
  # a convenience method
457
456
  @api(version="2.8")
458
457
  def schedule_extract_refresh(
459
458
  self, schedule_id: str, item: DatasourceItem
460
- ) -> List["AddResponse"]: # actually should return a task
459
+ ) -> list["AddResponse"]: # actually should return a task
461
460
  return self.parent_srv.schedules.add_to_schedule(schedule_id, datasource=item)
462
461
 
463
462
  @api(version="1.0")
464
- def add_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> Set[str]:
463
+ def add_tags(self, item: Union[DatasourceItem, str], tags: Union[Iterable[str], str]) -> set[str]:
465
464
  return super().add_tags(item, tags)
466
465
 
467
466
  @api(version="1.0")
@@ -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
 
@@ -8,12 +8,9 @@ from xml.etree.ElementTree import ParseError
8
8
  from typing import (
9
9
  Any,
10
10
  Callable,
11
- Dict,
12
11
  Generic,
13
- List,
14
12
  Optional,
15
13
  TYPE_CHECKING,
16
- Tuple,
17
14
  TypeVar,
18
15
  Union,
19
16
  )
@@ -22,6 +19,7 @@ from tableauserverclient.models.pagination_item import PaginationItem
22
19
  from tableauserverclient.server.request_options import RequestOptions
23
20
 
24
21
  from tableauserverclient.server.endpoint.exceptions import (
22
+ FailedSignInError,
25
23
  ServerResponseError,
26
24
  InternalServerError,
27
25
  NonXMLResponseError,
@@ -56,7 +54,7 @@ class Endpoint:
56
54
  async_response = None
57
55
 
58
56
  @staticmethod
59
- def set_parameters(http_options, auth_token, content, content_type, parameters) -> Dict[str, Any]:
57
+ def set_parameters(http_options, auth_token, content, content_type, parameters) -> dict[str, Any]:
60
58
  parameters = parameters or {}
61
59
  parameters.update(http_options)
62
60
  if "headers" not in parameters:
@@ -82,7 +80,7 @@ class Endpoint:
82
80
  else:
83
81
  # only set the TSC user agent if not already populated
84
82
  _client_version: Optional[str] = get_versions()["version"]
85
- parameters["headers"][USER_AGENT_HEADER] = "Tableau Server Client/{}".format(_client_version)
83
+ parameters["headers"][USER_AGENT_HEADER] = f"Tableau Server Client/{_client_version}"
86
84
 
87
85
  # result: parameters["headers"]["User-Agent"] is set
88
86
  # return explicitly for testing only
@@ -90,12 +88,12 @@ class Endpoint:
90
88
 
91
89
  def _blocking_request(self, method, url, parameters={}) -> Optional[Union["Response", Exception]]:
92
90
  response = None
93
- logger.debug("[{}] Begin blocking request to {}".format(datetime.timestamp(), url))
91
+ logger.debug(f"[{datetime.timestamp()}] Begin blocking request to {url}")
94
92
  try:
95
93
  response = method(url, **parameters)
96
- logger.debug("[{}] Call finished".format(datetime.timestamp()))
94
+ logger.debug(f"[{datetime.timestamp()}] Call finished")
97
95
  except Exception as e:
98
- logger.debug("Error making request to server: {}".format(e))
96
+ logger.debug(f"Error making request to server: {e}")
99
97
  raise e
100
98
  return response
101
99
 
@@ -111,13 +109,13 @@ class Endpoint:
111
109
  content: Optional[bytes] = None,
112
110
  auth_token: Optional[str] = None,
113
111
  content_type: Optional[str] = None,
114
- parameters: Optional[Dict[str, Any]] = None,
112
+ parameters: Optional[dict[str, Any]] = None,
115
113
  ) -> "Response":
116
114
  parameters = Endpoint.set_parameters(
117
115
  self.parent_srv.http_options, auth_token, content, content_type, parameters
118
116
  )
119
117
 
120
- logger.debug("request method {}, url: {}".format(method.__name__, url))
118
+ logger.debug(f"request method {method.__name__}, url: {url}")
121
119
  if content:
122
120
  redacted = helpers.strings.redact_xml(content[:200])
123
121
  # this needs to be under a trace or something, it's a LOT
@@ -129,21 +127,21 @@ class Endpoint:
129
127
  server_response: Optional[Union["Response", Exception]] = self.send_request_while_show_progress_threaded(
130
128
  method, url, parameters, request_timeout
131
129
  )
132
- logger.debug("[{}] Async request returned: received {}".format(datetime.timestamp(), server_response))
130
+ logger.debug(f"[{datetime.timestamp()}] Async request returned: received {server_response}")
133
131
  # is this blocking retry really necessary? I guess if it was just the threading messing it up?
134
132
  if server_response is None:
135
133
  logger.debug(server_response)
136
- logger.debug("[{}] Async request failed: retrying".format(datetime.timestamp()))
134
+ logger.debug(f"[{datetime.timestamp()}] Async request failed: retrying")
137
135
  server_response = self._blocking_request(method, url, parameters)
138
136
  if server_response is None:
139
- logger.debug("[{}] Request failed".format(datetime.timestamp()))
137
+ logger.debug(f"[{datetime.timestamp()}] Request failed")
140
138
  raise RuntimeError
141
139
  if isinstance(server_response, Exception):
142
140
  raise server_response
143
141
  self._check_status(server_response, url)
144
142
 
145
143
  loggable_response = self.log_response_safely(server_response)
146
- logger.debug("Server response from {0}".format(url))
144
+ logger.debug(f"Server response from {url}")
147
145
  # uncomment the following to log full responses in debug mode
148
146
  # BE CAREFUL WHEN SHARING THESE RESULTS - MAY CONTAIN YOUR SENSITIVE DATA
149
147
  # logger.debug(loggable_response)
@@ -154,16 +152,16 @@ class Endpoint:
154
152
  return server_response
155
153
 
156
154
  def _check_status(self, server_response: "Response", url: Optional[str] = None):
157
- logger.debug("Response status: {}".format(server_response))
155
+ logger.debug(f"Response status: {server_response}")
158
156
  if not hasattr(server_response, "status_code"):
159
- raise EnvironmentError("Response is not a http response?")
157
+ raise OSError("Response is not a http response?")
160
158
  if server_response.status_code >= 500:
161
159
  raise InternalServerError(server_response, url)
162
160
  elif server_response.status_code not in Success_codes:
163
161
  try:
164
162
  if server_response.status_code == 401:
165
163
  # TODO: catch this in server.py and attempt to sign in again, in case it's a session expiry
166
- raise NotSignedInError(server_response.content, url)
164
+ raise FailedSignInError.from_response(server_response.content, self.parent_srv.namespace, url)
167
165
 
168
166
  raise ServerResponseError.from_response(server_response.content, self.parent_srv.namespace, url)
169
167
  except ParseError:
@@ -183,9 +181,9 @@ class Endpoint:
183
181
  # content-type is an octet-stream accomplishes the same goal without eagerly loading content.
184
182
  # This check is to determine if the response is a text response (xml or otherwise)
185
183
  # so that we do not attempt to log bytes and other binary data.
186
- loggable_response = "Content type `{}`".format(content_type)
184
+ loggable_response = f"Content type `{content_type}`"
187
185
  if content_type == "application/octet-stream":
188
- loggable_response = "A stream of type {} [Truncated File Contents]".format(content_type)
186
+ loggable_response = f"A stream of type {content_type} [Truncated File Contents]"
189
187
  elif server_response.encoding and len(server_response.content) > 0:
190
188
  loggable_response = helpers.strings.redact_xml(server_response.content.decode(server_response.encoding))
191
189
  return loggable_response
@@ -313,7 +311,7 @@ def parameter_added_in(**params: str) -> Callable[[Callable[Concatenate[E, P], R
313
311
  for p in params_to_check:
314
312
  min_ver = Version(str(params[p]))
315
313
  if server_ver < min_ver:
316
- error = "{!r} not available in {}, it will be ignored. Added in {}".format(p, server_ver, min_ver)
314
+ error = f"{p!r} not available in {server_ver}, it will be ignored. Added in {min_ver}"
317
315
  warnings.warn(error)
318
316
  return func(self, *args, **kwargs)
319
317
 
@@ -353,5 +351,5 @@ class QuerysetEndpoint(Endpoint, Generic[T]):
353
351
  return queryset
354
352
 
355
353
  @abc.abstractmethod
356
- def get(self, request_options: Optional[RequestOptions] = None) -> Tuple[List[T], PaginationItem]:
354
+ def get(self, request_options: Optional[RequestOptions] = None) -> tuple[list[T], PaginationItem]:
357
355
  raise NotImplementedError(f".get has not been implemented for {self.__class__.__qualname__}")