From 160fcede3b5b207b9b23f1169739d15fa4c44d9e Mon Sep 17 00:00:00 2001 From: shinchris Date: Thu, 19 Nov 2020 12:02:57 -0800 Subject: [PATCH 01/19] Fixes data_acceleration field always in workbook update payload --- tableauserverclient/server/request_factory.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 65ce5a069..2325a52de 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -593,13 +593,12 @@ def update_req(self, workbook_item): if workbook_item.owner_id: owner_element = ET.SubElement(workbook_element, 'owner') owner_element.attrib['id'] = workbook_item.owner_id - if workbook_item.data_acceleration_config is not None and \ - 'acceleration_enabled' in workbook_item.data_acceleration_config: + if workbook_item.data_acceleration_config['acceleration_enabled'] is not None: data_acceleration_config = workbook_item.data_acceleration_config data_acceleration_element = ET.SubElement(workbook_element, 'dataAccelerationConfig') data_acceleration_element.attrib['accelerationEnabled'] = str(data_acceleration_config ["acceleration_enabled"]).lower() - if "accelerate_now" in data_acceleration_config: + if data_acceleration_config['accelerate_now'] is not None: data_acceleration_element.attrib['accelerateNow'] = str(data_acceleration_config ["accelerate_now"]).lower() From ef95e53924b3c099419b8f23017ca98bf9cf2e7c Mon Sep 17 00:00:00 2001 From: Brian Cantoni Date: Thu, 19 Nov 2020 22:00:15 -0800 Subject: [PATCH 02/19] Improve debug logging - show contents of methods such as put, post - for empty responses, don't try to print them (avoid [Truncated File Contents] which might appear like an error) --- tableauserverclient/server/endpoint/endpoint.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index 7f7fec3ee..8d5597ed4 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -50,6 +50,10 @@ def _make_request(self, method, url, content=None, request_object=None, if content is not None: parameters['data'] = content + logger.debug(u'request {}, url: {}'.format(method.__name__, url)) + if content: + logger.debug(u'request content: {}'.format(content)) + server_response = method(url, **parameters) self.parent_srv._namespace.detect(server_response.content) self._check_status(server_response) @@ -62,7 +66,8 @@ def _make_request(self, method, url, content=None, request_object=None, return server_response def _check_status(self, server_response): - logger.debug(self._safe_to_log(server_response)) + if len(server_response.content) > 0: + logger.debug(self._safe_to_log(server_response)) if server_response.status_code >= 500: raise InternalServerError(server_response) elif server_response.status_code not in Success_codes: From 00bde4e3adebc77a204ba515deca8cf423c5b530 Mon Sep 17 00:00:00 2001 From: Brian Cantoni Date: Thu, 19 Nov 2020 22:13:55 -0800 Subject: [PATCH 03/19] Add Python 3.9 for CI builds --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 41316d700..9085632f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "3.6" - "3.7" - "3.8" + - "3.9" # command to install dependencies install: - "pip install -e ." From 3f9c65d64f709bd411404b8133b1ff7f589af81b Mon Sep 17 00:00:00 2001 From: shinchris Date: Fri, 20 Nov 2020 14:33:55 -0800 Subject: [PATCH 04/19] Adds support for older server versions that expect different query string encoding --- .../server/endpoint/endpoint.py | 35 +++++----- tableauserverclient/server/request_options.py | 4 +- test/test_request_option.py | 66 +++++++++++++------ test/test_requests.py | 12 +--- test/test_sort.py | 29 ++------ 5 files changed, 72 insertions(+), 74 deletions(-) diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index 7f7fec3ee..c2d0c67e8 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -1,4 +1,4 @@ -from .exceptions import ServerResponseError, InternalServerError, NonXMLResponseError +from .exceptions import ServerResponseError, InternalServerError, NonXMLResponseError, EndpointUnavailableError from functools import wraps from xml.etree.ElementTree import ParseError from ..query import QuerySet @@ -39,11 +39,9 @@ def _safe_to_log(server_response): else: return server_response.content - def _make_request(self, method, url, content=None, request_object=None, - auth_token=None, content_type=None, parameters=None): + def _make_request(self, method, url, content=None, auth_token=None, + content_type=None, parameters=None): parameters = parameters or {} - if request_object is not None: - parameters["params"] = request_object.get_query_params() parameters.update(self.parent_srv.http_options) parameters['headers'] = Endpoint._make_common_headers(auth_token, content_type) @@ -78,28 +76,33 @@ def _check_status(self, server_response): # anything else re-raise here raise - def get_unauthenticated_request(self, url, request_object=None): - return self._make_request(self.parent_srv.session.get, url, request_object=request_object) + def get_unauthenticated_request(self, url): + return self._make_request(self.parent_srv.session.get, url) def get_request(self, url, request_object=None, parameters=None): + if request_object is not None: + try: + # Query param encoding is not needed for versions before 3.7 (2020.1) + self.parent_srv.assert_at_least_version("3.7") + parameters = parameters or {} + parameters["params"] = request_object.get_query_params() + except EndpointUnavailableError: + url = request_object.apply_query_params(url) + return self._make_request(self.parent_srv.session.get, url, auth_token=self.parent_srv.auth_token, - request_object=request_object, parameters=parameters) + parameters=parameters) def delete_request(self, url): # We don't return anything for a delete self._make_request(self.parent_srv.session.delete, url, auth_token=self.parent_srv.auth_token) def put_request(self, url, xml_request=None, content_type='text/xml'): - return self._make_request(self.parent_srv.session.put, url, - content=xml_request, - auth_token=self.parent_srv.auth_token, - content_type=content_type) + return self._make_request(self.parent_srv.session.put, url, content=xml_request, + auth_token=self.parent_srv.auth_token, content_type=content_type) def post_request(self, url, xml_request, content_type='text/xml'): - return self._make_request(self.parent_srv.session.post, url, - content=xml_request, - auth_token=self.parent_srv.auth_token, - content_type=content_type) + return self._make_request(self.parent_srv.session.post, url, content=xml_request, + auth_token=self.parent_srv.auth_token, content_type=content_type) def api(version): diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py index 47dfe29f8..22d0a4ef0 100644 --- a/tableauserverclient/server/request_options.py +++ b/tableauserverclient/server/request_options.py @@ -2,10 +2,8 @@ class RequestOptionsBase(object): + # This method is used if server api version is below 3.7 (2020.1) def apply_query_params(self, url): - import warnings - warnings.simplefilter('always', DeprecationWarning) - warnings.warn('apply_query_params is deprecated, please use get_query_params instead.', DeprecationWarning) try: params = self.get_query_params() params_list = ["{}={}".format(k, v) for (k, v) in params.items()] diff --git a/test/test_request_option.py b/test/test_request_option.py index c6270dd32..37b4fc945 100644 --- a/test/test_request_option.py +++ b/test/test_request_option.py @@ -20,6 +20,7 @@ def setUp(self): self.server = TSC.Server('http://test') # Fake signin + self.server.version = "3.10" self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67' self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM' @@ -141,54 +142,77 @@ def test_multiple_filter_options(self): def test_double_query_params(self): with requests_mock.mock() as m: m.get(requests_mock.ANY) - url = "http://test/api/2.3/sites/12345/views?queryParamExists=true" + url = self.baseurl + "/views?queryParamExists=true" opts = TSC.RequestOptions() opts.filter.add(TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ['stocks', 'market'])) + opts.sort.add(TSC.Sort(TSC.RequestOptions.Field.Name, + TSC.RequestOptions.Direction.Asc)) - resp = self.server.workbooks._make_request(requests.get, - url, - content=None, - request_object=opts, - auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM', - content_type='text/xml') + resp = self.server.workbooks.get_request(url, request_object=opts) self.assertTrue(re.search('queryparamexists=true', resp.request.query)) self.assertTrue(re.search('filter=tags%3ain%3a%5bstocks%2cmarket%5d', resp.request.query)) + self.assertTrue(re.search('sort=name%3aasc', resp.request.query)) + + # Test req_options for versions below 3.7 + def test_filter_sort_legacy(self): + self.server.version = "3.6" + with requests_mock.mock() as m: + m.get(requests_mock.ANY) + url = self.baseurl + "/views?queryParamExists=true" + opts = TSC.RequestOptions() + + opts.filter.add(TSC.Filter(TSC.RequestOptions.Field.Tags, + TSC.RequestOptions.Operator.In, + ['stocks', 'market'])) + opts.sort.add(TSC.Sort(TSC.RequestOptions.Field.Name, + TSC.RequestOptions.Direction.Asc)) + + resp = self.server.workbooks.get_request(url, request_object=opts) + self.assertTrue(re.search('queryparamexists=true', resp.request.query)) + self.assertTrue(re.search('filter=tags:in:%5bstocks,market%5d', resp.request.query)) + self.assertTrue(re.search('sort=name:asc', resp.request.query)) def test_vf(self): with requests_mock.mock() as m: m.get(requests_mock.ANY) - url = "http://test/api/2.3/sites/123/views/456/data" + url = self.baseurl + "/views/456/data" opts = TSC.PDFRequestOptions() opts.vf("name1#", "value1") opts.vf("name2$", "value2") opts.page_type = TSC.PDFRequestOptions.PageType.Tabloid - resp = self.server.workbooks._make_request(requests.get, - url, - content=None, - request_object=opts, - auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM', - content_type='text/xml') + resp = self.server.workbooks.get_request(url, request_object=opts) self.assertTrue(re.search('vf_name1%23=value1', resp.request.query)) self.assertTrue(re.search('vf_name2%24=value2', resp.request.query)) self.assertTrue(re.search('type=tabloid', resp.request.query)) + # Test req_options for versions beloe 3.7 + def test_vf_legacy(self): + self.server.version = "3.6" + with requests_mock.mock() as m: + m.get(requests_mock.ANY) + url = self.baseurl + "/views/456/data" + opts = TSC.PDFRequestOptions() + opts.vf("name1@", "value1") + opts.vf("name2$", "value2") + opts.page_type = TSC.PDFRequestOptions.PageType.Tabloid + + resp = self.server.workbooks.get_request(url, request_object=opts) + self.assertTrue(re.search('vf_name1@=value1', resp.request.query)) + self.assertTrue(re.search('vf_name2\\$=value2', resp.request.query)) + self.assertTrue(re.search('type=tabloid', resp.request.query)) + def test_all_fields(self): with requests_mock.mock() as m: m.get(requests_mock.ANY) - url = "http://test/api/2.3/sites/123/views/456/data" + url = self.baseurl + "/views/456/data" opts = TSC.RequestOptions() opts._all_fields = True - resp = self.server.users._make_request(requests.get, - url, - content=None, - request_object=opts, - auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM', - content_type='text/xml') + resp = self.server.users.get_request(url, request_object=opts) self.assertTrue(re.search('fields=_all_', resp.request.query)) def test_multiple_filter_options_shorthand(self): diff --git a/test/test_requests.py b/test/test_requests.py index c21853dbb..2976e8f3e 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -23,12 +23,7 @@ def test_make_get_request(self): m.get(requests_mock.ANY) url = "http://test/api/2.3/sites/dad65087-b08b-4603-af4e-2887b8aafc67/workbooks" opts = TSC.RequestOptions(pagesize=13, pagenumber=15) - resp = self.server.workbooks._make_request(requests.get, - url, - content=None, - request_object=opts, - auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM', - content_type='text/xml') + resp = self.server.workbooks.get_request(url, request_object=opts) self.assertTrue(re.search('pagesize=13', resp.request.query)) self.assertTrue(re.search('pagenumber=15', resp.request.query)) @@ -37,10 +32,7 @@ def test_make_post_request(self): with requests_mock.mock() as m: m.post(requests_mock.ANY) url = "http://test/api/2.3/sites/dad65087-b08b-4603-af4e-2887b8aafc67/workbooks" - resp = self.server.workbooks._make_request(requests.post, - url, - content=b'1337', - request_object=None, + resp = self.server.workbooks._make_request(requests.post, url, content=b'1337', auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM', content_type='multipart/mixed') self.assertEqual(resp.request.headers['x-tableau-auth'], 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM') diff --git a/test/test_sort.py b/test/test_sort.py index 106153cf6..525f0441d 100644 --- a/test/test_sort.py +++ b/test/test_sort.py @@ -8,6 +8,7 @@ class SortTests(unittest.TestCase): def setUp(self): self.server = TSC.Server('http://test') + self.server.version = "3.7" self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67' self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM' self.baseurl = self.server.workbooks.baseurl @@ -24,12 +25,7 @@ def test_filter_equals(self): TSC.RequestOptions.Operator.Equals, 'Superstore')) - resp = self.server.workbooks._make_request(requests.get, - url, - content=None, - request_object=opts, - auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM', - content_type='text/xml') + resp = self.server.workbooks.get_request(url, request_object=opts) self.assertTrue(re.search('pagenumber=13', resp.request.query)) self.assertTrue(re.search('pagesize=13', resp.request.query)) @@ -53,12 +49,7 @@ def test_filter_in(self): TSC.RequestOptions.Operator.In, ['stocks', 'market'])) - resp = self.server.workbooks._make_request(requests.get, - url, - content=None, - request_object=opts, - auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM', - content_type='text/xml') + resp = self.server.workbooks.get_request(url, request_object=opts) self.assertTrue(re.search('pagenumber=13', resp.request.query)) self.assertTrue(re.search('pagesize=13', resp.request.query)) self.assertTrue(re.search('filter=tags%3ain%3a%5bstocks%2cmarket%5d', resp.request.query)) @@ -71,12 +62,7 @@ def test_sort_asc(self): opts.sort.add(TSC.Sort(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Direction.Asc)) - resp = self.server.workbooks._make_request(requests.get, - url, - content=None, - request_object=opts, - auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM', - content_type='text/xml') + resp = self.server.workbooks.get_request(url, request_object=opts) self.assertTrue(re.search('pagenumber=13', resp.request.query)) self.assertTrue(re.search('pagesize=13', resp.request.query)) @@ -96,12 +82,7 @@ def test_filter_combo(self): TSC.RequestOptions.Operator.Equals, 'Publisher')) - resp = self.server.workbooks._make_request(requests.get, - url, - content=None, - request_object=opts, - auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM', - content_type='text/xml') + resp = self.server.workbooks.get_request(url, request_object=opts) expected = 'pagenumber=13&pagesize=13&filter=lastlogin%3agte%3a' \ '2017-01-15t00%3a00%3a00%3a00z%2csiterole%3aeq%3apublisher' From 0bcff26cf1d588b6eab1007b32268db4c2997e58 Mon Sep 17 00:00:00 2001 From: shinchris Date: Fri, 20 Nov 2020 14:38:24 -0800 Subject: [PATCH 05/19] Minor fixes --- tableauserverclient/server/endpoint/endpoint.py | 17 +++++++++++------ test/test_sort.py | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index c2d0c67e8..170f79f6e 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -82,14 +82,15 @@ def get_unauthenticated_request(self, url): def get_request(self, url, request_object=None, parameters=None): if request_object is not None: try: - # Query param encoding is not needed for versions before 3.7 (2020.1) + # Query param delimiters don't need to be encoded for versions before 3.7 (2020.1) self.parent_srv.assert_at_least_version("3.7") parameters = parameters or {} parameters["params"] = request_object.get_query_params() except EndpointUnavailableError: url = request_object.apply_query_params(url) - return self._make_request(self.parent_srv.session.get, url, auth_token=self.parent_srv.auth_token, + return self._make_request(self.parent_srv.session.get, url, + auth_token=self.parent_srv.auth_token, parameters=parameters) def delete_request(self, url): @@ -97,12 +98,16 @@ def delete_request(self, url): self._make_request(self.parent_srv.session.delete, url, auth_token=self.parent_srv.auth_token) def put_request(self, url, xml_request=None, content_type='text/xml'): - return self._make_request(self.parent_srv.session.put, url, content=xml_request, - auth_token=self.parent_srv.auth_token, content_type=content_type) + return self._make_request(self.parent_srv.session.put, url, + content=xml_request, + auth_token=self.parent_srv.auth_token, + content_type=content_type) def post_request(self, url, xml_request, content_type='text/xml'): - return self._make_request(self.parent_srv.session.post, url, content=xml_request, - auth_token=self.parent_srv.auth_token, content_type=content_type) + return self._make_request(self.parent_srv.session.post, url, + content=xml_request, + auth_token=self.parent_srv.auth_token, + content_type=content_type) def api(version): diff --git a/test/test_sort.py b/test/test_sort.py index 525f0441d..0572a1e10 100644 --- a/test/test_sort.py +++ b/test/test_sort.py @@ -8,7 +8,7 @@ class SortTests(unittest.TestCase): def setUp(self): self.server = TSC.Server('http://test') - self.server.version = "3.7" + self.server.version = "3.10" self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67' self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM' self.baseurl = self.server.workbooks.baseurl From 0aca85b743b9fa000892320b716abb3c098aaa1e Mon Sep 17 00:00:00 2001 From: Brian Cantoni Date: Wed, 2 Dec 2020 15:26:19 -0800 Subject: [PATCH 06/19] Limit request content to 1000 bytes, remove redundant response logging --- tableauserverclient/server/endpoint/endpoint.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index 8d5597ed4..0060510d8 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -52,7 +52,7 @@ def _make_request(self, method, url, content=None, request_object=None, logger.debug(u'request {}, url: {}'.format(method.__name__, url)) if content: - logger.debug(u'request content: {}'.format(content)) + logger.debug(u'request content: {}'.format(content[:1000])) server_response = method(url, **parameters) self.parent_srv._namespace.detect(server_response.content) @@ -60,14 +60,12 @@ def _make_request(self, method, url, content=None, request_object=None, # This check is to determine if the response is a text response (xml or otherwise) # so that we do not attempt to log bytes and other binary data. - if server_response.encoding: + if len(server_response.content) > 0 and server_response.encoding: logger.debug(u'Server response from {0}:\n\t{1}'.format( url, server_response.content.decode(server_response.encoding))) return server_response def _check_status(self, server_response): - if len(server_response.content) > 0: - logger.debug(self._safe_to_log(server_response)) if server_response.status_code >= 500: raise InternalServerError(server_response) elif server_response.status_code not in Success_codes: From 15f7b56f2fc7666c31d893a5d79e45f904b56365 Mon Sep 17 00:00:00 2001 From: Brian Cantoni Date: Thu, 3 Dec 2020 15:12:34 -0800 Subject: [PATCH 07/19] Add Get View by ID --- .../server/endpoint/views_endpoint.py | 10 +++++++++ test/assets/view_get_id.xml | 12 ++++++++++ test/test_view.py | 22 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 test/assets/view_get_id.xml diff --git a/tableauserverclient/server/endpoint/views_endpoint.py b/tableauserverclient/server/endpoint/views_endpoint.py index cd2792f5d..8c848c295 100644 --- a/tableauserverclient/server/endpoint/views_endpoint.py +++ b/tableauserverclient/server/endpoint/views_endpoint.py @@ -36,6 +36,16 @@ def get(self, req_options=None, usage=False): all_view_items = ViewItem.from_response(server_response.content, self.parent_srv.namespace) return all_view_items, pagination_item + @api(version="3.1") + def get_by_id(self, view_id): + if not view_id: + error = "View item missing ID." + raise MissingRequiredFieldError(error) + logger.info('Querying single view (ID: {0})'.format(view_id)) + url = "{0}/{1}".format(self.baseurl, view_id) + server_response = self.get_request(url) + return ViewItem.from_response(server_response.content, self.parent_srv.namespace)[0] + @api(version="2.0") def populate_preview_image(self, view_item): if not view_item.id or not view_item.workbook_id: diff --git a/test/assets/view_get_id.xml b/test/assets/view_get_id.xml new file mode 100644 index 000000000..6110a0a3a --- /dev/null +++ b/test/assets/view_get_id.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/test/test_view.py b/test/test_view.py index 1bd88995a..e32971ea2 100644 --- a/test/test_view.py +++ b/test/test_view.py @@ -10,6 +10,7 @@ ADD_TAGS_XML = os.path.join(TEST_ASSET_DIR, 'view_add_tags.xml') GET_XML = os.path.join(TEST_ASSET_DIR, 'view_get.xml') +GET_XML_ID = os.path.join(TEST_ASSET_DIR, 'view_get_id.xml') GET_XML_USAGE = os.path.join(TEST_ASSET_DIR, 'view_get_usage.xml') POPULATE_PREVIEW_IMAGE = os.path.join(TEST_ASSET_DIR, 'Sample View Image.png') POPULATE_PDF = os.path.join(TEST_ASSET_DIR, 'populate_pdf.pdf') @@ -60,6 +61,27 @@ def test_get(self): self.assertEqual('2002-06-05T08:00:59Z', format_datetime(all_views[1].updated_at)) self.assertEqual('story', all_views[1].sheet_type) + def test_get_by_id(self): + with open(GET_XML_ID, 'rb') as f: + response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.get(self.baseurl + '/d79634e1-6063-4ec9-95ff-50acbf609ff5', text=response_xml) + view = self.server.views.get_by_id('d79634e1-6063-4ec9-95ff-50acbf609ff5') + + self.assertEqual('d79634e1-6063-4ec9-95ff-50acbf609ff5', view.id) + self.assertEqual('ENDANGERED SAFARI', view.name) + self.assertEqual('SafariSample/sheets/ENDANGEREDSAFARI', view.content_url) + self.assertEqual('3cc6cd06-89ce-4fdc-b935-5294135d6d42', view.workbook_id) + self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', view.owner_id) + self.assertEqual('5241e88d-d384-4fd7-9c2f-648b5247efc5', view.project_id) + self.assertEqual(set(['tag1', 'tag2']), view.tags) + self.assertEqual('2002-05-30T09:00:00Z', format_datetime(view.created_at)) + self.assertEqual('2002-06-05T08:00:59Z', format_datetime(view.updated_at)) + self.assertEqual('story', view.sheet_type) + + def test_get_by_id_missing_id(self): + self.assertRaises(TSC.MissingRequiredFieldError, self.server.views.get_by_id, None) + def test_get_with_usage(self): with open(GET_XML_USAGE, 'rb') as f: response_xml = f.read().decode('utf-8') From e624178b44b0112eff2f6773b62d01ea87c66bf8 Mon Sep 17 00:00:00 2001 From: Chris Shin Date: Wed, 9 Dec 2020 11:03:08 -0800 Subject: [PATCH 08/19] Fixes issue #754 by moving file read logic inside generator --- .../server/endpoint/fileuploads_endpoint.py | 16 +++-- test/assets/fileupload_append.xml | 3 + test/assets/fileupload_initialize.xml | 3 + test/test_fileuploads.py | 70 +++++++++++++++++++ 4 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 test/assets/fileupload_append.xml create mode 100644 test/assets/fileupload_initialize.xml create mode 100644 test/test_fileuploads.py diff --git a/tableauserverclient/server/endpoint/fileuploads_endpoint.py b/tableauserverclient/server/endpoint/fileuploads_endpoint.py index 62224c894..c89a595d4 100644 --- a/tableauserverclient/server/endpoint/fileuploads_endpoint.py +++ b/tableauserverclient/server/endpoint/fileuploads_endpoint.py @@ -40,10 +40,18 @@ def append(self, xml_request, content_type): return FileuploadItem.from_response(server_response.content, self.parent_srv.namespace) def read_chunks(self, file): + file_opened = False + try: + file_content = open(file, 'rb') + file_opened = True + except TypeError: + file_content = file while True: - chunked_content = file.read(CHUNK_SIZE) + chunked_content = file_content.read(CHUNK_SIZE) if not chunked_content: + if file_opened: + file_content.close() break yield chunked_content @@ -52,11 +60,7 @@ def upload_chunks(cls, parent_srv, file): file_uploader = cls(parent_srv) upload_id = file_uploader.initiate() - try: - with open(file, 'rb') as f: - chunks = file_uploader.read_chunks(f) - except TypeError: - chunks = file_uploader.read_chunks(file) + chunks = file_uploader.read_chunks(file) for chunk in chunks: xml_request, content_type = RequestFactory.Fileupload.chunk_req(chunk) fileupload_item = file_uploader.append(xml_request, content_type) diff --git a/test/assets/fileupload_append.xml b/test/assets/fileupload_append.xml new file mode 100644 index 000000000..325ee66a9 --- /dev/null +++ b/test/assets/fileupload_append.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/test/assets/fileupload_initialize.xml b/test/assets/fileupload_initialize.xml new file mode 100644 index 000000000..073ad0edc --- /dev/null +++ b/test/assets/fileupload_initialize.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/test/test_fileuploads.py b/test/test_fileuploads.py new file mode 100644 index 000000000..9d115636f --- /dev/null +++ b/test/test_fileuploads.py @@ -0,0 +1,70 @@ +import os +import requests_mock +import unittest + +from ._utils import asset +from tableauserverclient.server import Server +from tableauserverclient.server.endpoint.fileuploads_endpoint import Fileuploads + +TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets') +FILEUPLOAD_INITIALIZE = os.path.join(TEST_ASSET_DIR, 'fileupload_initialize.xml') +FILEUPLOAD_APPEND = os.path.join(TEST_ASSET_DIR, 'fileupload_append.xml') + + +class FileuploadsTests(unittest.TestCase): + def setUp(self): + self.server = Server('http://test') + + # Fake sign in + self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67' + self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM' + + self.baseurl = '{}/sites/{}/fileUploads'.format(self.server.baseurl, self.server.site_id) + + def test_read_chunks_file_path(self): + fileuploads = Fileuploads(self.server) + + file_path = asset('SampleWB.twbx') + chunks = fileuploads.read_chunks(file_path) + for chunk in chunks: + self.assertIsNotNone(chunk) + + def test_read_chunks_file_object(self): + fileuploads = Fileuploads(self.server) + + with open(asset('SampleWB.twbx'), 'rb') as f: + chunks = fileuploads.read_chunks(f) + for chunk in chunks: + self.assertIsNotNone(chunk) + + def test_upload_chunks_file_path(self): + fileuploads = Fileuploads(self.server) + file_path = asset('SampleWB.twbx') + upload_id = '7720:170fe6b1c1c7422dadff20f944d58a52-1:0' + + with open(FILEUPLOAD_INITIALIZE, 'rb') as f: + initialize_response_xml = f.read().decode('utf-8') + with open(FILEUPLOAD_APPEND, 'rb') as f: + append_response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.post(self.baseurl, text=initialize_response_xml) + m.put(self.baseurl + '/' + upload_id, text=append_response_xml) + actual = fileuploads.upload_chunks(self.server, file_path) + + self.assertEqual(upload_id, actual) + + def test_upload_chunks_file_object(self): + fileuploads = Fileuploads(self.server) + upload_id = '7720:170fe6b1c1c7422dadff20f944d58a52-1:0' + + with open(asset('SampleWB.twbx'), 'rb') as file_content: + with open(FILEUPLOAD_INITIALIZE, 'rb') as f: + initialize_response_xml = f.read().decode('utf-8') + with open(FILEUPLOAD_APPEND, 'rb') as f: + append_response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.post(self.baseurl, text=initialize_response_xml) + m.put(self.baseurl + '/' + upload_id, text=append_response_xml) + actual = fileuploads.upload_chunks(self.server, file_content) + + self.assertEqual(upload_id, actual) From 861c65307916811bb0bbc025298947af262c7180 Mon Sep 17 00:00:00 2001 From: Chris Shin Date: Thu, 7 Jan 2021 09:48:12 -0800 Subject: [PATCH 09/19] Improves group creation for both local and AD (#770) * Fixes local and ad group creation * Adds tests for validating group field values --- tableauserverclient/models/group_item.py | 14 +++++++++----- tableauserverclient/server/request_factory.py | 8 +++----- test/test_group_model.py | 10 ++++++++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tableauserverclient/models/group_item.py b/tableauserverclient/models/group_item.py index ba9beec27..3cf82621f 100644 --- a/tableauserverclient/models/group_item.py +++ b/tableauserverclient/models/group_item.py @@ -9,13 +9,17 @@ class GroupItem(object): tag_name = 'group' - def __init__(self, name=None): - self._domain_name = None + class LicenseMode: + onLogin = 'onLogin' + onSync = 'onSync' + + def __init__(self, name=None, domain_name=None): self._id = None - self._users = None - self.name = name self._license_mode = None self._minimum_site_role = None + self._users = None + self.name = name + self.domain_name = domain_name @property def domain_name(self): @@ -43,8 +47,8 @@ def license_mode(self): return self._license_mode @license_mode.setter + @property_is_enum(LicenseMode) def license_mode(self, value): - # valid values = onSync, onLogin self._license_mode = value @property diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 2325a52de..d6e962be3 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -277,10 +277,8 @@ def create_local_req(self, group_item): xml_request = ET.Element('tsRequest') group_element = ET.SubElement(xml_request, 'group') group_element.attrib['name'] = group_item.name - if group_item.license_mode is not None: - group_element.attrib['grantLicenseMode'] = group_item.license_mode if group_item.minimum_site_role is not None: - group_element.attrib['SiteRole'] = group_item.minimum_site_role + group_element.attrib['minimumSiteRole'] = group_item.minimum_site_role return ET.tostring(xml_request) def create_ad_req(self, group_item): @@ -295,9 +293,9 @@ def create_ad_req(self, group_item): import_element.attrib['domainName'] = group_item.domain_name if group_item.license_mode is not None: - import_element.attrib['grantLicenseMode'] = group_item.license + import_element.attrib['grantLicenseMode'] = group_item.license_mode if group_item.minimum_site_role is not None: - import_element.attrib['SiteRole'] = group_item.minimum_site_role + import_element.attrib['siteRole'] = group_item.minimum_site_role return ET.tostring(xml_request) def update_req(self, group_item, default_site_role=None): diff --git a/test/test_group_model.py b/test/test_group_model.py index eb11adcdd..617a5d954 100644 --- a/test/test_group_model.py +++ b/test/test_group_model.py @@ -12,3 +12,13 @@ def test_invalid_name(self): with self.assertRaises(ValueError): group.name = "" + + def test_invalid_minimum_site_role(self): + group = TSC.GroupItem("grp") + with self.assertRaises(ValueError): + group.minimum_site_role = "Captain" + + def test_invalid_license_mode(self): + group = TSC.GroupItem("grp") + with self.assertRaises(ValueError): + group.license_mode = "off" From 1c7480f7127235ddf6331f99aed40208aa0d0890 Mon Sep 17 00:00:00 2001 From: Chris Shin Date: Wed, 20 Jan 2021 13:05:13 -0800 Subject: [PATCH 10/19] Fixes groups.update to match server requests/responses (#772) --- tableauserverclient/models/group_item.py | 21 ++++++++-------- .../server/endpoint/groups_endpoint.py | 21 ++++++++++++---- tableauserverclient/server/request_factory.py | 25 ++++++++++++++----- test/assets/group_update.xml | 6 ++--- test/test_group.py | 10 ++++++++ 5 files changed, 59 insertions(+), 24 deletions(-) diff --git a/tableauserverclient/models/group_item.py b/tableauserverclient/models/group_item.py index 3cf82621f..af9465dfb 100644 --- a/tableauserverclient/models/group_item.py +++ b/tableauserverclient/models/group_item.py @@ -83,17 +83,18 @@ def from_response(cls, resp, ns): name = group_xml.get('name', None) group_item = cls(name) group_item._id = group_xml.get('id', None) - # AD groups have an extra element under this + + # Domain name is returned in a domain element for some calls + domain_elem = group_xml.find('.//t:domain', namespaces=ns) + if domain_elem is not None: + group_item.domain_name = domain_elem.get('name', None) + + # Import element is returned for both local and AD groups (2020.3+) import_elem = group_xml.find('.//t:import', namespaces=ns) - if (import_elem is not None): - group_item.domain_name = import_elem.get('domainName') - group_item.license_mode = import_elem.get('grantLicenseMode') - group_item.minimum_site_role = import_elem.get('siteRole') - else: - # local group, we will just have two extra attributes here - group_item.domain_name = 'local' - group_item.license_mode = group_xml.get('grantLicenseMode') - group_item.minimum_site_role = group_xml.get('siteRole') + if import_elem is not None: + group_item.domain_name = import_elem.get('domainName', None) + group_item.license_mode = import_elem.get('grantLicenseMode', None) + group_item.minimum_site_role = import_elem.get('siteRole', None) all_group_items.append(group_item) return all_group_items diff --git a/tableauserverclient/server/endpoint/groups_endpoint.py b/tableauserverclient/server/endpoint/groups_endpoint.py index c873dc159..6a9b81afd 100644 --- a/tableauserverclient/server/endpoint/groups_endpoint.py +++ b/tableauserverclient/server/endpoint/groups_endpoint.py @@ -7,8 +7,6 @@ logger = logging.getLogger('tableau.endpoint.groups') -UNLICENSED_USER = UserItem.Roles.Unlicensed - class Groups(Endpoint): @property @@ -58,15 +56,28 @@ def delete(self, group_id): logger.info('Deleted single group (ID: {0})'.format(group_id)) @api(version="2.0") - def update(self, group_item, default_site_role=UNLICENSED_USER, as_job=False): + def update(self, group_item, default_site_role=None, as_job=False): + # (1/8/2021): Deprecated starting v0.15 + if default_site_role is not None: + import warnings + warnings.simplefilter('always', DeprecationWarning) + warnings.warn('Groups.update(...default_site_role=""...) is deprecated, ' + 'please set the minimum_site_role field of GroupItem', + DeprecationWarning) + group_item.minimum_site_role = default_site_role + if not group_item.id: error = "Group item missing ID." raise MissingRequiredFieldError(error) + if as_job and (group_item.domain_name is None or group_item.domain_name == 'local'): + error = "Local groups cannot be updated asynchronously." + raise ValueError(error) + url = "{0}/{1}".format(self.baseurl, group_item.id) - update_req = RequestFactory.Group.update_req(group_item, default_site_role) + update_req = RequestFactory.Group.update_req(group_item, None) server_response = self.put_request(url, update_req) logger.info('Updated group item (ID: {0})'.format(group_item.id)) - if (as_job): + if as_job: return JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] else: return GroupItem.from_response(server_response.content, self.parent_srv.namespace)[0] diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index d6e962be3..14c755bbd 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -299,17 +299,30 @@ def create_ad_req(self, group_item): return ET.tostring(xml_request) def update_req(self, group_item, default_site_role=None): + # (1/8/2021): Deprecated starting v0.15 if default_site_role is not None: + import warnings + warnings.simplefilter('always', DeprecationWarning) + warnings.warn('RequestFactory.Group.update_req(...default_site_role="") is deprecated, ' + 'please set the minimum_site_role field of GroupItem', + DeprecationWarning) group_item.minimum_site_role = default_site_role + xml_request = ET.Element('tsRequest') group_element = ET.SubElement(xml_request, 'group') group_element.attrib['name'] = group_item.name - if group_item.domain_name != 'local': - project_element = ET.SubElement(group_element, 'import') - project_element.attrib['source'] = "ActiveDirectory" - project_element.attrib['domainName'] = group_item.domain_name - project_element.attrib['siteRole'] = group_item.minimum_site_role - project_element.attrib['grantLicenseMode'] = group_item.license_mode + if group_item.domain_name is not None and group_item.domain_name != 'local': + # Import element is only accepted in the request for AD groups + import_element = ET.SubElement(group_element, 'import') + import_element.attrib['source'] = "ActiveDirectory" + import_element.attrib['domainName'] = group_item.domain_name + import_element.attrib['siteRole'] = group_item.minimum_site_role + if group_item.license_mode is not None: + import_element.attrib['grantLicenseMode'] = group_item.license_mode + else: + # Local group request does not accept an 'import' element + if group_item.minimum_site_role is not None: + group_element.attrib['minimumSiteRole'] = group_item.minimum_site_role return ET.tostring(xml_request) diff --git a/test/assets/group_update.xml b/test/assets/group_update.xml index 828e3f251..3c54524c0 100644 --- a/test/assets/group_update.xml +++ b/test/assets/group_update.xml @@ -2,7 +2,7 @@ - /> + + + \ No newline at end of file diff --git a/test/test_group.py b/test/test_group.py index 8aeb4817d..082a63ba3 100644 --- a/test/test_group.py +++ b/test/test_group.py @@ -224,3 +224,13 @@ def test_update(self): self.assertEqual('Group updated name', group.name) self.assertEqual('ExplorerCanPublish', group.minimum_site_role) self.assertEqual('onLogin', group.license_mode) + + # async update is not supported for local groups + def test_update_local_async(self): + group = TSC.GroupItem("myGroup") + group._id = 'ef8b19c0-43b6-11e6-af50-63f5805dbe3c' + self.assertRaises(ValueError, self.server.groups.update, group, as_job=True) + + # mimic group returned from server where domain name is set to 'local' + group.domain_name = "local" + self.assertRaises(ValueError, self.server.groups.update, group, as_job=True) From f566c050ed6103de5b686c785d7b355585878377 Mon Sep 17 00:00:00 2001 From: jorwoods Date: Mon, 1 Feb 2021 13:15:56 -0600 Subject: [PATCH 11/19] Fetch project owner on get (#784) Fetch project owner on get, and throw NotImplementedError if we try to set it, until Server itself supports the action Co-authored-by: Jordan Woods --- tableauserverclient/models/project_item.py | 21 +++++++++++++++++---- test/assets/project_get.xml | 6 +++--- test/test_project.py | 7 +++++-- test/test_project_model.py | 5 +++++ 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py index d6aece83b..4cfbcb4e9 100644 --- a/tableauserverclient/models/project_item.py +++ b/tableauserverclient/models/project_item.py @@ -74,6 +74,14 @@ def name(self): def name(self, value): self._name = value + @property + def owner_id(self): + return self._owner_id + + @owner_id.setter + def owner_id(self, value): + raise NotImplementedError('REST API does not currently support updating project owner.') + def is_default(self): return self.name.lower() == 'default' @@ -86,7 +94,7 @@ def _parse_common_tags(self, project_xml, ns): self._set_values(None, name, description, content_permissions, parent_id) return self - def _set_values(self, project_id, name, description, content_permissions, parent_id): + def _set_values(self, project_id, name, description, content_permissions, parent_id, owner_id): if project_id is not None: self._id = project_id if name: @@ -97,6 +105,8 @@ def _set_values(self, project_id, name, description, content_permissions, parent self._content_permissions = content_permissions if parent_id: self.parent_id = parent_id + if owner_id: + self._owner_id = owner_id def _set_permissions(self, permissions): self._permissions = permissions @@ -111,9 +121,9 @@ def from_response(cls, resp, ns): all_project_xml = parsed_response.findall('.//t:project', namespaces=ns) for project_xml in all_project_xml: - (id, name, description, content_permissions, parent_id) = cls._parse_element(project_xml) + (id, name, description, content_permissions, parent_id, owner_id) = cls._parse_element(project_xml) project_item = cls(name) - project_item._set_values(id, name, description, content_permissions, parent_id) + project_item._set_values(id, name, description, content_permissions, parent_id, owner_id) all_project_items.append(project_item) return all_project_items @@ -124,5 +134,8 @@ def _parse_element(project_xml): description = project_xml.get('description', None) content_permissions = project_xml.get('contentPermissions', None) parent_id = project_xml.get('parentProjectId', None) + owner_id = None + for owner in project_xml: + owner_id = owner.get('id', None) - return id, name, description, content_permissions, parent_id + return id, name, description, content_permissions, parent_id, owner_id diff --git a/test/assets/project_get.xml b/test/assets/project_get.xml index 777412b30..7898c8c13 100644 --- a/test/assets/project_get.xml +++ b/test/assets/project_get.xml @@ -2,8 +2,8 @@ - - - + + + diff --git a/test/test_project.py b/test/test_project.py index 5e9869c6e..045f0a43e 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -39,16 +39,19 @@ def test_get(self): all_projects[0].description) self.assertEqual('ManagedByOwner', all_projects[0].content_permissions) self.assertEqual(None, all_projects[0].parent_id) + self.assertEqual('dd2239f6-ddf1-4107-981a-4cf94e415794', all_projects[0].owner_id) self.assertEqual('1d0304cd-3796-429f-b815-7258370b9b74', all_projects[1].id) self.assertEqual('Tableau', all_projects[1].name) self.assertEqual('ManagedByOwner', all_projects[1].content_permissions) self.assertEqual(None, all_projects[1].parent_id) + self.assertEqual('2a47bbf8-8900-4ebb-b0a4-2723bd7c46c3', all_projects[1].owner_id) self.assertEqual('4cc52973-5e3a-4d1f-a4fb-5b5f73796edf', all_projects[2].id) self.assertEqual('Tableau > Child 1', all_projects[2].name) self.assertEqual('ManagedByOwner', all_projects[2].content_permissions) self.assertEqual('1d0304cd-3796-429f-b815-7258370b9b74', all_projects[2].parent_id) + self.assertEqual('dd2239f6-ddf1-4107-981a-4cf94e415794', all_projects[2].owner_id) def test_get_before_signin(self): self.server._auth_token = None @@ -156,7 +159,7 @@ def test_populate_workbooks(self): m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/workbooks', text=response_xml) single_project = TSC.ProjectItem('test', '1d0304cd-3796-429f-b815-7258370b9b74') - single_project.owner_id = 'dd2239f6-ddf1-4107-981a-4cf94e415794' + single_project._owner_id = 'dd2239f6-ddf1-4107-981a-4cf94e415794' single_project._id = '9dbd2263-16b5-46e1-9c43-a76bb8ab65fb' self.server.projects.populate_workbook_default_permissions(single_project) @@ -227,7 +230,7 @@ def test_delete_workbook_default_permission(self): single_group._id = 'c8f2773a-c83a-11e8-8c8f-33e6d787b506' single_project = TSC.ProjectItem('test', '1d0304cd-3796-429f-b815-7258370b9b74') - single_project.owner_id = 'dd2239f6-ddf1-4107-981a-4cf94e415794' + single_project._owner_id = 'dd2239f6-ddf1-4107-981a-4cf94e415794' single_project._id = '9dbd2263-16b5-46e1-9c43-a76bb8ab65fb' self.server.projects.populate_workbook_default_permissions(single_project) diff --git a/test/test_project_model.py b/test/test_project_model.py index 56e6c3d11..55cf20b26 100644 --- a/test/test_project_model.py +++ b/test/test_project_model.py @@ -22,3 +22,8 @@ def test_parent_id(self): project = TSC.ProjectItem("proj") project.parent_id = "foo" self.assertEqual(project.parent_id, "foo") + + def test_owner_id(self): + project = TSC.ProjectItem("proj") + with self.assertRaises(NotImplementedError): + project.owner_id = "new_owner" From 857199bba6c4d1c4b40199e02321707f8f0ea638 Mon Sep 17 00:00:00 2001 From: tjones-commits <70481977+tjones-commits@users.noreply.github.com> Date: Fri, 5 Feb 2021 17:09:32 -0500 Subject: [PATCH 12/19] Update site properties and functions (#777) * Update publish_workbook.py (#694) * Update publish_workbook.py Added below arguments, without this there is a sign-in error on publishing a test file to Tableau Online parser.add_argument('--sitename', '-S', default='', help='sitename required') tableau_auth = TSC.TableauAuth(args.username, password,site_id=args.sitename) * Update publish_workbook.py Edits (as requested) to publish workbooks on Tableau Online which removes the Sign-in Error. * Update publish_workbook.py * Merge pull request #745 from tableau/fix_732 Server versions before 2020.1 do not accept encoded query param delimiters * Merge pull request #757 from tableau/fix_754 Fixes issue #754 by moving file read logic inside generator * Updates changelog for v0.14.1 * update the site item to reflect api response * update test model * remove extra test assets * trimming line length * unit test all properties. fix some properties. Remove extra code * make requested changes * make requested changes Co-authored-by: Chris Shin Co-authored-by: Madhura Selvarajan Co-authored-by: Terrence Jones --- CHANGELOG.md | 4 + tableauserverclient/models/site_item.py | 532 +++++++++++++++++- tableauserverclient/server/request_factory.py | 154 ++++- test/assets/site_create.xml | 2 +- test/assets/site_get.xml | 4 +- test/assets/site_get_by_id.xml | 2 +- test/assets/site_get_by_name.xml | 3 +- test/assets/site_update.xml | 2 +- test/test_site.py | 54 +- 9 files changed, 735 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e6be649b..85dc8a702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.14.1 (9 Dec 2020) +* Fixed filter query issue for server version below 2020.1 (#745) +* Fixed large workbook/datasource publish issue (#757) + ## 0.14.0 (6 Nov 2020) * Added django-style filtering and sorting (#615) * Added encoding tag-name before deleting (#687) diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py index 1ba854e72..f562289ce 100644 --- a/tableauserverclient/models/site_item.py +++ b/tableauserverclient/models/site_item.py @@ -17,7 +17,19 @@ class State: def __init__(self, name, content_url, admin_mode=None, user_quota=None, storage_quota=None, disable_subscriptions=False, subscribe_others_enabled=True, revision_history_enabled=False, - revision_limit=None, data_acceleration_mode=None, flows_enabled=None, cataloging_enabled=None): + revision_limit=None, data_acceleration_mode=None, flows_enabled=True, cataloging_enabled=True, + editing_flows_enabled=True, scheduling_flows_enabled=True, allow_subscription_attachments=True, + guest_access_enabled=False, cache_warmup_enabled=True, commenting_enabled=True, + extract_encryption_mode=None, request_access_enabled=False, run_now_enabled=True, + tier_explorer_capacity=None, tier_creator_capacity=None, tier_viewer_capacity=None, + data_alerts_enabled=True, commenting_mentions_enabled=True, catalog_obfuscation_enabled=False, + flow_auto_save_enabled=True, web_extraction_enabled=True, metrics_content_type_enabled=True, + notify_site_admins_on_throttle=False, authoring_enabled=True, custom_subscription_email_enabled=False, + custom_subscription_email=False, custom_subscription_footer_enabled=False, + custom_subscription_footer=False, ask_data_mode='EnabledByDefault', named_sharing_enabled=True, + mobile_biometrics_enabled=False, sheet_image_enabled=True, derived_permissions_enabled=False, + user_visibility_mode='FULL', use_default_time_zone=True, time_zone=None, + auto_suspend_refresh_enabled=True, auto_suspend_refresh_inactivity_window=30): self._admin_mode = None self._id = None self._num_users = None @@ -36,6 +48,40 @@ def __init__(self, name, content_url, admin_mode=None, user_quota=None, storage_ self.data_acceleration_mode = data_acceleration_mode self.cataloging_enabled = cataloging_enabled self.flows_enabled = flows_enabled + self.editing_flows_enabled = editing_flows_enabled + self.scheduling_flows_enabled = scheduling_flows_enabled + self.allow_subscription_attachments = allow_subscription_attachments + self.guest_access_enabled = guest_access_enabled + self.cache_warmup_enabled = cache_warmup_enabled + self.commenting_enabled = commenting_enabled + self.extract_encryption_mode = extract_encryption_mode + self.request_access_enabled = request_access_enabled + self.run_now_enabled = run_now_enabled + self.tier_explorer_capacity = tier_explorer_capacity + self.tier_creator_capacity = tier_creator_capacity + self.tier_viewer_capacity = tier_viewer_capacity + self.data_alerts_enabled = data_alerts_enabled + self.commenting_mentions_enabled = commenting_mentions_enabled + self.catalog_obfuscation_enabled = catalog_obfuscation_enabled + self.flow_auto_save_enabled = flow_auto_save_enabled + self.web_extraction_enabled = web_extraction_enabled + self.metrics_content_type_enabled = metrics_content_type_enabled + self.notify_site_admins_on_throttle = notify_site_admins_on_throttle + self.authoring_enabled = authoring_enabled + self.custom_subscription_footer_enabled = custom_subscription_footer_enabled + self.custom_subscription_email_enabled = custom_subscription_email_enabled + self.custom_subscription_email = custom_subscription_email + self.custom_subscription_footer = custom_subscription_footer + self.ask_data_mode = ask_data_mode + self.named_sharing_enabled = named_sharing_enabled + self.mobile_biometrics_enabled = mobile_biometrics_enabled + self.sheet_image_enabled = sheet_image_enabled + self.derived_permissions_enabled = derived_permissions_enabled + self.user_visibility_mode = user_visibility_mode + self.use_default_time_zone = use_default_time_zone + self.time_zone = time_zone + self.auto_suspend_refresh_enabled = auto_suspend_refresh_enabled + self.auto_suspend_refresh_inactivity_window = auto_suspend_refresh_inactivity_window @property def admin_mode(self): @@ -147,12 +193,307 @@ def flows_enabled(self): return self._flows_enabled @flows_enabled.setter + @property_is_boolean def flows_enabled(self, value): self._flows_enabled = value def is_default(self): return self.name.lower() == 'default' + @property + def editing_flows_enabled(self): + return self._editing_flows_enabled + + @editing_flows_enabled.setter + @property_is_boolean + def editing_flows_enabled(self, value): + self._editing_flows_enabled = value + + @property + def scheduling_flows_enabled(self): + return self._scheduling_flows_enabled + + @scheduling_flows_enabled.setter + @property_is_boolean + def scheduling_flows_enabled(self, value): + self._scheduling_flows_enabled = value + + @property + def allow_subscription_attachments(self): + return self._allow_subscription_attachments + + @allow_subscription_attachments.setter + @property_is_boolean + def allow_subscription_attachments(self, value): + self._allow_subscription_attachments = value + + @property + def guest_access_enabled(self): + return self._guest_access_enabled + + @guest_access_enabled.setter + @property_is_boolean + def guest_access_enabled(self, value): + self._guest_access_enabled = value + + @property + def cache_warmup_enabled(self): + return self._cache_warmup_enabled + + @cache_warmup_enabled.setter + @property_is_boolean + def cache_warmup_enabled(self, value): + self._cache_warmup_enabled = value + + @property + def commenting_enabled(self): + return self._commenting_enabled + + @commenting_enabled.setter + @property_is_boolean + def commenting_enabled(self, value): + self._commenting_enabled = value + + @property + def extract_encryption_mode(self): + return self._extract_encryption_mode + + @extract_encryption_mode.setter + def extract_encryption_mode(self, value): + self._extract_encryption_mode = value + + @property + def request_access_enabled(self): + return self._request_access_enabled + + @request_access_enabled.setter + @property_is_boolean + def request_access_enabled(self, value): + self._request_access_enabled = value + + @property + def run_now_enabled(self): + return self._run_now_enabled + + @run_now_enabled.setter + @property_is_boolean + def run_now_enabled(self, value): + self._run_now_enabled = value + + @property + def tier_explorer_capacity(self): + return self._tier_explorer_capacity + + @tier_explorer_capacity.setter + def tier_explorer_capacity(self, value): + self._tier_explorer_capacity = value + + @property + def tier_creator_capacity(self): + return self._tier_creator_capacity + + @tier_creator_capacity.setter + def tier_creator_capacity(self, value): + self._tier_creator_capacity = value + + @property + def tier_viewer_capacity(self): + return self._tier_viewer_capacity + + @tier_viewer_capacity.setter + def tier_viewer_capacity(self, value): + self._tier_viewer_capacity = value + + @property + def data_alerts_enabled(self): + return self._data_alerts_enabled + + @data_alerts_enabled.setter + @property_is_boolean + def data_alerts_enabled(self, value): + self._data_alerts_enabled = value + + @property + def commenting_mentions_enabled(self): + return self._commenting_mentions_enabled + + @commenting_mentions_enabled.setter + @property_is_boolean + def commenting_mentions_enabled(self, value): + self._commenting_mentions_enabled = value + + @property + def catalog_obfuscation_enabled(self): + return self._catalog_obfuscation_enabled + + @catalog_obfuscation_enabled.setter + @property_is_boolean + def catalog_obfuscation_enabled(self, value): + self._catalog_obfuscation_enabled = value + + @property + def flow_auto_save_enabled(self): + return self._flow_auto_save_enabled + + @flow_auto_save_enabled.setter + @property_is_boolean + def flow_auto_save_enabled(self, value): + self._flow_auto_save_enabled = value + + @property + def web_extraction_enabled(self): + return self._web_extraction_enabled + + @web_extraction_enabled.setter + @property_is_boolean + def web_extraction_enabled(self, value): + self._web_extraction_enabled = value + + @property + def metrics_content_type_enabled(self): + return self._metrics_content_type_enabled + + @metrics_content_type_enabled.setter + @property_is_boolean + def metrics_content_type_enabled(self, value): + self._metrics_content_type_enabled = value + + @property + def notify_site_admins_on_throttle(self): + return self._notify_site_admins_on_throttle + + @notify_site_admins_on_throttle.setter + @property_is_boolean + def notify_site_admins_on_throttle(self, value): + self._notify_site_admins_on_throttle = value + + @property + def authoring_enabled(self): + return self._authoring_enabled + + @authoring_enabled.setter + @property_is_boolean + def authoring_enabled(self, value): + self._authoring_enabled = value + + @property + def custom_subscription_email_enabled(self): + return self._custom_subscription_email_enabled + + @custom_subscription_email_enabled.setter + @property_is_boolean + def custom_subscription_email_enabled(self, value): + self._custom_subscription_email_enabled = value + + @property + def custom_subscription_email(self): + return self._custom_subscription_email + + @custom_subscription_email.setter + def custom_subscription_email(self, value): + self._custom_subscription_email = value + + @property + def custom_subscription_footer_enabled(self): + return self._custom_subscription_footer_enabled + + @custom_subscription_footer_enabled.setter + @property_is_boolean + def custom_subscription_footer_enabled(self, value): + self._custom_subscription_footer_enabled = value + + @property + def custom_subscription_footer(self): + return self._custom_subscription_footer + + @custom_subscription_footer.setter + def custom_subscription_footer(self, value): + self._custom_subscription_footer = value + + @property + def ask_data_mode(self): + return self._ask_data_mode + + @ask_data_mode.setter + def ask_data_mode(self, value): + self._ask_data_mode = value + + @property + def named_sharing_enabled(self): + return self._named_sharing_enabled + + @named_sharing_enabled.setter + @property_is_boolean + def named_sharing_enabled(self, value): + self._named_sharing_enabled = value + + @property + def mobile_biometrics_enabled(self): + return self._mobile_biometrics_enabled + + @mobile_biometrics_enabled.setter + @property_is_boolean + def mobile_biometrics_enabled(self, value): + self._mobile_biometrics_enabled = value + + @property + def sheet_image_enabled(self): + return self._sheet_image_enabled + + @sheet_image_enabled.setter + @property_is_boolean + def sheet_image_enabled(self, value): + self._sheet_image_enabled = value + + @property + def derived_permissions_enabled(self): + return self._derived_permissions_enabled + + @derived_permissions_enabled.setter + @property_is_boolean + def derived_permissions_enabled(self, value): + self._derived_permissions_enabled = value + + @property + def user_visibility_mode(self): + return self._user_visibility_mode + + @user_visibility_mode.setter + def user_visibility_mode(self, value): + self._user_visibility_mode = value + + @property + def use_default_time_zone(self): + return self._use_default_time_zone + + @use_default_time_zone.setter + def use_default_time_zone(self, value): + self._use_default_time_zone = value + + @property + def time_zone(self): + return self._time_zone + + @time_zone.setter + def time_zone(self, value): + self._time_zone = value + + @property + def auto_suspend_refresh_inactivity_window(self): + return self._auto_suspend_refresh_inactivity_window + + @auto_suspend_refresh_inactivity_window.setter + def auto_suspend_refresh_inactivity_window(self, value): + self._auto_suspend_refresh_inactivity_window = value + + @property + def auto_suspend_refresh_enabled(self): + return self._auto_suspend_refresh_enabled + + @auto_suspend_refresh_enabled.setter + def auto_suspend_refresh_enabled(self, value): + self._auto_suspend_refresh_enabled = value + def _parse_common_tags(self, site_xml, ns): if not isinstance(site_xml, ET.Element): site_xml = ET.fromstring(site_xml).find('.//t:site', namespaces=ns) @@ -160,18 +501,46 @@ def _parse_common_tags(self, site_xml, ns): (_, name, content_url, _, admin_mode, state, subscribe_others_enabled, disable_subscriptions, revision_history_enabled, user_quota, storage_quota, revision_limit, num_users, storage, - data_acceleration_mode, cataloging_enabled, flows_enabled) = self._parse_element(site_xml, ns) + data_acceleration_mode, flows_enabled, cataloging_enabled, editing_flows_enabled, + scheduling_flows_enabled, allow_subscription_attachments, guest_access_enabled, + cache_warmup_enabled, commenting_enabled, extract_encryption_mode, request_access_enabled, + run_now_enabled, tier_explorer_capacity, tier_creator_capacity, tier_viewer_capacity, data_alerts_enabled, + commenting_mentions_enabled, catalog_obfuscation_enabled, flow_auto_save_enabled, web_extraction_enabled, + metrics_content_type_enabled, notify_site_admins_on_throttle, authoring_enabled, + custom_subscription_email_enabled, custom_subscription_email, custom_subscription_footer_enabled, + custom_subscription_footer, ask_data_mode, named_sharing_enabled, mobile_biometrics_enabled, + sheet_image_enabled, derived_permissions_enabled, user_visibility_mode, use_default_time_zone, time_zone, + auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window) = self._parse_element(site_xml, ns) self._set_values(None, name, content_url, None, admin_mode, state, subscribe_others_enabled, disable_subscriptions, revision_history_enabled, user_quota, storage_quota, - revision_limit, num_users, storage, data_acceleration_mode, cataloging_enabled, - flows_enabled) + revision_limit, num_users, storage, data_acceleration_mode, flows_enabled, + cataloging_enabled, editing_flows_enabled, scheduling_flows_enabled, + allow_subscription_attachments, guest_access_enabled, cache_warmup_enabled, + commenting_enabled, extract_encryption_mode, request_access_enabled, run_now_enabled, + tier_explorer_capacity, tier_creator_capacity, tier_viewer_capacity, data_alerts_enabled, + commenting_mentions_enabled, catalog_obfuscation_enabled, flow_auto_save_enabled, + web_extraction_enabled, metrics_content_type_enabled, notify_site_admins_on_throttle, + authoring_enabled, custom_subscription_email_enabled, custom_subscription_email, + custom_subscription_footer_enabled, custom_subscription_footer, ask_data_mode, + named_sharing_enabled, mobile_biometrics_enabled, sheet_image_enabled, + derived_permissions_enabled, user_visibility_mode, use_default_time_zone, time_zone, + auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window) return self def _set_values(self, id, name, content_url, status_reason, admin_mode, state, subscribe_others_enabled, disable_subscriptions, revision_history_enabled, user_quota, storage_quota, revision_limit, num_users, storage, data_acceleration_mode, - flows_enabled, cataloging_enabled): + flows_enabled, cataloging_enabled, editing_flows_enabled, scheduling_flows_enabled, + allow_subscription_attachments, guest_access_enabled, cache_warmup_enabled, commenting_enabled, + extract_encryption_mode, request_access_enabled, run_now_enabled, tier_explorer_capacity, + tier_creator_capacity, tier_viewer_capacity, data_alerts_enabled, commenting_mentions_enabled, + catalog_obfuscation_enabled, flow_auto_save_enabled, web_extraction_enabled, + metrics_content_type_enabled, notify_site_admins_on_throttle, authoring_enabled, + custom_subscription_email_enabled, custom_subscription_email, custom_subscription_footer_enabled, + custom_subscription_footer, ask_data_mode, named_sharing_enabled, mobile_biometrics_enabled, + sheet_image_enabled, derived_permissions_enabled, user_visibility_mode, use_default_time_zone, + time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window): if id is not None: self._id = id if name: @@ -206,6 +575,74 @@ def _set_values(self, id, name, content_url, status_reason, admin_mode, state, self.flows_enabled = flows_enabled if cataloging_enabled is not None: self.cataloging_enabled = cataloging_enabled + if editing_flows_enabled is not None: + self.editing_flows_enabled = editing_flows_enabled + if scheduling_flows_enabled is not None: + self.scheduling_flows_enabled = scheduling_flows_enabled + if allow_subscription_attachments is not None: + self.allow_subscription_attachments = allow_subscription_attachments + if guest_access_enabled is not None: + self.guest_access_enabled = guest_access_enabled + if cache_warmup_enabled is not None: + self.cache_warmup_enabled = cache_warmup_enabled + if commenting_enabled is not None: + self.commenting_enabled = commenting_enabled + if extract_encryption_mode is not None: + self.extract_encryption_mode = extract_encryption_mode + if request_access_enabled is not None: + self.request_access_enabled = request_access_enabled + if run_now_enabled is not None: + self.run_now_enabled = run_now_enabled + if tier_explorer_capacity: + self.tier_explorer_capacity = tier_explorer_capacity + if tier_creator_capacity: + self.tier_creator_capacity = tier_creator_capacity + if tier_viewer_capacity: + self.tier_viewer_capacity = tier_viewer_capacity + if data_alerts_enabled is not None: + self.data_alerts_enabled = data_alerts_enabled + if commenting_mentions_enabled is not None: + self.commenting_mentions_enabled = commenting_mentions_enabled + if catalog_obfuscation_enabled is not None: + self.catalog_obfuscation_enabled = catalog_obfuscation_enabled + if flow_auto_save_enabled is not None: + self.flow_auto_save_enabled = flow_auto_save_enabled + if web_extraction_enabled is not None: + self.web_extraction_enabled = web_extraction_enabled + if metrics_content_type_enabled is not None: + self.metrics_content_type_enabled = metrics_content_type_enabled + if notify_site_admins_on_throttle is not None: + self.notify_site_admins_on_throttle = notify_site_admins_on_throttle + if authoring_enabled is not None: + self.authoring_enabled = authoring_enabled + if custom_subscription_email_enabled is not None: + self.custom_subscription_email_enabled = custom_subscription_email_enabled + if custom_subscription_email is not None: + self.custom_subscription_email = custom_subscription_email + if custom_subscription_footer_enabled is not None: + self.custom_subscription_footer_enabled = custom_subscription_footer_enabled + if custom_subscription_footer is not None: + self.custom_subscription_footer = custom_subscription_footer + if ask_data_mode is not None: + self.ask_data_mode = ask_data_mode + if named_sharing_enabled is not None: + self.named_sharing_enabled = named_sharing_enabled + if mobile_biometrics_enabled is not None: + self.mobile_biometrics_enabled = mobile_biometrics_enabled + if sheet_image_enabled is not None: + self.sheet_image_enabled = sheet_image_enabled + if derived_permissions_enabled is not None: + self.derived_permissions_enabled = derived_permissions_enabled + if user_visibility_mode is not None: + self.user_visibility_mode = user_visibility_mode + if use_default_time_zone is not None: + self.use_default_time_zone = use_default_time_zone + if time_zone is not None: + self.time_zone = time_zone + if auto_suspend_refresh_enabled is not None: + self.auto_suspend_refresh_enabled = auto_suspend_refresh_enabled + if auto_suspend_refresh_inactivity_window is not None: + self.auto_suspend_refresh_inactivity_window = auto_suspend_refresh_inactivity_window @classmethod def from_response(cls, resp, ns): @@ -215,14 +652,34 @@ def from_response(cls, resp, ns): for site_xml in all_site_xml: (id, name, content_url, status_reason, admin_mode, state, subscribe_others_enabled, disable_subscriptions, revision_history_enabled, user_quota, storage_quota, - revision_limit, num_users, storage, data_acceleration_mode, flows_enabled, - cataloging_enabled) = cls._parse_element(site_xml, ns) + revision_limit, num_users, storage, data_acceleration_mode, flows_enabled, cataloging_enabled, + editing_flows_enabled, scheduling_flows_enabled, allow_subscription_attachments, guest_access_enabled, + cache_warmup_enabled, commenting_enabled, extract_encryption_mode, request_access_enabled, + run_now_enabled, tier_explorer_capacity, tier_creator_capacity, tier_viewer_capacity, + data_alerts_enabled, commenting_mentions_enabled, catalog_obfuscation_enabled, flow_auto_save_enabled, + web_extraction_enabled, metrics_content_type_enabled, notify_site_admins_on_throttle, + authoring_enabled, custom_subscription_email_enabled, custom_subscription_email, + custom_subscription_footer_enabled, custom_subscription_footer, ask_data_mode, named_sharing_enabled, + mobile_biometrics_enabled, sheet_image_enabled, derived_permissions_enabled, user_visibility_mode, + use_default_time_zone, time_zone, auto_suspend_refresh_enabled, + auto_suspend_refresh_inactivity_window) = cls._parse_element(site_xml, ns) site_item = cls(name, content_url) - site_item._set_values(id, name, content_url, status_reason, admin_mode, state, - subscribe_others_enabled, disable_subscriptions, revision_history_enabled, - user_quota, storage_quota, revision_limit, num_users, storage, - data_acceleration_mode, flows_enabled, cataloging_enabled) + site_item._set_values(id, name, content_url, status_reason, admin_mode, state, subscribe_others_enabled, + disable_subscriptions, revision_history_enabled, user_quota, storage_quota, + revision_limit, num_users, storage, data_acceleration_mode, flows_enabled, + cataloging_enabled, editing_flows_enabled, scheduling_flows_enabled, + allow_subscription_attachments, guest_access_enabled, cache_warmup_enabled, + commenting_enabled, extract_encryption_mode, request_access_enabled, run_now_enabled, + tier_explorer_capacity, tier_creator_capacity, tier_viewer_capacity, + data_alerts_enabled, commenting_mentions_enabled, catalog_obfuscation_enabled, + flow_auto_save_enabled, web_extraction_enabled, metrics_content_type_enabled, + notify_site_admins_on_throttle, authoring_enabled, custom_subscription_email_enabled, + custom_subscription_email, custom_subscription_footer_enabled, + custom_subscription_footer, ask_data_mode, named_sharing_enabled, + mobile_biometrics_enabled, sheet_image_enabled, derived_permissions_enabled, + user_visibility_mode, use_default_time_zone, time_zone, auto_suspend_refresh_enabled, + auto_suspend_refresh_inactivity_window) all_site_items.append(site_item) return all_site_items @@ -237,6 +694,48 @@ def _parse_element(site_xml, ns): subscribe_others_enabled = string_to_bool(site_xml.get('subscribeOthersEnabled', '')) disable_subscriptions = string_to_bool(site_xml.get('disableSubscriptions', '')) revision_history_enabled = string_to_bool(site_xml.get('revisionHistoryEnabled', '')) + editing_flows_enabled = string_to_bool(site_xml.get('editingFlowsEnabled', '')) + scheduling_flows_enabled = string_to_bool(site_xml.get('schedulingFlowsEnabled', '')) + allow_subscription_attachments = string_to_bool(site_xml.get('allowSubscriptionAttachments', '')) + guest_access_enabled = string_to_bool(site_xml.get('guestAccessEnabled', '')) + cache_warmup_enabled = string_to_bool(site_xml.get('cacheWarmupEnabled', '')) + commenting_enabled = string_to_bool(site_xml.get('commentingEnabled', '')) + extract_encryption_mode = site_xml.get('extractEncryptionMode', None) + request_access_enabled = string_to_bool(site_xml.get('requestAccessEnabled', '')) + run_now_enabled = string_to_bool(site_xml.get('runNowEnabled', '')) + tier_explorer_capacity = site_xml.get('tierExplorerCapacity', None) + if tier_explorer_capacity: + tier_explorer_capacity = int(tier_explorer_capacity) + tier_creator_capacity = site_xml.get('tierCreatorCapacity', None) + if tier_creator_capacity: + tier_creator_capacity = int(tier_creator_capacity) + tier_viewer_capacity = site_xml.get('tierViewerCapacity', None) + if tier_viewer_capacity: + tier_viewer_capacity = int(tier_viewer_capacity) + data_alerts_enabled = string_to_bool(site_xml.get('dataAlertsEnabled', '')) + commenting_mentions_enabled = string_to_bool(site_xml.get('commentingMentionsEnabled', '')) + catalog_obfuscation_enabled = string_to_bool(site_xml.get('catalogObfuscationEnabled', '')) + flow_auto_save_enabled = string_to_bool(site_xml.get('flowAutoSaveEnabled', '')) + web_extraction_enabled = string_to_bool(site_xml.get('webExtractionEnabled', '')) + metrics_content_type_enabled = string_to_bool(site_xml.get('metricsContentTypeEnabled', '')) + notify_site_admins_on_throttle = string_to_bool(site_xml.get('notifySiteAdminsOnThrottle', '')) + authoring_enabled = string_to_bool(site_xml.get('authoringEnabled', '')) + custom_subscription_email_enabled = string_to_bool(site_xml.get('customSubscriptionEmailEnabled', '')) + custom_subscription_email = site_xml.get('customSubscriptionEmail', None) + custom_subscription_footer_enabled = string_to_bool(site_xml.get('customSubscriptionFooterEnabled', '')) + custom_subscription_footer = site_xml.get('customSubscriptionFooter', None) + ask_data_mode = site_xml.get('askDataMode', None) + named_sharing_enabled = string_to_bool(site_xml.get('namedSharingEnabled', '')) + mobile_biometrics_enabled = string_to_bool(site_xml.get('mobileBiometricsEnabled', '')) + sheet_image_enabled = string_to_bool(site_xml.get('sheetImageEnabled', '')) + derived_permissions_enabled = string_to_bool(site_xml.get('derivedPermissionsEnabled', '')) + user_visibility_mode = site_xml.get('userVisibilityMode', '') + use_default_time_zone = string_to_bool(site_xml.get('useDefaultTimeZone', '')) + time_zone = site_xml.get('timeZone', None) + auto_suspend_refresh_enabled = string_to_bool(site_xml.get('autoSuspendRefreshEnabled', '')) + auto_suspend_refresh_inactivity_window = site_xml.get('autoSuspendRefreshInactivityWindow', None) + if auto_suspend_refresh_inactivity_window: + auto_suspend_refresh_inactivity_window = int(auto_suspend_refresh_inactivity_window) user_quota = site_xml.get('userQuota', None) if user_quota: @@ -264,7 +763,16 @@ def _parse_element(site_xml, ns): return id, name, content_url, status_reason, admin_mode, state, subscribe_others_enabled,\ disable_subscriptions, revision_history_enabled, user_quota, storage_quota,\ - revision_limit, num_users, storage, data_acceleration_mode, flows_enabled, cataloging_enabled + revision_limit, num_users, storage, data_acceleration_mode, flows_enabled, cataloging_enabled,\ + editing_flows_enabled, scheduling_flows_enabled, allow_subscription_attachments, guest_access_enabled,\ + cache_warmup_enabled, commenting_enabled, extract_encryption_mode, request_access_enabled, run_now_enabled,\ + tier_explorer_capacity, tier_creator_capacity, tier_viewer_capacity, data_alerts_enabled,\ + commenting_mentions_enabled, catalog_obfuscation_enabled, flow_auto_save_enabled, web_extraction_enabled,\ + metrics_content_type_enabled, notify_site_admins_on_throttle, authoring_enabled,\ + custom_subscription_email_enabled, custom_subscription_email, custom_subscription_footer_enabled,\ + custom_subscription_footer, ask_data_mode, named_sharing_enabled, mobile_biometrics_enabled,\ + sheet_image_enabled, derived_permissions_enabled, user_visibility_mode, use_default_time_zone, time_zone,\ + auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window # Used to convert string represented boolean to a boolean type diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 14c755bbd..84e7afe0f 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -472,7 +472,7 @@ def update_req(self, site_item): site_element.attrib['subscribeOthersEnabled'] = str(site_item.subscribe_others_enabled).lower() if site_item.revision_limit: site_element.attrib['revisionLimit'] = str(site_item.revision_limit) - if site_item.subscribe_others_enabled is not None: + if site_item.revision_history_enabled is not None: site_element.attrib['revisionHistoryEnabled'] = str(site_item.revision_history_enabled).lower() if site_item.data_acceleration_mode is not None: site_element.attrib['dataAccelerationMode'] = str(site_item.data_acceleration_mode).lower() @@ -480,6 +480,78 @@ def update_req(self, site_item): site_element.attrib['flowsEnabled'] = str(site_item.flows_enabled).lower() if site_item.cataloging_enabled is not None: site_element.attrib['catalogingEnabled'] = str(site_item.cataloging_enabled).lower() + if site_item.editing_flows_enabled is not None: + site_element.attrib['editingFlowsEnabled'] = str(site_item.editing_flows_enabled).lower() + if site_item.scheduling_flows_enabled is not None: + site_element.attrib['schedulingFlowsEnabled'] = str(site_item.scheduling_flows_enabled).lower() + if site_item.allow_subscription_attachments is not None: + site_element.attrib['allowSubscriptionAttachments'] = str(site_item.allow_subscription_attachments).lower() + if site_item.guest_access_enabled is not None: + site_element.attrib['guestAccessEnabled'] = str(site_item.guest_access_enabled).lower() + if site_item.cache_warmup_enabled is not None: + site_element.attrib['cacheWarmupEnabled'] = str(site_item.cache_warmup_enabled).lower() + if site_item.commenting_enabled is not None: + site_element.attrib['commentingEnabled'] = str(site_item.commenting_enabled).lower() + if site_item.extract_encryption_mode is not None: + site_element.attrib['extractEncryptionMode'] = str(site_item.extract_encryption_mode).lower() + if site_item.request_access_enabled is not None: + site_element.attrib['requestAccessEnabled'] = str(site_item.request_access_enabled).lower() + if site_item.run_now_enabled is not None: + site_element.attrib['runNowEnabled'] = str(site_item.run_now_enabled).lower() + if site_item.tier_creator_capacity is not None: + site_element.attrib['tierCreatorCapacity'] = str(site_item.tier_creator_capacity).lower() + if site_item.tier_explorer_capacity is not None: + site_element.attrib['tierExplorerCapacity'] = str(site_item.tier_explorer_capacity).lower() + if site_item.tier_viewer_capacity is not None: + site_element.attrib['tierViewerCapacity'] = str(site_item.tier_viewer_capacity).lower() + if site_item.data_alerts_enabled is not None: + site_element.attrib['dataAlertsEnabled'] = str(site_item.data_alerts_enabled) + if site_item.commenting_mentions_enabled is not None: + site_element.attrib['commentingMentionsEnabled'] = str(site_item.commenting_mentions_enabled).lower() + if site_item.catalog_obfuscation_enabled is not None: + site_element.attrib['catalogObfuscationEnabled'] = str(site_item.catalog_obfuscation_enabled).lower() + if site_item.flow_auto_save_enabled is not None: + site_element.attrib['flowAutoSaveEnabled'] = str(site_item.flow_auto_save_enabled).lower() + if site_item.web_extraction_enabled is not None: + site_element.attrib['webExtractionEnabled'] = str(site_item.web_extraction_enabled).lower() + if site_item.metrics_content_type_enabled is not None: + site_element.attrib['metricsContentTypeEnabled'] = str(site_item.metrics_content_type_enabled).lower() + if site_item.notify_site_admins_on_throttle is not None: + site_element.attrib['notifySiteAdminsOnThrottle'] = str(site_item.notify_site_admins_on_throttle).lower() + if site_item.authoring_enabled is not None: + site_element.attrib['authoringEnabled'] = str(site_item.authoring_enabled).lower() + if site_item.custom_subscription_email_enabled is not None: + site_element.attrib['customSubscriptionEmailEnabled'] = \ + str(site_item.custom_subscription_email_enabled).lower() + if site_item.custom_subscription_email is not None: + site_element.attrib['customSubscriptionEmail'] = str(site_item.custom_subscription_email).lower() + if site_item.custom_subscription_footer_enabled is not None: + site_element.attrib['customSubscriptionFooterEnabled'] =\ + str(site_item.custom_subscription_footer_enabled).lower() + if site_item.custom_subscription_footer is not None: + site_element.attrib['customSubscriptionFooter'] = str(site_item.custom_subscription_footer).lower() + if site_item.ask_data_mode is not None: + site_element.attrib['askDataMode'] = str(site_item.ask_data_mode) + if site_item.named_sharing_enabled is not None: + site_element.attrib['namedSharingEnabled'] = str(site_item.named_sharing_enabled).lower() + if site_item.mobile_biometrics_enabled is not None: + site_element.attrib['mobileBiometricsEnabled'] = str(site_item.mobile_biometrics_enabled).lower() + if site_item.sheet_image_enabled is not None: + site_element.attrib['sheetImageEnabled'] = str(site_item.sheet_image_enabled).lower() + if site_item.derived_permissions_enabled is not None: + site_element.attrib['derivedPermissionsEnabled'] = str(site_item.derived_permissions_enabled).lower() + if site_item.user_visibility_mode is not None: + site_element.attrib['userVisibilityMode'] = str(site_item.user_visibility_mode) + if site_item.use_default_time_zone is not None: + site_element.attrib['useDefaultTimeZone'] = str(site_item.use_default_time_zone).lower() + if site_item.time_zone is not None: + site_element.attrib['timeZone'] = str(site_item.time_zone) + if site_item.auto_suspend_refresh_enabled is not None: + site_element.attrib['autoSuspendRefreshEnabled'] = str(site_item.auto_suspend_refresh_enabled).lower() + if site_item.auto_suspend_refresh_inactivity_window is not None: + site_element.attrib['autoSuspendRefreshInactivityWindow'] =\ + str(site_item.auto_suspend_refresh_inactivity_window) + return ET.tostring(xml_request) def create_req(self, site_item): @@ -495,10 +567,90 @@ def create_req(self, site_item): site_element.attrib['storageQuota'] = str(site_item.storage_quota) if site_item.disable_subscriptions is not None: site_element.attrib['disableSubscriptions'] = str(site_item.disable_subscriptions).lower() + if site_item.subscribe_others_enabled is not None: + site_element.attrib['subscribeOthersEnabled'] = str(site_item.subscribe_others_enabled).lower() + if site_item.revision_limit: + site_element.attrib['revisionLimit'] = str(site_item.revision_limit) + if site_item.data_acceleration_mode is not None: + site_element.attrib['dataAccelerationMode'] = str(site_item.data_acceleration_mode).lower() if site_item.flows_enabled is not None: site_element.attrib['flowsEnabled'] = str(site_item.flows_enabled).lower() + if site_item.editing_flows_enabled is not None: + site_element.attrib['editingFlowsEnabled'] = str(site_item.editing_flows_enabled).lower() + if site_item.scheduling_flows_enabled is not None: + site_element.attrib['schedulingFlowsEnabled'] = str(site_item.scheduling_flows_enabled).lower() + if site_item.allow_subscription_attachments is not None: + site_element.attrib['allowSubscriptionAttachments'] = str(site_item.allow_subscription_attachments).lower() + if site_item.guest_access_enabled is not None: + site_element.attrib['guestAccessEnabled'] = str(site_item.guest_access_enabled).lower() + if site_item.cache_warmup_enabled is not None: + site_element.attrib['cacheWarmupEnabled'] = str(site_item.cache_warmup_enabled).lower() + if site_item.commenting_enabled is not None: + site_element.attrib['commentingEnabled'] = str(site_item.commenting_enabled).lower() + if site_item.revision_history_enabled is not None: + site_element.attrib['revisionHistoryEnabled'] = str(site_item.revision_history_enabled).lower() + if site_item.extract_encryption_mode is not None: + site_element.attrib['extractEncryptionMode'] = str(site_item.extract_encryption_mode).lower() + if site_item.request_access_enabled is not None: + site_element.attrib['requestAccessEnabled'] = str(site_item.request_access_enabled).lower() + if site_item.run_now_enabled is not None: + site_element.attrib['runNowEnabled'] = str(site_item.run_now_enabled).lower() + if site_item.tier_creator_capacity is not None: + site_element.attrib['tierCreatorCapacity'] = str(site_item.tier_creator_capacity).lower() + if site_item.tier_explorer_capacity is not None: + site_element.attrib['tierExplorerCapacity'] = str(site_item.tier_explorer_capacity).lower() + if site_item.tier_viewer_capacity is not None: + site_element.attrib['tierViewerCapacity'] = str(site_item.tier_viewer_capacity).lower() + if site_item.data_alerts_enabled is not None: + site_element.attrib['dataAlertsEnabled'] = str(site_item.data_alerts_enabled).lower() + if site_item.commenting_mentions_enabled is not None: + site_element.attrib['commentingMentionsEnabled'] = str(site_item.commenting_mentions_enabled).lower() + if site_item.catalog_obfuscation_enabled is not None: + site_element.attrib['catalogObfuscationEnabled'] = str(site_item.catalog_obfuscation_enabled).lower() + if site_item.flow_auto_save_enabled is not None: + site_element.attrib['flowAutoSaveEnabled'] = str(site_item.flow_auto_save_enabled).lower() + if site_item.web_extraction_enabled is not None: + site_element.attrib['webExtractionEnabled'] = str(site_item.web_extraction_enabled).lower() + if site_item.metrics_content_type_enabled is not None: + site_element.attrib['metricsContentTypeEnabled'] = str(site_item.metrics_content_type_enabled).lower() + if site_item.notify_site_admins_on_throttle is not None: + site_element.attrib['notifySiteAdminsOnThrottle'] = str(site_item.notify_site_admins_on_throttle).lower() + if site_item.authoring_enabled is not None: + site_element.attrib['authoringEnabled'] = str(site_item.authoring_enabled).lower() + if site_item.custom_subscription_email_enabled is not None: + site_element.attrib['customSubscriptionEmailEnabled'] =\ + str(site_item.custom_subscription_email_enabled).lower() + if site_item.custom_subscription_email is not None: + site_element.attrib['customSubscriptionEmail'] = str(site_item.custom_subscription_email).lower() + if site_item.custom_subscription_footer_enabled is not None: + site_element.attrib['customSubscriptionFooterEnabled'] =\ + str(site_item.custom_subscription_footer_enabled).lower() + if site_item.custom_subscription_footer is not None: + site_element.attrib['customSubscriptionFooter'] = str(site_item.custom_subscription_footer).lower() + if site_item.ask_data_mode is not None: + site_element.attrib['askDataMode'] = str(site_item.ask_data_mode) + if site_item.named_sharing_enabled is not None: + site_element.attrib['namedSharingEnabled'] = str(site_item.named_sharing_enabled).lower() + if site_item.mobile_biometrics_enabled is not None: + site_element.attrib['mobileBiometricsEnabled'] = str(site_item.mobile_biometrics_enabled).lower() + if site_item.sheet_image_enabled is not None: + site_element.attrib['sheetImageEnabled'] = str(site_item.sheet_image_enabled).lower() if site_item.cataloging_enabled is not None: site_element.attrib['catalogingEnabled'] = str(site_item.cataloging_enabled).lower() + if site_item.derived_permissions_enabled is not None: + site_element.attrib['derivedPermissionsEnabled'] = str(site_item.derived_permissions_enabled).lower() + if site_item.user_visibility_mode is not None: + site_element.attrib['userVisibilityMode'] = str(site_item.user_visibility_mode) + if site_item.use_default_time_zone is not None: + site_element.attrib['useDefaultTimeZone'] = str(site_item.use_default_time_zone).lower() + if site_item.time_zone is not None: + site_element.attrib['timeZone'] = str(site_item.time_zone) + if site_item.auto_suspend_refresh_enabled is not None: + site_element.attrib['autoSuspendRefreshEnabled'] = str(site_item.auto_suspend_refresh_enabled).lower() + if site_item.auto_suspend_refresh_inactivity_window is not None: + site_element.attrib['autoSuspendRefreshInactivityWindow'] =\ + str(site_item.auto_suspend_refresh_inactivity_window) + return ET.tostring(xml_request) diff --git a/test/assets/site_create.xml b/test/assets/site_create.xml index 9fafb5f02..9d9c4a009 100644 --- a/test/assets/site_create.xml +++ b/test/assets/site_create.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/test/assets/site_get.xml b/test/assets/site_get.xml index e3c7a781c..7ffa91eb7 100644 --- a/test/assets/site_get.xml +++ b/test/assets/site_get.xml @@ -2,7 +2,7 @@ - - + + \ No newline at end of file diff --git a/test/assets/site_get_by_id.xml b/test/assets/site_get_by_id.xml index 98bc3e4e6..a47703fb6 100644 --- a/test/assets/site_get_by_id.xml +++ b/test/assets/site_get_by_id.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/test/assets/site_get_by_name.xml b/test/assets/site_get_by_name.xml index 5b3042e61..852f9594f 100644 --- a/test/assets/site_get_by_name.xml +++ b/test/assets/site_get_by_name.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/test/assets/site_update.xml b/test/assets/site_update.xml index 30e434373..dbb166de1 100644 --- a/test/assets/site_update.xml +++ b/test/assets/site_update.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/test/test_site.py b/test/test_site.py index a06876e2a..8fbb4eda3 100644 --- a/test/test_site.py +++ b/test/test_site.py @@ -36,13 +36,30 @@ def test_get(self): self.assertEqual('ContentOnly', all_sites[0].admin_mode) self.assertEqual(False, all_sites[0].revision_history_enabled) self.assertEqual(True, all_sites[0].subscribe_others_enabled) - + self.assertEqual(25, all_sites[0].revision_limit) + self.assertEqual(None, all_sites[0].num_users) + self.assertEqual(None, all_sites[0].storage) + self.assertEqual(True, all_sites[0].cataloging_enabled) + self.assertEqual(False, all_sites[0].editing_flows_enabled) + self.assertEqual(False, all_sites[0].scheduling_flows_enabled) + self.assertEqual(True, all_sites[0].allow_subscription_attachments) self.assertEqual('6b7179ba-b82b-4f0f-91ed-812074ac5da6', all_sites[1].id) self.assertEqual('Active', all_sites[1].state) self.assertEqual('Samples', all_sites[1].name) self.assertEqual('ContentOnly', all_sites[1].admin_mode) self.assertEqual(False, all_sites[1].revision_history_enabled) self.assertEqual(True, all_sites[1].subscribe_others_enabled) + self.assertEqual(False, all_sites[1].guest_access_enabled) + self.assertEqual(True, all_sites[1].cache_warmup_enabled) + self.assertEqual(True, all_sites[1].commenting_enabled) + self.assertEqual(True, all_sites[1].cache_warmup_enabled) + self.assertEqual(False, all_sites[1].request_access_enabled) + self.assertEqual(True, all_sites[1].run_now_enabled) + self.assertEqual(1, all_sites[1].tier_explorer_capacity) + self.assertEqual(2, all_sites[1].tier_creator_capacity) + self.assertEqual(1, all_sites[1].tier_viewer_capacity) + self.assertEqual(False, all_sites[1].flows_enabled) + self.assertEqual(None, all_sites[1].data_acceleration_mode) def test_get_before_signin(self): self.server._auth_token = None @@ -62,6 +79,9 @@ def test_get_by_id(self): self.assertEqual(False, single_site.revision_history_enabled) self.assertEqual(True, single_site.subscribe_others_enabled) self.assertEqual(False, single_site.disable_subscriptions) + self.assertEqual(False, single_site.data_alerts_enabled) + self.assertEqual(False, single_site.commenting_mentions_enabled) + self.assertEqual(True, single_site.catalog_obfuscation_enabled) def test_get_by_id_missing_id(self): self.assertRaises(ValueError, self.server.sites.get_by_id, '') @@ -93,7 +113,18 @@ def test_update(self): admin_mode=TSC.SiteItem.AdminMode.ContentAndUsers, user_quota=15, storage_quota=1000, disable_subscriptions=True, revision_history_enabled=False, - data_acceleration_mode='disable') + data_acceleration_mode='disable', flow_auto_save_enabled=True, + web_extraction_enabled=False, metrics_content_type_enabled=True, + notify_site_admins_on_throttle=False, authoring_enabled=True, + custom_subscription_email_enabled=True, + custom_subscription_email='test@test.com', + custom_subscription_footer_enabled=True, + custom_subscription_footer='example_footer', ask_data_mode='EnabledByDefault', + named_sharing_enabled=False, mobile_biometrics_enabled=True, + sheet_image_enabled=False, derived_permissions_enabled=True, + user_visibility_mode='FULL', use_default_time_zone=False, + time_zone='America/Los_Angeles', auto_suspend_refresh_enabled=True, + auto_suspend_refresh_inactivity_window=55) single_site._id = '6b7179ba-b82b-4f0f-91ed-812074ac5da6' single_site = self.server.sites.update(single_site) @@ -109,6 +140,25 @@ def test_update(self): self.assertEqual('disable', single_site.data_acceleration_mode) self.assertEqual(True, single_site.flows_enabled) self.assertEqual(True, single_site.cataloging_enabled) + self.assertEqual(True, single_site.flow_auto_save_enabled) + self.assertEqual(False, single_site.web_extraction_enabled) + self.assertEqual(True, single_site.metrics_content_type_enabled) + self.assertEqual(False, single_site.notify_site_admins_on_throttle) + self.assertEqual(True, single_site.authoring_enabled) + self.assertEqual(True, single_site.custom_subscription_email_enabled) + self.assertEqual('test@test.com', single_site.custom_subscription_email) + self.assertEqual(True, single_site.custom_subscription_footer_enabled) + self.assertEqual('example_footer', single_site.custom_subscription_footer) + self.assertEqual('EnabledByDefault', single_site.ask_data_mode) + self.assertEqual(False, single_site.named_sharing_enabled) + self.assertEqual(True, single_site.mobile_biometrics_enabled) + self.assertEqual(False, single_site.sheet_image_enabled) + self.assertEqual(True, single_site.derived_permissions_enabled) + self.assertEqual('FULL', single_site.user_visibility_mode) + self.assertEqual(False, single_site.use_default_time_zone) + self.assertEqual('America/Los_Angeles', single_site.time_zone) + self.assertEqual(True, single_site.auto_suspend_refresh_enabled) + self.assertEqual(55, single_site.auto_suspend_refresh_inactivity_window) def test_update_missing_id(self): single_site = TSC.SiteItem('test', 'test') From 026bca8dd48f7fea85473599261bb673f6ad43be Mon Sep 17 00:00:00 2001 From: Chris Shin Date: Thu, 11 Feb 2021 15:34:08 -0800 Subject: [PATCH 13/19] Adds skipConnectionCheck to publish workbook (#791) * Adds skipConnectionCheck flag to publish workbook * Removes unnecessary lines * Fixes style error --- samples/publish_workbook.py | 7 ++++-- .../server/endpoint/workbooks_endpoint.py | 5 ++++- test/test_workbook.py | 22 +++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/samples/publish_workbook.py b/samples/publish_workbook.py index be2c9599f..ca366cf9e 100644 --- a/samples/publish_workbook.py +++ b/samples/publish_workbook.py @@ -31,6 +31,7 @@ def main(): parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error', help='desired logging level (set to error by default)') parser.add_argument('--as-job', '-a', help='Publishing asynchronously', action='store_true') + parser.add_argument('--skip-connection-check', '-c', help='Skip live connection check', action='store_true') parser.add_argument('--site', '-S', default='', help='id (contentUrl) of site to sign into') args = parser.parse_args() @@ -71,11 +72,13 @@ def main(): new_workbook = TSC.WorkbookItem(default_project.id) if args.as_job: new_job = server.workbooks.publish(new_workbook, args.filepath, overwrite_true, - connections=all_connections, as_job=args.as_job) + connections=all_connections, as_job=args.as_job, + skip_connection_check=args.skip_connection_check) print("Workbook published. JOB ID: {0}".format(new_job.id)) else: new_workbook = server.workbooks.publish(new_workbook, args.filepath, overwrite_true, - connections=all_connections, as_job=args.as_job) + connections=all_connections, as_job=args.as_job, + skip_connection_check=args.skip_connection_check) print("Workbook published. ID: {0}".format(new_workbook.id)) else: error = "The default project could not be found." diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index 62f94f99a..e40d9e1dd 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -256,7 +256,7 @@ def delete_permission(self, item, capability_item): def publish( self, workbook_item, file, mode, connection_credentials=None, connections=None, as_job=False, - hidden_views=None + hidden_views=None, skip_connection_check=False ): if connection_credentials is not None: @@ -318,6 +318,9 @@ def publish( if as_job: url += '&{0}=true'.format('asJob') + if skip_connection_check: + url += '&{0}=true'.format('skipConnectionCheck') + # Determine if chunking is required (64MB is the limit for single upload method) if file_size >= FILESIZE_LIMIT: logger.info('Publishing {0} to server with chunking method (workbook over 64MB)'.format(workbook_item.name)) diff --git a/test/test_workbook.py b/test/test_workbook.py index f14e4d96f..fc1344b9e 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -544,6 +544,28 @@ def test_publish_with_hidden_view(self): self.assertTrue(re.search(rb'<\/views>', request_body)) self.assertTrue(re.search(rb'<\/views>', request_body)) + def test_publish_with_query_params(self): + with open(PUBLISH_ASYNC_XML, 'rb') as f: + response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.post(self.baseurl, text=response_xml) + + new_workbook = TSC.WorkbookItem(name='Sample', + show_tabs=False, + project_id='ee8c6e70-43b6-11e6-af4f-f7b0d8e20760') + + sample_workbook = os.path.join(TEST_ASSET_DIR, 'SampleWB.twbx') + publish_mode = self.server.PublishMode.CreateNew + + self.server.workbooks.publish(new_workbook, sample_workbook, publish_mode, + as_job=True, skip_connection_check=True) + + request_query_params = m._adapter.request_history[0].qs + self.assertTrue('asjob' in request_query_params) + self.assertTrue(request_query_params['asjob']) + self.assertTrue('skipconnectioncheck' in request_query_params) + self.assertTrue(request_query_params['skipconnectioncheck']) + def test_publish_async(self): self.server.version = '3.0' baseurl = self.server.workbooks.baseurl From 88a01886b26165bd73bb8c4dd061efa4a3083a44 Mon Sep 17 00:00:00 2001 From: Chris Shin Date: Tue, 16 Feb 2021 09:17:35 -0800 Subject: [PATCH 14/19] [Subscriptions] Add new fields and ability to update (#794) * Add fields and parsing logic * Update subscription create request * Adds update request to subscriptions * Changes subscription change request creation to use tsrequest annotation * Update tests for parsing new fields * Fixes codestyle issues * Removes user and schedule name * Fixes test failure --- .../models/subscription_item.py | 92 +++++++++++++++++-- .../server/endpoint/subscriptions_endpoint.py | 12 +++ tableauserverclient/server/request_factory.py | 52 ++++++++++- test/assets/subscription_get.xml | 8 +- test/test_subscription.py | 27 +++++- 5 files changed, 172 insertions(+), 19 deletions(-) diff --git a/tableauserverclient/models/subscription_item.py b/tableauserverclient/models/subscription_item.py index 1a93c60d2..cdcc468a1 100644 --- a/tableauserverclient/models/subscription_item.py +++ b/tableauserverclient/models/subscription_item.py @@ -1,15 +1,23 @@ import xml.etree.ElementTree as ET from .target import Target +from .property_decorators import property_is_boolean class SubscriptionItem(object): def __init__(self, subject, schedule_id, user_id, target): - self.id = None - self.subject = subject + self._id = None + self.attach_image = True + self.attach_pdf = False + self.message = None + self.page_orientation = None + self.page_size_option = None self.schedule_id = schedule_id - self.user_id = user_id + self.send_if_view_empty = True + self.subject = subject + self.suspended = False self.target = target + self.user_id = user_id def __repr__(self): if self.id is not None: @@ -19,8 +27,45 @@ def __repr__(self): return " - - + + - - + + diff --git a/test/test_subscription.py b/test/test_subscription.py index 2e4b1eadf..15b845e56 100644 --- a/test/test_subscription.py +++ b/test/test_subscription.py @@ -28,14 +28,37 @@ def test_get_subscriptions(self): m.get(self.baseurl, text=response_xml) all_subscriptions, pagination_item = self.server.subscriptions.get() + self.assertEqual(2, pagination_item.total_available) subscription = all_subscriptions[0] self.assertEqual('382e9a6e-0c08-4a95-b6c1-c14df7bac3e4', subscription.id) - self.assertEqual('View', subscription.target.type) + self.assertEqual('NOT FOUND!', subscription.message) + self.assertTrue(subscription.attach_image) + self.assertFalse(subscription.attach_pdf) + self.assertFalse(subscription.suspended) + self.assertFalse(subscription.send_if_view_empty) + self.assertIsNone(subscription.page_orientation) + self.assertIsNone(subscription.page_size_option) + self.assertEqual('Not Found Alert', subscription.subject) self.assertEqual('cdd716ca-5818-470e-8bec-086885dbadee', subscription.target.id) + self.assertEqual('View', subscription.target.type) self.assertEqual('c0d5fc44-ad8c-4957-bec0-b70ed0f8df1e', subscription.user_id) - self.assertEqual('Not Found Alert', subscription.subject) self.assertEqual('7617c389-cdca-4940-a66e-69956fcebf3e', subscription.schedule_id) + subscription = all_subscriptions[1] + self.assertEqual('23cb7630-afc8-4c8e-b6cd-83ae0322ec66', subscription.id) + self.assertEqual('overview', subscription.message) + self.assertFalse(subscription.attach_image) + self.assertTrue(subscription.attach_pdf) + self.assertTrue(subscription.suspended) + self.assertTrue(subscription.send_if_view_empty) + self.assertEqual('PORTRAIT', subscription.page_orientation) + self.assertEqual('A5', subscription.page_size_option) + self.assertEqual('Last 7 Days', subscription.subject) + self.assertEqual('2e6b4e8f-22dd-4061-8f75-bf33703da7e5', subscription.target.id) + self.assertEqual('Workbook', subscription.target.type) + self.assertEqual('c0d5fc44-ad8c-4957-bec0-b70ed0f8df1e', subscription.user_id) + self.assertEqual('3407cd38-7b39-4983-86a6-67a1506a5e3f', subscription.schedule_id) + def test_get_subscription_by_id(self): with open(GET_XML_BY_ID, "rb") as f: response_xml = f.read().decode("utf-8") From f64fcf9a1c6d775a953aaedd966ca3c16fcb79fd Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 17 Feb 2021 01:21:46 +0800 Subject: [PATCH 15/19] MANIFEST.in: Add docs and test data (#780) * Update publish_workbook.py (#694) * Update publish_workbook.py Added below arguments, without this there is a sign-in error on publishing a test file to Tableau Online parser.add_argument('--sitename', '-S', default='', help='sitename required') tableau_auth = TSC.TableauAuth(args.username, password,site_id=args.sitename) * Update publish_workbook.py Edits (as requested) to publish workbooks on Tableau Online which removes the Sign-in Error. * Update publish_workbook.py * Merge pull request #745 from tableau/fix_732 Server versions before 2020.1 do not accept encoded query param delimiters * Merge pull request #757 from tableau/fix_754 Fixes issue #754 by moving file read logic inside generator * Updates changelog for v0.14.1 * MANIFEST.in: Add docs and test data Closes https://github.com/tableau/server-client-python/issues/779 Co-authored-by: Chris Shin Co-authored-by: Madhura Selvarajan --- MANIFEST.in | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index ae0a2ec7d..b4b1425f3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,22 @@ include versioneer.py include tableauserverclient/_version.py include LICENSE include LICENSE.versioneer +include README.md +include CHANGELOG.md +recursive-include docs *.md +recursive-include samples *.py +recursive-include samples *.txt +recursive-include smoke *.py +recursive-include test *.csv +recursive-include test *.dict +recursive-include test *.hyper +recursive-include test *.json +recursive-include test *.pdf +recursive-include test *.png +recursive-include test *.py +recursive-include test *.tde +recursive-include test *.tds +recursive-include test *.tdsx +recursive-include test *.twb +recursive-include test *.twbx +recursive-include test *.xml From fe992ee909cf7a01749504388b84f3afadf43bf5 Mon Sep 17 00:00:00 2001 From: Chris Shin Date: Tue, 16 Feb 2021 14:33:59 -0800 Subject: [PATCH 16/19] [Tasks] Translate task type from server to TSC enum (#796) * Adds task type mapping to translate server response * Updates tests to match server response for task type * Fixes pycodestyle error --- tableauserverclient/models/task_item.py | 15 ++++++++++++++- test/assets/tasks_no_workbook_or_datasource.xml | 6 +++--- test/assets/tasks_with_dataacceleration_task.xml | 2 +- test/assets/tasks_with_datasource.xml | 2 +- test/assets/tasks_with_workbook.xml | 2 +- .../assets/tasks_with_workbook_and_datasource.xml | 6 +++--- test/test_task.py | 2 ++ 7 files changed, 25 insertions(+), 10 deletions(-) diff --git a/tableauserverclient/models/task_item.py b/tableauserverclient/models/task_item.py index 2f3e6f3aa..27d26f215 100644 --- a/tableauserverclient/models/task_item.py +++ b/tableauserverclient/models/task_item.py @@ -9,6 +9,10 @@ class Type: ExtractRefresh = "extractRefresh" DataAcceleration = "dataAcceleration" + # This mapping is used to convert task type returned from server + _TASK_TYPE_MAPPING = {'RefreshExtractTask': Type.ExtractRefresh, + 'MaterializeViewsTask': Type.DataAcceleration} + def __init__(self, id_, task_type, priority, consecutive_failed_count=0, schedule_id=None, schedule_item=None, last_run_at=None, target=None): self.id = id_ @@ -58,9 +62,18 @@ def _parse_element(cls, element, ns): if last_run_at_element is not None: last_run_at = parse_datetime(last_run_at_element.text) - task_type = element.get('type', None) + # Server response has different names for task types + task_type = cls._translate_task_type(element.get('type', None)) + priority = int(element.get('priority', -1)) consecutive_failed_count = int(element.get('consecutiveFailedCount', 0)) id_ = element.get('id', None) return cls(id_, task_type, priority, consecutive_failed_count, schedule_item.id, schedule_item, last_run_at, target) + + @staticmethod + def _translate_task_type(task_type): + if task_type in TaskItem._TASK_TYPE_MAPPING: + return TaskItem._TASK_TYPE_MAPPING[task_type] + else: + return task_type diff --git a/test/assets/tasks_no_workbook_or_datasource.xml b/test/assets/tasks_no_workbook_or_datasource.xml index 7ddbcae62..da84194bf 100644 --- a/test/assets/tasks_no_workbook_or_datasource.xml +++ b/test/assets/tasks_no_workbook_or_datasource.xml @@ -4,17 +4,17 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.6.xsd"> - + - + - + diff --git a/test/assets/tasks_with_dataacceleration_task.xml b/test/assets/tasks_with_dataacceleration_task.xml index cbe837405..beb5d59eb 100644 --- a/test/assets/tasks_with_dataacceleration_task.xml +++ b/test/assets/tasks_with_dataacceleration_task.xml @@ -2,7 +2,7 @@ - + diff --git a/test/assets/tasks_with_datasource.xml b/test/assets/tasks_with_datasource.xml index 68e23a417..097161bf7 100644 --- a/test/assets/tasks_with_datasource.xml +++ b/test/assets/tasks_with_datasource.xml @@ -4,7 +4,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.6.xsd"> - + diff --git a/test/assets/tasks_with_workbook.xml b/test/assets/tasks_with_workbook.xml index 1565abf74..81e974e78 100644 --- a/test/assets/tasks_with_workbook.xml +++ b/test/assets/tasks_with_workbook.xml @@ -4,7 +4,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.6.xsd"> - + diff --git a/test/assets/tasks_with_workbook_and_datasource.xml b/test/assets/tasks_with_workbook_and_datasource.xml index 4389fa06c..81777bb46 100644 --- a/test/assets/tasks_with_workbook_and_datasource.xml +++ b/test/assets/tasks_with_workbook_and_datasource.xml @@ -4,19 +4,19 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://tableau.com/api http://tableau.com/api/ts-api-2.6.xsd"> - + - + - + diff --git a/test/test_task.py b/test/test_task.py index 789f97187..566167d4a 100644 --- a/test/test_task.py +++ b/test/test_task.py @@ -104,6 +104,7 @@ def test_get_materializeviews_tasks(self): self.assertEqual('b22190b4-6ac2-4eed-9563-4afc03444413', task.schedule_id) self.assertEqual(parse_datetime('2019-12-09T22:30:00Z'), task.schedule_item.next_run_at) self.assertEqual(parse_datetime('2019-12-09T20:45:04Z'), task.last_run_at) + self.assertEqual(TSC.TaskItem.Type.DataAcceleration, task.task_type) def test_delete_data_acceleration(self): with requests_mock.mock() as m: @@ -124,6 +125,7 @@ def test_get_by_id(self): self.assertEqual('c7a9327e-1cda-4504-b026-ddb43b976d1d', task.target.id) self.assertEqual('workbook', task.target.type) self.assertEqual('b60b4efd-a6f7-4599-beb3-cb677e7abac1', task.schedule_id) + self.assertEqual(TSC.TaskItem.Type.ExtractRefresh, task.task_type) def test_run_now(self): task_id = 'f84901ac-72ad-4f9b-a87e-7a3500402ad6' From 9179637b8cd3eb00ad7a12a44de9083e4ad39344 Mon Sep 17 00:00:00 2001 From: Lee Boynton Date: Tue, 16 Feb 2021 23:09:22 +0000 Subject: [PATCH 17/19] Add support for getting groups that a user belongs to (#799) * Add support for getting groups that a user belongs to * Use more descriptive name for pager function --- tableauserverclient/models/user_item.py | 11 ++++++++ .../server/endpoint/users_endpoint.py | 22 ++++++++++++++- test/assets/user_populate_groups.xml | 15 +++++++++++ test/test_user.py | 27 +++++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 test/assets/user_populate_groups.xml diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py index 9be38210f..b5a05b0d1 100644 --- a/tableauserverclient/models/user_item.py +++ b/tableauserverclient/models/user_item.py @@ -43,6 +43,7 @@ def __init__(self, name=None, site_role=None, auth_setting=None): self._last_login = None self._workbooks = None self._favorites = None + self._groups = None self.email = None self.fullname = None self.name = name @@ -107,12 +108,22 @@ def favorites(self): raise UnpopulatedPropertyError(error) return self._favorites + @property + def groups(self): + if self._groups is None: + error = "User item must be populated with groups first." + raise UnpopulatedPropertyError(error) + return self._groups() + def to_reference(self): return ResourceReference(id_=self.id, tag_name=self.tag_name) def _set_workbooks(self, workbooks): self._workbooks = workbooks + def _set_groups(self, groups): + self._groups = groups + def _parse_common_tags(self, user_xml, ns): if not isinstance(user_xml, ET.Element): user_xml = ET.fromstring(user_xml).find('.//t:user', namespaces=ns) diff --git a/tableauserverclient/server/endpoint/users_endpoint.py b/tableauserverclient/server/endpoint/users_endpoint.py index 5d3c69b26..17e12a8b1 100644 --- a/tableauserverclient/server/endpoint/users_endpoint.py +++ b/tableauserverclient/server/endpoint/users_endpoint.py @@ -1,6 +1,6 @@ from .endpoint import QuerysetEndpoint, api from .exceptions import MissingRequiredFieldError -from .. import RequestFactory, RequestOptions, UserItem, WorkbookItem, PaginationItem +from .. import RequestFactory, RequestOptions, UserItem, WorkbookItem, PaginationItem, GroupItem from ..pager import Pager import copy @@ -96,3 +96,23 @@ def _get_wbs_for_user(self, user_item, req_options=None): def populate_favorites(self, user_item): self.parent_srv.favorites.get(user_item) + + # Get groups for user + @api(version="3.7") + def populate_groups(self, user_item, req_options=None): + if not user_item.id: + error = "User item missing ID." + raise MissingRequiredFieldError(error) + + def groups_for_user_pager(): + return Pager(lambda options: self._get_groups_for_user(user_item, options), req_options) + + user_item._set_groups(groups_for_user_pager) + + def _get_groups_for_user(self, user_item, req_options=None): + url = "{0}/{1}/groups".format(self.baseurl, user_item.id) + server_response = self.get_request(url, req_options) + logger.info('Populated groups for user (ID: {0})'.format(user_item.id)) + group_item = GroupItem.from_response(server_response.content, self.parent_srv.namespace) + pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace) + return group_item, pagination_item diff --git a/test/assets/user_populate_groups.xml b/test/assets/user_populate_groups.xml new file mode 100644 index 000000000..567f1dbf8 --- /dev/null +++ b/test/assets/user_populate_groups.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/test/test_user.py b/test/test_user.py index db0f829f7..e4d1d6717 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -13,6 +13,7 @@ ADD_XML = os.path.join(TEST_ASSET_DIR, 'user_add.xml') POPULATE_WORKBOOKS_XML = os.path.join(TEST_ASSET_DIR, 'user_populate_workbooks.xml') GET_FAVORITES_XML = os.path.join(TEST_ASSET_DIR, 'favorites_get.xml') +POPULATE_GROUPS_XML = os.path.join(TEST_ASSET_DIR, 'user_populate_groups.xml') class UserTests(unittest.TestCase): @@ -175,3 +176,29 @@ def test_populate_favorites(self): self.assertEqual(view.id, 'd79634e1-6063-4ec9-95ff-50acbf609ff5') self.assertEqual(datasource.id, 'e76a1461-3b1d-4588-bf1b-17551a879ad9') self.assertEqual(project.id, '1d0304cd-3796-429f-b815-7258370b9b74') + + def test_populate_groups(self): + self.server.version = '3.7' + with open(POPULATE_GROUPS_XML, 'rb') as f: + response_xml = f.read().decode('utf-8') + with requests_mock.mock() as m: + m.get(self.server.users.baseurl + '/dd2239f6-ddf1-4107-981a-4cf94e415794/groups', + text=response_xml) + single_user = TSC.UserItem('test', 'Interactor') + single_user._id = 'dd2239f6-ddf1-4107-981a-4cf94e415794' + self.server.users.populate_groups(single_user) + + group_list = list(single_user.groups) + + self.assertEqual(3, len(group_list)) + self.assertEqual('ef8b19c0-43b6-11e6-af50-63f5805dbe3c', group_list[0].id) + self.assertEqual('All Users', group_list[0].name) + self.assertEqual('local', group_list[0].domain_name) + + self.assertEqual('e7833b48-c6f7-47b5-a2a7-36e7dd232758', group_list[1].id) + self.assertEqual('Another group', group_list[1].name) + self.assertEqual('local', group_list[1].domain_name) + + self.assertEqual('86a66d40-f289-472a-83d0-927b0f954dc8', group_list[2].id) + self.assertEqual('TableauExample', group_list[2].name) + self.assertEqual('local', group_list[2].domain_name) From 6c7a87b3e4e119621490f049d38d234a1c840a5b Mon Sep 17 00:00:00 2001 From: Chris Shin Date: Tue, 16 Feb 2021 15:09:45 -0800 Subject: [PATCH 18/19] Removes travis and adds linting/testing into github action (#798) * Removes travis and adds github workflow * Addressing code review feedback --- .github/workflows/run-tests.yml | 33 +++++++++++++++++++++++++++++++++ .travis.yml | 17 ----------------- setup.py | 10 +++++----- 3 files changed, 38 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/run-tests.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 000000000..e12d61383 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,33 @@ +name: Python package + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: [3.5, 3.6, 3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[test] + + - name: Lint with pycodestyle + run: | + pycodestyle tableauserverclient test samples + + - name: Test with pytest + run: | + pytest test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9085632f4..000000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -dist: xenial -language: python -python: - - "3.5" - - "3.6" - - "3.7" - - "3.8" - - "3.9" -# command to install dependencies -install: - - "pip install -e ." - - "pip install pycodestyle" -# command to run tests -script: - # Tests - - python setup.py test - - pycodestyle tableauserverclient test samples diff --git a/setup.py b/setup.py index 5586e4716..8b374f0ce 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ # This makes work easier for offline installs or low bandwidth machines needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv) pytest_runner = ['pytest-runner'] if needs_pytest else [] +test_requirements = ['mock', 'pycodestyle', 'pytest', 'requests-mock>=1.0,<2.0'] setup( name='tableauserverclient', @@ -34,9 +35,8 @@ install_requires=[ 'requests>=2.11,<3.0', ], - tests_require=[ - 'requests-mock>=1.0,<2.0', - 'pytest', - 'mock' - ] + tests_require=test_requirements, + extras_require={ + 'test': test_requirements + } ) From 004ab31140b3a4ee6157c36d8c6c434597151dd8 Mon Sep 17 00:00:00 2001 From: Chris Shin Date: Tue, 16 Feb 2021 16:49:19 -0800 Subject: [PATCH 19/19] Updates changelog and contributors list for v0.15 --- CHANGELOG.md | 17 +++++++++++++++++ CONTRIBUTORS.md | 3 +++ 2 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85dc8a702..45a44b251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## 0.15.0 (16 Feb 2021) +* Added support for python version 3.9 (#744) +* Added support for 'Get View by ID' (#750) +* Added docs and test data to MANIFEST.in file (#780) +* Added owner_id property to ProjectItem (#784) +* Added support for skipping connection check while publishing workbook (#791) +* Added support for 'Update Subscription' (#794) +* Added support for 'Get Groups for a User' (#799) +* Improved debug logging by including put/post request contents (#743) +* Improved local and active-directory group creation (#770) +* Improved 'Update Group' to match server requests/responses (#772) +* Improved SiteItem with new properties and functions (#777) +* Improved SubscriptionItem with new properties (#794) +* Improved the 'type' property of TaskItem to convert server response to enum (#796) +* Improved repository to use Github Actions for running tests/linter (#798) +* Fixed data_acceleration field causing error in workbook update payload (#741) + ## 0.14.1 (9 Dec 2020) * Fixed filter query issue for server version below 2020.1 (#745) * Fixed large workbook/datasource publish issue (#757) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 811f5c5bf..2a19b1317 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -41,6 +41,9 @@ The following people have contributed to this project to make it possible, and w * [Paul Vickers](https://github.com/paulvic) * [Madhura Selvarajan](https://github.com/maddy-at-leisure) * [Niklas Nevalainen](https://github.com/nnevalainen) +* [Terrence Jones](https://github.com/tjones-commits) +* [John Vandenberg](https://github.com/jayvdb) +* [Lee Boynton](https://github.com/lboynton) ## Core Team