Skip to content

Commit 2ca2847

Browse files
committed
feat: retrieve tableau server product name
Closes #1592 Calls a VizPortal API to retrieve the detailed product name, and attaches it to the TSC.Server object which will make determining what requests to build for things like subscriptions easier. Adds this functionality into the pre-existing `use_server_version` function because: 1. Users of the library will already know to call that method. 2. Users already aware that method makes API calls. If the request errors or if the payload doesn't match the expected format, defaults to assuming TSC is talking with an on-prem Server instance. Co-authored-by: emeric-dsj
1 parent e51369c commit 2ca2847

File tree

6 files changed

+75
-5
lines changed

6 files changed

+75
-5
lines changed

tableauserverclient/server/endpoint/endpoint.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ def log_response_safely(self, server_response: "Response") -> str:
189189
loggable_response = helpers.strings.redact_xml(server_response.content.decode(server_response.encoding))
190190
return loggable_response
191191

192-
def get_unauthenticated_request(self, url):
193-
return self._make_request(self.parent_srv.session.get, url)
192+
def get_unauthenticated_request(self, url, parameters=None):
193+
return self._make_request(self.parent_srv.session.get, url, parameters=parameters)
194194

195195
def get_request(self, url, request_object=None, parameters=None):
196196
if request_object is not None:

tableauserverclient/server/endpoint/server_info_endpoint.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import Union
2+
from typing import Literal, Union, TYPE_CHECKING
33

44
from .endpoint import Endpoint, api
55
from .exceptions import ServerResponseError
@@ -9,10 +9,15 @@
99
)
1010
from tableauserverclient.models import ServerInfoItem
1111

12+
if TYPE_CHECKING:
13+
from tableauserverclient.server import Server
14+
15+
Products = Literal["TableauServer", "TableauOnline"]
16+
1217

1318
class ServerInfo(Endpoint):
1419
def __init__(self, server):
15-
self.parent_srv = server
20+
self.parent_srv: "Server" = server
1621
self._info = None
1722

1823
@property
@@ -80,3 +85,25 @@ def get(self) -> Union[ServerInfoItem, None]:
8085
logging.getLogger(self.__class__.__name__).debug(e)
8186
logging.getLogger(self.__class__.__name__).debug(server_response.content)
8287
return self._info
88+
89+
def _get_product_info(self) -> Products:
90+
"""
91+
Retrieve the server product information to determine if the server is
92+
Tableau Server or Tableau Online.
93+
"""
94+
method = "getServerSettingsUnauthenticated"
95+
response = self.parent_srv.session.post(
96+
f"{self.parent_srv.server_address}/vizportal/api/web/v1/{method}",
97+
headers={"Content-Type": "application/json"},
98+
verify=self.parent_srv.http_options.get("verify", True),
99+
json={"method": method, "params": {}},
100+
)
101+
if not response.ok:
102+
return "TableauServer"
103+
else:
104+
try:
105+
return response.json().get("result", {}).get("product", "TableauServer")
106+
except Exception as e:
107+
logging.getLogger(self.__class__.__name__).debug(e)
108+
logging.getLogger(self.__class__.__name__).debug("Failed to parse product info response.")
109+
return "TableauServer"

tableauserverclient/server/server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def __init__(self, server_address, use_server_version=False, http_options=None,
145145
self._site_id = None
146146
self._user_id = None
147147
self._ssl_context = None
148+
self._product = "TableauServer" # default product type
148149

149150
# TODO: this needs to change to default to https, but without breaking existing code
150151
if not server_address.startswith("http://") and not server_address.startswith("https://"):
@@ -269,6 +270,7 @@ def _determine_highest_version(self):
269270

270271
def use_server_version(self):
271272
self.version = self._determine_highest_version()
273+
self._product = self.server_info._get_product_info()
272274

273275
def use_highest_version(self):
274276
self.use_server_version()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"result": {"product": "TableauOnline"}}

test/http/test_http_requests.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ def __init__(self, status_code):
2727
return MockResponse(200)
2828

2929

30+
# This method will be used by the mock to replace requests.get
31+
def mocked_requests_post(*args, **kwargs):
32+
class MockResponse:
33+
def __init__(self, status_code):
34+
self.headers = {}
35+
self.encoding = None
36+
self.content = '{"result": {"product": "TableauOnline"}}'
37+
self.status_code = status_code
38+
self.ok = True
39+
40+
return MockResponse(200)
41+
42+
3043
class ServerTests(unittest.TestCase):
3144
def test_init_server_model_empty_throws(self):
3245
with self.assertRaises(TypeError):
@@ -46,7 +59,8 @@ def test_init_server_model_bad_server_name_not_version_check(self):
4659
server = TSC.Server("fake-url", use_server_version=False)
4760

