Skip to content

Commit

Permalink
Patch fix: 0.23.1 (#1129)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
5 people authored Oct 11, 2022
1 parent ef9e7fd commit 4873a58
Show file tree
Hide file tree
Showing 12 changed files with 156 additions and 31 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/pypi-smoke-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# This workflow will install TSC from pypi and validate that it runs. For more information see:
# https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Pypi smoke tests

on:
workflow_dispatch:
schedule:
- cron: 0 11 * * * # Every day at 11AM UTC (7AM EST)

permissions:
contents: read

jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ['3.x']

runs-on: ${{ matrix.os }}

steps:
- name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: pip install
run: |
pip uninstall tableauserverclient
pip install tableauserverclient
- name: Launch app
run: |
python -c "import tableauserverclient as TSC
server = TSC.Server('http://example.com', use_server_version=False)"
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2016 Tableau
Copyright (c) 2022 Tableau

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
8 changes: 8 additions & 0 deletions samples/smoke_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This sample verifies that tableau server client is installed
# and you can run it. It also shows the version of the client.

import tableauserverclient as TSC

server = TSC.Server("Fake-Server-Url", use_server_version=False)
print("Client details:")
print(TSC.server.endpoint.Endpoint._make_common_headers("fake-token", "any-content"))
1 change: 1 addition & 0 deletions tableauserverclient/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from ._version import get_versions
from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
from .models import (
BackgroundJobItem,
Expand Down
2 changes: 1 addition & 1 deletion tableauserverclient/models/tableau_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@ def credentials(self):
}

def __repr__(self):
return "<PersonalAccessToken name={} token={} site={}>".format(
return "<PersonalAccessToken name={} token={}>(site={})".format(
self.token_name, self.personal_access_token[:2] + "...", self.site_id
)
2 changes: 2 additions & 0 deletions tableauserverclient/models/tableau_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@

class Resource:
Database = "database"
Datarole = "datarole"
Datasource = "datasource"
Flow = "flow"
Lens = "lens"
Metric = "metric"
Project = "project"
Table = "table"
View = "view"
Expand Down
13 changes: 12 additions & 1 deletion tableauserverclient/server/endpoint/auth_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,18 @@ def baseurl(self):
def sign_in(self, auth_req):
url = "{0}/{1}".format(self.baseurl, "signin")
signin_req = RequestFactory.Auth.signin_req(auth_req)
server_response = self.parent_srv.session.post(url, data=signin_req, **self.parent_srv.http_options)
server_response = self.parent_srv.session.post(
url, data=signin_req, **self.parent_srv.http_options, allow_redirects=False
)
# manually handle a redirect so that we send the correct POST request instead of GET
# this will make e.g http://online.tableau.com work to redirect to http://east.online.tableau.com
if server_response.status_code == 301:
server_response = self.parent_srv.session.post(
server_response.headers["Location"],
data=signin_req,
**self.parent_srv.http_options,
allow_redirects=False,
)
self.parent_srv._namespace.detect(server_response.content)
self._check_status(server_response, url)
parsed_response = fromstring(server_response.content)
Expand Down
23 changes: 11 additions & 12 deletions tableauserverclient/server/endpoint/endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
EndpointUnavailableError,
)
from ..query import QuerySet
from ... import helpers
from ..._version import get_versions
from ... import helpers, get_versions

__TSC_VERSION__ = get_versions()["version"]
del get_versions
if TYPE_CHECKING:
from ..server import Server
from requests import Response

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

Expand All @@ -25,11 +25,9 @@
XML_CONTENT_TYPE = "text/xml"
JSON_CONTENT_TYPE = "application/json"

USERAGENT_HEADER = "User-Agent"

if TYPE_CHECKING:
from ..server import Server
from requests import Response
CONTENT_TYPE_HEADER = "content-type"
TABLEAU_AUTH_HEADER = "x-tableau-auth"
USER_AGENT_HEADER = "User-Agent"


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

@staticmethod
def _make_common_headers(auth_token, content_type):
_client_version: Optional[str] = get_versions()["version"]
headers = {}
if auth_token is not None:
headers["x-tableau-auth"] = auth_token
headers[TABLEAU_AUTH_HEADER] = auth_token
if content_type is not None:
headers["content-type"] = content_type
headers["User-Agent"] = "Tableau Server Client/{}".format(__TSC_VERSION__)
headers[CONTENT_TYPE_HEADER] = content_type
headers[USER_AGENT_HEADER] = "Tableau Server Client/{}".format(_client_version)
return headers

def _make_request(
Expand Down
24 changes: 24 additions & 0 deletions tableauserverclient/server/endpoint/projects_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ def populate_workbook_default_permissions(self, item):
def populate_datasource_default_permissions(self, item):
self._default_permissions.populate_default_permissions(item, Resource.Datasource)

@api(version="3.2")
def populate_metric_default_permissions(self, item):
self._default_permissions.populate_default_permissions(item, Resource.Metric)

@api(version="3.4")
def populate_datarole_default_permissions(self, item):
self._default_permissions.populate_default_permissions(item, Resource.Datarole)

@api(version="3.4")
def populate_flow_default_permissions(self, item):
self._default_permissions.populate_default_permissions(item, Resource.Flow)
Expand All @@ -115,6 +123,14 @@ def update_workbook_default_permissions(self, item, rules):
def update_datasource_default_permissions(self, item, rules):
return self._default_permissions.update_default_permissions(item, rules, Resource.Datasource)

@api(version="3.2")
def update_metric_default_permissions(self, item, rules):
return self._default_permissions.update_default_permissions(item, rules, Resource.Metric)

@api(version="3.4")
def update_datarole_default_permissions(self, item, rules):
return self._default_permissions.update_default_permissions(item, rules, Resource.Datarole)

@api(version="3.4")
def update_flow_default_permissions(self, item, rules):
return self._default_permissions.update_default_permissions(item, rules, Resource.Flow)
Expand All @@ -131,6 +147,14 @@ def delete_workbook_default_permissions(self, item, rule):
def delete_datasource_default_permissions(self, item, rule):
self._default_permissions.delete_default_permission(item, rule, Resource.Datasource)

@api(version="3.2")
def delete_metric_default_permissions(self, item, rule):
self._default_permissions.delete_default_permission(item, rule, Resource.Metric)

@api(version="3.4")
def delete_datarole_default_permissions(self, item, rule):
self._default_permissions.delete_default_permission(item, rule, Resource.Datarole)

@api(version="3.4")
def delete_flow_default_permissions(self, item, rule):
self._default_permissions.delete_default_permission(item, rule, Resource.Flow)
Expand Down
24 changes: 16 additions & 8 deletions tableauserverclient/server/server.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging
import warnings

import requests
import urllib3

from defusedxml.ElementTree import fromstring
from defusedxml.ElementTree import fromstring, ParseError
from packaging.version import Version
from .endpoint import (
Sites,
Expand Down Expand Up @@ -61,7 +62,7 @@ def __init__(self, server_address, use_server_version=False, http_options=None,
self._site_id = None
self._user_id = None

self._server_address = server_address
self._server_address: str = server_address
self._session_factory = session_factory or requests.session

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

def validate_server_connection(self):
try:
self._session.prepare_request(requests.Request("GET", url=self._server_address, params=self._http_options))
if not self._server_address.startswith("http://") and not self._server_address.startswith("https://"):
self._server_address = "http://" + self._server_address
self._session.prepare_request(
requests.Request("GET", url=self._server_address, params=self._http_options)
)
except Exception as req_ex:
warnings.warn("Invalid server initialization\n {}".format(req_ex.__str__()), UserWarning)
print("==================")
raise ValueError("Invalid server initialization", req_ex)

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

def _get_legacy_version(self):
response = self._session.get(self.server_address + "/auth?format=xml")
info_xml = fromstring(response.content)
try:
info_xml = fromstring(response.content)
except ParseError as parseError:
logging.getLogger("TSC.server").info(
"Could not read server version info. The server may not be running or configured."
)
return self.version
prod_version = info_xml.find(".//product_version").text
version = _PRODUCT_TO_REST_VERSION.get(prod_version, "2.1") # 2.1
return version
Expand All @@ -164,8 +174,6 @@ def use_server_version(self):

def use_highest_version(self):
self.use_server_version()
import warnings

warnings.warn("use use_server_version instead", DeprecationWarning)

def check_at_least_version(self, target: str):
Expand Down
52 changes: 44 additions & 8 deletions test/http/test_http_requests.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,58 @@
import tableauserverclient as TSC
import unittest
import requests
import requests_mock

from requests_mock import adapter, mock
from unittest import mock
from requests.exceptions import MissingSchema


# This method will be used by the mock to replace requests.get
def mocked_requests_get(*args, **kwargs):
class MockResponse:
def __init__(self, status_code):
self.content = (
"<xml>"
"<version version='0.31'>"
"<api_version>0.31</api_version>"
"<server_api_version>0.31</server_api_version>"
"<product_version>2022.3</product_version>"
"</version>"
"</xml>"
)
self.status_code = status_code

return MockResponse(200)


class ServerTests(unittest.TestCase):
def test_init_server_model_empty_throws(self):
with self.assertRaises(TypeError):
server = TSC.Server()

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

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

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

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

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

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

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

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

def test_validate_connection_http(self):
url = "http://cookies.com"
server = TSC.Server(url)
server.validate_server_connection()
self.assertEqual(url, server.server_address)

def test_validate_connection_https(self):
url = "https://cookies.com"
server = TSC.Server(url)
server.validate_server_connection()
self.assertEqual(url, server.server_address)

def test_validate_connection_no_protocol(self):
url = "cookies.com"
fixed_url = "http://cookies.com"
server = TSC.Server(url)
server.validate_server_connection()
self.assertEqual(fixed_url, server.server_address)


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

def test_session_factory_adds_headers(self):
test_request_bin = "http://capture-this-with-mock.com"
with mock() as m:
with requests_mock.mock() as m:
m.get(url="http://capture-this-with-mock.com/api/2.4/serverInfo", request_headers=SessionTests.test_header)
server = TSC.Server(test_request_bin, use_server_version=True, session_factory=SessionTests.session_factory)
Empty file modified versioneer.py
100755 → 100644
Empty file.

0 comments on commit 4873a58

Please sign in to comment.