Skip to content
Merged
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
12 changes: 6 additions & 6 deletions samples/update_connection_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@


def main():
parser = argparse.ArgumentParser(description="Update a single connection on a datasource or workbook to embed credentials")
parser = argparse.ArgumentParser(
description="Update a single connection on a datasource or workbook to embed credentials"
)

# Common options
parser.add_argument("--server", "-s", help="Server address", required=True)
parser.add_argument("--site", "-S", help="Site name", required=True)
parser.add_argument("--token-name", "-p", help="Personal access token name", required=True)
parser.add_argument("--token-value", "-v", help="Personal access token value", required=True)
parser.add_argument(
"--logging-level", "-l",
"--logging-level",
"-l",
choices=["debug", "info", "error"],
default="error",
help="Logging level (default: error)",
Expand All @@ -36,10 +39,7 @@ def main():
server = TSC.Server(args.server, use_server_version=True)

with server.auth.sign_in(tableau_auth):
endpoint = {
"workbook": server.workbooks,
"datasource": server.datasources
}.get(args.resource_type)
endpoint = {"workbook": server.workbooks, "datasource": server.datasources}.get(args.resource_type)

update_function = endpoint.update_connection
resource = endpoint.get_by_id(args.resource_id)
Expand Down
11 changes: 5 additions & 6 deletions samples/update_connections_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ def main():
parser.add_argument("datasource_username")
parser.add_argument("authentication_type")
parser.add_argument("--datasource_password", default=None, help="Datasource password (optional)")
parser.add_argument("--embed_password", default="true", choices=["true", "false"], help="Embed password (default: true)")
parser.add_argument(
"--embed_password", default="true", choices=["true", "false"], help="Embed password (default: true)"
)

args = parser.parse_args()

Expand All @@ -37,10 +39,7 @@ def main():
server = TSC.Server(args.server, use_server_version=True)

with server.auth.sign_in(tableau_auth):
endpoint = {
"workbook": server.workbooks,
"datasource": server.datasources
}.get(args.resource_type)
endpoint = {"workbook": server.workbooks, "datasource": server.datasources}.get(args.resource_type)

resource = endpoint.get_by_id(args.resource_id)
endpoint.populate_connections(resource)
Expand All @@ -55,7 +54,7 @@ def main():
authentication_type=args.authentication_type,
username=args.datasource_username,
password=args.datasource_password,
embed_password=embed_password
embed_password=embed_password,
)

print(f"Updated connections on {args.resource_type} {args.resource_id}: {updated_ids}")
Expand Down
12 changes: 8 additions & 4 deletions tableauserverclient/models/connection_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,6 @@ def connection_type(self) -> Optional[str]:
def query_tagging(self) -> Optional[bool]:
return self._query_tagging

@property
def auth_type(self) -> Optional[str]:
return self._auth_type

@query_tagging.setter
@property_is_boolean
def query_tagging(self, value: Optional[bool]):
Expand All @@ -99,6 +95,14 @@ def query_tagging(self, value: Optional[bool]):
return
self._query_tagging = value

@property
def auth_type(self) -> Optional[str]:
return self._auth_type

@auth_type.setter
def auth_type(self, value: Optional[str]):
self._auth_type = value

