diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 819cfcfa6..4e148b327 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -1,5 +1,11 @@ from tableauserverclient.bin._version import get_versions -from tableauserverclient.namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE + +# Inlined as a string literal (rather than imported from server.server) so +# loading `tableauserverclient` doesn't force the full server + endpoint +# import graph to load just to read a constant. Kept in sync with the +# NAMESPACE constant defined in tableauserverclient/server/server.py. +DEFAULT_NAMESPACE = "http://tableau.com/api" + from tableauserverclient.models import ( AuthFederationConnectorItem, AuthFederationItem, diff --git a/tableauserverclient/namespace.py b/tableauserverclient/namespace.py deleted file mode 100644 index 2b5e5b2b5..000000000 --- a/tableauserverclient/namespace.py +++ /dev/null @@ -1,40 +0,0 @@ -import re - -from defusedxml.ElementTree import fromstring - -OLD_NAMESPACE = "http://tableausoftware.com/api" -NEW_NAMESPACE = "http://tableau.com/api" -NAMESPACE_RE = re.compile(r"\{(.*?)\}") - - -class UnknownNamespaceError(Exception): - """Raised when an XML response contains an unrecognized Tableau API namespace.""" - - -class Namespace: - """Detects and stores the Tableau REST API XML namespace from server responses.""" - - def __init__(self): - self._namespace = {"t": NEW_NAMESPACE} - self._detected = False - - def __call__(self): - return self._namespace - - def detect(self, xml): - """Detect the XML namespace from raw response bytes, updating the stored namespace on first call. Raises UnknownNamespaceError if the namespace is not a recognized Tableau REST API namespace.""" - if self._detected: - return - - if not xml.startswith(b" contextmgr: return Auth.contextmgr(self.sign_out) else: raise e - self.parent_srv._namespace.detect(server_response.content) self._check_status(server_response, url) parsed_response = fromstring(server_response.content) site_id = parsed_response.find(".//t:site", namespaces=self.parent_srv.namespace).get("id", None) diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index 10d3cba84..fa9540534 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -181,9 +181,6 @@ def _make_request( # BE CAREFUL WHEN SHARING THESE RESULTS - MAY CONTAIN YOUR SENSITIVE DATA # logger.debug(loggable_response) - if content_type == "application/xml": - self.parent_srv._namespace.detect(server_response.content) - return server_response def _follow_redirect_if_any( diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index 853bdea68..fc5cdb976 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -51,7 +51,6 @@ EndpointUnavailableError, ) from tableauserverclient.server.endpoint.exceptions import NotSignedInError -from tableauserverclient.namespace import Namespace _PRODUCT_TO_REST_VERSION = { "10.0": "2.3", @@ -64,6 +63,11 @@ minimum_supported_server_version = "2.3" default_server_version = "2.4" # first version that dropped the legacy auth endpoint +# The REST API has used the "http://tableau.com/api" XML namespace since Tableau +# Server 8.3 (2015). Callers on older servers are unsupported. +NAMESPACE = "http://tableau.com/api" +_NAMESPACE_MAP = {"t": NAMESPACE} + class _AuthState(NamedTuple): """ @@ -224,7 +228,6 @@ def __init__(self, server_address, use_server_version=False, http_options=None, self.data_acceleration_report = DataAccelerationReport(self) self.data_alerts = DataAlerts(self) self.fileuploads = Fileuploads(self) - self._namespace = Namespace() self.flow_runs = FlowRuns(self) self.metrics = Metrics(self) self.custom_views = CustomViews(self) @@ -381,7 +384,7 @@ def baseurl(self): @property def namespace(self): - return self._namespace() + return dict(_NAMESPACE_MAP) @property def auth_token(self): diff --git a/test/test_namespace.py b/test/test_namespace.py new file mode 100644 index 000000000..2b4a891cc --- /dev/null +++ b/test/test_namespace.py @@ -0,0 +1,20 @@ +import tableauserverclient as TSC +from tableauserverclient.server import server as server_module + + +def test_default_namespace_matches_server_namespace() -> None: + # TSC.DEFAULT_NAMESPACE is a literal in __init__.py (importing it from server.py is circular). + assert TSC.DEFAULT_NAMESPACE == server_module.NAMESPACE + + +def test_server_namespace_maps_t_prefix() -> None: + server = TSC.Server("http://test", False) + assert server.namespace == {"t": TSC.DEFAULT_NAMESPACE} + + +def test_server_namespace_is_a_copy() -> None: + server = TSC.Server("http://test", False) + server.namespace["t"] = "mutated" + server.namespace["x"] = "extra" + assert server.namespace == {"t": TSC.DEFAULT_NAMESPACE} + assert TSC.Server("http://other", False).namespace == {"t": TSC.DEFAULT_NAMESPACE}