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
@@ -1,30 +1,39 @@
1
+ from typing_extensions import Concatenate, ParamSpec
1
2
  from tableauserverclient import datetime_helpers as datetime
2
3
 
3
4
  import abc
4
5
  from packaging.version import Version
5
6
  from functools import wraps
6
7
  from xml.etree.ElementTree import ParseError
7
- from typing import Any, Callable, Dict, Generic, List, Optional, TYPE_CHECKING, Tuple, TypeVar, Union
8
+ from typing import (
9
+ Any,
10
+ Callable,
11
+ Generic,
12
+ Optional,
13
+ TYPE_CHECKING,
14
+ TypeVar,
15
+ Union,
16
+ )
8
17
 
9
18
  from tableauserverclient.models.pagination_item import PaginationItem
10
19
  from tableauserverclient.server.request_options import RequestOptions
11
20
 
12
- from .exceptions import (
21
+ from tableauserverclient.server.endpoint.exceptions import (
22
+ FailedSignInError,
13
23
  ServerResponseError,
14
24
  InternalServerError,
15
25
  NonXMLResponseError,
16
26
  NotSignedInError,
17
27
  )
18
- from ..exceptions import EndpointUnavailableError
28
+ from tableauserverclient.server.exceptions import EndpointUnavailableError
19
29
 
20
30
  from tableauserverclient.server.query import QuerySet
21
31
  from tableauserverclient import helpers, get_versions
22
32
 
23
33
  from tableauserverclient.helpers.logging import logger
24
- from tableauserverclient.config import DELAY_SLEEP_SECONDS
25
34
 
26
35
  if TYPE_CHECKING:
27
- from ..server import Server
36
+ from tableauserverclient.server.server import Server
28
37
  from requests import Response
29
38
 
30
39
 
@@ -38,14 +47,14 @@ TABLEAU_AUTH_HEADER = "x-tableau-auth"
38
47
  USER_AGENT_HEADER = "User-Agent"
39
48
 
40
49
 
41
- class Endpoint(object):
50
+ class Endpoint:
42
51
  def __init__(self, parent_srv: "Server"):
43
52
  self.parent_srv = parent_srv
44
53
 
45
54
  async_response = None
46
55
 
47
56
  @staticmethod
48
- 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]:
49
58
  parameters = parameters or {}
50
59
  parameters.update(http_options)
51
60
  if "headers" not in parameters:
@@ -71,7 +80,7 @@ class Endpoint(object):
71
80
  else:
72
81
  # only set the TSC user agent if not already populated
73
82
  _client_version: Optional[str] = get_versions()["version"]
74
- parameters["headers"][USER_AGENT_HEADER] = "Tableau Server Client/{}".format(_client_version)
83
+ parameters["headers"][USER_AGENT_HEADER] = f"Tableau Server Client/{_client_version}"
75
84
 
76
85
  # result: parameters["headers"]["User-Agent"] is set
77
86
  # return explicitly for testing only
@@ -79,12 +88,12 @@ class Endpoint(object):
79
88
 
80
89
  def _blocking_request(self, method, url, parameters={}) -> Optional[Union["Response", Exception]]:
81
90
  response = None
82
- logger.debug("[{}] Begin blocking request to {}".format(datetime.timestamp(), url))
91
+ logger.debug(f"[{datetime.timestamp()}] Begin blocking request to {url}")
83
92
  try:
84
93
  response = method(url, **parameters)
85
- logger.debug("[{}] Call finished".format(datetime.timestamp()))
94
+ logger.debug(f"[{datetime.timestamp()}] Call finished")
86
95
  except Exception as e:
87
- logger.debug("Error making request to server: {}".format(e))
96
+ logger.debug(f"Error making request to server: {e}")
88
97
  raise e
89
98
  return response
90
99
 
@@ -100,13 +109,13 @@ class Endpoint(object):
100
109
  content: Optional[bytes] = None,
101
110
  auth_token: Optional[str] = None,
102
111
  content_type: Optional[str] = None,
103
- parameters: Optional[Dict[str, Any]] = None,
112
+ parameters: Optional[dict[str, Any]] = None,
104
113
  ) -> "Response":
105
114
  parameters = Endpoint.set_parameters(
106
115
  self.parent_srv.http_options, auth_token, content, content_type, parameters
107
116
  )
108
117
 
109
- logger.debug("request method {}, url: {}".format(method.__name__, url))
118
+ logger.debug(f"request method {method.__name__}, url: {url}")
110
119
  if content:
