Skip to content

Commit 39c85d0

Browse files
committed
feat: enable toggling attribute capture for a site
According to https://help.tableau.com/current/api/embedding_api/en-us/docs/embedding_api_user_attributes.html#:~:text=For%20security%20purposes%2C%20user%20attributes,a%20site%20admin%20(on%20Tableau setting this site setting to `true` is required to enable use of user attributes with Tableau Server and embedding workflows.
1 parent 5e49f38 commit 39c85d0

File tree

4 files changed

+63
-0
lines changed

4 files changed

+63
-0
lines changed

tableauserverclient/models/site_item.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ class SiteItem:
8585
state: str
8686
Shows the current state of the site (Active or Suspended).
8787
88+
attribute_capture_enabled: Optional[str]
89+
Enables user attributes for all Tableau Server embedding workflows.
90+
8891
"""
8992

9093
_user_quota: Optional[int] = None
@@ -164,6 +167,7 @@ def __init__(
164167
time_zone=None,
165168
auto_suspend_refresh_enabled: bool = True,
166169
auto_suspend_refresh_inactivity_window: int = 30,
170+
attribute_capture_enabled: Optional[bool] = None,
167171
):
168172
self._admin_mode = None
169173
self._id: Optional[str] = None
@@ -217,6 +221,7 @@ def __init__(
217221
self.time_zone = time_zone
218222
self.auto_suspend_refresh_enabled = auto_suspend_refresh_enabled
219223
self.auto_suspend_refresh_inactivity_window = auto_suspend_refresh_inactivity_window
224+
self.attribute_capture_enabled = attribute_capture_enabled
220225

221226
@property
222227
def admin_mode(self) -> Optional[str]:
@@ -720,6 +725,7 @@ def _parse_common_tags(self, site_xml, ns):
720725
time_zone,
721726
auto_suspend_refresh_enabled,
722727
auto_suspend_refresh_inactivity_window,
728+
attribute_capture_enabled,
723729
) = self._parse_element(site_xml, ns)
724730

725731
self._set_values(
@@ -774,6 +780,7 @@ def _parse_common_tags(self, site_xml, ns):
774780
time_zone,
775781
auto_suspend_refresh_enabled,
776782
auto_suspend_refresh_inactivity_window,
783+
attribute_capture_enabled,
777784
)
778785
return self
779786

@@ -830,6 +837,7 @@ def _set_values(
830837
time_zone,
831838
auto_suspend_refresh_enabled,
832839
auto_suspend_refresh_inactivity_window,
840+
attribute_capture_enabled,
833841
):
834842
if id is not None:
835843
self._id = id
@@ -937,6 +945,7 @@ def _set_values(
937945
self.auto_suspend_refresh_enabled = auto_suspend_refresh_enabled
938946
if auto_suspend_refresh_inactivity_window is not None:
939947
self.auto_suspend_refresh_inactivity_window = auto_suspend_refresh_inactivity_window
948+
self.attribute_capture_enabled = attribute_capture_enabled
940949

941950
@classmethod
942951
def from_response(cls, resp, ns) -> list["SiteItem"]:
@@ -996,6 +1005,7 @@ def from_response(cls, resp, ns) -> list["SiteItem"]:
9961005
time_zone,
9971006
auto_suspend_refresh_enabled,
9981007
auto_suspend_refresh_inactivity_window,
1008+
attribute_capture_enabled,
9991009
) = cls._parse_element(site_xml, ns)
10001010

10011011
site_item = cls(name, content_url)
@@ -1051,6 +1061,7 @@ def from_response(cls, resp, ns) -> list["SiteItem"]:
10511061
time_zone,
10521062
auto_suspend_refresh_enabled,
10531063
auto_suspend_refresh_inactivity_window,
1064+
attribute_capture_enabled,
10541065
)
10551066
all_site_items.append(site_item)
10561067
return all_site_items
@@ -1132,6 +1143,7 @@ def _parse_element(site_xml, ns):
11321143

11331144
flows_enabled = string_to_bool(site_xml.get("flowsEnabled", ""))
11341145
cataloging_enabled = string_to_bool(site_xml.get("catalogingEnabled", ""))
1146+
attribute_capture_enabled = string_to_bool(ace) if (ace := site_xml.get("attributeCaptureEnabled")) is not None else None
11351147

11361148
return (
11371149
id,
@@ -1185,6 +1197,7 @@ def _parse_element(site_xml, ns):
11851197
time_zone,
11861198
auto_suspend_refresh_enabled,
11871199
auto_suspend_refresh_inactivity_window,
1200+
attribute_capture_enabled,
11881201
)
11891202

11901203

tableauserverclient/server/request_factory.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,8 @@ def update_req(self, site_item: "SiteItem", parent_srv: Optional["Server"] = Non
715715
site_element.attrib["autoSuspendRefreshInactivityWindow"] = str(
716716
site_item.auto_suspend_refresh_inactivity_window
717717
)
718+
if site_item.attribute_capture_enabled is not None:
719+
site_element.attrib["attributeCaptureEnabled"] = str(site_item.attribute_capture_enabled).lower()
718720

719721
return ET.tostring(xml_request)
720722

@@ -819,6 +821,8 @@ def create_req(self, site_item: "SiteItem", parent_srv: Optional["Server"] = Non
819821
site_element.attrib["autoSuspendRefreshInactivityWindow"] = str(
820822
site_item.auto_suspend_refresh_inactivity_window
821823
)
824+
if site_item.attribute_capture_enabled is not None:
825+
site_element.attrib["attributeCaptureEnabled"] = str(site_item.attribute_capture_enabled).lower()
822826

823827
return ET.tostring(xml_request)
824828

test/_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os.path
22
import unittest
3+
from typing import Optional
34
from xml.etree import ElementTree as ET
45
from contextlib import contextmanager
56

@@ -31,6 +32,16 @@ def server_response_error_factory(code: str, summary: str, detail: str) -> str:
3132
detail_element.text = detail
3233
return ET.tostring(root, encoding="utf-8").decode("utf-8")
3334

35+
def server_response_factory(tag: str, **attributes: str | bool | int | None) -> bytes:
36+
ns = "http://tableau.com/api"
37+
ET.register_namespace("", ns)
38+
root = ET.Element(f"{{{ns}}}tsResponse",)
39+
if attributes is None:
40+
attributes = {}
41+
42+
elem = ET.SubElement(root, f"{{{ns}}}{tag}", **attributes)
43+
return ET.tostring(root, encoding="utf-8")
44+
3445

3546
@contextmanager
3647
def mocked_time():

test/test_site.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
from itertools import product
12
import os.path
23
import unittest
34

5+
from defusedxml import ElementTree as ET
46
import pytest
57
import requests_mock
68

79
import tableauserverclient as TSC
10+
from tableauserverclient.server.request_factory import RequestFactory
11+
12+
from . import _utils
813

914
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets")
1015

@@ -286,3 +291,33 @@ def test_list_auth_configurations(self) -> None:
286291
assert configs[1].idp_configuration_id == "11111111-1111-1111-1111-111111111111"
287292
assert configs[1].idp_configuration_name == "Initial SAML"
288293
assert configs[1].known_provider_alias is None
294+
295+
@pytest.mark.parametrize("capture", [True, False, None])
296+
def test_parsing_attr_capture(capture):
297+
server = TSC.Server("http://test", False)
298+
server.version = "3.10"
299+
attrs = {"contentUrl": "test", "name": "test"}
300+
if capture is not None:
301+
attrs |= {"attributeCaptureEnabled": str(capture).lower()}
302+
xml = _utils.server_response_factory("site", **attrs)
303+
site = TSC.SiteItem.from_response(xml, server.namespace)[0]
304+
305+
assert site.attribute_capture_enabled is capture, "Attribute capture not captured correctly"
306+
307+
@pytest.mark.filterwarnings("ignore:FlowsEnabled has been removed")
308+
@pytest.mark.parametrize("req, capture", product(["create_req", "update_req"], [True, False, None]))
309+
def test_encoding_attr_capture(req, capture):
310+
site = TSC.SiteItem(
311+
content_url="test",
312+
name="test",
313+
attribute_capture_enabled=capture,
314+
)
315+
xml = getattr(RequestFactory.Site, req)(site)
316+
site_elem = ET.fromstring(xml).find(".//site")
317+
assert site_elem is not None, "Site element missing from XML body."
318+
319+
if capture is not None:
320+
assert site_elem.attrib["attributeCaptureEnabled"] == str(capture).lower(), "Attribute capture not encoded correctly"
321+
else:
322+
assert "attributeCaptureEnabled" not in site_elem.attrib, "Attribute capture should not be encoded when None"
323+

0 commit comments

Comments
 (0)