tableauserverclient 0.33__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 +28 -22
- tableauserverclient/_version.py +3 -3
- tableauserverclient/config.py +5 -3
- tableauserverclient/models/column_item.py +1 -1
- tableauserverclient/models/connection_credentials.py +1 -1
- tableauserverclient/models/connection_item.py +6 -6
- 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 +3 -3
- 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 +6 -6
- tableauserverclient/models/flow_run_item.py +3 -3
- tableauserverclient/models/group_item.py +4 -4
- tableauserverclient/models/groupset_item.py +4 -4
- tableauserverclient/models/interval_item.py +9 -9
- tableauserverclient/models/job_item.py +8 -8
- tableauserverclient/models/linked_tasks_item.py +5 -5
- tableauserverclient/models/metric_item.py +5 -5
- tableauserverclient/models/pagination_item.py +1 -1
- tableauserverclient/models/permissions_item.py +12 -10
- 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 +2 -2
- 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 +6 -5
- 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/auth_endpoint.py +65 -8
- tableauserverclient/server/endpoint/custom_views_endpoint.py +62 -18
- 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 +13 -12
- tableauserverclient/server/endpoint/datasources_endpoint.py +49 -54
- tableauserverclient/server/endpoint/default_permissions_endpoint.py +19 -18
- tableauserverclient/server/endpoint/dqw_endpoint.py +9 -9
- tableauserverclient/server/endpoint/endpoint.py +19 -21
- tableauserverclient/server/endpoint/exceptions.py +23 -7
- tableauserverclient/server/endpoint/favorites_endpoint.py +31 -31
- tableauserverclient/server/endpoint/fileuploads_endpoint.py +9 -11
- tableauserverclient/server/endpoint/flow_runs_endpoint.py +15 -13
- tableauserverclient/server/endpoint/flow_task_endpoint.py +2 -2
- tableauserverclient/server/endpoint/flows_endpoint.py +30 -29
- tableauserverclient/server/endpoint/groups_endpoint.py +18 -17
- tableauserverclient/server/endpoint/groupsets_endpoint.py +2 -2
- tableauserverclient/server/endpoint/jobs_endpoint.py +7 -7
- tableauserverclient/server/endpoint/linked_tasks_endpoint.py +2 -2
- 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 +81 -30
- tableauserverclient/server/endpoint/resource_tagger.py +14 -13
- 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 +15 -14
- tableauserverclient/server/endpoint/tasks_endpoint.py +8 -8
- tableauserverclient/server/endpoint/users_endpoint.py +366 -19
- tableauserverclient/server/endpoint/views_endpoint.py +19 -18
- tableauserverclient/server/endpoint/virtual_connections_endpoint.py +6 -5
- tableauserverclient/server/endpoint/webhooks_endpoint.py +11 -11
- tableauserverclient/server/endpoint/workbooks_endpoint.py +647 -61
- tableauserverclient/server/filter.py +2 -2
- tableauserverclient/server/pager.py +5 -6
- tableauserverclient/server/query.py +68 -19
- tableauserverclient/server/request_factory.py +37 -36
- tableauserverclient/server/request_options.py +123 -145
- tableauserverclient/server/server.py +65 -9
- tableauserverclient/server/sort.py +2 -2
- {tableauserverclient-0.33.dist-info → tableauserverclient-0.34.dist-info}/METADATA +6 -6
- tableauserverclient-0.34.dist-info/RECORD +106 -0
- {tableauserverclient-0.33.dist-info → tableauserverclient-0.34.dist-info}/WHEEL +1 -1
- tableauserverclient-0.33.dist-info/RECORD +0 -106
- {tableauserverclient-0.33.dist-info → tableauserverclient-0.34.dist-info}/LICENSE +0 -0
- {tableauserverclient-0.33.dist-info → tableauserverclient-0.34.dist-info}/LICENSE.versioneer +0 -0
- {tableauserverclient-0.33.dist-info → tableauserverclient-0.34.dist-info}/top_level.txt +0 -0
|
@@ -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) ->
|
|
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/{}"
|
|
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 {}"
|
|
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"
|
|
94
|
+
logger.debug(f"[{datetime.timestamp()}] Call finished")
|
|
97
95
|
except Exception as e:
|
|
98
|
-
logger.debug("Error making request to server: {}"
|
|
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[
|
|
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: {}"
|
|
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 {}"
|
|
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"
|
|
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"
|
|
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 {
|
|
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: {}"
|
|
155
|
+
logger.debug(f"Response status: {server_response}")
|
|
158
156
|
if not hasattr(server_response, "status_code"):
|
|
159
|
-
raise
|
|
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
|
|
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 `{}`"
|
|
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]"
|
|
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 {}"
|
|
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) ->
|
|
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__}")
|
|
@@ -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)
|
|
@@ -9,11 +9,11 @@ from tableauserverclient.server import RequestFactory
|
|
|
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):
|
|
@@ -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,9 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Optional, TYPE_CHECKING, Union
|
|
3
3
|
|
|
4
4
|
from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api
|
|
5
5
|
from tableauserverclient.server.endpoint.exceptions import FlowRunFailedException, FlowRunCancelledException
|
|
6
|
-
from tableauserverclient.models import FlowRunItem
|
|
6
|
+
from tableauserverclient.models import FlowRunItem
|
|
7
7
|
from tableauserverclient.exponential_backoff import ExponentialBackoffTimer
|
|
8
8
|
|
|
9
9
|
from tableauserverclient.helpers.logging import logger
|
|
@@ -16,22 +16,24 @@ if TYPE_CHECKING:
|
|
|
16
16
|
|
|
17
17
|
class FlowRuns(QuerysetEndpoint[FlowRunItem]):
|
|
18
18
|
def __init__(self, parent_srv: "Server") -> None:
|
|
19
|
-
super(
|
|
19
|
+
super().__init__(parent_srv)
|
|
20
20
|
return None
|
|
21
21
|
|
|
22
22
|
@property
|
|
23
23
|
def baseurl(self) -> str:
|
|
24
|
-
return "{
|
|
24
|
+
return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/flows/runs"
|
|
25
25
|
|
|
26
26
|
# Get all flows
|
|
27
27
|
@api(version="3.10")
|
|
28
|
-
|
|
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]
|
|
29
32
|
logger.info("Querying all flow runs on site")
|
|
30
33
|
url = self.baseurl
|
|
31
34
|
server_response = self.get_request(url, req_options)
|
|
32
|
-
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
33
35
|
all_flow_run_items = FlowRunItem.from_response(server_response.content, self.parent_srv.namespace)
|
|
34
|
-
return all_flow_run_items
|
|
36
|
+
return all_flow_run_items
|
|
35
37
|
|
|
36
38
|
# Get 1 flow by id
|
|
37
39
|
@api(version="3.10")
|
|
@@ -39,21 +41,21 @@ class FlowRuns(QuerysetEndpoint[FlowRunItem]):
|
|
|
39
41
|
if not flow_run_id:
|
|
40
42
|
error = "Flow ID undefined."
|
|
41
43
|
raise ValueError(error)
|
|
42
|
-
logger.info("Querying single flow (ID: {
|
|
43
|
-
url = "{
|
|
44
|
+
logger.info(f"Querying single flow (ID: {flow_run_id})")
|
|
45
|
+
url = f"{self.baseurl}/{flow_run_id}"
|
|
44
46
|
server_response = self.get_request(url)
|
|
45
47
|
return FlowRunItem.from_response(server_response.content, self.parent_srv.namespace)[0]
|
|
46
48
|
|
|
47
49
|
# Cancel 1 flow run by id
|
|
48
50
|
@api(version="3.10")
|
|
49
|
-
def cancel(self, flow_run_id: str) -> None:
|
|
51
|
+
def cancel(self, flow_run_id: Union[str, FlowRunItem]) -> None:
|
|
50
52
|
if not flow_run_id:
|
|
51
53
|
error = "Flow ID undefined."
|
|
52
54
|
raise ValueError(error)
|
|
53
55
|
id_ = getattr(flow_run_id, "id", flow_run_id)
|
|
54
|
-
url = "{
|
|
56
|
+
url = f"{self.baseurl}/{id_}"
|
|
55
57
|
self.put_request(url)
|
|
56
|
-
logger.info("Deleted single flow (ID: {
|
|
58
|
+
logger.info(f"Deleted single flow (ID: {id_})")
|
|
57
59
|
|
|
58
60
|
@api(version="3.10")
|
|
59
61
|
def wait_for_job(self, flow_run_id: str, *, timeout: Optional[int] = None) -> FlowRunItem:
|
|
@@ -69,7 +71,7 @@ class FlowRuns(QuerysetEndpoint[FlowRunItem]):
|
|
|
69
71
|
flow_run = self.get_by_id(flow_run_id)
|
|
70
72
|
logger.debug(f"\tFlowRun {flow_run_id} progress={flow_run.progress}")
|
|
71
73
|
|
|
72
|
-
logger.info("FlowRun {} Completed: Status: {
|
|
74
|
+
logger.info(f"FlowRun {flow_run_id} Completed: Status: {flow_run.status}")
|
|
73
75
|
|
|
74
76
|
if flow_run.status == "Success":
|
|
75
77
|
return flow_run
|
|
@@ -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:
|