4861
@mock.patch("requests.sessions.Session.get", side_effect=mocked_requests_get)
49-
def test_init_server_model_bad_server_name_do_version_check(self, mock_get):
62+
@mock.patch("requests.sessions.Session.post", side_effect=mocked_requests_post)
63+
def test_init_server_model_bad_server_name_do_version_check(self, mock_get, mock_post):
5064
server = TSC.Server("fake-url", use_server_version=True)
5165

5266
def test_init_server_model_bad_server_name_not_version_check_random_options(self):
@@ -114,4 +128,5 @@ def test_session_factory_adds_headers(self):
114128
test_request_bin = "http://capture-this-with-mock.com"
115129
with requests_mock.mock() as m:
116130
m.get(url="http://capture-this-with-mock.com/api/2.4/serverInfo", request_headers=SessionTests.test_header)
131+
m.post(f"{test_request_bin}/vizportal/api/web/v1/getServerSettingsUnauthenticated", json={})
117132
server = TSC.Server(test_request_bin, use_server_version=True, session_factory=SessionTests.session_factory)

test/test_server_info.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os.path
23
import unittest
34

@@ -13,6 +14,7 @@
1314
SERVER_INFO_404 = os.path.join(TEST_ASSET_DIR, "server_info_404.xml")
1415
SERVER_INFO_AUTH_INFO_XML = os.path.join(TEST_ASSET_DIR, "server_info_auth_info.xml")
1516
SERVER_INFO_WRONG_SITE = os.path.join(TEST_ASSET_DIR, "server_info_wrong_site.html")
17+
SERVER_PRODUCT_INFO = os.path.join(TEST_ASSET_DIR, "getServerSettingsUnauthenticated.json")
1618

1719

1820
class ServerInfoTests(unittest.TestCase):
@@ -26,6 +28,7 @@ def test_server_info_get(self):
2628
response_xml = f.read().decode("utf-8")
2729
with requests_mock.mock() as m:
2830
m.get(self.server.server_info.baseurl, text=response_xml)
31+
m.post(f"{self.server.server_address}/vizportal/api/web/v1/getServerSettingsUnauthenticated", json={})
2932
actual = self.server.server_info.get()
3033

3134
self.assertEqual("10.1.0", actual.product_version)
@@ -43,6 +46,8 @@ def test_server_info_use_highest_version_downgrades(self):
4346
# Return a 404 for serverInfo so we can pretend this is an old Server
4447
m.get(self.server.server_address + "/api/2.4/serverInfo", text=si_response_xml, status_code=404)
4548
m.get(self.server.server_address + "/auth?format=xml", text=auth_response_xml)
49+
m.post(f"{self.server.server_address}/vizportal/api/web/v1/getServerSettingsUnauthenticated", json={})
50+
4651
self.server.use_server_version()
4752
# does server-version[9.2] lookup in PRODUCT_TO_REST_VERSION
4853
self.assertEqual(self.server.version, "2.2")
@@ -52,6 +57,7 @@ def test_server_info_use_highest_version_upgrades(self):
5257
si_response_xml = f.read().decode("utf-8")
5358
with requests_mock.mock() as m:
5459
m.get(self.server.server_address + "/api/2.8/serverInfo", text=si_response_xml)
60+
m.post(f"{self.server.server_address}/vizportal/api/web/v1/getServerSettingsUnauthenticated", json={})
5561
# Pretend we're old
5662
self.server.version = "2.8"
5763
self.server.use_server_version()
@@ -63,6 +69,7 @@ def test_server_use_server_version_flag(self):
6369
si_response_xml = f.read().decode("utf-8")
6470
with requests_mock.mock() as m:
6571
m.get("http://test/api/2.4/serverInfo", text=si_response_xml)
72+
m.post(f"{self.server.server_address}/vizportal/api/web/v1/getServerSettingsUnauthenticated", json={})
6673
server = TSC.Server("http://test", use_server_version=True)
6774
self.assertEqual(server.version, "2.5")
6875

@@ -73,3 +80,21 @@ def test_server_wrong_site(self):
7380
m.get(self.server.server_info.baseurl, text=response, status_code=404)
7481
with self.assertRaises(NonXMLResponseError):
7582
self.server.server_info.get()
83+
84+
def test_server_info_product(self):
85+
with open(SERVER_PRODUCT_INFO) as f:
86+
product_info_json = json.load(f)
87+
88+
with requests_mock.mock() as m:
89+
m.post(
90+
f"{self.server.server_address}/vizportal/api/web/v1/getServerSettingsUnauthenticated",
91+
json=product_info_json,
92+
)
93+
self.server.use_server_version()
94+
assert self.server._product == "TableauOnline"
95+
96+
def test_server_info_product_no_response(self):
97+
with requests_mock.mock() as m:
98+
m.post(f"{self.server.server_address}/vizportal/api/web/v1/getServerSettingsUnauthenticated", json={})
99+
self.server.use_server_version()
100+
assert self.server._product == "TableauServer"

0 commit comments

Comments
 (0)