Skip to content

Commit f66bd09

Browse files
jorwoodsbcantoni
andauthored
feat: support extensions api (#1672)
Adding support for the REST APIs which provide access to Tableau Extensions configuration. --------- Co-authored-by: Jordan Woods <[email protected]> Co-authored-by: Brian Cantoni <[email protected]>
1 parent 5146c09 commit f66bd09

File tree

12 files changed

+565
-2
lines changed

12 files changed

+565
-2
lines changed

tableauserverclient/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
DatabaseItem,
1414
DataFreshnessPolicyItem,
1515
DatasourceItem,
16+
ExtensionsServer,
17+
ExtensionsSiteSettings,
1618
FavoriteItem,
1719
FlowItem,
1820
FlowRunItem,
@@ -36,6 +38,7 @@
3638
ProjectItem,
3739
Resource,
3840
RevisionItem,
41+
SafeExtension,
3942
ScheduleItem,
4043
SiteAuthConfiguration,
4144
SiteOIDCConfiguration,
@@ -88,6 +91,8 @@
8891
"DEFAULT_NAMESPACE",
8992
"DQWItem",
9093
"ExcelRequestOptions",
94+
"ExtensionsServer",
95+
"ExtensionsSiteSettings",
9196
"FailedSignInError",
9297
"FavoriteItem",
9398
"FileuploadItem",
@@ -121,6 +126,7 @@
121126
"RequestOptions",
122127
"Resource",
123128
"RevisionItem",
129+
"SafeExtension",
124130
"ScheduleItem",
125131
"Server",
126132
"ServerInfoItem",

tableauserverclient/models/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from tableauserverclient.models.datasource_item import DatasourceItem
1111
from tableauserverclient.models.dqw_item import DQWItem
1212
from tableauserverclient.models.exceptions import UnpopulatedPropertyError
13+
from tableauserverclient.models.extensions_item import ExtensionsServer, ExtensionsSiteSettings, SafeExtension
1314
from tableauserverclient.models.favorites_item import FavoriteItem
1415
from tableauserverclient.models.fileupload_item import FileuploadItem
1516
from tableauserverclient.models.flow_item import FlowItem
@@ -113,4 +114,7 @@
113114
"LinkedTaskStepItem",
114115
"LinkedTaskFlowRunItem",
115116
"ExtractItem",
117+
"ExtensionsServer",
118+
"ExtensionsSiteSettings",
119+
"SafeExtension",
116120
]
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
from typing import overload
2+
from typing_extensions import Self
3+
4+
from defusedxml.ElementTree import fromstring
5+
6+
from tableauserverclient.models.property_decorators import property_is_boolean
7+
8+
9+
class ExtensionsServer:
10+
def __init__(self) -> None:
11+
self._enabled: bool | None = None
12+
self._block_list: list[str] | None = None
13+
14+
@property
15+
def enabled(self) -> bool | None:
16+
"""Indicates whether the extensions server is enabled."""
17+
return self._enabled
18+
19+
@enabled.setter
20+
@property_is_boolean
21+
def enabled(self, value: bool | None) -> None:
22+
self._enabled = value
23+
24+
@property
25+
def block_list(self) -> list[str] | None:
26+
"""List of blocked extensions."""
27+
return self._block_list
28+
29+
@block_list.setter
30+
def block_list(self, value: list[str] | None) -> None:
31+
self._block_list = value
32+
33+
@classmethod
34+
def from_response(cls: type[Self], response, ns) -> Self:
35+
xml = fromstring(response)
36+
obj = cls()
37+
element = xml.find(".//t:extensionsServerSettings", namespaces=ns)
38+
if element is None:
39+
raise ValueError("Missing extensionsServerSettings element in response")
40+
41+
if (enabled_element := element.find("./t:extensionsGloballyEnabled", namespaces=ns)) is not None:
42+
obj.enabled = string_to_bool(enabled_element.text)
43+
obj.block_list = [e.text for e in element.findall("./t:blockList", namespaces=ns) if e.text is not None]
44+
45+
return obj
46+
47+
48+
class SafeExtension:
49+
def __init__(
50+
self, url: str | None = None, full_data_allowed: bool | None = None, prompt_needed: bool | None = None
51+
) -> None:
52+
self.url = url
53+
self._full_data_allowed = full_data_allowed
54+
self._prompt_needed = prompt_needed
55+
56+
@property
57+
def full_data_allowed(self) -> bool | None:
58+
return self._full_data_allowed
59+
60+
@full_data_allowed.setter
61+
@property_is_boolean
62+
def full_data_allowed(self, value: bool | None) -> None:
63+
self._full_data_allowed = value
64+
65+
@property
66+
def prompt_needed(self) -> bool | None:
67+
return self._prompt_needed
68+
69+
@prompt_needed.setter
70+
@property_is_boolean
71+
def prompt_needed(self, value: bool | None) -> None:
72+
self._prompt_needed = value
73+
74+
75+
class ExtensionsSiteSettings:
76+
def __init__(self) -> None:
77+
self._enabled: bool | None = None
78+
self._use_default_setting: bool | None = None
79+
self.safe_list: list[SafeExtension] | None = None
80+
self._allow_trusted: bool | None = None
81+
self._include_tableau_built: bool | None = None
82+
self._include_partner_built: bool | None = None
83+
self._include_sandboxed: bool | None = None
84+
85+
@property
86+
def enabled(self) -> bool | None:
87+
return self._enabled
88+
89+
@enabled.setter
90+
@property_is_boolean
91+
def enabled(self, value: bool | None) -> None:
92+
self._enabled = value
93+
94+
@property
95+
def use_default_setting(self) -> bool | None:
96+
return self._use_default_setting
97+
98+
@use_default_setting.setter
99+
@property_is_boolean
100+
def use_default_setting(self, value: bool | None) -> None:
101+
self._use_default_setting = value
102+
103+
@property
104+
def allow_trusted(self) -> bool | None:
105+
return self._allow_trusted
106+
107+
@allow_trusted.setter
108+
@property_is_boolean
109+
def allow_trusted(self, value: bool | None) -> None:
110+
self._allow_trusted = value
111+
112+
@property
113+
def include_tableau_built(self) -> bool | None:
114+
return self._include_tableau_built
115+
116+
@include_tableau_built.setter
117+
@property_is_boolean
118+
def include_tableau_built(self, value: bool | None) -> None:
119+
self._include_tableau_built = value
120+
121+
@property
122+
def include_partner_built(self) -> bool | None:
123+
return self._include_partner_built
124+
125+
@include_partner_built.setter
126+
@property_is_boolean
127+
def include_partner_built(self, value: bool | None) -> None:
128+
self._include_partner_built = value
129+
130+
@property
131+
def include_sandboxed(self) -> bool | None:
132+
return self._include_sandboxed
133+
134+
@include_sandboxed.setter
135+
@property_is_boolean
136+
def include_sandboxed(self, value: bool | None) -> None:
137+
self._include_sandboxed = value
138+
139+
@classmethod
140+
def from_response(cls: type[Self], response, ns) -> Self:
141+
xml = fromstring(response)
142+
element = xml.find(".//t:extensionsSiteSettings", namespaces=ns)
143+
obj = cls()
144+
if element is None:
145+
raise ValueError("Missing extensionsSiteSettings element in response")
146+
147+
if (enabled_element := element.find("./t:extensionsEnabled", namespaces=ns)) is not None:
148+
obj.enabled = string_to_bool(enabled_element.text)
149+
if (default_settings_element := element.find("./t:useDefaultSetting", namespaces=ns)) is not None:
150+
obj.use_default_setting = string_to_bool(default_settings_element.text)
151+
if (allow_trusted_element := element.find("./t:allowTrusted", namespaces=ns)) is not None:
152+
obj.allow_trusted = string_to_bool(allow_trusted_element.text)
153+
if (include_tableau_built_element := element.find("./t:includeTableauBuilt", namespaces=ns)) is not None:
154+
obj.include_tableau_built = string_to_bool(include_tableau_built_element.text)
155+
if (include_partner_built_element := element.find("./t:includePartnerBuilt", namespaces=ns)) is not None:
156+
obj.include_partner_built = string_to_bool(include_partner_built_element.text)
157+
if (include_sandboxed_element := element.find("./t:includeSandboxed", namespaces=ns)) is not None:
158+
obj.include_sandboxed = string_to_bool(include_sandboxed_element.text)
159+
160+
safe_list = []
161+
for safe_extension_element in element.findall("./t:safeList", namespaces=ns):
162+
url = safe_extension_element.find("./t:url", namespaces=ns)
163+
full_data_allowed = safe_extension_element.find("./t:fullDataAllowed", namespaces=ns)
164+
prompt_needed = safe_extension_element.find("./t:promptNeeded", namespaces=ns)
165+
166+
safe_extension = SafeExtension(
167+
url=url.text if url is not None else None,
168+
full_data_allowed=string_to_bool(full_data_allowed.text) if full_data_allowed is not None else None,
169+
prompt_needed=string_to_bool(prompt_needed.text) if prompt_needed is not None else None,
170+
)
171+
safe_list.append(safe_extension)
172+
173+
obj.safe_list = safe_list
174+
return obj
175+
176+
177+
@overload
178+
def string_to_bool(s: str) -> bool: ...
179+
180+
181+
@overload
182+
def string_to_bool(s: None) -> None: ...
183+
184+
185+
def string_to_bool(s):
186+
return s.lower() == "true" if s is not None else None

