Skip to content

Commit f13b26d

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 f13b26d

File tree

4 files changed

+71
-0
lines changed

4 files changed

+71
-0
lines changed

tableauserverclient/models/site_item.py

Lines changed: 15 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,9 @@ 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 = (
1147+
string_to_bool(ace) if (ace := site_xml.get("attributeCaptureEnabled")) is not None else None
1148+
)
11351149

11361150
return (
11371151
id,
@@ -1185,6 +1199,7 @@ def _parse_element(site_xml, ns):
11851199
time_zone,
11861200
auto_suspend_refresh_enabled,
11871201
auto_suspend_refresh_inactivity_window,
1202+
attribute_capture_enabled,
11881203
)
11891204

11901205

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: 14 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

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

3435

36+
def server_response_factory(tag: str, **attributes: str | bool | int | None) -> bytes:
37+
ns = "http://tableau.com/api"
38+
ET.register_namespace("", ns)
39+
root = ET.Element(
40+
f"{{{ns}}}tsResponse",
41+
)
42+
if attributes is None:
43+
attributes = {}
44+
45+
elem = ET.SubElement(root, f"{{{ns}}}{tag}", **attributes)
46+
return ET.tostring(root, encoding="utf-8")
47+
48+
3549
@contextmanager
3650
def mocked_time():
3751
mock_time = 0

test/test_site.py

Lines changed: 38 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,36 @@ 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+
296+
@pytest.mark.parametrize("capture", [True, False, None])
297+
def test_parsing_attr_capture(capture):
298+
server = TSC.Server("http://test", False)
299+
server.version = "3.10"
300+
attrs = {"contentUrl": "test", "name": "test"}
301+
if capture is not None:
302+
attrs |= {"attributeCaptureEnabled": str(capture).lower()}
303+
xml = _utils.server_response_factory("site", **attrs)
304+
site = TSC.SiteItem.from_response(xml, server.namespace)[0]
305+
306+
assert site.attribute_capture_enabled is capture, "Attribute capture not captured correctly"
307+
308+
309+
@pytest.mark.filterwarnings("ignore:FlowsEnabled has been removed")
310+
@pytest.mark.parametrize("req, capture", product(["create_req", "update_req"], [True, False, None]))
311+
def test_encoding_attr_capture(req, capture):
312+
site = TSC.SiteItem(
313+
content_url="test",
314+
name="test",
315+
attribute_capture_enabled=capture,
316+
)
317+
xml = getattr(RequestFactory.Site, req)(site)
318+
site_elem = ET.fromstring(xml).find(".//site")
319+
assert site_elem is not None, "Site element missing from XML body."
320+
321+
if capture is not None:
322+
assert (
323+
site_elem.attrib["attributeCaptureEnabled"] == str(capture).lower()
324+
), "Attribute capture not encoded correctly"
325+
else:
326+
assert "attributeCaptureEnabled" not in site_elem.attrib, "Attribute capture should not be encoded when None"

0 commit comments

Comments
 (0)