def __repr__(self):
return "<ConnectionItem#{_id} embed={embed_password} type={_connection_type} auth={_auth_type} username={username}>".format(
**self.__dict__
Expand Down
48 changes: 18 additions & 30 deletions tableauserverclient/server/endpoint/datasources_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,14 @@ def update_connection(

@api(version="3.26")
def update_connections(
self, datasource_item: DatasourceItem, connection_luids: list[str], authentication_type: str, username: Optional[str] = None, password: Optional[str] = None, embed_password: Optional[bool] = None
) -> list[str]:
self,
datasource_item: DatasourceItem,
connection_luids: Iterable[str],
authentication_type: str,
username: Optional[str] = None,
password: Optional[str] = None,
embed_password: Optional[bool] = None,
) -> Iterable[str]:
"""
Bulk updates one or more datasource connections by LUID.

Expand All @@ -331,7 +337,7 @@ def update_connections(
datasource_item : DatasourceItem
The datasource item containing the connections.

connection_luids : list of str
connection_luids : Iterable of str
The connection LUIDs to update.

authentication_type : str
Expand All @@ -348,41 +354,23 @@ def update_connections(

Returns
-------
list of str
Iterable of str
The connection LUIDs that were updated.
"""
from xml.etree.ElementTree import Element, SubElement, tostring

url = f"{self.baseurl}/{datasource_item.id}/connections"
print("Method URL:", url)

ts_request = Element("tsRequest")

# <connectionLuids>
conn_luids_elem = SubElement(ts_request, "connectionLuids")
for luid in connection_luids:
SubElement(conn_luids_elem, "connectionLuid").text = luid

# <connection>
connection_elem = SubElement(ts_request, "connection")
connection_elem.set("authenticationType", authentication_type)

if username:
connection_elem.set("userName", username)

if password:
connection_elem.set("password", password)

if embed_password is not None:
connection_elem.set("embedPassword", str(embed_password).lower())

request_body = tostring(ts_request)

request_body = RequestFactory.Datasource.update_connections_req(
connection_luids=connection_luids,
authentication_type=authentication_type,
username=username,
password=password,
embed_password=embed_password,
)
response = self.put_request(url, request_body)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response doesn't seem to get used. Can you give me an example response XML?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response is just a success message, so not necessarily needs to be used.


logger.info(
f"Updated connections for datasource {datasource_item.id}: {', '.join(connection_luids)}"
)
logger.info(f"Updated connections for datasource {datasource_item.id}: {', '.join(connection_luids)}")
return connection_luids
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As outlined in this PR, the function works with any Iterable[str], not just a list. The only problem is if the user were to pass in an Iterator, the Iterator gets consumed by this function and returns an exhausted Iterator. If the connection ids are in the response, we can generate it from the response and have it work in the general case.


@api(version="2.8")
Expand Down
94 changes: 42 additions & 52 deletions tableauserverclient/server/endpoint/workbooks_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,69 +327,59 @@ def update_connection(self, workbook_item: WorkbookItem, connection_item: Connec

# Update workbook_connections
@api(version="3.26")
def update_connections(self, workbook_item: WorkbookItem, connection_luids: list[str], authentication_type: str, username: Optional[str] = None, password: Optional[str] = None, embed_password: Optional[bool] = None
) -> list[str]:
"""
Bulk updates one or more workbook connections by LUID, including authenticationType, username, password, and embedPassword.

Parameters
----------
workbook_item : WorkbookItem
The workbook item containing the connections.

connection_luids : list of str
The connection LUIDs to update.

authentication_type : str
The authentication type to use (e.g., 'AD Service Principal').

username : str, optional
The username to set (e.g., client ID for keypair auth).

password : str, optional
The password or secret to set.

embed_password : bool, optional
Whether to embed the password.
def update_connections(
self,
workbook_item: WorkbookItem,
connection_luids: Iterable[str],
authentication_type: str,
username: Optional[str] = None,
password: Optional[str] = None,
embed_password: Optional[bool] = None,
) -> Iterable[str]:
"""
Bulk updates one or more workbook connections by LUID, including authenticationType, username, password, and embedPassword.

Returns
-------
list of str
The connection LUIDs that were updated.
"""
from xml.etree.ElementTree import Element, SubElement, tostring
Parameters
----------
workbook_item : WorkbookItem
The workbook item containing the connections.

url = f"{self.baseurl}/{workbook_item.id}/connections"
connection_luids : Iterable of str
The connection LUIDs to update.

ts_request = Element("tsRequest")
authentication_type : str
The authentication type to use (e.g., 'AD Service Principal').

# <connectionLuids>
conn_luids_elem = SubElement(ts_request, "connectionLuids")
for luid in connection_luids:
SubElement(conn_luids_elem, "connectionLuid").text = luid
username : str, optional
The username to set (e.g., client ID for keypair auth).

# <connection>
connection_elem = SubElement(ts_request, "connection")
connection_elem.set("authenticationType", authentication_type)
password : str, optional
The password or secret to set.

if username:
connection_elem.set("userName", username)
embed_password : bool, optional
Whether to embed the password.

if password:
connection_elem.set("password", password)
Returns
-------
Iterable of str
The connection LUIDs that were updated.
"""

if embed_password is not None:
connection_elem.set("embedPassword", str(embed_password).lower())
url = f"{self.baseurl}/{workbook_item.id}/connections"

request_body = tostring(ts_request)
request_body = RequestFactory.Workbook.update_connections_req(
connection_luids,
authentication_type,
username=username,
password=password,
embed_password=embed_password,
)

# Send request
response = self.put_request(url, request_body)
# Send request
response = self.put_request(url, request_body)

logger.info(
f"Updated connections for workbook {workbook_item.id}: {', '.join(connection_luids)}"
)
return connection_luids
logger.info(f"Updated connections for workbook {workbook_item.id}: {', '.join(connection_luids)}")
return connection_luids

# Download workbook contents with option of passing in filepath
@api(version="2.0")
Expand Down
52 changes: 52 additions & 0 deletions tableauserverclient/server/request_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,32 @@ def publish_req_chunked(self, datasource_item, connection_credentials=None, conn
parts = {"request_payload": ("", xml_request, "text/xml")}
return _add_multipart(parts)

@_tsrequest_wrapped
def update_connections_req(
self,
element: ET.Element,
connection_luids: Iterable[str],
authentication_type: str,
username: Optional[str] = None,
password: Optional[str] = None,
embed_password: Optional[bool] = None,
):
conn_luids_elem = ET.SubElement(element, "connectionLUIDs")
for luid in connection_luids:
ET.SubElement(conn_luids_elem, "connectionLUID").text = luid

connection_elem = ET.SubElement(element, "connection")
connection_elem.set("authenticationType", authentication_type)

if username is not None:
connection_elem.set("userName", username)

if password is not None:
connection_elem.set("password", password)

if embed_password is not None:
connection_elem.set("embedPassword", str(embed_password).lower())


class DQWRequest:
def add_req(self, dqw_item):
Expand Down Expand Up @@ -1092,6 +1118,32 @@ def embedded_extract_req(
if (id_ := datasource_item.id) is not None:
datasource_element.attrib["id"] = id_

@_tsrequest_wrapped
def update_connections_req(
self,
element: ET.Element,
connection_luids: Iterable[str],
authentication_type: str,
username: Optional[str] = None,
password: Optional[str] = None,
embed_password: Optional[bool] = None,
):
conn_luids_elem = ET.SubElement(element, "connectionLUIDs")
for luid in connection_luids:
ET.SubElement(conn_luids_elem, "connectionLUID").text = luid

connection_elem = ET.SubElement(element, "connection")
connection_elem.set("authenticationType", authentication_type)

if username is not None:
connection_elem.set("userName", username)

if password is not None:
connection_elem.set("password", password)

if embed_password is not None:
connection_elem.set("embedPassword", str(embed_password).lower())


class Connection:
@_tsrequest_wrapped
Expand Down
25 changes: 11 additions & 14 deletions test/test_datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,29 +219,27 @@ def test_update_connection(self) -> None:
self.assertEqual("foo", new_connection.username)

def test_update_connections(self) -> None:
populate_xml, response_xml = read_xml_assets(
POPULATE_CONNECTIONS_XML,
UPDATE_CONNECTIONS_XML
)
populate_xml, response_xml = read_xml_assets(POPULATE_CONNECTIONS_XML, UPDATE_CONNECTIONS_XML)

with requests_mock.Mocker() as m:

datasource_id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb"
connection_luids = [
"be786ae0-d2bf-4a4b-9b34-e2de8d2d4488",
"a1b2c3d4-e5f6-7a8b-9c0d-123456789abc"
]
connection_luids = ["be786ae0-d2bf-4a4b-9b34-e2de8d2d4488", "a1b2c3d4-e5f6-7a8b-9c0d-123456789abc"]

datasource = TSC.DatasourceItem(datasource_id)
datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb"
datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794"
self.server.version = "3.26"

url = f"{self.server.baseurl}/{datasource.id}/connections"
m.get("http://test/api/3.26/sites/dad65087-b08b-4603-af4e-2887b8aafc67/datasources/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections", text=populate_xml)
m.put("http://test/api/3.26/sites/dad65087-b08b-4603-af4e-2887b8aafc67/datasources/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections", text=response_xml)


m.get(
"http://test/api/3.26/sites/dad65087-b08b-4603-af4e-2887b8aafc67/datasources/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections",
text=populate_xml,
)
m.put(
"http://test/api/3.26/sites/dad65087-b08b-4603-af4e-2887b8aafc67/datasources/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections",
text=response_xml,
)

print("BASEURL:", self.server.baseurl)
print("Calling PUT on:", f"{self.server.baseurl}/{datasource.id}/connections")
Expand All @@ -252,12 +250,11 @@ def test_update_connections(self) -> None:
authentication_type="auth-keypair",
username="testuser",
password="testpass",
embed_password=True
embed_password=True,
)

self.assertEqual(updated_luids, connection_luids)


def test_populate_permissions(self) -> None:
with open(asset(POPULATE_PERMISSIONS_XML), "rb") as f:
response_xml = f.read().decode("utf-8")
Expand Down
Loading
Loading