8000 Jac/show server info (#1118) · LehmD/server-client-python@d71b978 · GitHub
[go: up one dir, main page]

Skip to content

Commit d71b978

Browse files
authored
Jac/show server info (tableau#1118)
1 parent a62ad5a commit d71b978

File tree

7 files changed

+100
-45
lines changed

7 files changed

+100
-45
lines changed

contributing.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,22 @@ pytest
6666
pip install .
6767
```
6868

69+
### Debugging Tools
70+
See what your outgoing requests look like: https://requestbin.net/ (unaffiliated link not under our control)
71+
72+
6973
### Before Committing
7074

7175
Our CI runs include a Python lint run, so you should run this locally and fix complaints before committing as this will fail your checkin.
7276

7377
```shell
7478
# this will run the formatter without making changes
75-
black --line-length 120 tableauserverclient test samples --check
79+
black . --check
7680

7781
# this will format the directory and code for you
78-
black --line-length 120 tableauserverclient test samples
82+
black .
7983

8084
# this will run type checking
8185
pip install mypy
82-
mypy --show-error-codes --disable-error-code misc --disable-error-code import tableauserverclient test
86+
mypy tableauserverclient test samples
8387
```

tableauserverclient/models/server_info_item.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import warnings
2+
import xml
3+
14
from defusedxml.ElementTree import fromstring
25

36

@@ -32,7 +35,11 @@ def rest_api_version(self):
3235

3336
@classmethod
3437
def from_response(cls, resp, ns):
35-
parsed_response = fromstring(resp)
38+
try:
39+
parsed_response = fromstring(resp)
40+
except xml.etree.ElementTree.ParseError as error:
41+
warnings.warn("Unexpected response for ServerInfo: {}".format(resp))
42+
return cls("Unknown", "Unknown", "Unknown")
3643
product_version_tag = parsed_response.find(".//t:productVersion", namespaces=ns)
3744
rest_api_version_tag = parsed_response.find(".//t:restApiVersion", namespaces=ns)
3845

tableauserverclient/models/site_item.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import warnings
22
import xml.etree.ElementTree as ET
33

4-
from distutils.version import Version
54
from defusedxml.ElementTree import fromstring
65
from .property_decorators import (
76
property_is_enum,

tableauserverclient/server/endpoint/endpoint.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import requests
22
import logging
3-
from distutils.version import LooseVersion as Version
3+
from packaging.version import Version
44
from functools import wraps
55
from xml.etree.ElementTree import ParseError
66
from typing import Any, Callable, Dict, Optional, TYPE_CHECKING
@@ -83,14 +83,12 @@ def _check_status(self, server_response, url: str = None):
8383
if server_response.status_code >= 500:
8484
raise InternalServerError(server_response, url)
8585
elif server_response.status_code not in Success_codes:
86-
# todo: is an error reliably of content-type application/xml?
8786
try:
8887
raise ServerResponseError.from_response(server_response.content, self.parent_srv.namespace, url)
8988
except ParseError:
90-
# This will happen if we get a non-success HTTP code that
91-
# doesn't return an xml error object (like metadata endpoints or 503 pages)
92-
# we convert this to a better exception and pass through the raw
93-
E41A # response body
89+
# This will happen if we get a non-success HTTP code that doesn't return an xml error object
90+
# e.g metadata endpoints, 503 pages, totally different servers
91+
# we convert this to a better exception and pass through the raw response body
9492
raise NonXMLResponseError(server_response.content)
9593
except Exception:
9694
# anything else re-raise here
@@ -188,7 +186,7 @@ def api(version):
188186
def _decorator(func):
189187
@wraps(func)
190188
def wrapper(self, *args, **kwargs):
191-
self.parent_srv.assert_at_least_version(version, "endpoint")
189+
self.parent_srv.assert_at_least_version(version, self.__class__.__name__)
192190
return func(self, *args, **kwargs)
193191

194192
return wrapper

tableauserverclient/server/endpoint/server_info_endpoint.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@
1212

1313

1414
class ServerInfo(Endpoint):
15+
def __init__(self, server):
16+
self.parent_srv = server
17+
self._info = None
18+
19+
@property
20+
def serverInfo(self):
21+
if not self._info:
22+
self.get()
23+
return self._info
24+
25+
def __repr__(self):
26+
return "<Endpoint {}>".format(self.serverInfo)
27+
1528
@property
1629
def baseurl(self):
1730
return "{0}/serverInfo".format(self.parent_srv.baseurl)
@@ -23,10 +36,10 @@ def get(self):
2336
server_response = self.get_unauthenticated_request(self.baseurl)
2437
except ServerResponseError as e:
2538
if e.code == "404003":
26-
raise ServerInfoEndpointNotFoundError
39+
raise ServerInfoEndpointNotFoundError(e)
2740
if e.code == "404001":
28-
raise EndpointUnavailableError
41+
raise EndpointUnavailableError(e)
2942
raise e
3043

31-
server_info = ServerInfoItem.from_response(server_response.content, self.parent_srv.namespace)
32-
return server_info
44+
self._info = ServerInfoItem.from_response(server_response.content, self.parent_srv.namespace)
45+
return self._info

tableauserverclient/server/server.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
import requests
24
import urllib3
35

@@ -54,16 +56,14 @@ class PublishMode:
5456
Overwrite = "Overwrite"
5557
CreateNew = "CreateNew"
5658

57-
def __init__(self, server_address, use_server_version=False, http_options=None, session_factory=requests.Session):
58-
self._server_address = server_address
59+
def __init__(self, server_address, use_server_version=False, http_options=None, session_factory=None):
5960
self._auth_token = None
6061
self._site_id = None
6162
self._user_id = None
62-
self._session_factory = session_factory
63-
self._session = session_factory()
64-
self._http_options = dict()
6563

66-
self.version = default_server_version
64+
self._server_address = server_address
65+
self._session_factory = session_factory or requests.session
66+
6767
self.auth = Auth(self)
6868
self.views = Views(self)
6969
self.users = Users(self)
@@ -90,29 +90,39 @@ def __init__(self, server_address, use_server_version=False, http_options=None,
9090
self.flow_runs = FlowRuns(self)
9191
self.metrics = Metrics(self)
9292

93-
# must set this before calling use_server_version, because that's a server call
93+
self._session = self._session_factory()
94+
self._http_options = dict() # must set this before making a server call
9495
if http_options:
9596
self.add_http_options(http_options)
9697

98+
self.validate_server_connection()
99+
100+
self.version = default_server_version
97101
if use_server_version:
98-
self.use_server_version()
99-
100-
def add_http_options(self, option_pair: dict):
101-
if not option_pair:
102-
# log debug message
103-
return
104-
if len(option_pair) != 1:
105-
raise ValueError(
106-
"Update headers one at a time. Expected type: ",
107-
{"key": 12}.__class__,
108-
"Actual type: ",
109-
option_pair,
110-
option_pair.__class__,
111-
)
112-
self._http_options.update(option_pair)
113-
if "verify" in option_pair.keys() and self._http_options.get("verify") is False:
114-
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
115-
# would be nice if you could turn them back on
102+
self.use_server_version() # this makes a server call
103+
104+
def validate_server_connection(self):
105+
try:
106+
self._session.prepare_request(requests.Request("GET", url=self._server_address, params=self._http_options))
107+
except Exception as req_ex:
108+
warnings.warn("Invalid server initialization\n {}".format(req_ex.__str__()), UserWarning)
109+
print("==================")
110+
111+
def __repr__(self):
112+
return "<TableauServerClient> [Connection: {}, {}]".format(self.baseurl, self.server_info.serverInfo)
113+
114+
def add_http_options(self, options_dict: dict):
115+
try:
116+
self._http_options.update(options_dict)
117+
if "verify" in options_dict.keys() and self._http_options.get("verify") is False:
118+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
119+
# would be nice if you could turn them back on
120+
except BaseException as be:
121+
print(be)
122+
# expected errors on invalid input:
123+
# 'set' object has no attribute 'keys', 'list' object has no attribute 'keys'
124+
# TypeError: cannot convert dictionary update sequence element #0 to a sequence (input is a tuple)
125+
raise ValueError("Invalid http options given: {}".format(options_dict))
116126

117127
def clear_http_options(self):
118128
self._http_options = dict()
@@ -142,9 +152,10 @@ def _determine_highest_version(self):
142152
version = self.server_info.get().rest_api_version
143153
except ServerInfoEndpointNotFoundError:
144154
version = self._get_legacy_version()
155+
except BaseException:
156+
version = self._get_legacy_version()
145157

146-
finally:
147-
self.version = old_version
158+
self.version = old_version
148159

149160
return version
150161

test/http/test_http_requests.py

Lines changed: 25 additions & 2 deletions
32CA
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import tableauserverclient as TSC
22
import unittest
3+
import requests
4+
5+
from requests_mock import adapter, mock
36
from requests.exceptions import MissingSchema
47

58

@@ -33,16 +36,20 @@ def test_init_server_model_bad_server_name_not_version_check_random_options(self
3336
server = TSC.Server("fake-url", use_server_version=False, http_options={"foo": 1})
3437

3538
def test_init_server_model_bad_server_name_not_version_check_real_options(self):
36-
# by default, it will attempt to contact the server to check it's version
3739
server = TSC.Server("fake-url", use_server_version=False, http_options={"verify": False})
3840

3941
def test_http_options_skip_ssl_works(self):
4042
http_options = {"verify": False}
4143
server = TSC.Server("http://fake-url")
4244
server.add_http_options(http_options)
4345

46+
def test_http_options_multiple_options_works(self):
47+
http_options = {"verify": False, "birdname": "Parrot"}
48+
server = TSC.Server("http://fake-url")
49+
server.add_http_options(http_options)
50+
4451
# ValueError: dictionary update sequence element #0 has length 1; 2 is required
45-
def test_http_options_multiple_options_fails(self):
52+
def test_http_options_multiple_dicts_fails(self):
4653
http_options_1 = {"verify": False}
4754
http_options_2 = {"birdname": "Parrot"}
4855
server = TSC.Server("http://fake-url")
@@ -54,3 +61,19 @@ def test_http_options_not_sequence_fails(self):
5461
server = TSC.Server("http://fake-url")
5562
with self.assertRaises(ValueError):
5663
server.add_http_options({1, 2, 3})
64+
65+
66+
class SessionTests(unittest.TestCase):
67+
test_header = {"x-test": "true"}
68+
69+
@staticmethod
70+
def session_factory():
71+
session = requests.session()
72+
session.headers.update(SessionTests.test_header)
73+
return session
74+
75+
def test_session_factory_adds_headers(self):
76+
test_request_bin = "http://capture-this-with-mock.com"
77+
with mock() as m:
78+
m.get(url="http://capture-this-with-mock.com/api/2.4/serverInfo", request_headers=SessionTests.test_header)
79+
server = TSC.Server(test_request_bin, use_server_version=True, session_factory=SessionTests.session_factory)

0 commit comments

Comments
 (0)
0