Skip to content

Commit d8922ed

Browse files
New APIs: Update multiple connections in a single workbook/datasource (#1638)
Update multiple connections in a single workbook - Takes multiple connection, authType and credentials as input Update multiple connections in a single datasource - Takes multiple connection, authType and credentials as input --------- Co-authored-by: Jordan Woods <[email protected]>
1 parent 5e49f38 commit d8922ed

10 files changed

+421
-2
lines changed

samples/update_connection_auth.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import argparse
2+
import logging
3+
import tableauserverclient as TSC
4+
5+
6+
def main():
7+
parser = argparse.ArgumentParser(
8+
description="Update a single connection on a datasource or workbook to embed credentials"
9+
)
10+
11+
# Common options
12+
parser.add_argument("--server", "-s", help="Server address", required=True)
13+
parser.add_argument("--site", "-S", help="Site name", required=True)
14+
parser.add_argument("--token-name", "-p", help="Personal access token name", required=True)
15+
parser.add_argument("--token-value", "-v", help="Personal access token value", required=True)
16+
parser.add_argument(
17+
"--logging-level",
18+
"-l",
19+
choices=["debug", "info", "error"],
20+
default="error",
21+
help="Logging level (default: error)",
22+
)
23+
24+
# Resource and connection details
25+
parser.add_argument("resource_type", choices=["workbook", "datasource"])
26+
parser.add_argument("resource_id", help="Workbook or datasource ID")
27+
parser.add_argument("connection_id", help="Connection ID to update")
28+
parser.add_argument("datasource_username", help="Username to set for the connection")
29+
parser.add_argument("datasource_password", help="Password to set for the connection")
30+
parser.add_argument("authentication_type", help="Authentication type")
31+
32+
args = parser.parse_args()
33+
34+
# Logging setup
35+
logging_level = getattr(logging, args.logging_level.upper())
36+
logging.basicConfig(level=logging_level)
37+
38+
tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site)
39+
server = TSC.Server(args.server, use_server_version=True)
40+
41+
with server.auth.sign_in(tableau_auth):
42+
endpoint = {"workbook": server.workbooks, "datasource": server.datasources}.get(args.resource_type)
43+
44+
update_function = endpoint.update_connection
45+
resource = endpoint.get_by_id(args.resource_id)
46+
endpoint.populate_connections(resource)
47+
48+
connections = [conn for conn in resource.connections if conn.id == args.connection_id]
49+
assert len(connections) == 1, f"Connection ID '{args.connection_id}' not found."
50+
51+
connection = connections[0]
52+
connection.username = args.datasource_username
53+
connection.password = args.datasource_password
54+
connection.authentication_type = args.authentication_type
55+
connection.embed_password = True
56+
57+
updated_connection = update_function(resource, connection)
58+
print(f"Updated connection: {updated_connection.__dict__}")
59+
60+
61+
if __name__ == "__main__":
62+
main()

samples/update_connections_auth.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import argparse
2+
import logging
3+
import tableauserverclient as TSC
4+
5+
6+
def main():
7+
parser = argparse.ArgumentParser(description="Bulk update all workbook or datasource connections")
8+
9+
# Common options
10+
parser.add_argument("--server", "-s", help="Server address", required=True)
11+
parser.add_argument("--site", "-S", help="Site name", required=True)
12+
parser.add_argument("--token-name", "-p", help="Personal access token name", required=True)
13+
parser.add_argument("--token-value", "-v", help="Personal access token value", required=True)
14+
parser.add_argument(
15+
"--logging-level",
16+
"-l",
17+
choices=["debug", "info", "error"],
18+
default="error",
19+
help="Logging level (default: error)",
20+
)
21+
22+
# Resource-specific
23+
parser.add_argument("resource_type", choices=["workbook", "datasource"])
24+
parser.add_argument("resource_id")
25+
parser.add_argument("datasource_username")
26+
parser.add_argument("authentication_type")
27+
parser.add_argument("--datasource_password", default=None, help="Datasource password (optional)")
28+
parser.add_argument(
29+
"--embed_password", default="true", choices=["true", "false"], help="Embed password (default: true)"
30+
)
31+
32+
args = parser.parse_args()
33+
34+
# Set logging level
35+
logging_level = getattr(logging, args.logging_level.upper())
36+
logging.basicConfig(level=logging_level)
37+
38+
tableau_auth = TSC.TableauAuth(args.token_name, args.token_value, site_id=args.site)
39+
server = TSC.Server(args.server, use_server_version=True)
40+
41+
with server.auth.sign_in(tableau_auth):
42+
endpoint = {"workbook": server.workbooks, "datasource": server.datasources}.get(args.resource_type)
43+
44+
resource = endpoint.get_by_id(args.resource_id)
45+
endpoint.populate_connections(resource)
46+
47+
connection_luids = [conn.id for conn in resource.connections]
48+
embed_password = args.embed_password.lower() == "true"
49+
50+
# Call unified update_connections method
51+
connection_items = endpoint.update_connections(
52+
resource,
53+
connection_luids=connection_luids,
54+
authentication_type=args.authentication_type,
55+
username=args.datasource_username,
56+
password=args.datasource_password,
57+
embed_password=embed_password,
58+
)
59+
60+
print(f"Updated connections on {args.resource_type} {args.resource_id}: {connection_items}")
61+
62+
63+
if __name__ == "__main__":
64+
main()

