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
62 changes: 62 additions & 0 deletions samples/update_connection_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import argparse
import logging
import tableauserverclient as TSC


def main():
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",
choices=["debug", "info", "error"],
default="error",
help="Logging level (default: error)",
)

# Resource and connection details
parser.add_argument("resource_type", choices=["workbook", "datasource"])
parser.add_argument("resource_id", help="Workbook or datasource ID")
parser.add_argument("connection_id", help="Connection ID to update")
parser.add_argument("datasource_username", help="Username to set for the connection")
parser.add_argument("datasource_password", help="Password to set for the connection")
parser.add_argument("authentication_type", help="Authentication type")

args = parser.parse_args()

# Logging setup
logging_level = getattr(logging, args.logging_level.upper())
logging.basicConfig(level=logging_level)

tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site)
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)

update_function = endpoint.update_connection
resource = endpoint.get_by_id(args.resource_id)
endpoint.populate_connections(resource)

connections = [conn for conn in resource.connections if conn.id == args.connection_id]
assert len(connections) == 1, f"Connection ID '{args.connection_id}' not found."

connection = connections[0]
connection.username = args.datasource_username
connection.password = args.datasource_password
connection.authentication_type = args.authentication_type
connection.embed_password = True

updated_connection = update_function(resource, connection)
print(f"Updated connection: {updated_connection.__dict__}")


if __name__ == "__main__":
main()
64 changes: 64 additions & 0 deletions samples/update_connections_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import argparse
import logging
import tableauserverclient as TSC


def main():
parser = argparse.ArgumentParser(description="Bulk update all workbook or datasource connections")

# 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",
choices=["debug", "info", "error"],
default="error",
help="Logging level (default: error)",
)

# Resource-specific
parser.add_argument("resource_type", choices=["workbook", "datasource"])
parser.add_argument("resource_id")
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)"
)

args = parser.parse_args()

# Set logging level
logging_level = getattr(logging, args.logging_level.upper())
logging.basicConfig(level=logging_level)

tableau_auth = TSC.TableauAuth(args.token_name, args.token_value, site_id=args.site)
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)

resource = endpoint.get_by_id(args.resource_id)
endpoint.populate_connections(resource)

connection_luids = [conn.id for conn in resource.connections]
embed_password = args.embed_password.lower() == "true"

# Call unified update_connections method
connection_items = endpoint.update_connections(
resource,
connection_luids=connection_luids,
authentication_type=args.authentication_type,
username=args.datasource_username,
password=args.datasource_password,
embed_password=embed_password,
)

print(f"Updated connections on {args.resource_type} {args.resource_id}: {connection_items}")


if __name__ == "__main__":
main()
16 changes: 15 additions & 1 deletion tableauserverclient/models/connection_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ class ConnectionItem:
server_port: str
The port used for the connection.

auth_type: str
Specifies the type of authentication used by the connection.

connection_credentials: ConnectionCredentials
The Connection Credentials object containing authentication details for
the connection. Replaces username/password/embed_password when
Expand All @@ -59,6 +62,7 @@ def __init__(self):
self.username: Optional[str] = None
self.connection_credentials: Optional[ConnectionCredentials] = None
self._query_tagging: Optional[bool] = None
self._auth_type: Optional[str] = None

@property
def datasource_id(self) -> Optional[str]:
Expand Down Expand Up @@ -91,8 +95,16 @@ 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} username={username}>".format(
return "<ConnectionItem#{_id} embed={embed_password} type={_connection_type} auth={_auth_type} username={username}>".format(
**self.__dict__
)

Expand All @@ -112,6 +124,7 @@ def from_response(cls, resp, ns) -> list["ConnectionItem"]:
connection_item._query_tagging = (
string_to_bool(s) if (s := connection_xml.get("queryTagging", None)) else None
)
connection_item._auth_type = connection_xml.get("authenticationType", None)
datasource_elem = connection_xml.find(".//t:datasource", namespaces=ns)
if datasource_elem is not None:
connection_item._datasource_id = datasource_elem.get("id", None)
Expand Down Expand Up @@ -139,6 +152,7 @@ def from_xml_element(cls, parsed_response, ns) -> list["ConnectionItem"]:

connection_item.server_address = connection_xml.get("serverAddress", None)
connection_item.server_port = connection_xml.get("serverPort", None)
connection_item._auth_type = connection_xml.get("authenticationType", None)

connection_credentials = connection_xml.find(".//t:connectionCredentials", namespaces=ns)

Expand Down
55 changes: 55 additions & 0 deletions tableauserverclient/server/endpoint/datasources_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,61 @@ def update_connection(
logger.info(f"Updated datasource item (ID: {datasource_item.id} & connection item {connection_item.id}")
return connection

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

Parameters
----------
datasource_item : DatasourceItem
The datasource item containing the connections.

connection_luids : Iterable of str
The connection LUIDs to update.

authentication_type : str
The authentication type to use (e.g., 'auth-keypair').

username : str, optional
The username to set.

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

embed_password : bool, optional
Whether to embed the password.

Returns
-------
Iterable of str
The connection LUIDs that were updated.
"""

url = f"{self.baseurl}/{datasource_item.id}/connections"

request_body = RequestFactory.Datasource.update_connections_req(
connection_luids=connection_luids,
authentication_type=authentication_type,
username=username,
password=password,
embed_password=embed_password,
)
server_response = self.put_request(url, request_body)
connection_items = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
updated_ids: list[str] = [conn.id for conn in connection_items]

logger.info(f"Updated connections for datasource {datasource_item.id}: {', '.join(updated_ids)}")
return connection_items

@api(version="2.8")
def refresh(self, datasource_item: DatasourceItem, incremental: bool = False) -> JobItem:
"""
Expand Down
58 changes: 58 additions & 0 deletions tableauserverclient/server/endpoint/workbooks_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,64 @@ def update_connection(self, workbook_item: WorkbookItem, connection_item: Connec
logger.info(f"Updated workbook item (ID: {workbook_item.id} & connection item {connection_item.id})")
return connection

# Update workbook_connections
@api(version="3.26")
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,
) -> list[ConnectionItem]:
"""
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 : Iterable 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.

Returns
-------
Iterable of str
The connection LUIDs that were updated.
"""

url = f"{self.baseurl}/{workbook_item.id}/connections"

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

# Send request
server_response = self.put_request(url, request_body)
connection_items = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
updated_ids: list[str] = [conn.id for conn in connection_items]

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

# Download workbook contents with option of passing in filepath
@api(version="2.0")
@parameter_added_in(no_extract="2.5")
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
21 changes: 21 additions & 0 deletions test/assets/datasource_connections_update.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<tsResponse xmlns="http://tableau.com/api"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tableau.com/api https://help.tableau.com/samples/en-us/rest_api/ts-api_3_25.xsd">
<connections>
<connection id="be786ae0-d2bf-4a4b-9b34-e2de8d2d4488"
type="sqlserver"
serverAddress="updated-server"
serverPort="1433"
userName="user1"
embedPassword="true"
authenticationType="auth-keypair" />
<connection id="a1b2c3d4-e5f6-7a8b-9c0d-123456789abc"
type="sqlserver"
serverAddress="updated-server"
serverPort="1433"
userName="user1"
embedPassword="true"
authenticationType="auth-keypair" />
</connections>
</tsResponse>
Loading
Loading