Skip to content

Commit 4873a58

Browse files
jacalataMrwanBaghdadjorwoodsbcantoniTrimPeachu
authored
Patch fix: 0.23.1 (#1129)
* Allow injection of session_factory to allow use of a custom session * Jac/show server info (#1118) * Fix bug in exposing ExcelRequestOptions and test (#1123) * Fix a few pylint errors (#1124) * fix behavior when url has no protocol (#1125) * smoke test for pypi * Add permission control for Data Roles and Metrics (Issue #1063) (#1120) Co-authored-by: Marwan Baghdad <[email protected]> Co-authored-by: jorwoods <[email protected]> Co-authored-by: Brian Cantoni <[email protected]> Co-authored-by: TrimPeachu <[email protected]>
1 parent ef9e7fd commit 4873a58

File tree

12 files changed

+156
-31
lines changed

12 files changed

+156
-31
lines changed
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# This workflow will install TSC from pypi and validate that it runs. For more information see:
2+
# https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3+
4+
name: Pypi smoke tests
5+
6+
on:
7+
workflow_dispatch:
8+
schedule:
9+
- cron: 0 11 * * * # Every day at 11AM UTC (7AM EST)
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
build:
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
os: [ubuntu-latest, macos-latest, windows-latest]
20+
python-version: ['3.x']
21+
22+
runs-on: ${{ matrix.os }}
23+
24+
steps:
25+
- name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }}
26+
uses: actions/setup-python@v4
27+
with:
28+
python-version: ${{ matrix.python-version }}
29+
- name: pip install
30+
run: |
31+
pip uninstall tableauserverclient
32+
pip install tableauserverclient
33+
- name: Launch app
34+
run: |
35+
python -c "import tableauserverclient as TSC
36+
server = TSC.Server('http://example.com', use_server_version=False)"

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2016 Tableau
3+
Copyright (c) 2022 Tableau
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

samples/smoke_test.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# This sample verifies that tableau server client is installed
2+
# and you can run it. It also shows the version of the client.
3+
4+
import tableauserverclient as TSC
5+
6+
server = TSC.Server("Fake-Server-Url", use_server_version=False)
7+
print("Client details:")
8+
print(TSC.server.endpoint.Endpoint._make_common_headers("fake-token", "any-content"))

tableauserverclient/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ._version import get_versions
12
from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
23
from .models import (
34
BackgroundJobItem,

tableauserverclient/models/tableau_auth.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,6 @@ def credentials(self):
6565
}
6666

6767
def __repr__(self):
68-
return "<PersonalAccessToken name={} token={} site={}>".format(
68+
return "<PersonalAccessToken name={} token={}>(site={})".format(
6969
self.token_name, self.personal_access_token[:2] + "...", self.site_id
7070
)

tableauserverclient/models/tableau_types.py

+2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111

1212
class Resource:
1313
Database = "database"
14+
Datarole = "datarole"
1415
Datasource = "datasource"
1516
Flow = "flow"
1617
Lens = "lens"
18+
Metric = "metric"
1719
Project = "project"
1820
Table = "table"
1921
View = "view"

tableauserverclient/server/endpoint/auth_endpoint.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,18 @@ def baseurl(self):
2828
def sign_in(self, auth_req):
2929
url = "{0}/{1}".format(self.baseurl, "signin")
3030
signin_req = RequestFactory.Auth.signin_req(auth_req)
31-
server_response = self.parent_srv.session.post(url, data=signin_req, **self.parent_srv.http_options)
31+
server_response = self.parent_srv.session.post(
32+
url, data=signin_req, **self.parent_srv.http_options, allow_redirects=False
33+
)
34+
# manually handle a redirect so that we send the correct POST request instead of GET
35+
# this will make e.g http://online.tableau.com work to redirect to http://east.online.tableau.com
36+
if server_response.status_code == 301:
37+
server_response = self.parent_srv.session.post(
38+
server_response.headers["Location"],
39+
data=signin_req,
40+
**self.parent_srv.http_options,
41+
allow_redirects=False,
42+
)
3243
self.parent_srv._namespace.detect(server_response.content)
3344
self._check_status(server_response, url)
3445
parsed_response = fromstring(server_response.content)

tableauserverclient/server/endpoint/endpoint.py

+11-12
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
EndpointUnavailableError,
1313
)
1414
from ..query import QuerySet
15-
from ... import helpers
16-
from ..._version import get_versions
15+
from ... import helpers, get_versions
1716

18-
__TSC_VERSION__ = get_versions()["version"]
19-
del get_versions
17+
if TYPE_CHECKING:
18+
from ..server import Server
19+
from requests import Response
2020

2121
logger = logging.getLogger("tableau.endpoint")
2222

@@ -25,11 +25,9 @@
2525
XML_CONTENT_TYPE = "text/xml"
2626
JSON_CONTENT_TYPE = "application/json"
2727

28-
USERAGENT_HEADER = "User-Agent"
29-
30-
if TYPE_CHECKING:
31-
from ..server import Server
32-
from requests import Response
28+
CONTENT_TYPE_HEADER = "content-type"
29+
TABLEAU_AUTH_HEADER = "x-tableau-auth"
30+
USER_AGENT_HEADER = "User-Agent"
3331

