Skip to content

feat: enable toggling attribute capture for a site #1619

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
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
15 changes: 15 additions & 0 deletions tableauserverclient/models/site_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"]:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1185,6 +1199,7 @@ def _parse_element(site_xml, ns):
time_zone,
auto_suspend_refresh_enabled,
auto_suspend_refresh_inactivity_window,
attribute_capture_enabled,
)


Expand Down
4 changes: 4 additions & 0 deletions tableauserverclient/server/request_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
14 changes: 14 additions & 0 deletions test/_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os.path
import unittest
from typing import Optional
from xml.etree import ElementTree as ET
from contextlib import contextmanager

Expand Down Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions test/test_site.py
Original file line number Diff line number Diff line change
@@ -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")

Expand Down Expand Up @@ -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"
Loading