111
120
  redacted = helpers.strings.redact_xml(content[:200])
112
121
  # this needs to be under a trace or something, it's a LOT
@@ -118,22 +127,24 @@ class Endpoint(object):
118
127
  server_response: Optional[Union["Response", Exception]] = self.send_request_while_show_progress_threaded(
119
128
  method, url, parameters, request_timeout
120
129
  )
121
- logger.debug("[{}] Async request returned: received {}".format(datetime.timestamp(), server_response))
130
+ logger.debug(f"[{datetime.timestamp()}] Async request returned: received {server_response}")
122
131
  # is this blocking retry really necessary? I guess if it was just the threading messing it up?
123
132
  if server_response is None:
124
133
  logger.debug(server_response)
125
- logger.debug("[{}] Async request failed: retrying".format(datetime.timestamp()))
134
+ logger.debug(f"[{datetime.timestamp()}] Async request failed: retrying")
126
135
  server_response = self._blocking_request(method, url, parameters)
127
136
  if server_response is None:
128
- logger.debug("[{}] Request failed".format(datetime.timestamp()))
137
+ logger.debug(f"[{datetime.timestamp()}] Request failed")
129
138
  raise RuntimeError
130
139
  if isinstance(server_response, Exception):
131
140
  raise server_response
132
141
  self._check_status(server_response, url)
133
142
 
134
143
  loggable_response = self.log_response_safely(server_response)
135
- logger.debug("Server response from {0}".format(url))
136
- # logger.debug("\n\t{1}".format(loggable_response))
144
+ logger.debug(f"Server response from {url}")
145
+ # uncomment the following to log full responses in debug mode
146
+ # BE CAREFUL WHEN SHARING THESE RESULTS - MAY CONTAIN YOUR SENSITIVE DATA
147
+ # logger.debug(loggable_response)
137
148
 
138
149
  if content_type == "application/xml":
139
150
  self.parent_srv._namespace.detect(server_response.content)
@@ -141,16 +152,16 @@ class Endpoint(object):
141
152
  return server_response
142
153
 
143
154
  def _check_status(self, server_response: "Response", url: Optional[str] = None):
144
- logger.debug("Response status: {}".format(server_response))
155
+ logger.debug(f"Response status: {server_response}")
145
156
  if not hasattr(server_response, "status_code"):
146
- raise EnvironmentError("Response is not a http response?")
157
+ raise OSError("Response is not a http response?")
147
158
  if server_response.status_code >= 500:
148
159
  raise InternalServerError(server_response, url)
149
160
  elif server_response.status_code not in Success_codes:
150
161
  try:
151
162
  if server_response.status_code == 401:
152
163
  # TODO: catch this in server.py and attempt to sign in again, in case it's a session expiry
153
- raise NotSignedInError(server_response.content, url)
164
+ raise FailedSignInError.from_response(server_response.content, self.parent_srv.namespace, url)
154
165
 
155
166
  raise ServerResponseError.from_response(server_response.content, self.parent_srv.namespace, url)
156
167
  except ParseError:
@@ -170,9 +181,9 @@ class Endpoint(object):
170
181
  # content-type is an octet-stream accomplishes the same goal without eagerly loading content.
171
182
  # This check is to determine if the response is a text response (xml or otherwise)
172
183
  # so that we do not attempt to log bytes and other binary data.
173
- loggable_response = "Content type `{}`".format(content_type)
184
+ loggable_response = f"Content type `{content_type}`"
174
185
  if content_type == "application/octet-stream":
175
- 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]"
176
187
  elif server_response.encoding and len(server_response.content) > 0:
177
188
  loggable_response = helpers.strings.redact_xml(server_response.content.decode(server_response.encoding))
178
189
  return loggable_response
@@ -232,7 +243,12 @@ class Endpoint(object):
232
243
  )
233
244
 
234
245
 
235
- def api(version):
246
+ E = TypeVar("E", bound="Endpoint")
247
+ P = ParamSpec("P")
248
+ R = TypeVar("R")
249
+
250
+
251
+ def api(version: str) -> Callable[[Callable[Concatenate[E, P], R]], Callable[Concatenate[E, P], R]]:
236
252
  """Annotate the minimum supported version for an endpoint.
237
253
 
238
254
  Checks the version on the server object and compares normalized versions.
@@ -251,9 +267,9 @@ def api(version):
251
267
  >>> ...
252
268
  """
