Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
DatabaseItem,
DataFreshnessPolicyItem,
DatasourceItem,
ExtensionsServer,
ExtensionsSiteSettings,
FavoriteItem,
FlowItem,
FlowRunItem,
Expand All @@ -36,6 +38,7 @@
ProjectItem,
Resource,
RevisionItem,
SafeExtension,
ScheduleItem,
SiteAuthConfiguration,
SiteOIDCConfiguration,
Expand Down Expand Up @@ -88,6 +91,8 @@
"DEFAULT_NAMESPACE",
"DQWItem",
"ExcelRequestOptions",
"ExtensionsServer",
"ExtensionsSiteSettings",
"FailedSignInError",
"FavoriteItem",
"FileuploadItem",
Expand Down Expand Up @@ -121,6 +126,7 @@
"RequestOptions",
"Resource",
"RevisionItem",
"SafeExtension",
"ScheduleItem",
"Server",
"ServerInfoItem",
Expand Down
4 changes: 4 additions & 0 deletions tableauserverclient/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from tableauserverclient.models.datasource_item import DatasourceItem
from tableauserverclient.models.dqw_item import DQWItem
from tableauserverclient.models.exceptions import UnpopulatedPropertyError
from tableauserverclient.models.extensions_item import ExtensionsServer, ExtensionsSiteSettings, SafeExtension
from tableauserverclient.models.favorites_item import FavoriteItem
from tableauserverclient.models.fileupload_item import FileuploadItem
from tableauserverclient.models.flow_item import FlowItem
Expand Down Expand Up @@ -113,4 +114,7 @@
"LinkedTaskStepItem",
"LinkedTaskFlowRunItem",
"ExtractItem",
"ExtensionsServer",
"ExtensionsSiteSettings",
"SafeExtension",
]
186 changes: 186 additions & 0 deletions tableauserverclient/models/extensions_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
from typing import overload
from typing_extensions import Self

from defusedxml.ElementTree import fromstring

from tableauserverclient.models.property_decorators import property_is_boolean


class ExtensionsServer:
def __init__(self) -> None:
self._enabled: bool | None = None
self._block_list: list[str] | None = None

@property
def enabled(self) -> bool | None:
"""Indicates whether the extensions server is enabled."""
return self._enabled

@enabled.setter
@property_is_boolean
def enabled(self, value: bool | None) -> None:
self._enabled = value

@property
def block_list(self) -> list[str] | None:
"""List of blocked extensions."""
return self._block_list

@block_list.setter
def block_list(self, value: list[str] | None) -> None:
self._block_list = value

@classmethod
def from_response(cls: type[Self], response, ns) -> Self:
xml = fromstring(response)
obj = cls()
element = xml.find(".//t:extensionsServerSettings", namespaces=ns)
if element is None:
raise ValueError("Missing extensionsServerSettings element in response")

if (enabled_element := element.find("./t:extensionsGloballyEnabled", namespaces=ns)) is not None:
obj.enabled = string_to_bool(enabled_element.text)
obj.block_list = [e.text for e in element.findall("./t:blockList", namespaces=ns)]

return obj


class SafeExtension:
def __init__(
self, url: str | None = None, full_data_allowed: bool | None = None, prompt_needed: bool | None = None
) -> None:
self.url = url
self._full_data_allowed = full_data_allowed
self._prompt_needed = prompt_needed

@property
def full_data_allowed(self) -> bool | None:
return self._full_data_allowed

@full_data_allowed.setter
@property_is_boolean
def full_data_allowed(self, value: bool | None) -> None:
self._full_data_allowed = value

@property
def prompt_needed(self) -> bool | None:
return self._prompt_needed

@prompt_needed.setter
@property_is_boolean
def prompt_needed(self, value: bool | None) -> None:
self._prompt_needed = value


class ExtensionsSiteSettings:
def __init__(self) -> None:
self._enabled: bool | None = None
self._use_default_setting: bool | None = None
self.safe_list: list[SafeExtension] | None = None
self._allow_trusted: bool | None = None
self._include_tableau_built: bool | None = None
self._include_partner_built: bool | None = None
self._include_sandboxed: bool | None = None

@property
def enabled(self) -> bool | None:
return self._enabled

@enabled.setter
@property_is_boolean
def enabled(self, value: bool | None) -> None:
self._enabled = value

@property
def use_default_setting(self) -> bool | None:
return self._use_default_setting

@use_default_setting.setter
@property_is_boolean
def use_default_setting(self, value: bool | None) -> None:
self._use_default_setting = value

@property
def allow_trusted(self) -> bool | None:
return self._allow_trusted

@allow_trusted.setter
@property_is_boolean
def allow_trusted(self, value: bool | None) -> None:
self._allow_trusted = value

@property
def include_tableau_built(self) -> bool | None:
return self._include_tableau_built

@include_tableau_built.setter
@property_is_boolean
def include_tableau_built(self, value: bool | None) -> None:
self._include_tableau_built = value

@property
def include_partner_built(self) -> bool | None:
return self._include_partner_built

@include_partner_built.setter
@property_is_boolean
def include_partner_built(self, value: bool | None) -> None:
self._include_partner_built = value

@property
def include_sandboxed(self) -> bool | None:
return self._include_sandboxed

@include_sandboxed.setter
@property_is_boolean
def include_sandboxed(self, value: bool | None) -> None:
self._include_sandboxed = value

@classmethod
def from_response(cls: type[Self], response, ns) -> Self:
xml = fromstring(response)
element = xml.find(".//t:extensionsSiteSettings", namespaces=ns)
obj = cls()
if element is None:
raise ValueError("Missing extensionsSiteSettings element in response")

