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'