253
269
 
254
- def _decorator(func):
270
+ def _decorator(func: Callable[Concatenate[E, P], R]) -> Callable[Concatenate[E, P], R]:
255
271
  @wraps(func)
256
- def wrapper(self, *args, **kwargs):
272
+ def wrapper(self: E, *args: P.args, **kwargs: P.kwargs) -> R:
257
273
  self.parent_srv.assert_at_least_version(version, self.__class__.__name__)
258
274
  return func(self, *args, **kwargs)
259
275
 
@@ -262,7 +278,7 @@ def api(version):
262
278
  return _decorator
263
279
 
264
280
 
265
- def parameter_added_in(**params):
281
+ def parameter_added_in(**params: str) -> Callable[[Callable[Concatenate[E, P], R]], Callable[Concatenate[E, P], R]]:
266
282
  """Annotate minimum versions for new parameters or request options on an endpoint.
267
283
 
268
284
  The api decorator documents when an endpoint was added, this decorator annotates
@@ -285,9 +301,9 @@ def parameter_added_in(**params):
285
301
  >>> ...
286
302
  """
287
303
 
288
- def _decorator(func):
304
+ def _decorator(func: Callable[Concatenate[E, P], R]) -> Callable[Concatenate[E, P], R]:
289
305
  @wraps(func)
290
- def wrapper(self, *args, **kwargs):
306
+ def wrapper(self: E, *args: P.args, **kwargs: P.kwargs) -> R:
291
307
  import warnings
292
308
 
293
309
  server_ver = Version(self.parent_srv.version or "0.0")
@@ -295,7 +311,7 @@ def parameter_added_in(**params):
295
311
  for p in params_to_check:
296
312
  min_ver = Version(str(params[p]))
297
313
  if server_ver < min_ver:
298
- 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}"
299
315
  warnings.warn(error)
300
316
  return func(self, *args, **kwargs)
301
317
 
@@ -335,5 +351,5 @@ class QuerysetEndpoint(Endpoint, Generic[T]):
335
351
  return queryset
336
352
 
337
353
  @abc.abstractmethod
338
- def get(self, request_options: RequestOptions) -> Tuple[List[T], PaginationItem]:
354
+ def get(self, request_options: Optional[RequestOptions] = None) -> tuple[list[T], PaginationItem]:
339
355
  raise NotImplementedError(f".get has not been implemented for {self.__class__.__qualname__}")
@@ -1,24 +1,31 @@
1
1
  from defusedxml.ElementTree import fromstring
2
- from typing import Optional
2
+ from typing import Mapping, Optional, TypeVar
3
+
4
+
5
+ def split_pascal_case(s: str) -> str:
6
+ return "".join([f" {c}" if c.isupper() else c for c in s]).strip()
3
7
 
4
8
 
5
9
  class TableauError(Exception):
6
10
  pass
7
11
 
8
12
 
9
- class ServerResponseError(TableauError):
10
- def __init__(self, code, summary, detail, url=None):
13
+ T = TypeVar("T")
14
+
15
+
16
+ class XMLError(TableauError):
17
+ def __init__(self, code: str, summary: str, detail: str, url: Optional[str] = None) -> None:
11
18
  self.code = code
12
19
  self.summary = summary
13
20
  self.detail = detail
14
21
  self.url = url
15
- super(ServerResponseError, self).__init__(str(self))
22
+ super().__init__(str(self))
16
23
 
17
24
  def __str__(self):
18
- return "\n\n\t{0}: {1}\n\t\t{2}".format(self.code, self.summary, self.detail)
25
+ return f"\n\n\t{self.code}: {self.summary}\n\t\t{self.detail}"
19
26
 
20
27
  @classmethod
21
- def from_response(cls, resp, ns, url=None):
28
+ def from_response(cls, resp, ns, url):
22
29
  # Check elements exist before .text
23
30
  parsed_response = fromstring(resp)
24
31
  try:
@@ -33,6 +40,10 @@ class ServerResponseError(TableauError):
33
40
  return error_response
34
41
 
35
42
 
43
+ class ServerResponseError(XMLError):
44
+ pass
45
+
46
+
36
47
  class InternalServerError(TableauError):
37
48
  def __init__(self, server_response, request_url: Optional[str] = None):
38
49
  self.code = server_response.status_code
@@ -40,7 +51,7 @@ class InternalServerError(TableauError):
40
51
  self.url = request_url or "server"