tableauserverclient/models/property_decorators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime
22
import re
33
from functools import wraps
4-
from typing import Any, Optional
4+
from typing import Any
55
from collections.abc import Container
66

77
from tableauserverclient.datetime_helpers import parse_datetime
@@ -67,7 +67,7 @@ def wrapper(self, value):
6767
return wrapper
6868

6969

70-
def property_is_int(range: tuple[int, int], allowed: Optional[Container[Any]] = None):
70+
def property_is_int(range: tuple[int, int], allowed: Container[Any] | None = None):
7171
"""Takes a range of ints and a list of exemptions to check against
7272
when setting a property on a model. The range is a tuple of (min, max) and the
7373
allowed list (empty by default) allows values outside that range.

tableauserverclient/server/endpoint/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from tableauserverclient.server.endpoint.datasources_endpoint import Datasources
77
from tableauserverclient.server.endpoint.endpoint import Endpoint, QuerysetEndpoint
88
from tableauserverclient.server.endpoint.exceptions import ServerResponseError, MissingRequiredFieldError
9+
from tableauserverclient.server.endpoint.extensions_endpoint import Extensions
910
from tableauserverclient.server.endpoint.favorites_endpoint import Favorites
1011
from tableauserverclient.server.endpoint.fileuploads_endpoint import Fileuploads
1112
from tableauserverclient.server.endpoint.flow_runs_endpoint import FlowRuns
@@ -42,6 +43,7 @@
4243
"QuerysetEndpoint",
4344
"MissingRequiredFieldError",
4445
"Endpoint",
46+
"Extensions",
4547
"Favorites",
4648
"Fileuploads",
4749
"FlowRuns",
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from tableauserverclient.models.extensions_item import ExtensionsServer, ExtensionsSiteSettings
2+
from tableauserverclient.server.endpoint.endpoint import Endpoint
3+
from tableauserverclient.server.endpoint.endpoint import api
4+
from tableauserverclient.server.request_factory import RequestFactory
5+
6+
7+
class Extensions(Endpoint):
8+
def __init__(self, parent_srv):
9+
super().__init__(parent_srv)
10+
11+
@property
12+
def _server_baseurl(self) -> str:
13+
return f"{self.parent_srv.baseurl}/settings/extensions"
14+
15+
@property
16+
def baseurl(self) -> str:
17+
return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/settings/extensions"
18+
19+
@api(version="3.21")
20+
def get_server_settings(self) -> ExtensionsServer:
21+
"""Lists the settings for extensions of a server
22+
23+
Returns
24+
-------
25+
ExtensionsServer
26+
The server extensions settings
27+
"""
28+
response = self.get_request(self._server_baseurl)
29+
return ExtensionsServer.from_response(response.content, self.parent_srv.namespace)
30+
31+
@api(version="3.21")
32+
def update_server_settings(self, extensions_server: ExtensionsServer) -> ExtensionsServer:
33+
"""Updates the settings for extensions of a server. Overwrites all existing settings. Any
34+
sites omitted from the block list will be unblocked.
35+
36+
Parameters
37+
----------
38+
extensions_server : ExtensionsServer
39+
The server extensions settings to update
40+
41+
Returns
42+
-------
43+
ExtensionsServer
44+
The updated server extensions settings
45+
"""
46+
req = RequestFactory.Extensions.update_server_extensions(extensions_server)
47+
response = self.put_request(self._server_baseurl, req)
48+
return ExtensionsServer.from_response(response.content, self.parent_srv.namespace)
49+
50+
@api(version="3.21")
51+
def get(self) -> ExtensionsSiteSettings:
52+
"""Lists the extensions settings for the site
53+
54+
Returns
55+
-------
56+
ExtensionsSiteSettings
57+
The site extensions settings
58+
"""
59+
response = self.get_request(self.baseurl)
60+
return ExtensionsSiteSettings.from_response(response.content, self.parent_srv.namespace)
61+
62+
@api(version="3.21")
63+
def update(self, extensions_site_settings: ExtensionsSiteSettings) -> ExtensionsSiteSettings:
64+
"""Updates the extensions settings for the site. Any extensions omitted
65+
from the safe extensions list will be removed.
66+
67+
Parameters
68+
----------
69+
extensions_site_settings : ExtensionsSiteSettings
70+
The site extensions settings to update
71+
72+
Returns
73+
-------
74+
ExtensionsSiteSettings
75+
The updated site extensions settings
76+
"""
77+
req = RequestFactory.Extensions.update_site_extensions(extensions_site_settings)
78+
response = self.put_request(self.baseurl, req)
79+
return ExtensionsSiteSettings.from_response(response.content, self.parent_srv.namespace)

0 commit comments

Comments
 (0)