tableauserverclient/models/connection_item.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ class ConnectionItem:
4141
server_port: str
4242
The port used for the connection.
4343
44+
auth_type: str
45+
Specifies the type of authentication used by the connection.
46+
4447
connection_credentials: ConnectionCredentials
4548
The Connection Credentials object containing authentication details for
4649
the connection. Replaces username/password/embed_password when
@@ -59,6 +62,7 @@ def __init__(self):
5962
self.username: Optional[str] = None
6063
self.connection_credentials: Optional[ConnectionCredentials] = None
6164
self._query_tagging: Optional[bool] = None
65+
self._auth_type: Optional[str] = None
6266

6367
@property
6468
def datasource_id(self) -> Optional[str]:
@@ -91,8 +95,16 @@ def query_tagging(self, value: Optional[bool]):
9195
return
9296
self._query_tagging = value
9397

98+
@property
99+
def auth_type(self) -> Optional[str]:
100+
return self._auth_type
101+
102+
@auth_type.setter
103+
def auth_type(self, value: Optional[str]):
104+
self._auth_type = value
105+
94106
def __repr__(self):
95-
return "<ConnectionItem#{_id} embed={embed_password} type={_connection_type} username={username}>".format(
107+
return "<ConnectionItem#{_id} embed={embed_password} type={_connection_type} auth={_auth_type} username={username}>".format(
96108
**self.__dict__
97109
)
98110

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

140153
connection_item.server_address = connection_xml.get("serverAddress", None)
141154
connection_item.server_port = connection_xml.get("serverPort", None)
155+
connection_item._auth_type = connection_xml.get("authenticationType", None)
142156

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

tableauserverclient/server/endpoint/datasources_endpoint.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,61 @@ def update_connection(
319319
logger.info(f"Updated datasource item (ID: {datasource_item.id} & connection item {connection_item.id}")
320320
return connection
321321

322+
@api(version="3.26")
323+
def update_connections(
324+
self,
325+
datasource_item: DatasourceItem,
326+
connection_luids: Iterable[str],
327+
authentication_type: str,
328+
username: Optional[str] = None,
329+
password: Optional[str] = None,
330+
embed_password: Optional[bool] = None,
331+
) -> list[ConnectionItem]:
332+
"""
333+
Bulk updates one or more datasource connections by LUID.
334+
335+
Parameters
336+
----------
337+
datasource_item : DatasourceItem
338+
The datasource item containing the connections.
339+
340+
connection_luids : Iterable of str
341+
The connection LUIDs to update.
342+
343+
authentication_type : str
344+
The authentication type to use (e.g., 'auth-keypair').
345+
346+
username : str, optional
347+
The username to set.
348+
349+
password : str, optional
350+
The password or secret to set.
351+
352+
embed_password : bool, optional
353+
Whether to embed the password.
354+
355+
Returns
356+
-------
357+
Iterable of str
358+
The connection LUIDs that were updated.
359+
"""
360+
361+
url = f"{self.baseurl}/{datasource_item.id}/connections"
362+
363+
request_body = RequestFactory.Datasource.update_connections_req(
364+
connection_luids=connection_luids,
365+
authentication_type=authentication_type,
366+
username=username,
367+
password=password,
368+
embed_password=embed_password,
369+
)
370+
server_response = self.put_request(url, request_body)
371+
connection_items = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
372+
updated_ids: list[str] = [conn.id for conn in connection_items]
373+
374+
logger.info(f"Updated connections for datasource {datasource_item.id}: {', '.join(updated_ids)}")
375+
return connection_items
376+
322377
@api(version="2.8")
323378
def refresh(self, datasource_item: DatasourceItem, incremental: bool = False) -> JobItem:
324379
"""

tableauserverclient/server/endpoint/workbooks_endpoint.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,64 @@ def update_connection(self, workbook_item: WorkbookItem, connection_item: Connec
325325
logger.info(f"Updated workbook item (ID: {workbook_item.id} & connection item {connection_item.id})")
326326
return connection
327327

328+
# Update workbook_connections
329+
@api(version="3.26")
330+
def update_connections(
331+
self,
332+
workbook_item: WorkbookItem,
333+
connection_luids: Iterable[str],
334+
authentication_type: str,
335+
username: Optional[str] = None,
336+
password: Optional[str] = None,
337+
embed_password: Optional[bool] = None,
338+
) -> list[ConnectionItem]:
339+
"""
340+
Bulk updates one or more workbook connections by LUID, including authenticationType, username, password, and embedPassword.
341+
342+
Parameters
343+
----------
344+
workbook_item : WorkbookItem
345+
The workbook item containing the connections.
346+
347+
connection_luids : Iterable of str
348+
The connection LUIDs to update.
349+
350+
authentication_type : str
351+
The authentication type to use (e.g., 'AD Service Principal').
352+
353+
username : str, optional
354+
The username to set (e.g., client ID for keypair auth).
355+
356+
password : str, optional
357+
The password or secret to set.
358+
359+
embed_password : bool, optional
360+
Whether to embed the password.
361+
362+
Returns
363+
-------
364+
Iterable of str
365+
The connection LUIDs that were updated.
366+
"""
367+
368+
url = f"{self.baseurl}/{workbook_item.id}/connections"
369+
370+
request_body = RequestFactory.Workbook.update_connections_req(
371+
connection_luids,
372+
authentication_type,
373+
username=username,
374+
password=password,
375+
embed_password=embed_password,
376+
)
377+
378+
# Send request
379+
server_response = self.put_request(url, request_body)
380+
connection_items = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace)
381+
updated_ids: list[str] = [conn.id for conn in connection_items]
382+
383+
logger.info(f"Updated connections for workbook {workbook_item.id}: {', '.join(updated_ids)}")
384+
return connection_items
385+
328386
# Download workbook contents with option of passing in filepath
329387
@api(version="2.0")
330388
@parameter_added_in(no_extract="2.5")

tableauserverclient/server/request_factory.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,32 @@ def publish_req_chunked(self, datasource_item, connection_credentials=None, conn
244244
parts = {"request_payload": ("", xml_request, "text/xml")}
245245
return _add_multipart(parts)
246246

247+
@_tsrequest_wrapped
248+
def update_connections_req(
249+
self,
250+
element: ET.Element,
251+
connection_luids: Iterable[str],
252+
authentication_type: str,
253+
username: Optional[str] = None,
254+
password: Optional[str] = None,
255+
embed_password: Optional[bool] = None,
256+
):
257+
conn_luids_elem = ET.SubElement(element, "connectionLUIDs")
258+
for luid in connection_luids:
259+
ET.SubElement(conn_luids_elem, "connectionLUID").text = luid
260+
261+
connection_elem = ET.SubElement(element, "connection")
262+
connection_elem.set("authenticationType", authentication_type)
263+
264+
if username is not None:
265+
connection_elem.set("userName", username)
266+
267+
if password is not None:
268+
connection_elem.set("password", password)
269+
270+
if embed_password is not None:
271+
connection_elem.set("embedPassword", str(embed_password).lower())
272+
247273

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

1121+
@_tsrequest_wrapped
1122+
def update_connections_req(
1123+
self,
1124+
element: ET.Element,
1125+
connection_luids: Iterable[str],
1126+
authentication_type: str,
1127+
username: Optional[str] = None,
1128+
password: Optional[str] = None,
1129+
embed_password: Optional[bool] = None,
1130+
):
1131+
conn_luids_elem = ET.SubElement(element, "connectionLUIDs")
1132+
for luid in connection_luids:
1133+
ET.SubElement(conn_luids_elem, "connectionLUID").text = luid
1134+
1135+
connection_elem = ET.SubElement(element, "connection")
1136+
connection_elem.set("authenticationType", authentication_type)
1137+
1138+
if username is not None:
1139+
connection_elem.set("userName", username)
1140+
1141+
if password is not None:
1142+
connection_elem.set("password", password)
1143+
1144+
if embed_password is not None:
1145+
connection_elem.set("embedPassword", str(embed_password).lower())
1146+
10951147

10961148
class Connection:
10971149
@_tsrequest_wrapped
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<tsResponse xmlns="http://tableau.com/api"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://tableau.com/api https://help.tableau.com/samples/en-us/rest_api/ts-api_3_25.xsd">
5+
<connections>
6+
<connection id="be786ae0-d2bf-4a4b-9b34-e2de8d2d4488"
7+
type="sqlserver"
8+
serverAddress="updated-server"
9+
serverPort="1433"
10+
userName="user1"
11+
embedPassword="true"
12+
authenticationType="auth-keypair" />
13+
<connection id="a1b2c3d4-e5f6-7a8b-9c0d-123456789abc"
14+
type="sqlserver"
15+
serverAddress="updated-server"
16+
serverPort="1433"
17+
userName="user1"
18+
embedPassword="true"
19+
authenticationType="auth-keypair" />
20+
</connections>
21+
</tsResponse>

0 commit comments

Comments
 (0)