41
52
 
42
53
  def __str__(self):
43
- return "\n\nInternal error {0} at {1}\n{2}".format(self.code, self.url, self.content)
54
+ return f"\n\nInternal error {self.code} at {self.url}\n{self.content}"
44
55
 
45
56
 
46
57
  class MissingRequiredFieldError(TableauError):
@@ -51,6 +62,11 @@ class NotSignedInError(TableauError):
51
62
  pass
52
63
 
53
64
 
65
+ class FailedSignInError(XMLError, NotSignedInError):
66
+ def __str__(self):
67
+ return f"{split_pascal_case(self.__class__.__name__)}: {super().__str__()}"
68
+
69
+
54
70
  class ItemTypeNotAllowed(TableauError):
55
71
  pass
56
72
 
@@ -20,13 +20,13 @@ from typing import Optional
20
20
  class Favorites(Endpoint):
21
21
  @property
22
22
  def baseurl(self) -> str:
23
- return "{0}/sites/{1}/favorites".format(self.parent_srv.baseurl, self.parent_srv.site_id)
23
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/favorites"
24
24
 
25
25
  # Gets all favorites
26
26
  @api(version="2.5")
27
27
  def get(self, user_item: UserItem, req_options: Optional[RequestOptions] = None) -> None:
28
- logger.info("Querying all favorites for user {0}".format(user_item.name))
29
- url = "{0}/{1}".format(self.baseurl, user_item.id)
28
+ logger.info(f"Querying all favorites for user {user_item.name}")
29
+ url = f"{self.baseurl}/{user_item.id}"
30
30
  server_response = self.get_request(url, req_options)
31
31
  user_item._favorites = FavoriteItem.from_response(server_response.content, self.parent_srv.namespace)
32
32
 
@@ -34,53 +34,53 @@ class Favorites(Endpoint):
34
34
 
35
35
  @api(version="3.15")
36
36
  def add_favorite(self, user_item: UserItem, content_type: str, item: TableauItem) -> "Response":
37
- url = "{0}/{1}".format(self.baseurl, user_item.id)
37
+ url = f"{self.baseurl}/{user_item.id}"
38
38
  add_req = RequestFactory.Favorite.add_request(item.id, content_type, item.name)
39
39
  server_response = self.put_request(url, add_req)
40
- logger.info("Favorited {0} for user (ID: {1})".format(item.name, user_item.id))
40
+ logger.info(f"Favorited {item.name} for user (ID: {user_item.id})")
41
41
  return server_response
42
42
 
43
43
  @api(version="2.0")
44
44
  def add_favorite_workbook(self, user_item: UserItem, workbook_item: WorkbookItem) -> None:
45
- url = "{0}/{1}".format(self.baseurl, user_item.id)
45
+ url = f"{self.baseurl}/{user_item.id}"
46
46
  add_req = RequestFactory.Favorite.add_workbook_req(workbook_item.id, workbook_item.name)
47
47
  server_response = self.put_request(url, add_req)
48
- logger.info("Favorited {0} for user (ID: {1})".format(workbook_item.name, user_item.id))
48
+ logger.info(f"Favorited {workbook_item.name} for user (ID: {user_item.id})")
49
49
 
50
50
  @api(version="2.0")
51
51
  def add_favorite_view(self, user_item: UserItem, view_item: ViewItem) -> None:
52
- url = "{0}/{1}".format(self.baseurl, user_item.id)
52
+ url = f"{self.baseurl}/{user_item.id}"
53
53
  add_req = RequestFactory.Favorite.add_view_req(view_item.id, view_item.name)
54
54
  server_response = self.put_request(url, add_req)
55
- logger.info("Favorited {0} for user (ID: {1})".format(view_item.name, user_item.id))
55
+ logger.info(f"Favorited {view_item.name} for user (ID: {user_item.id})")
56
56
 
57
57
  @api(version="2.3")
58
58
  def add_favorite_datasource(self, user_item: UserItem, datasource_item: DatasourceItem) -> None:
59
- url = "{0}/{1}".format(self.baseurl, user_item.id)
59
+ url = f"{self.baseurl}/{user_item.id}"
60
60
  add_req = RequestFactory.Favorite.add_datasource_req(datasource_item.id, datasource_item.name)
61
61
  server_response = self.put_request(url, add_req)
