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/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 diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index 7f7fec3ee..170f79f6e 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,12 +76,22 @@ 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): - return self._make_request(self.parent_srv.session.get, url, auth_token=self.parent_srv.auth_token, - request_object=request_object, parameters=parameters) + if request_object is not None: + try: + # 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, + parameters=parameters) def delete_request(self, url): # We don't return anything for a delete 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/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/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) 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..0572a1e10 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.10" 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'