if (enabled_element := element.find("./t:extensionsEnabled", namespaces=ns)) is not None:
obj.enabled = string_to_bool(enabled_element.text)
if (default_settings_element := element.find("./t:useDefaultSetting", namespaces=ns)) is not None:
obj.use_default_setting = string_to_bool(default_settings_element.text)
if (allow_trusted_element := element.find("./t:allowTrusted", namespaces=ns)) is not None:
obj.allow_trusted = string_to_bool(allow_trusted_element.text)
if (include_tableau_built_element := element.find("./t:includeTableauBuilt", namespaces=ns)) is not None:
obj.include_tableau_built = string_to_bool(include_tableau_built_element.text)
if (include_partner_built_element := element.find("./t:includePartnerBuilt", namespaces=ns)) is not None:
obj.include_partner_built = string_to_bool(include_partner_built_element.text)
if (include_sandboxed_element := element.find("./t:includeSandboxed", namespaces=ns)) is not None:
obj.include_sandboxed = string_to_bool(include_sandboxed_element.text)

safe_list = []
for safe_extension_element in element.findall("./t:safeList", namespaces=ns):
url = safe_extension_element.find("./t:url", namespaces=ns)
full_data_allowed = safe_extension_element.find("./t:fullDataAllowed", namespaces=ns)
prompt_needed = safe_extension_element.find("./t:promptNeeded", namespaces=ns)

safe_extension = SafeExtension(
url=url.text if url is not None else None,
full_data_allowed=string_to_bool(full_data_allowed.text) if full_data_allowed is not None else None,
prompt_needed=string_to_bool(prompt_needed.text) if prompt_needed is not None else None,
)
safe_list.append(safe_extension)

obj.safe_list = safe_list
return obj


@overload
def string_to_bool(s: str) -> bool: ...


@overload
def string_to_bool(s: None) -> None: ...


def string_to_bool(s):
return s.lower() == "true" if s is not None else None
2 changes: 1 addition & 1 deletion tableauserverclient/models/property_decorators.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import datetime
import re
from functools import wraps
from typing import Any, Optional
from typing import Any, Optional, Tuple
from collections.abc import Container

from tableauserverclient.datetime_helpers import parse_datetime
Expand Down
2 changes: 2 additions & 0 deletions tableauserverclient/server/endpoint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from tableauserverclient.server.endpoint.datasources_endpoint import Datasources
from tableauserverclient.server.endpoint.endpoint import Endpoint, QuerysetEndpoint
from tableauserverclient.server.endpoint.exceptions import ServerResponseError, MissingRequiredFieldError
from tableauserverclient.server.endpoint.extensions_endpoint import Extensions
from tableauserverclient.server.endpoint.favorites_endpoint import Favorites
from tableauserverclient.server.endpoint.fileuploads_endpoint import Fileuploads
from tableauserverclient.server.endpoint.flow_runs_endpoint import FlowRuns
Expand Down Expand Up @@ -42,6 +43,7 @@
"QuerysetEndpoint",
"MissingRequiredFieldError",
"Endpoint",
"Extensions",
"Favorites",
"Fileuploads",
"FlowRuns",
Expand Down
79 changes: 79 additions & 0 deletions tableauserverclient/server/endpoint/extensions_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from tableauserverclient.models.extensions_item import ExtensionsServer, ExtensionsSiteSettings
from tableauserverclient.server.endpoint.endpoint import Endpoint
from tableauserverclient.server.endpoint.endpoint import api
from tableauserverclient.server.request_factory import RequestFactory


class Extensions(Endpoint):
def __init__(self, parent_srv):
super().__init__(parent_srv)

@property
def _server_baseurl(self) -> str:
return f"{self.parent_srv.baseurl}/settings/extensions"

@property
def baseurl(self) -> str:
return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/settings/extensions"

@api(version="3.21")
def get_server_settings(self) -> ExtensionsServer:
"""Lists the settings for extensions of a server

Returns
-------
ExtensionsServer
The server extensions settings
"""
response = self.get_request(self._server_baseurl)
return ExtensionsServer.from_response(response.content, self.parent_srv.namespace)

@api(version="3.21")
def update_server_settings(self, extensions_server: ExtensionsServer) -> ExtensionsServer:
"""Updates the settings for extensions of a server. Overwrites all existing settings. Any
sites omitted from the block list will be unblocked.

Parameters
----------
extensions_server : ExtensionsServer
The server extensions settings to update

Returns
-------
ExtensionsServer
The updated server extensions settings
"""
req = RequestFactory.Extensions.update_server_extensions(extensions_server)
response = self.put_request(self._server_baseurl, req)
return ExtensionsServer.from_response(response.content, self.parent_srv.namespace)

@api(version="3.21")
def get(self) -> ExtensionsSiteSettings:
"""Lists the extensions settings for the site

Returns
-------
list[ExtensionsSiteSettings]
The site extensions settings
"""
response = self.get_request(self.baseurl)
return ExtensionsSiteSettings.from_response(response.content, self.parent_srv.namespace)

@api(version="3.21")
def update(self, extensions_site_settings: ExtensionsSiteSettings) -> ExtensionsSiteSettings:
"""Updates the extensions settings for the site. Overwrites all existing settings.
Any extensions omitted from the safe extensions list will be removed.

Parameters
----------
extensions_site_settings : ExtensionsSiteSettings
The site extensions settings to update

Returns
-------
ExtensionsSiteSettings
The updated site extensions settings
"""
req = RequestFactory.Extensions.update_site_extensions(extensions_site_settings)
response = self.put_request(self.baseurl, req)
return ExtensionsSiteSettings.from_response(response.content, self.parent_srv.namespace)
Loading
Loading