62
- logger.info("Favorited {0} for user (ID: {1})".format(datasource_item.name, user_item.id))
62
+ logger.info(f"Favorited {datasource_item.name} for user (ID: {user_item.id})")
63
63
 
64
64
  @api(version="3.1")
65
65
  def add_favorite_project(self, user_item: UserItem, project_item: ProjectItem) -> None:
66
- url = "{0}/{1}".format(self.baseurl, user_item.id)
66
+ url = f"{self.baseurl}/{user_item.id}"
67
67
  add_req = RequestFactory.Favorite.add_project_req(project_item.id, project_item.name)
68
68
  server_response = self.put_request(url, add_req)
69
- logger.info("Favorited {0} for user (ID: {1})".format(project_item.name, user_item.id))
69
+ logger.info(f"Favorited {project_item.name} for user (ID: {user_item.id})")
70
70
 
71
71
  @api(version="3.3")
72
72
  def add_favorite_flow(self, user_item: UserItem, flow_item: FlowItem) -> None:
73
- url = "{0}/{1}".format(self.baseurl, user_item.id)
73
+ url = f"{self.baseurl}/{user_item.id}"
74
74
  add_req = RequestFactory.Favorite.add_flow_req(flow_item.id, flow_item.name)
75
75
  server_response = self.put_request(url, add_req)
76
- logger.info("Favorited {0} for user (ID: {1})".format(flow_item.name, user_item.id))
76
+ logger.info(f"Favorited {flow_item.name} for user (ID: {user_item.id})")
77
77
 
78
78
  @api(version="3.3")
79
79
  def add_favorite_metric(self, user_item: UserItem, metric_item: MetricItem) -> None:
80
- url = "{0}/{1}".format(self.baseurl, user_item.id)
80
+ url = f"{self.baseurl}/{user_item.id}"
81
81
  add_req = RequestFactory.Favorite.add_request(metric_item.id, Resource.Metric, metric_item.name)
82
82
  server_response = self.put_request(url, add_req)
83
- logger.info("Favorited metric {0} for user (ID: {1})".format(metric_item.name, user_item.id))
83
+ logger.info(f"Favorited metric {metric_item.name} for user (ID: {user_item.id})")
84
84
 
85
85
  # ------- delete from favorites
86
86
  # Response:
@@ -94,42 +94,42 @@ class Favorites(Endpoint):
94
94
 
95
95
  @api(version="3.15")
96
96
  def delete_favorite(self, user_item: UserItem, content_type: Resource, item: TableauItem) -> None:
97
- url = "{0}/{1}/{2}/{3}".format(self.baseurl, user_item.id, content_type, item.id)
98
- logger.info("Removing favorite {0}({1}) for user (ID: {2})".format(content_type, item.id, user_item.id))
97
+ url = f"{self.baseurl}/{user_item.id}/{content_type}/{item.id}"
98
+ logger.info(f"Removing favorite {content_type}({item.id}) for user (ID: {user_item.id})")
99
99
  self.delete_request(url)
100
100
 
101
101
  @api(version="2.0")
102
102
  def delete_favorite_workbook(self, user_item: UserItem, workbook_item: WorkbookItem) -> None:
103
- url = "{0}/{1}/workbooks/{2}".format(self.baseurl, user_item.id, workbook_item.id)
104
- logger.info("Removing favorite workbook {0} for user (ID: {1})".format(workbook_item.id, user_item.id))
103
+ url = f"{self.baseurl}/{user_item.id}/workbooks/{workbook_item.id}"
104
+ logger.info(f"Removing favorite workbook {workbook_item.id} for user (ID: {user_item.id})")
105
105
  self.delete_request(url)
106
106
 
107
107
  @api(version="2.0")
108
108
  def delete_favorite_view(self, user_item: UserItem, view_item: ViewItem) -> None:
109
- url = "{0}/{1}/views/{2}".format(self.baseurl, user_item.id, view_item.id)
110
- logger.info("Removing favorite view {0} for user (ID: {1})".format(view_item.id, user_item.id))
109
+ url = f"{self.baseurl}/{user_item.id}/views/{view_item.id}"
110
+ logger.info(f"Removing favorite view {view_item.id} for user (ID: {user_item.id})")
111
111
  self.delete_request(url)
112
112
 
113
113
  @api(version="2.3")
114
114
  def delete_favorite_datasource(self, user_item: UserItem, datasource_item: DatasourceItem) -> None:
