tableauserverclient 0.32__py3-none-any.whl → 0.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tableauserverclient/__init__.py +34 -18
- tableauserverclient/_version.py +3 -3
- tableauserverclient/config.py +20 -6
- tableauserverclient/models/__init__.py +12 -0
- tableauserverclient/models/column_item.py +1 -1
- tableauserverclient/models/connection_credentials.py +1 -1
- tableauserverclient/models/connection_item.py +10 -8
- tableauserverclient/models/custom_view_item.py +29 -6
- tableauserverclient/models/data_acceleration_report_item.py +2 -2
- tableauserverclient/models/data_alert_item.py +5 -5
- tableauserverclient/models/data_freshness_policy_item.py +6 -6
- tableauserverclient/models/database_item.py +8 -2
- tableauserverclient/models/datasource_item.py +10 -10
- tableauserverclient/models/dqw_item.py +1 -1
- tableauserverclient/models/favorites_item.py +5 -6
- tableauserverclient/models/fileupload_item.py +1 -1
- tableauserverclient/models/flow_item.py +12 -12
- tableauserverclient/models/flow_run_item.py +3 -3
- tableauserverclient/models/group_item.py +4 -4
- tableauserverclient/models/groupset_item.py +53 -0
- tableauserverclient/models/interval_item.py +36 -23
- tableauserverclient/models/job_item.py +26 -10
- tableauserverclient/models/linked_tasks_item.py +102 -0
- tableauserverclient/models/metric_item.py +5 -5
- tableauserverclient/models/pagination_item.py +1 -1
- tableauserverclient/models/permissions_item.py +19 -14
- tableauserverclient/models/project_item.py +35 -19
- tableauserverclient/models/property_decorators.py +12 -11
- tableauserverclient/models/reference_item.py +2 -2
- tableauserverclient/models/revision_item.py +3 -3
- tableauserverclient/models/schedule_item.py +2 -2
- tableauserverclient/models/server_info_item.py +26 -6
- tableauserverclient/models/site_item.py +69 -3
- tableauserverclient/models/subscription_item.py +3 -3
- tableauserverclient/models/table_item.py +1 -1
- tableauserverclient/models/tableau_auth.py +115 -5
- tableauserverclient/models/tableau_types.py +11 -9
- tableauserverclient/models/tag_item.py +3 -4
- tableauserverclient/models/task_item.py +4 -4
- tableauserverclient/models/user_item.py +47 -17
- tableauserverclient/models/view_item.py +11 -10
- tableauserverclient/models/virtual_connection_item.py +78 -0
- tableauserverclient/models/webhook_item.py +6 -6
- tableauserverclient/models/workbook_item.py +90 -12
- tableauserverclient/namespace.py +1 -1
- tableauserverclient/server/__init__.py +2 -1
- tableauserverclient/server/endpoint/__init__.py +8 -0
- tableauserverclient/server/endpoint/auth_endpoint.py +68 -11
- tableauserverclient/server/endpoint/custom_views_endpoint.py +124 -19
- tableauserverclient/server/endpoint/data_acceleration_report_endpoint.py +2 -2
- tableauserverclient/server/endpoint/data_alert_endpoint.py +14 -14
- tableauserverclient/server/endpoint/databases_endpoint.py +32 -17
- tableauserverclient/server/endpoint/datasources_endpoint.py +150 -59
- tableauserverclient/server/endpoint/default_permissions_endpoint.py +19 -18
- tableauserverclient/server/endpoint/dqw_endpoint.py +9 -9
- tableauserverclient/server/endpoint/endpoint.py +47 -31
- tableauserverclient/server/endpoint/exceptions.py +23 -7
- tableauserverclient/server/endpoint/favorites_endpoint.py +31 -31
- tableauserverclient/server/endpoint/fileuploads_endpoint.py +11 -13
- tableauserverclient/server/endpoint/flow_runs_endpoint.py +59 -17
- tableauserverclient/server/endpoint/flow_task_endpoint.py +2 -2
- tableauserverclient/server/endpoint/flows_endpoint.py +73 -35
- tableauserverclient/server/endpoint/groups_endpoint.py +96 -27
- tableauserverclient/server/endpoint/groupsets_endpoint.py +127 -0
- tableauserverclient/server/endpoint/jobs_endpoint.py +79 -12
- tableauserverclient/server/endpoint/linked_tasks_endpoint.py +45 -0
- tableauserverclient/server/endpoint/metadata_endpoint.py +2 -2
- tableauserverclient/server/endpoint/metrics_endpoint.py +10 -10
- tableauserverclient/server/endpoint/permissions_endpoint.py +13 -15
- tableauserverclient/server/endpoint/projects_endpoint.py +124 -30
- tableauserverclient/server/endpoint/resource_tagger.py +139 -6
- tableauserverclient/server/endpoint/schedules_endpoint.py +17 -18
- tableauserverclient/server/endpoint/server_info_endpoint.py +40 -5
- tableauserverclient/server/endpoint/sites_endpoint.py +282 -17
- tableauserverclient/server/endpoint/subscriptions_endpoint.py +10 -10
- tableauserverclient/server/endpoint/tables_endpoint.py +33 -19
- tableauserverclient/server/endpoint/tasks_endpoint.py +8 -8
- tableauserverclient/server/endpoint/users_endpoint.py +405 -19
- tableauserverclient/server/endpoint/views_endpoint.py +111 -25
- tableauserverclient/server/endpoint/virtual_connections_endpoint.py +174 -0
- tableauserverclient/server/endpoint/webhooks_endpoint.py +11 -11
- tableauserverclient/server/endpoint/workbooks_endpoint.py +735 -68
- tableauserverclient/server/filter.py +2 -2
- tableauserverclient/server/pager.py +8 -10
- tableauserverclient/server/query.py +70 -20
- tableauserverclient/server/request_factory.py +213 -41
- tableauserverclient/server/request_options.py +125 -145
- tableauserverclient/server/server.py +73 -9
- tableauserverclient/server/sort.py +2 -2
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/METADATA +17 -17
- tableauserverclient-0.34.dist-info/RECORD +106 -0
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/WHEEL +1 -1
- tableauserverclient-0.32.dist-info/RECORD +0 -100
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/LICENSE +0 -0
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/LICENSE.versioneer +0 -0
- {tableauserverclient-0.32.dist-info → tableauserverclient-0.34.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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) ->
|
|
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/{}"
|
|
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 {}"
|
|
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"
|
|
94
|
+
logger.debug(f"[{datetime.timestamp()}] Call finished")
|
|
86
95
|
except Exception as e:
|
|
87
|
-
logger.debug("Error making request to server: {}"
|
|
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[
|
|
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: {}"
|
|
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 {}"
|
|
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"
|
|
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"
|
|
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 {
|
|
136
|
-
#
|
|
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: {}"
|
|
155
|
+
logger.debug(f"Response status: {server_response}")
|
|
145
156
|
if not hasattr(server_response, "status_code"):
|
|
146
|
-
raise
|
|
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
|
|
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 `{}`"
|
|
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]"
|
|
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
|
-
|
|
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 {}"
|
|
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) ->
|
|
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
|
-
|
|
10
|
-
|
|
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(
|
|
22
|
+
super().__init__(str(self))
|
|
16
23
|
|
|
17
24
|
def __str__(self):
|
|
18
|
-
return "\n\n\t{
|
|
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
|
|
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 {
|
|
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 "{
|
|
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 {
|
|
29
|
-
url = "{
|
|
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 = "{
|
|
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 {
|
|
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 = "{
|
|
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 {
|
|
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 = "{
|
|
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 {
|
|
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 = "{
|
|
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 {
|
|
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 = "{
|
|
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 {
|
|
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 = "{
|
|
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 {
|
|
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 = "{
|
|
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 {
|
|
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 = "{
|
|
98
|
-
logger.info("Removing favorite {
|
|
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 = "{
|
|
104
|
-
logger.info("Removing favorite workbook {
|
|
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 = "{
|
|
110
|
-
logger.info("Removing favorite view {
|
|
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 = "{
|
|
116
|
-
logger.info("Removing favorite {
|
|
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 = "{
|
|
122
|
-
logger.info("Removing favorite project {
|
|
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 = "{
|
|
128
|
-
logger.info("Removing favorite flow {
|
|
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 = "{
|
|
134
|
-
logger.info("Removing favorite metric {
|
|
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,
|
|
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(
|
|
12
|
+
super().__init__(parent_srv)
|
|
13
13
|
|
|
14
14
|
@property
|
|
15
15
|
def baseurl(self):
|
|
16
|
-
return "{
|
|
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: {
|
|
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 = "{
|
|
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: {
|
|
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..."
|
|
55
|
+
logger.debug(f"{datetime.timestamp()} processing chunk...")
|
|
56
56
|
request, content_type = RequestFactory.Fileupload.chunk_req(chunk)
|
|
57
|
-
logger.debug("{} created chunk request"
|
|
57
|
+
logger.debug(f"{datetime.timestamp()} created chunk request")
|
|
58
58
|
fileupload_item = self.append(upload_id, request, content_type)
|
|
59
|
-
logger.info(
|
|
60
|
-
|
|
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
|
|
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
|
|
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
|
|
13
|
-
from
|
|
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(
|
|
19
|
+
super().__init__(parent_srv)
|
|
19
20
|
return None
|
|
20
21
|
|
|
21
22
|
@property
|
|
22
23
|
def baseurl(self) -> str:
|
|
23
|
-
return "{
|
|
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
|
-
|
|
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
|
|
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: {
|
|
42
|
-
url = "{
|
|
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 = "{
|
|
56
|
+
url = f"{self.baseurl}/{id_}"
|
|
54
57
|
self.put_request(url)
|
|
55
|
-
logger.info("Deleted single flow (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: {
|
|
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
|
|
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 "{
|
|
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:
|