Skip to content
Open
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
21 changes: 21 additions & 0 deletions tableauserverclient/models/connection_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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 "<ConnectionItem#{_id} embed={embed_password} type={_connection_type} auth={_auth_type} username={username}>".format(
**self.__dict__
Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions tableauserverclient/server/request_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion test/assets/datasource_populate_connections.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?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 http://tableau.com/api/ts-api-2.8.xsd">
<connections>
<connection id="be786ae0-d2bf-4a4b-9b34-e2de8d2d4488" type="textscan" serverAddress="forty-two.net" userName="duo" embedPassword="true"/>
<connection id="be786ae0-d2bf-4a4b-9b34-e2de8d2d4488" type="textscan" serverAddress="forty-two.net" userName="duo" embedPassword="true" dbName="SalesDB"/>
<connection id="970e24bc-e200-4841-a3e9-66e7d122d77e" type="sqlserver" serverAddress="database.com" userName="heero" embedPassword="false" />
</connections>
</tsResponse>
82 changes: 82 additions & 0 deletions test/test_connection_.py
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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 = """<?xml version='1.0' encoding='UTF-8'?>
<tsResponse xmlns="http://tableau.com/api">
<connections>
<connection id="abc-123" type="sqlserver" serverAddress="db.example.com"
userName="user" embedPassword="false" dbName="SalesDB"/>
</connections>
</tsResponse>"""
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 = """<?xml version='1.0' encoding='UTF-8'?>
<tsResponse xmlns="http://tableau.com/api">
<connections>
<connection id="abc-123" type="sqlserver" serverAddress="db.example.com"
userName="user" embedPassword="false"/>
</connections>
</tsResponse>"""
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 = """<?xml version='1.0' encoding='UTF-8'?>
<tsResponse xmlns="http://tableau.com/api">
<connections>
<connection id="abc-123" type="sqlserver" serverAddress="db.example.com"
userName="user" embedPassword="false" dbName="Northwind"/>
</connections>
</tsResponse>"""
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
Loading