3432

3533
class Endpoint(object):
@@ -38,12 +36,13 @@ def __init__(self, parent_srv: "Server"):
3836

3937
@staticmethod
4038
def _make_common_headers(auth_token, content_type):
39+
_client_version: Optional[str] = get_versions()["version"]
4140
headers = {}
4241
if auth_token is not None:
43-
headers["x-tableau-auth"] = auth_token
42+
headers[TABLEAU_AUTH_HEADER] = auth_token
4443
if content_type is not None:
45-
headers["content-type"] = content_type
46-
headers["User-Agent"] = "Tableau Server Client/{}".format(__TSC_VERSION__)
44+
headers[CONTENT_TYPE_HEADER] = content_type
45+
headers[USER_AGENT_HEADER] = "Tableau Server Client/{}".format(_client_version)
4746
return headers
4847

4948
def _make_request(

tableauserverclient/server/endpoint/projects_endpoint.py

+24
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ def populate_workbook_default_permissions(self, item):
9999
def populate_datasource_default_permissions(self, item):
100100
self._default_permissions.populate_default_permissions(item, Resource.Datasource)
101101

102+
@api(version="3.2")
103+
def populate_metric_default_permissions(self, item):
104+
self._default_permissions.populate_default_permissions(item, Resource.Metric)
105+
106+
@api(version="3.4")
107+
def populate_datarole_default_permissions(self, item):
108+
self._default_permissions.populate_default_permissions(item, Resource.Datarole)
109+
102110
@api(version="3.4")
103111
def populate_flow_default_permissions(self, item):
104112
self._default_permissions.populate_default_permissions(item, Resource.Flow)
@@ -115,6 +123,14 @@ def update_workbook_default_permissions(self, item, rules):
115123
def update_datasource_default_permissions(self, item, rules):
116124
return self._default_permissions.update_default_permissions(item, rules, Resource.Datasource)
117125

126+
@api(version="3.2")
127+
def update_metric_default_permissions(self, item, rules):
128+
return self._default_permissions.update_default_permissions(item, rules, Resource.Metric)
129+
130+
@api(version="3.4")
131+
def update_datarole_default_permissions(self, item, rules):
132+
return self._default_permissions.update_default_permissions(item, rules, Resource.Datarole)
133+
118134
@api(version="3.4")
119135
def update_flow_default_permissions(self, item, rules):
120136
return self._default_permissions.update_default_permissions(item, rules, Resource.Flow)
@@ -131,6 +147,14 @@ def delete_workbook_default_permissions(self, item, rule):
131147
def delete_datasource_default_permissions(self, item, rule):
132148
self._default_permissions.delete_default_permission(item, rule, Resource.Datasource)
133149

150+
@api(version="3.2")
151+
def delete_metric_default_permissions(self, item, rule):
152+
self._default_permissions.delete_default_permission(item, rule, Resource.Metric)
153+
154+
@api(version="3.4")
155+
def delete_datarole_default_permissions(self, item, rule):
156+
self._default_permissions.delete_default_permission(item, rule, Resource.Datarole)
157+
134158
@api(version="3.4")
135159
def delete_flow_default_permissions(self, item, rule):
136160
self._default_permissions.delete_default_permission(item, rule, Resource.Flow)

tableauserverclient/server/server.py

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import logging
12
import warnings
23

34
import requests
45
import urllib3
56

6-
from defusedxml.ElementTree import fromstring
7+
from defusedxml.ElementTree import fromstring, ParseError
78
from packaging.version import Version
89
from .endpoint import (
910
Sites,
@@ -61,7 +62,7 @@ def __init__(self, server_address, use_server_version=False, http_options=None,
6162
self._site_id = None
6263
self._user_id = None
6364

64-
self._server_address = server_address
65+
self._server_address: str = server_address
6566
self._session_factory = session_factory or requests.session
6667

6768
self.auth = Auth(self)
@@ -103,10 +104,13 @@ def __init__(self, server_address, use_server_version=False, http_options=None,
103104

104105
def validate_server_connection(self):
105106
try:
106-
self._session.prepare_request(requests.Request("GET", url=self._server_address, params=self._http_options))
107+
if not self._server_address.startswith("http://") and not self._server_address.startswith("https://"):
108+
self._server_address = "http://" + self._server_address
109+
self._session.prepare_request(
110+
requests.Request("GET", url=self._server_address, params=self._http_options)
111+
)
107112
except Exception as req_ex:
108-
warnings.warn("Invalid server initialization\n {}".format(req_ex.__str__()), UserWarning)
109-
print("==================")
113+
raise ValueError("Invalid server initialization", req_ex)
110114

111115
def __repr__(self):
112116
return "<TableauServerClient> [Connection: {}, {}]".format(self.baseurl, self.server_info.serverInfo)
@@ -140,7 +144,13 @@ def _set_auth(self, site_id, user_id, auth_token):
140144

141145
def _get_legacy_version(self):
142146
response = self._session.get(self.server_address + "/auth?format=xml")
143-
info_xml = fromstring(response.content)
147+
try:
148+
info_xml = fromstring(response.content)
149+
except ParseError as parseError:
150+
logging.getLogger("TSC.server").info(
151+
"Could not read server version info. The server may not be running or configured."
152+
)
153+
return self.version
144154
prod_version = info_xml.find(".//product_version").text
145155
version = _PRODUCT_TO_REST_VERSION.get(prod_version, "2.1") # 2.1
146156
return version
@@ -164,8 +174,6 @@ def use_server_version(self):
164174

165175
def use_highest_version(self):
166176
self.use_server_version()
167-
import warnings
168-
169177
warnings.warn("use use_server_version instead", DeprecationWarning)
170178

171179
def check_at_least_version(self, target: str):

test/http/test_http_requests.py

+44-8
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,58 @@
11
import tableauserverclient as TSC
22
import unittest
33
import requests
4+
import requests_mock
45

5-
from requests_mock import adapter, mock
6+
from unittest import mock
67
from requests.exceptions import MissingSchema
78

89

10+
# This method will be used by the mock to replace requests.get
11+
def mocked_requests_get(*args, **kwargs):
12+
class MockResponse:
13+
def __init__(self, status_code):
14+
self.content = (
15+
"<xml>"
16+
"<version version='0.31'>"
17+
"<api_version>0.31</api_version>"
18+
"<server_api_version>0.31</server_api_version>"
19+
"<product_version>2022.3</product_version>"
20+
"</version>"
21+
"</xml>"
22+
)
23+
self.status_code = status_code
24+
25+
return MockResponse(200)
26+
27+
928
class ServerTests(unittest.TestCase):
1029
def test_init_server_model_empty_throws(self):
1130
with self.assertRaises(TypeError):
1231
server = TSC.Server()
1332

14-
def test_init_server_model_bad_server_name_complains(self):
15-
# by default, it will just set the version to 2.3
33+
def test_init_server_model_no_protocol_defaults_htt(self):
1634
server = TSC.Server("fake-url")
1735

1836
def test_init_server_model_valid_server_name_works(self):
19-
# by default, it will just set the version to 2.3
2037
server = TSC.Server("http://fake-url")
2138

2239
def test_init_server_model_valid_https_server_name_works(self):
2340
# by default, it will just set the version to 2.3
2441
server = TSC.Server("https://fake-url")
2542

2643
def test_init_server_model_bad_server_name_not_version_check(self):
27-
# by default, it will just set the version to 2.3
2844
server = TSC.Server("fake-url", use_server_version=False)
2945

3046
def test_init_server_model_bad_server_name_do_version_check(self):
31-
with self.assertRaises(MissingSchema):
47+
with self.assertRaises(requests.exceptions.ConnectionError):
3248
server = TSC.Server("fake-url", use_server_version=True)
3349

3450
def test_init_server_model_bad_server_name_not_version_check_random_options(self):
35-
# by default, it will just set the version to 2.3
51+
# with self.assertRaises(MissingSchema):
3652
server = TSC.Server("fake-url", use_server_version=False, http_options={"foo": 1})
3753

3854
def test_init_server_model_bad_server_name_not_version_check_real_options(self):
55+
# with self.assertRaises(ValueError):
3956
server = TSC.Server("fake-url", use_server_version=False, http_options={"verify": False})
4057

4158
def test_http_options_skip_ssl_works(self):
@@ -62,6 +79,25 @@ def test_http_options_not_sequence_fails(self):
6279
with self.assertRaises(ValueError):
6380
server.add_http_options({1, 2, 3})
6481

82+
def test_validate_connection_http(self):
83+
url = "http://cookies.com"
84+
server = TSC.Server(url)
85+
server.validate_server_connection()
86+
self.assertEqual(url, server.server_address)
87+
88+
def test_validate_connection_https(self):
89+
url = "https://cookies.com"
90+
server = TSC.Server(url)
91+
server.validate_server_connection()
92+
self.assertEqual(url, server.server_address)
93+
94+
def test_validate_connection_no_protocol(self):
95+
url = "cookies.com"
96+
fixed_url = "http://cookies.com"
97+
server = TSC.Server(url)
98+
server.validate_server_connection()
99+
self.assertEqual(fixed_url, server.server_address)
100+
65101

66102
class SessionTests(unittest.TestCase):
67103
test_header = {"x-test": "true"}
@@ -74,6 +110,6 @@ def session_factory():
74110

75111
def test_session_factory_adds_headers(self):
76112
test_request_bin = "http://capture-this-with-mock.com"
77-
with mock() as m:
113+
with requests_mock.mock() as m:
78114
m.get(url="http://capture-this-with-mock.com/api/2.4/serverInfo", request_headers=SessionTests.test_header)
79115
server = TSC.Server(test_request_bin, use_server_version=True, session_factory=SessionTests.session_factory)

versioneer.py

100755100644
File mode changed.

0 commit comments

Comments
 (0)