115
- url = "{0}/{1}/datasources/{2}".format(self.baseurl, user_item.id, datasource_item.id)
116
- logger.info("Removing favorite {0} for user (ID: {1})".format(datasource_item.id, user_item.id))
115
+ url = f"{self.baseurl}/{user_item.id}/datasources/{datasource_item.id}"
116
+ logger.info(f"Removing favorite {datasource_item.id} for user (ID: {user_item.id})")
117
117
  self.delete_request(url)
118
118
 
119
119
  @api(version="3.1")
120
120
  def delete_favorite_project(self, user_item: UserItem, project_item: ProjectItem) -> None:
121
- url = "{0}/{1}/projects/{2}".format(self.baseurl, user_item.id, project_item.id)
122
- logger.info("Removing favorite project {0} for user (ID: {1})".format(project_item.id, user_item.id))
121
+ url = f"{self.baseurl}/{user_item.id}/projects/{project_item.id}"
122
+ logger.info(f"Removing favorite project {project_item.id} for user (ID: {user_item.id})")
123
123
  self.delete_request(url)
124
124
 
125
125
  @api(version="3.3")
126
126
  def delete_favorite_flow(self, user_item: UserItem, flow_item: FlowItem) -> None:
127
- url = "{0}/{1}/flows/{2}".format(self.baseurl, user_item.id, flow_item.id)
128
- logger.info("Removing favorite flow {0} for user (ID: {1})".format(flow_item.id, user_item.id))
127
+ url = f"{self.baseurl}/{user_item.id}/flows/{flow_item.id}"
128
+ logger.info(f"Removing favorite flow {flow_item.id} for user (ID: {user_item.id})")
129
129
  self.delete_request(url)
130
130
 
131
131
  @api(version="3.15")
132
132
  def delete_favorite_metric(self, user_item: UserItem, metric_item: MetricItem) -> None:
133
- url = "{0}/{1}/metrics/{2}".format(self.baseurl, user_item.id, metric_item.id)
134
- logger.info("Removing favorite metric {0} for user (ID: {1})".format(metric_item.id, user_item.id))
133
+ url = f"{self.baseurl}/{user_item.id}/metrics/{metric_item.id}"
134
+ logger.info(f"Removing favorite metric {metric_item.id} for user (ID: {user_item.id})")
135
135
  self.delete_request(url)
@@ -2,18 +2,18 @@ from .endpoint import Endpoint, api
2
2
  from tableauserverclient import datetime_helpers as datetime
3
3
  from tableauserverclient.helpers.logging import logger
4
4
 
5
- from tableauserverclient.config import BYTES_PER_MB, CHUNK_SIZE_MB
5
+ from tableauserverclient.config import BYTES_PER_MB, config
6
6
  from tableauserverclient.models import FileuploadItem
7
7
  from tableauserverclient.server import RequestFactory
8
8
 
9
9
 
10
10
  class Fileuploads(Endpoint):
11
11
  def __init__(self, parent_srv):
12
- super(Fileuploads, self).__init__(parent_srv)
12
+ super().__init__(parent_srv)
13
13
 
14
14
  @property
15
15
  def baseurl(self):
16
- return "{0}/sites/{1}/fileUploads".format(self.parent_srv.baseurl, self.parent_srv.site_id)
16
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/fileUploads"
17
17
 
18
18
  @api(version="2.0")
19
19
  def initiate(self):
@@ -21,14 +21,14 @@ class Fileuploads(Endpoint):
21
21
  server_response = self.post_request(url, "")
22
22
  fileupload_item = FileuploadItem.from_response(server_response.content, self.parent_srv.namespace)
23
23
  upload_id = fileupload_item.upload_session_id
24
- logger.info("Initiated file upload session (ID: {0})".format(upload_id))
24
+ logger.info(f"Initiated file upload session (ID: {upload_id})")
25
25
  return upload_id
26
26
 
27
27
  @api(version="2.0")
28
28
  def append(self, upload_id, data, content_type):
29
- url = "{0}/{1}".format(self.baseurl, upload_id)
29
+ url = f"{self.baseurl}/{upload_id}"
30
30
  server_response = self.put_request(url, data, content_type)
31
- logger.info("Uploading a chunk to session (ID: {0})".format(upload_id))
31
+ logger.info(f"Uploading a chunk to session (ID: {upload_id})")
32
32
  return FileuploadItem.from_response(server_response.content, self.parent_srv.namespace)
33
33
 
34
34
  def _read_chunks(self, file):
