Skip to content

Commit b62e43b

Browse files
t8y8Russell Hay
authored andcommitted
Download with extract_only and parameter checking (#143)
* Add a new decorator that checks parameters against the version. Used with the @api decorator. * Add extract_only flags to workbooks and data sources, protected behind v2.5 flag
1 parent fbe9b54 commit b62e43b

File tree

5 files changed

+74
-4
lines changed

5 files changed

+74
-4
lines changed

tableauserverclient/server/endpoint/datasources_endpoint.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .endpoint import Endpoint, api
1+
from .endpoint import Endpoint, api, parameter_added_in
22
from .exceptions import MissingRequiredFieldError
33
from .fileuploads_endpoint import Fileuploads
44
from .resource_tagger import _ResourceTagger
@@ -70,11 +70,16 @@ def delete(self, datasource_id):
7070

7171
# Download 1 datasource by id
7272
@api(version="2.0")
73-
def download(self, datasource_id, filepath=None):
73+
@parameter_added_in(version="2.5", parameters=['extract_only'])
74+
def download(self, datasource_id, filepath=None, extract_only=False):
7475
if not datasource_id:
7576
error = "Datasource ID undefined."
7677
raise ValueError(error)
7778
url = "{0}/{1}/content".format(self.baseurl, datasource_id)
79+
80+
if extract_only:
81+
url += "?includeExtract=False"
82+
7883
with closing(self.get_request(url, parameters={'stream': True})) as server_response:
7984
_, params = cgi.parse_header(server_response.headers['Content-Disposition'])
8085
filename = os.path.basename(params['filename'])

tableauserverclient/server/endpoint/endpoint.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,37 @@ def wrapper(self, *args, **kwargs):
107107
return func(self, *args, **kwargs)
108108
return wrapper
109109
return _decorator
110+
111+
112+
def parameter_added_in(version, parameters):
113+
'''Annotate minimum versions for new parameters or request options on an endpoint.
114+
115+
The api decorator documents when an endpoint was added, this decorator annotates
116+
keyword arguments on endpoints that may control functionality added after an endpoint was introduced.
117+
118+
The REST API will ignore invalid parameters in most cases, so this raises a warning instead of throwing
119+
an exception
120+
121+
Example:
122+
>>> @api(version="2.0")
123+
>>> @parameter_added_in(version="2.5", parameters=['extract_only'])
124+
>>> def download(self, workbook_id, filepath=None, extract_only=False):
125+
>>> ...
126+
'''
127+
def _decorator(func):
128+
@wraps(func)
129+
def wrapper(self, *args, **kwargs):
130+
params = set(parameters)
131+
invalid_params = params & set(kwargs)
132+
133+
if invalid_params:
134+
import warnings
135+
server_version = Version(self.parent_srv.version or "0.0")
136+
minimum_supported = Version(version)
137+
if server_version < minimum_supported:
138+
error = "The parameter(s) {!r} are not available in {} and will be ignored. Added in {}".format(
139+
invalid_params, server_version, minimum_supported)
140+
warnings.warn(error)
141+
return func(self, *args, **kwargs)
142+
return wrapper
143+
return _decorator

tableauserverclient/server/endpoint/workbooks_endpoint.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .endpoint import Endpoint, api
1+
from .endpoint import Endpoint, api, parameter_added_in
22
from .exceptions import MissingRequiredFieldError
33
from .fileuploads_endpoint import Fileuploads
44
from .resource_tagger import _ResourceTagger
@@ -76,12 +76,16 @@ def update(self, workbook_item):
7676

7777
# Download workbook contents with option of passing in filepath
7878
@api(version="2.0")
79-
def download(self, workbook_id, filepath=None):
79+
@parameter_added_in(version="2.5", parameters=['extract_only'])
80+
def download(self, workbook_id, filepath=None, extract_only=False):
8081
if not workbook_id:
8182
error = "Workbook ID undefined."
8283
raise ValueError(error)
8384
url = "{0}/{1}/content".format(self.baseurl, workbook_id)
8485

86+
if extract_only:
87+
url += "?includeExtract=False"
88+
8589
with closing(self.get_request(url, parameters={"stream": True})) as server_response:
8690
_, params = cgi.parse_header(server_response.headers['Content-Disposition'])
8791
filename = os.path.basename(params['filename'])

test/test_datasource.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,19 @@ def test_download(self):
164164
self.assertTrue(os.path.exists(file_path))
165165
os.remove(file_path)
166166

167+
def test_download_extract_only(self):
168+
# Pretend we're 2.5 for 'extract_only'
169+
self.server.version = "2.5"
170+
self.baseurl = self.server.datasources.baseurl
171+
172+
with requests_mock.mock() as m:
173+
m.get(self.baseurl + '/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content?includeExtract=False',
174+
headers={'Content-Disposition': 'name="tableau_datasource"; filename="Sample datasource.tds"'},
175+
complete_qs=True)
176+
file_path = self.server.datasources.download('9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', extract_only=True)
177+
self.assertTrue(os.path.exists(file_path))
178+
os.remove(file_path)
179+
167180
def test_update_missing_id(self):
168181
single_datasource = TSC.DatasourceItem('test', 'ee8c6e70-43b6-11e6-af4f-f7b0d8e20760')
169182
self.assertRaises(TSC.MissingRequiredFieldError, self.server.datasources.update, single_datasource)

test/test_workbook.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@ def test_download(self):
170170
self.assertTrue(os.path.exists(file_path))
171171
os.remove(file_path)
172172

173+
def test_download_extract_only(self):
174+
# Pretend we're 2.5 for 'extract_only'
175+
self.server.version = "2.5"
176+
self.baseurl = self.server.workbooks.baseurl
177+
178+
with requests_mock.mock() as m:
179+
m.get(self.baseurl + '/1f951daf-4061-451a-9df1-69a8062664f2/content?includeExtract=False',
180+
headers={'Content-Disposition': 'name="tableau_workbook"; filename="RESTAPISample.twbx"'},
181+
complete_qs=True)
182+
# Technically this shouldn't download a twbx, but we are interested in the qs, not the file
183+
file_path = self.server.workbooks.download('1f951daf-4061-451a-9df1-69a8062664f2', extract_only=True)
184+
self.assertTrue(os.path.exists(file_path))
185+
os.remove(file_path)
186+
173187
def test_download_missing_id(self):
174188
self.assertRaises(ValueError, self.server.workbooks.download, '')
175189

0 commit comments

Comments
 (0)