From 00c31898d1394b2c51df7f047970db435f7ef3f0 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 17 Jun 2026 10:55:23 -0700 Subject: [PATCH] feat: add database_name to ConnectionItem Fixes #1571 Co-Authored-By: Claude Sonnet 4.6 --- tableauserverclient/models/connection_item.py | 21 +++++ tableauserverclient/server/request_factory.py | 2 + .../datasource_populate_connections.xml | 2 +- test/test_connection_.py | 82 +++++++++++++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) diff --git a/tableauserverclient/models/connection_item.py b/tableauserverclient/models/connection_item.py index 9e2f008a0..fa4b8cf9f 100644 --- a/tableauserverclient/models/connection_item.py +++ b/tableauserverclient/models/connection_item.py @@ -47,6 +47,9 @@ class ConnectionItem: The Connection Credentials object containing authentication details for the connection. Replaces username/password/embed_password when publishing a flow, document or workbook file in the request body. + + database_name: str + The name of the database for the connection. """ def __init__(self): @@ -62,6 +65,7 @@ def __init__(self): self.connection_credentials: ConnectionCredentials | None = None self._query_tagging: bool | None = None self._auth_type: str | None = None + self._database_name: str | None = None @property def datasource_id(self) -> str | None: @@ -102,6 +106,14 @@ def auth_type(self) -> str | None: def auth_type(self, value: str | None): self._auth_type = value + @property + def database_name(self) -> str | None: + return self._database_name + + @database_name.setter + def database_name(self, value: str | None): + self._database_name = value + def __repr__(self): return "".format( **self.__dict__ @@ -124,6 +136,11 @@ def from_response(cls, resp, ns) -> list["ConnectionItem"]: string_to_bool(s) if (s := connection_xml.get("queryTagging", None)) else None ) connection_item._auth_type = connection_xml.get("authenticationType", None) + # The REST API GET /connections response uses "dbName" for the database + # name attribute. This is different from the publish request body, which + # uses "databaseName" (see _add_connections_element in request_factory.py). + # Both names map to the same database_name property on ConnectionItem. + connection_item._database_name = connection_xml.get("dbName", 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) @@ -152,6 +169,10 @@ 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) + # Publish/update request bodies use "databaseName" (matching the + # publish-request schema), while GET responses use "dbName". See + # from_response() above and _add_connections_element() in request_factory.py. + connection_item._database_name = connection_xml.get("databaseName", None) connection_credentials = connection_xml.find(".//t:connectionCredentials", namespaces=ns) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index e22e43e2f..7d73ac575 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -47,6 +47,8 @@ def _add_connections_element(connections_element, connection): connection_element.attrib["serverAddress"] = connection.server_address if connection.server_port: connection_element.attrib["serverPort"] = connection.server_port + if connection.database_name: + connection_element.attrib["databaseName"] = connection.database_name if connection.connection_credentials: connection_credentials = connection.connection_credentials elif connection.username is not None and connection.password is not None and connection.embed_password is not None: diff --git a/test/assets/datasource_populate_connections.xml b/test/assets/datasource_populate_connections.xml index eaaa24934..f6fbd13f6 100644 --- a/test/assets/datasource_populate_connections.xml +++ b/test/assets/datasource_populate_connections.xml @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/test/test_connection_.py b/test/test_connection_.py index 8bfed79c7..d15cd969b 100644 --- a/test/test_connection_.py +++ b/test/test_connection_.py @@ -1,7 +1,13 @@ +import xml.etree.ElementTree as ET +from pathlib import Path + import tableauserverclient as TSC import pytest +ASSETS_DIR = Path(__file__).parent / "assets" +NS = {"t": "http://tableau.com/api"} + def test_require_boolean_query_tag_fails() -> None: conn = TSC.ConnectionItem() @@ -23,3 +29,79 @@ def test_ignore_query_tag(conn_type: str) -> None: conn._connection_type = conn_type conn.query_tagging = True assert conn.query_tagging is None + + +def test_database_name_default_none() -> None: + conn = TSC.ConnectionItem() + assert conn.database_name is None + + +def test_database_name_getter_setter() -> None: + conn = TSC.ConnectionItem() + conn.database_name = "my_database" + assert conn.database_name == "my_database" + + +def test_database_name_from_response_parses_db_name() -> None: + xml = """ + + + + +""" + connections = TSC.ConnectionItem.from_response(xml, NS) + assert len(connections) == 1 + assert connections[0].database_name == "SalesDB" + + +def test_database_name_from_response_none_when_absent() -> None: + xml = """ + + + + +""" + connections = TSC.ConnectionItem.from_response(xml, NS) + assert len(connections) == 1 + assert connections[0].database_name is None + + +def test_database_name_parsed_from_xml_asset() -> None: + response_xml = (ASSETS_DIR / "datasource_populate_connections.xml").read_text() + connections = TSC.ConnectionItem.from_response(response_xml, NS) + assert len(connections) == 2 + conn_with_db = next(c for c in connections if c.id == "be786ae0-d2bf-4a4b-9b34-e2de8d2d4488") + conn_without_db = next(c for c in connections if c.id == "970e24bc-e200-4841-a3e9-66e7d122d77e") + assert conn_with_db.database_name == "SalesDB" + assert conn_without_db.database_name is None + + +def test_database_name_round_trip() -> None: + """database_name parsed from GET response (dbName) round-trips through + _add_connections_element which emits databaseName in the publish request.""" + import xml.etree.ElementTree as ET + from tableauserverclient.server.request_factory import _add_connections_element + + # Parse from a GET-style response (attribute name: dbName) + xml = """ + + + + +""" + connections = TSC.ConnectionItem.from_response(xml, NS) + assert len(connections) == 1 + conn = connections[0] + assert conn.database_name == "Northwind" + + # Now emit as a publish request element and confirm databaseName is used + conn.server_address = "db.example.com" # already set, but make it explicit + parent_elem = ET.Element("connections") + _add_connections_element(parent_elem, conn) + connection_elem = parent_elem.find("connection") + assert connection_elem is not None + assert connection_elem.attrib.get("databaseName") == "Northwind" + assert "dbName" not in connection_elem.attrib