diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py index ab65b97b..ab32ad09 100644 --- a/tableauserverclient/models/site_item.py +++ b/tableauserverclient/models/site_item.py @@ -85,6 +85,9 @@ class SiteItem: state: str Shows the current state of the site (Active or Suspended). + attribute_capture_enabled: Optional[str] + Enables user attributes for all Tableau Server embedding workflows. + """ _user_quota: Optional[int] = None @@ -164,6 +167,7 @@ def __init__( time_zone=None, auto_suspend_refresh_enabled: bool = True, auto_suspend_refresh_inactivity_window: int = 30, + attribute_capture_enabled: Optional[bool] = None, ): self._admin_mode = None self._id: Optional[str] = None @@ -217,6 +221,7 @@ def __init__( self.time_zone = time_zone self.auto_suspend_refresh_enabled = auto_suspend_refresh_enabled self.auto_suspend_refresh_inactivity_window = auto_suspend_refresh_inactivity_window + self.attribute_capture_enabled = attribute_capture_enabled @property def admin_mode(self) -> Optional[str]: @@ -720,6 +725,7 @@ def _parse_common_tags(self, site_xml, ns): time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ) = self._parse_element(site_xml, ns) self._set_values( @@ -774,6 +780,7 @@ def _parse_common_tags(self, site_xml, ns): time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ) return self @@ -830,6 +837,7 @@ def _set_values( time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ): if id is not None: self._id = id @@ -937,6 +945,7 @@ def _set_values( self.auto_suspend_refresh_enabled = auto_suspend_refresh_enabled if auto_suspend_refresh_inactivity_window is not None: self.auto_suspend_refresh_inactivity_window = auto_suspend_refresh_inactivity_window + self.attribute_capture_enabled = attribute_capture_enabled @classmethod def from_response(cls, resp, ns) -> list["SiteItem"]: @@ -996,6 +1005,7 @@ def from_response(cls, resp, ns) -> list["SiteItem"]: time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ) = cls._parse_element(site_xml, ns) site_item = cls(name, content_url) @@ -1051,6 +1061,7 @@ def from_response(cls, resp, ns) -> list["SiteItem"]: time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ) all_site_items.append(site_item) return all_site_items @@ -1132,6 +1143,9 @@ def _parse_element(site_xml, ns): flows_enabled = string_to_bool(site_xml.get("flowsEnabled", "")) cataloging_enabled = string_to_bool(site_xml.get("catalogingEnabled", "")) + attribute_capture_enabled = ( + string_to_bool(ace) if (ace := site_xml.get("attributeCaptureEnabled")) is not None else None + ) return ( id, @@ -1185,6 +1199,7 @@ def _parse_element(site_xml, ns): time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index c898004f..8e7419f6 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -715,6 +715,8 @@ def update_req(self, site_item: "SiteItem", parent_srv: Optional["Server"] = Non site_element.attrib["autoSuspendRefreshInactivityWindow"] = str( site_item.auto_suspend_refresh_inactivity_window ) + if site_item.attribute_capture_enabled is not None: + site_element.attrib["attributeCaptureEnabled"] = str(site_item.attribute_capture_enabled).lower() return ET.tostring(xml_request) @@ -819,6 +821,8 @@ def create_req(self, site_item: "SiteItem", parent_srv: Optional["Server"] = Non site_element.attrib["autoSuspendRefreshInactivityWindow"] = str( site_item.auto_suspend_refresh_inactivity_window ) + if site_item.attribute_capture_enabled is not None: + site_element.attrib["attributeCaptureEnabled"] = str(site_item.attribute_capture_enabled).lower() return ET.tostring(xml_request) diff --git a/test/_utils.py b/test/_utils.py index b4ee93bc..a23f37b5 100644 --- a/test/_utils.py +++ b/test/_utils.py @@ -1,5 +1,6 @@ import os.path import unittest +from typing import Optional from xml.etree import ElementTree as ET from contextlib import contextmanager @@ -32,6 +33,19 @@ def server_response_error_factory(code: str, summary: str, detail: str) -> str: return ET.tostring(root, encoding="utf-8").decode("utf-8") +def server_response_factory(tag: str, **attributes: str) -> bytes: + ns = "http://tableau.com/api" + ET.register_namespace("", ns) + root = ET.Element( + f"{{{ns}}}tsResponse", + ) + if attributes is None: + attributes = {} + + elem = ET.SubElement(root, f"{{{ns}}}{tag}", attrib=attributes) + return ET.tostring(root, encoding="utf-8") + + @contextmanager def mocked_time(): mock_time = 0 diff --git a/test/test_site.py b/test/test_site.py index 24381025..034e7c84 100644 --- a/test/test_site.py +++ b/test/test_site.py @@ -1,10 +1,15 @@ +from itertools import product import os.path import unittest +from defusedxml import ElementTree as ET import pytest import requests_mock import tableauserverclient as TSC +from tableauserverclient.server.request_factory import RequestFactory + +from . import _utils TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") @@ -286,3 +291,36 @@ def test_list_auth_configurations(self) -> None: assert configs[1].idp_configuration_id == "11111111-1111-1111-1111-111111111111" assert configs[1].idp_configuration_name == "Initial SAML" assert configs[1].known_provider_alias is None + + +@pytest.mark.parametrize("capture", [True, False, None]) +def test_parsing_attr_capture(capture): + server = TSC.Server("http://test", False) + server.version = "3.10" + attrs = {"contentUrl": "test", "name": "test"} + if capture is not None: + attrs |= {"attributeCaptureEnabled": str(capture).lower()} + xml = _utils.server_response_factory("site", **attrs) + site = TSC.SiteItem.from_response(xml, server.namespace)[0] + + assert site.attribute_capture_enabled is capture, "Attribute capture not captured correctly" + + +@pytest.mark.filterwarnings("ignore:FlowsEnabled has been removed") +@pytest.mark.parametrize("req, capture", product(["create_req", "update_req"], [True, False, None])) +def test_encoding_attr_capture(req, capture): + site = TSC.SiteItem( + content_url="test", + name="test", + attribute_capture_enabled=capture, + ) + xml = getattr(RequestFactory.Site, req)(site) + site_elem = ET.fromstring(xml).find(".//site") + assert site_elem is not None, "Site element missing from XML body." + + if capture is not None: + assert ( + site_elem.attrib["attributeCaptureEnabled"] == str(capture).lower() + ), "Attribute capture not encoded correctly" + else: + assert "attributeCaptureEnabled" not in site_elem.attrib, "Attribute capture should not be encoded when None"