@@ -41,7 +41,7 @@ class Fileuploads(Endpoint):
41
41
 
42
42
  try:
43
43
  while True:
44
- chunked_content = file_content.read(CHUNK_SIZE_MB * BYTES_PER_MB)
44
+ chunked_content = file_content.read(config.CHUNK_SIZE_MB * BYTES_PER_MB)
45
45
  if not chunked_content:
46
46
  break
47
47
  yield chunked_content
@@ -52,12 +52,10 @@ class Fileuploads(Endpoint):
52
52
  def upload(self, file):
53
53
  upload_id = self.initiate()
54
54
  for chunk in self._read_chunks(file):
55
- logger.debug("{} processing chunk...".format(datetime.timestamp()))
55
+ logger.debug(f"{datetime.timestamp()} processing chunk...")
56
56
  request, content_type = RequestFactory.Fileupload.chunk_req(chunk)
57
- logger.debug("{} created chunk request".format(datetime.timestamp()))
57
+ logger.debug(f"{datetime.timestamp()} created chunk request")
58
58
  fileupload_item = self.append(upload_id, request, content_type)
59
- logger.info(
60
- "\t{0} Published {1}MB".format(datetime.timestamp(), (fileupload_item.file_size / BYTES_PER_MB))
61
- )
62
- logger.info("File upload finished (ID: {0})".format(upload_id))
59
+ logger.info(f"\t{datetime.timestamp()} Published {(fileupload_item.file_size / BYTES_PER_MB)}MB")
60
+ logger.info(f"File upload finished (ID: {upload_id})")
63
61
  return upload_id
@@ -1,36 +1,39 @@
1
1
  import logging
2
- from typing import List, Optional, Tuple, TYPE_CHECKING
2
+ from typing import Optional, TYPE_CHECKING, Union
3
3
 
4
- from .endpoint import QuerysetEndpoint, api
5
- from .exceptions import FlowRunFailedException, FlowRunCancelledException
6
- from tableauserverclient.models import FlowRunItem, PaginationItem
4
+ from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
5
+ from tableauserverclient.server.endpoint.exceptions import FlowRunFailedException, FlowRunCancelledException
6
+ from tableauserverclient.models import FlowRunItem
7
7
  from tableauserverclient.exponential_backoff import ExponentialBackoffTimer
8
8
 
9
9
  from tableauserverclient.helpers.logging import logger
10
+ from tableauserverclient.server.query import QuerySet
10
11
 
11
12
  if TYPE_CHECKING:
12
- from ..server import Server
13
- from ..request_options import RequestOptions
13
+ from tableauserverclient.server.server import Server
14
+ from tableauserverclient.server.request_options import RequestOptions
14
15
 
15
16
 
16
17
  class FlowRuns(QuerysetEndpoint[FlowRunItem]):
17
18
  def __init__(self, parent_srv: "Server") -> None:
18
- super(FlowRuns, self).__init__(parent_srv)
19
+ super().__init__(parent_srv)
19
20
  return None
20
21
 
21
22
  @property
22
23
  def baseurl(self) -> str:
23
- return "{0}/sites/{1}/flows/runs".format(self.parent_srv.baseurl, self.parent_srv.site_id)
24
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/flows/runs"
24
25
 
25
26
  # Get all flows
26
27
  @api(version="3.10")
27
- def get(self, req_options: Optional["RequestOptions"] = None) -> Tuple[List[FlowRunItem], PaginationItem]:
28
+ # QuerysetEndpoint expects a PaginationItem to be returned, but FlowRuns
29
+ # does not return a PaginationItem. Suppressing the mypy error because the
30
+ # changes to the QuerySet class should permit this to function regardless.
31
+ def get(self, req_options: Optional["RequestOptions"] = None) -> list[FlowRunItem]: # type: ignore[override]
28
32
  logger.info("Querying all flow runs on site")
29
33
  url = self.baseurl
30
34
  server_response = self.get_request(url, req_options)
31
- pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
32
35
  all_flow_run_items = FlowRunItem.from_response(server_response.content, self.parent_srv.namespace)
33
- return all_flow_run_items, pagination_item
36
+ return all_flow_run_items
34
37
 
35
38
  # Get 1 flow by id
36
39
  @api(version="3.10")
@@ -38,21 +41,21 @@ class FlowRuns(QuerysetEndpoint[FlowRunItem]):
38
41
  if not flow_run_id:
39
42
  error = "Flow ID undefined."
40
43
  raise ValueError(error)
41
- logger.info("Querying single flow (ID: {0})".format(flow_run_id))
42
- url = "{0}/{1}".format(self.baseurl, flow_run_id)
44
+ logger.info(f"Querying single flow (ID: {flow_run_id})")
45
+ url = f"{self.baseurl}/{flow_run_id}"
43
46
  server_response = self.get_request(url)
44
47
  return FlowRunItem.from_response(server_response.content, self.parent_srv.namespace)[0]
45
48
 
46
49
  # Cancel 1 flow run by id
47
50
  @api(version="3.10")
48
- def cancel(self, flow_run_id: str) -> None:
51
+ def cancel(self, flow_run_id: Union[str, FlowRunItem]) -> None:
49
52
  if not flow_run_id:
50
53
  error = "Flow ID undefined."
51
54
  raise ValueError(error)
52
55
  id_ = getattr(flow_run_id, "id", flow_run_id)
53
- url = "{0}/{1}".format(self.baseurl, id_)
56
+ url = f"{self.baseurl}/{id_}"
54
57
  self.put_request(url)
55
- logger.info("Deleted single flow (ID: {0})".format(id_))
58
+ logger.info(f"Deleted single flow (ID: {id_})")
56
59
 
57
60
  @api(version="3.10")
58
61
  def wait_for_job(self, flow_run_id: str, *, timeout: Optional[int] = None) -> FlowRunItem:
@@ -68,7 +71,7 @@ class FlowRuns(QuerysetEndpoint[FlowRunItem]):
68
71
  flow_run = self.get_by_id(flow_run_id)
69
72
  logger.debug(f"\tFlowRun {flow_run_id} progress={flow_run.progress}")
70
73
 
71
- logger.info("FlowRun {} Completed: Status: {}".format(flow_run_id, flow_run.status))
74
+ logger.info(f"FlowRun {flow_run_id} Completed: Status: {flow_run.status}")
72
75
 
73
76
  if flow_run.status == "Success":
74
77
  return flow_run
@@ -78,3 +81,42 @@ class FlowRuns(QuerysetEndpoint[FlowRunItem]):
78
81
  raise FlowRunCancelledException(flow_run)
79
82
  else:
80
83
  raise AssertionError("Unexpected status in flow_run", flow_run)
84
+
85
+ def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySet[FlowRunItem]:
86
+ """
87
+ Queries the Tableau Server for items using the specified filters. Page
88
+ size can be specified to limit the number of items returned in a single
89
+ request. If not specified, the default page size is 100. Page size can
90
+ be an integer between 1 and 1000.
91
+
92
+ No positional arguments are allowed. All filters must be specified as
93
+ keyword arguments. If you use the equality operator, you can specify it
94
+ through <field_name>=<value>. If you want to use a different operator,
95
+ you can specify it through <field_name>__<operator>=<value>. Field
96
+ names can either be in snake_case or camelCase.
97
+
98
+ This endpoint supports the following fields and operators:
99
+
100
+
101
+ complete_at=...
102
+ complete_at__gt=...
103
+ complete_at__gte=...
104
+ complete_at__lt=...
105
+ complete_at__lte=...
106
+ flow_id=...
107
+ flow_id__in=...
108
+ progress=...
109
+ progress__gt=...
110
+ progress__gte=...
111
+ progress__lt=...
112
+ progress__lte=...
113
+ started_at=...
114
+ started_at__gt=...
115
+ started_at__gte=...
116
+ started_at__lt=...
117
+ started_at__lte=...
118
+ user_id=...
119
+ user_id__in=...
120
+ """
121
+
122
+ return super().filter(*invalid, page_size=page_size, **kwargs)
@@ -1,5 +1,5 @@
1
1
  import logging
2
- from typing import List, Optional, Tuple, TYPE_CHECKING
2
+ from typing import TYPE_CHECKING
3
3
 
4
4
  from tableauserverclient.server.endpoint.endpoint import Endpoint, api
5
5
  from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
15
15
  class FlowTasks(Endpoint):
16
16
  @property
17
17
  def baseurl(self) -> str:
18
- return "{0}/sites/{1}/tasks/flows".format(self.parent_srv.baseurl, self.parent_srv.site_id)
18
+ return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tasks/flows"
19
19
 
20
20
  @api(version="3.22")
21
21
  def create(self, flow_item: TaskItem) -> TaskItem: