Skip to content
Closed
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()
65 changes: 65 additions & 0 deletions samples/update_connections_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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("--username", "-p", help="Personal access token name", required=True)
parser.add_argument("--password", "-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.username, args.password, 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
updated_ids = 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}: {updated_ids}")


if __name__ == "__main__":
main()
12 changes: 11 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 All @@ -80,6 +84,10 @@ 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 @@ -92,7 +100,7 @@ def query_tagging(self, value: Optional[bool]):
self._query_tagging = 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 +120,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 +148,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
2 changes: 1 addition & 1 deletion tableauserverclient/models/interval_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,4 +318,4 @@ def interval(self, interval_values):
self._interval = interval_values

def _interval_type_pairs(self):
return [(IntervalItem.Occurrence.MonthDay, self.interval)]
return [(IntervalItem.Occurrence.MonthDay, str(day)) for day in self.interval]
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated change

65 changes: 65 additions & 0 deletions tableauserverclient/server/endpoint/datasources_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,71 @@ 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: list[str], authentication_type: str, username: Optional[str] = None, password: Optional[str] = None, embed_password: Optional[bool] = None
Copy link
Contributor

Choose a reason for hiding this comment

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

Should accept an Iterable[ConnectionItem] rather than taking a list of ids and then Connection attributes as keyword args.

) -> list[str]:
"""
Bulk updates one or more datasource connections by LUID.

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

connection_luids : list 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
-------
list of str
The connection LUIDs that were updated.
"""
from xml.etree.ElementTree import Element, SubElement, tostring

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

ts_request = Element("tsRequest")
Copy link
Contributor

Choose a reason for hiding this comment

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

Creating the XML payload belongs in the request_factory.


# <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)

response = self.put_request(url, request_body)

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

@api(version="2.8")
def refresh(self, datasource_item: DatasourceItem, incremental: bool = False) -> JobItem:
"""
Expand Down
66 changes: 66 additions & 0 deletions tableauserverclient/server/endpoint/workbooks_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,72 @@ 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: list[str], authentication_type: str, username: Optional[str] = None, password: Optional[str] = None, embed_password: Optional[bool] = None
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as datasource method.

) -> 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.

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

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

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)

# 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

# Download workbook contents with option of passing in filepath
@api(version="2.0")
@parameter_added_in(no_extract="2.5")
Expand Down
Loading