8000 feat(exceptions): separate failed signin error (#1478) · TrimPeachu/server-client-python@b49eac5 · GitHub
[go: up one dir, main page]

Skip to content

Commit b49eac5

Browse files
authored
feat(exceptions): separate failed signin error (tableau#1478)
* feat(exceptions): separate failed signin error Closes tableau#1472 This makes sign in failures their own class of exceptions, while still inheriting from NotSignedInException to not break backwards compatability for any existing client code. This should allow users to get out more specific exceptions more easily on what failed with their authentication request. * fix(error): raise exception when ServerInfo.get fails If ServerInfoItem.from_response gets invalid XML, raise the error immediately instead of suppressing the error and setting an invalid version number * fix(test): add missing test asset --------- Co-authored-by: Jordan Woods <13803242+jorwoods@users.noreply.github.com>
1 parent e1b8281 commit b49eac5

File tree

8 files changed

+98
-14
lines changed

8 files changed

+98
-14
lines changed

tableauserverclient/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
PDFRequestOptions,
5757
RequestOptions,
5858
MissingRequiredFieldError,
59+
FailedSignInError,
5960
NotSignedInError,
6061
ServerResponseError,
6162
Filter,
@@ -79,6 +80,7 @@
7980
"DatabaseItem",
8081
"DataFreshnessPolicyItem",
8182
"DatasourceItem",
83+
"FailedSignInError",
8284
"FavoriteItem",
8385
"FlowItem",
8486
"FlowRunItem",

tableauserverclient/models/server_info_item.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,11 @@ def from_response(cls, resp, ns):
4040
try:
4141
parsed_response = fromstring(resp)
4242
except xml.etree.ElementTree.ParseError as error:
43-
logger.info(f"Unexpected response for ServerInfo: {resp}")
44-
logger.info(error)
43+
logger.exception(f"Unexpected response for ServerInfo: {resp}")
4544
return cls("Unknown", "Unknown", "Unknown")
4645
except Exception as error:
47-
logger.info(f"Unexpected response for ServerInfo: {resp}")
48-
logger.info(error)
49-
return cls("Unknown", "Unknown", "Unknown")
46+
logger.exception(f"Unexpected response for ServerInfo: {resp}")
47+
raise error
5048

5149
product_version_tag = parsed_response.find(".//t:productVersion", namespaces=ns)
5250
rest_api_version_tag = parsed_response.find(".//t:restApiVersion", namespaces=ns)

tableauserverclient/server/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from tableauserverclient.server.sort import Sort
1212
from tableauserverclient.server.server import Server
1313
from tableauserverclient.server.pager import Pager
14-
from tableauserverclient.server.endpoint.exceptions import NotSignedInError
14+
from tableauserverclient.server.endpoint.exceptions import FailedSignInError, NotSignedInError
1515

1616
from tableauserverclient.server.endpoint import (
1717
Auth,
@@ -57,6 +57,7 @@
5757
"Sort",
5858
"Server",
5959
"Pager",
60+
"FailedSignInError",
6061
"NotSignedInError",
6162
"Auth",
6263
"CustomViews",

tableauserverclient/server/endpoint/endpoint.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from tableauserverclient.server.request_options import RequestOptions
2020

2121
from tableauserverclient.server.endpoint.exceptions import (
22+
FailedSignInError,
2223
ServerResponseError,
2324
InternalServerError,
2425
NonXMLResponseError,
@@ -160,7 +161,7 @@ def _check_status(self, server_response: "Response", url: Optional[str] = None):
160161
try:
161162
if server_response.status_code == 401:
162163
# TODO: catch this in server.py and attempt to sign in again, in case it's a session expiry
163-
raise NotSignedInError(server_response.content, url)
164+
raise FailedSignInError.from_response(server_response.content, self.parent_srv.namespace, url)
164165

165166
raise ServerResponseError.from_response(server_response.content, self.parent_srv.namespace, url)
166167
except ParseError:

tableauserverclient/server/endpoint/exceptions.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
from defusedxml.ElementTree import fromstring
2-
from typing import Optional
2+
from typing import Mapping, Optional, TypeVar
3+
4+
5+
def split_pascal_case(s: str) -> str:
6+
return "".join([f" {c}" if c.isupper() else c for c in s]).strip()
37

48

59
class TableauError(Exception):
610
pass
711

812

9-
class ServerResponseError(TableauError):
10-
def __init__(self, code, summary, detail, url=None):
13+
T = TypeVar("T")
14+
15+
16+
class XMLError(TableauError):
17+
def __init__(self, code: str, summary: str, detail: str, url: Optional[str] = None) -> None:
1118
self.code = code
1219
self.summary = summary
1320
self.detail = detail
@@ -18,7 +25,7 @@ def __str__(self):
1825
return f"\n\n\t{self.code}: {self.summary}\n\t\t{self.detail}"
1926

2027
@classmethod
21-
def from_response(cls, resp, ns, url=None):
28+
def from_response(cls, resp, ns, url):
2229
# Check elements exist before .text
2330
parsed_response = fromstring(resp)
2431
try:
@@ -33,6 +40,10 @@ def from_response(cls, resp, ns, url=None):
3340
return error_response
3441

3542

43+
class ServerResponseError(XMLError):
44+
pass
45+
46+
3647
class InternalServerError(TableauError):
3748
def __init__(self, server_response, request_url: Optional[str] = None):
3849
self.code = server_response.status_code
@@ -51,6 +62,11 @@ class NotSignedInError(TableauError):
5162
pass
5263

5364

65+
class FailedSignInError(XMLError, NotSignedInError):
66+
def __str__(self):
67+
return f"{split_pascal_case(self.__class__.__name__)}: {super().__str__()}"
68+
69+
5470
class ItemTypeNotAllowed(TableauError):
5571
pass
5672

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<html lang="en">
2+
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Example website</title>
7+
</head>
8+
9+
<body>
10+
<table>
11+
<th>
12+
<td>A</td>
13+
<td>B</td>
14+
<td>C</td>
15+
<td>D</td>
16+
<td>E</td>
17+
</th>
18+
<tr>
19+
<td>1</td>
20+
<td>2</td>
21+
<td>3</td>
22+
<td>4</td>
23+
<td>5</td>
24+
</tr>
25+
<tr>
26+
<td>2</td>
27+
<td>3</td>
28+
<td>4</td>
29+
<td>5</td>
30+
<td>6</td>
31+
</tr>
32+
<tr>
33+
<td>3</td>
34+
<td>4</td>
35+
<td>5</td>
36+
<td>6</td>
37+
<td>7</td>
38+
</tr>
39+
<tr>
40+
<td>4</td>
41+
<td>5</td>
42+
<td>6</td>
43+
<td>7</td>
44+
<td>8</td>
45+
</tr>
46+
<tr>
47+
<td>5</td>
48+
<td>6</td>
49+
<td>7</td>
50+
<td>8</td>
51+
<td>9</td>
52+
</tr>
53+
</table>
54+
</body>
55+
56+
</html>

test/test_auth.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,23 @@ def test_sign_in_error(self):
6363
with requests_mock.mock() as m:
6464
m.post(self.baseurl + "/signin", text=response_xml, status_code=401)
6565
tableau_auth = TSC.TableauAuth("testuser", "wrongpassword")
66-
self.assertRaises(TSC.NotSignedInError, self.server.auth.sign_in, tableau_auth)
66+
self.assertRaises(TSC.FailedSignInError, self.server.auth.sign_in, tableau_auth)
6767

6868
def test_sign_in_invalid_token(self):
6969
with open(SIGN_IN_ERROR_XML, "rb") as f:
7070
response_xml = f.read().decode("utf-8")
7171
with requests_mock.mock() as m:
7272
m.post(self.baseurl + "/signin", text=response_xml, status_code=401)
7373
tableau_auth = TSC.PersonalAccessTokenAuth(token_name="mytoken", personal_access_token="invalid")
74-
self.assertRaises(TSC.NotSignedInError, self.server.auth.sign_in, tableau_auth)
74+
self.assertRaises(TSC.FailedSignInError, self.server.auth.sign_in, tableau_auth)
7575

7676
def test_sign_in_without_auth(self):
7777
with open(SIGN_IN_ERROR_XML, "rb") as f:
7878
response_xml = f.read().decode("utf-8")
7979
with requests_mock.mock() as m:
8080
m.post(self.baseurl + "/signin", text=response_xml, status_code=401)
8181
tableau_auth = TSC.TableauAuth("", "")
82-
self.assertRaises(TSC.NotSignedInError, self.server.auth.sign_in, tableau_auth)
82+
self.assertRaises(TSC.FailedSignInError, self.server.auth.sign_in, tableau_auth)
8383

8484
def test_sign_out(self):
8585
with open(SIGN_IN_XML, "rb") as f:

test/test_server_info.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import requests_mock
55

66
import tableauserverclient as TSC
7+
from tableauserverclient.server.endpoint.exceptions import NonXMLResponseError
78

89
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets")
910

1011
SERVER_INFO_GET_XML = os.path.join(TEST_ASSET_DIR, "server_info_get.xml")
1112
SERVER_INFO_25_XML = os.path.join(TEST_ASSET_DIR, "server_info_25.xml")
1213
SERVER_INFO_404 = os.path.join(TEST_ASSET_DIR, "server_info_404.xml")
1314
SERVER_INFO_AUTH_INFO_XML = os.path.join(TEST_ASSET_DIR, "server_info_auth_info.xml")
15+
SERVER_INFO_WRONG_SITE = os.path.join(TEST_ASSET_DIR, "server_info_wrong_site.html")
1416

1517

1618
class ServerInfoTests(unittest.TestCase):
@@ -63,3 +65,11 @@ def test_server_use_server_version_flag(self):
6365
m.get("http://test/api/2.4/serverInfo", text=si_response_xml)
6466
server = TSC.Server("http://test", use_server_version=True)
6567
self.assertEqual(server.version, "2.5")
68+
69+
def test_server_wrong_site(self):
70+
with open(SERVER_INFO_WRONG_SITE, "rb") as f:
71+
response = f.read().decode("utf-8")
72+
with requests_mock.mock() as m:
73+
m.get(self.server.server_info.baseurl, text=response, status_code=404)
74+
with self.assertRaises(NonXMLResponseError):
75+
self.server.server_info.get()

0 commit comments

Comments
 (0)
0