diff --git a/tableauserverclient/filesys_helpers.py b/tableauserverclient/filesys_helpers.py index 9d0b443bf..3d6417464 100644 --- a/tableauserverclient/filesys_helpers.py +++ b/tableauserverclient/filesys_helpers.py @@ -20,3 +20,23 @@ def make_download_path(filepath, filename): download_path = filepath + os.path.splitext(filename)[1] return download_path + + +def get_file_object_size(file): + # Returns the size of a file object + file.seek(0, os.SEEK_END) + file_size = file.tell() + file.seek(0) + return file_size + + +def file_is_compressed(file): + # Determine if file is a zip file or not + # This reference lists magic file signatures: https://www.garykessler.net/library/file_sigs.html + + zip_file_signature = b'PK\x03\x04' + + is_zip_file = file.read(len(zip_file_signature)) == zip_file_signature + file.seek(0) + + return is_zip_file diff --git a/tableauserverclient/server/endpoint/fileuploads_endpoint.py b/tableauserverclient/server/endpoint/fileuploads_endpoint.py index 088883f30..62224c894 100644 --- a/tableauserverclient/server/endpoint/fileuploads_endpoint.py +++ b/tableauserverclient/server/endpoint/fileuploads_endpoint.py @@ -39,23 +39,27 @@ def append(self, xml_request, content_type): logger.info('Uploading a chunk to session (ID: {0})'.format(self.upload_id)) return FileuploadItem.from_response(server_response.content, self.parent_srv.namespace) - def read_chunks(self, file_path): - with open(file_path, 'rb') as f: - while True: - chunked_content = f.read(CHUNK_SIZE) - if not chunked_content: - break - yield chunked_content + def read_chunks(self, file): + + while True: + chunked_content = file.read(CHUNK_SIZE) + if not chunked_content: + break + yield chunked_content @classmethod - def upload_chunks(cls, parent_srv, file_path): + def upload_chunks(cls, parent_srv, file): file_uploader = cls(parent_srv) upload_id = file_uploader.initiate() - chunks = file_uploader.read_chunks(file_path) + + try: + with open(file, 'rb') as f: + chunks = file_uploader.read_chunks(f) + except TypeError: + 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) - logger.info("\tPublished {0}MB of {1}".format(fileupload_item.file_size, - os.path.basename(file_path))) + logger.info("\tPublished {0}MB".format(fileupload_item.file_size)) logger.info("\tCommitting file upload...") return upload_id diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index 2bde77dc9..36663750e 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -5,7 +5,7 @@ from .resource_tagger import _ResourceTagger from .. import RequestFactory, WorkbookItem, ConnectionItem, ViewItem, PaginationItem from ...models.job_item import JobItem -from ...filesys_helpers import to_filename, make_download_path +from ...filesys_helpers import to_filename, make_download_path, file_is_compressed, get_file_object_size import os import logging @@ -252,7 +252,7 @@ def delete_permission(self, item, capability_item): @parameter_added_in(as_job='3.0') @parameter_added_in(connections='2.8') def publish( - self, workbook_item, file_path, mode, + self, workbook_item, file, mode, connection_credentials=None, connections=None, as_job=False, hidden_views=None ): @@ -262,23 +262,40 @@ def publish( warnings.warn("connection_credentials is being deprecated. Use connections instead", DeprecationWarning) - if not os.path.isfile(file_path): - error = "File path does not lead to an existing file." - raise IOError(error) + try: + # Expect file to be a filepath + if not os.path.isfile(file): + error = "File path does not lead to an existing file." + raise IOError(error) + + filename = os.path.basename(file) + file_extension = os.path.splitext(filename)[1][1:] + file_size = os.path.getsize(file) + + # If name is not defined, grab the name from the file to publish + if not workbook_item.name: + workbook_item.name = os.path.splitext(filename)[0] + if file_extension not in ALLOWED_FILE_EXTENSIONS: + error = "Only {} files can be published as workbooks.".format(', '.join(ALLOWED_FILE_EXTENSIONS)) + raise ValueError(error) + + except TypeError: + # Expect file to be a file object + file_size = get_file_object_size(file) + file_extension = 'twbx' if file_is_compressed(file) else 'twb' + + if not workbook_item.name: + error = "Workbook item must have a name when passing a file object" + raise ValueError(error) + + # Generate filename for file object. + # This is needed when publishing the workbook in a single request + filename = "{}.{}".format(workbook_item.name, file_extension) + if not hasattr(self.parent_srv.PublishMode, mode): error = 'Invalid mode defined.' raise ValueError(error) - filename = os.path.basename(file_path) - file_extension = os.path.splitext(filename)[1][1:] - - # If name is not defined, grab the name from the file to publish - if not workbook_item.name: - workbook_item.name = os.path.splitext(filename)[0] - if file_extension not in ALLOWED_FILE_EXTENSIONS: - error = "Only {} files can be published as workbooks.".format(', '.join(ALLOWED_FILE_EXTENSIONS)) - raise ValueError(error) - # Construct the url with the defined mode url = "{0}?workbookType={1}".format(self.baseurl, file_extension) if mode == self.parent_srv.PublishMode.Overwrite: @@ -291,9 +308,9 @@ def publish( url += '&{0}=true'.format('asJob') # Determine if chunking is required (64MB is the limit for single upload method) - if os.path.getsize(file_path) >= FILESIZE_LIMIT: - logger.info('Publishing {0} to server with chunking method (workbook over 64MB)'.format(filename)) - upload_session_id = Fileuploads.upload_chunks(self.parent_srv, file_path) + if file_size >= FILESIZE_LIMIT: + logger.info('Publishing {0} to server with chunking method (workbook over 64MB)'.format(workbook_item.name)) + upload_session_id = Fileuploads.upload_chunks(self.parent_srv, file) url = "{0}&uploadSessionId={1}".format(url, upload_session_id) conn_creds = connection_credentials xml_request, content_type = RequestFactory.Workbook.publish_req_chunked(workbook_item, @@ -302,8 +319,14 @@ def publish( hidden_views=hidden_views) else: logger.info('Publishing {0} to server'.format(filename)) - with open(file_path, 'rb') as f: - file_contents = f.read() + + try: + with open(file, 'rb') as f: + file_contents = f.read() + + except TypeError: + file_contents = file.read() + conn_creds = connection_credentials xml_request, content_type = RequestFactory.Workbook.publish_req(workbook_item, filename, @@ -323,9 +346,9 @@ def publish( if as_job: new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info('Published {0} (JOB_ID: {1}'.format(filename, new_job.id)) + logger.info('Published {0} (JOB_ID: {1}'.format(workbook_item.name, new_job.id)) return new_job else: new_workbook = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0] - logger.info('Published {0} (ID: {1})'.format(filename, new_workbook.id)) + logger.info('Published {0} (ID: {1})'.format(workbook_item.name, new_workbook.id)) return new_workbook diff --git a/test/assets/Data/Tableau Samples/World Indicators.tde b/test/assets/Data/Tableau Samples/World Indicators.tde new file mode 100644 index 000000000..72162829b Binary files /dev/null and b/test/assets/Data/Tableau Samples/World Indicators.tde differ diff --git a/test/assets/RESTAPISample.twb b/test/assets/RESTAPISample.twb new file mode 100644 index 000000000..616bed91e --- /dev/null +++ b/test/assets/RESTAPISample.twb @@ -0,0 +1,3573 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Country / Region + 129 + [Country / Region] + [Extract] + Country / Region + 0 + DATA$ + string + Count + 209 + 1 + 1073741823 + false + + + "Afghanistan" + "Zimbabwe" + + + "en_US_CI" + true + "heap" + true + 4294967292 + 7 + "asc" + 2 + "str" + + + + Date + 135 + [Date] + [Extract] + Date + 1 + DATA$ + datetime + Year + 11 + false + + #2000-07-01 00:00:00# + #2010-07-01 00:00:00# + + + true + "array" + true + "asc" + 8 + 0 + "asc" + 1 + "datetime" + + + + F: Deposit interest rate (%) + 4 + [F: Deposit interest rate (%)] + [Extract] + F: Deposit interest rate (%) + 2 + DATA$ + real + Sum + 50 + true + + 0.0 + 203.0 + + + true + "array" + true + 4 + 5 + "asc" + 1 + "float" + + + + F: GDP (curr $) + 5 + [F: GDP (curr $)] + [Extract] + F: GDP (curr $) + 3 + DATA$ + real + Sum + 2120 + true + + 63810762.0 + 14447100000000.0 + + + 8 + 10 + "asc" + "double" + + + + F: GDP per capita (curr $) + 4 + [F: GDP per capita (curr $)] + [Extract] + F: GDP per capita (curr $) + 4 + DATA$ + real + Sum + 1877 + true + + 87.0 + 186243.0 + + + 4 + 9 + "asc" + "float" + + + + F: Lending interest rate (%) + 4 + [F: Lending interest rate (%)] + [Extract] + F: Lending interest rate (%) + 5 + DATA$ + real + Sum + 72 + true + + 1.0 + 496.0 + + + true + "array" + true + 4 + 6 + "asc" + 1 + "float" + + + + H: Health exp (% GDP) + 4 + [H: Health exp (% GDP)] + [Extract] + H: Health exp (% GDP) + 6 + DATA$ + real + Sum + 22 + true + + 0.0 + 20.0 + + + true + "array" + true + 4 + 3 + "asc" + 1 + "float" + + + + H: Health exp/cap (curr $) + 4 + [H: Health exp/cap (curr $)] + [Extract] + H: Health exp/cap (curr $) + 7 + DATA$ + real + Sum + 936 + true + + 3.0 + 8362.0 + + + true + "array" + true + 4 + 8 + "asc" + 2 + "float" + + + + H: Life exp (years) + 4 + [H: Life exp (years)] + [Extract] + H: Life exp (years) + 8 + DATA$ + real + Sum + 45 + true + + 40.0 + 83.0 + + + true + "array" + true + 4 + 4 + "asc" + 1 + "float" + + + + Number of Records + 16 + [Number of Records] + [Extract] + Number of Records + 9 + integer + Sum + 1 + false + + 1 + 1 + + + "asc" + 1 + "sint8" + + + + P: Population (count) + 5 + [P: Population (count)] + [Extract] + P: Population (count) + 10 + DATA$ + real + Sum + 2295 + false + + 18873.0 + 1337825000.0 + + + 8 + 11 + "asc" + "double" + + + + Region + 129 + [Region] + [Extract] + Region + 11 + DATA$ + string + Count + 6 + 1 + 1073741823 + false + + + "Africa" + "The Americas" + + + "en_US_CI" + true + "heap" + true + 4294967292 + 1 + "asc" + 1 + "str" + + + + Subregion + 129 + [Subregion] + [Extract] + Subregion + 12 + DATA$ + string + Count + 12 + 1 + 1073741823 + true + + + "Caribbean" + "Western Africa" + + + "en_US_CI" + true + "heap" + true + 4294967292 + 2 + "asc" + 1 + "str" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Gross Domestic Product + in current US Dollars + + + + + + + Gross Domestic Product + per capita + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Region] + [Subregion] + [Country / Region] + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Europe" + "Middle East" + "The Americas" + "Oceania" + "Asia" + "Africa" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <formatted-text> + <run fontsize='12'>Country ranks by GDP, GDP per Capita, Population, and Life Expectancy</run> + </formatted-text> + + + + + + + + + + + + + + Gross Domestic Product + in current US Dollars + + + + + + + Gross Domestic Product + per capita + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "[World Indicators new].[sum:F: GDP (curr $):qk]" + "[World Indicators new].[rank:sum:F: GDP (curr $):qk]" + "[World Indicators new].[sum:F: GDP per capita (curr $):qk]" + "[World Indicators new].[rank:sum:F: GDP per capita (curr $):qk]" + "[World Indicators new].[sum:P: Population (count):qk]" + "[World Indicators new].[rank:sum:P: Population (count):qk]" + "[World Indicators new].[avg:H: Life exp (years):qk]" + "[World Indicators new].[rank:avg:H: Life exp (years) (copy):qk]" + + + + + + + + + [World Indicators new].[:Measure Names] + [World Indicators new].[yr:Date:ok] + [World Indicators new].[none:F: GDP (curr $):qk] + + + + + + + + + + + + + + + + <[World Indicators new].[none:Country / Region:nk]> + Æ + <[World Indicators new].[:Measure Names]>: + <[World Indicators new].[Multiple Values]> + + + + + + [World Indicators new].[none:Country / Region:nk] + [World Indicators new].[:Measure Names] +
+
+ + + + <formatted-text> + <run fontsize='11'><</run> + <run fontsize='11'>[World Indicators new].[yr:Date:ok]</run> + <run fontsize='11'>></run> + <run fontsize='11'> GDP per capita by country</run> + </formatted-text> + + + + + + + + + + + + + + + + + + + Gross Domestic Product + per capita + + + + + + + + + + + + + + + + + + + + + + [World Indicators new].[yr:Date:ok] + [World Indicators new].[none:Region:nk] + + + + + + + + + + + + + + + + + + + Country: + <[World Indicators new].[none:Country / Region:nk]> + Region: + <[World Indicators new].[none:Region:nk]> + GDP per capita (curr $): + <[World Indicators new].[avg:F: GDP per capita (curr $):qk]> + % of world average: + <[World Indicators new].[usr:Calculation1:qk]> + + + + + + [World Indicators new].[none:Country / Region:nk] + [World Indicators new].[avg:F: GDP per capita (curr $):qk] +
+
+ + + + <formatted-text> + <run fontsize='12'>GDP per capita by region </run> + <run>Click on a point to filter the map to a specific year.</run> + </formatted-text> + + + + + + + + + + + + + + + + + Gross Domestic Product + in current US Dollars + + + + + + + + + + + + + + + + + + [World Indicators new].[Action (Country Name)] + [World Indicators new].[Action (Region)] + + + + + + + + + + + + + + + + + <[World Indicators new].[none:Region:nk]> + Year: + <[World Indicators new].[yr:Date:ok]> + Average GDP (curr $): + <[World Indicators new].[avg:F: GDP (curr $):qk]> + GDP per capita (weighted): + <[World Indicators new].[usr:Calculation_1590906174513693:qk]> + + + + + + [World Indicators new].[usr:Calculation_1590906174513693:qk] + [World Indicators new].[yr:Date:ok] +
+
+ + + + <formatted-text> + <run fontsize='12'>GDP per capita by country </run> + <run>Currently filtered to </run> + <run fontcolor='#4f6e8d'><[World Indicators new].[yr:Date:ok]></run> + </formatted-text> + + + + + + + + + + + + + + + + + + + + + + Gross Domestic Product + per capita + + + + + + + + + + + + + + + + + + + + + + + + + + + + 199.0 + 104512.0 + + + + + + + + "The Americas" + "Europe" + %null% + "Oceania" + "Africa" + "Middle East" + "Asia" + %all% + + + + [World Indicators new].[avg:F: GDP per capita (curr $):qk] + [World Indicators new].[none:Region:nk] + [World Indicators new].[Action (YEAR(Date (year)))] + + + + + + + + + + + + + + + + + + + + + <[World Indicators new].[none:Country / Region:nk]> + Æ + Region: + <[World Indicators new].[none:Region:nk]> + Subregion: + <[World Indicators new].[none:Subregion:nk]> + GDP per capita (curr $): + <[World Indicators new].[avg:F: GDP per capita (curr $):qk]> + GDP % of Subregion average: + <[World Indicators new].[usr:Calculation1:qk:5]> + GDP % of World average: + <[World Indicators new].[usr:Calculation1:qk:1]> + + + + + + [World Indicators new].[Latitude (generated)] + [World Indicators new].[Longitude (generated)] +
+
+ + + + <formatted-text> + <run fontsize='12'><Sheet Name>, <Page Name></run> + <run>Æ </run> + <run fontcolor='#898989' fontsize='10'>Click the forward button on year to watch the change over time Hover over mark to see the history of that country</run> + </formatted-text> + + + + + + + + + + + + + + + + + + + + + + + [World Indicators new].[avg:H: Health exp/cap (curr $):qk] + [World Indicators new].[avg:H: Life exp (years):qk] + + + + + + + + + + + + + + + + + <[World Indicators new].[none:Country / Region:nk]> + Æ + Region: + <[World Indicators new].[none:Region:nk]> + Year: + <[World Indicators new].[yr:Date:ok]> + Health exp/cap (curr $): + <[World Indicators new].[avg:H: Health exp/cap (curr $):qk]> + Life Expectancy: + <[World Indicators new].[avg:H: Life exp (years):qk]> + + + + + + [World Indicators new].[avg:H: Life exp (years):qk] + [World Indicators new].[avg:H: Health exp/cap (curr $):qk] + + [World Indicators new].[yr:Date:ok] + + +
+
+ + + + <formatted-text> + <run fontsize='12'>Lending and deposit interest rates, GDP per capita and % of world GDP sorted by GDP per Capita for region and subregion, </run> + <run fontsize='12'><</run> + <run fontsize='12'>[World Indicators new].[yr:Date:ok]</run> + <run fontsize='12'>></run> + </formatted-text> + + + + + + + + + + + + + + + + + + + + Gross Domestic Product + in current US Dollars + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "[World Indicators new].[avg:F: Lending interest rate (\%):qk]" + "[World Indicators new].[avg:F: Deposit interest rate (\%):qk]" + "[World Indicators new].[usr:Calculation_8570907072742130:qk]" + "[World Indicators new].[usr:Calculation_1590906174513693:qk]" + "[World Indicators new].[pcto:sum:F: GDP (curr $):qk]" + + + + + + + + + [World Indicators new].[:Measure Names] + [World Indicators new].[yr:Date:ok] + + + + + + + + + + + + + + + + + ([World Indicators new].[none:Region:nk] / [World Indicators new].[none:Subregion:nk]) + [World Indicators new].[:Measure Names] +
+
+ + + + <formatted-text> + <run><[World Indicators new].[yr:Date:ok]> Country <Sheet Name></run> + </formatted-text> + + + + + + + + + + + + + + Gross Domestic Product + in current US Dollars + + + + + + + + + + + + + + + + + + + + [World Indicators new].[yr:Date:ok] + [World Indicators new].[sum:F: GDP (curr $):qk] + + + + + + + + + + + + + + + + + + + + <[World Indicators new].[none:Country / Region:nk]> + Æ + Region: + <[World Indicators new].[none:Region:nk]> + % of World GDP: + <[World Indicators new].[pcto:sum:F: GDP (curr $):qk:1]> + GDP (US $'s): + <[World Indicators new].[sum:F: GDP (curr $):qk]> + + + + + <[World Indicators new].[none:Country / Region:nk]> + Æ + <[World Indicators new].[pcto:sum:F: GDP (curr $):qk:1]> <[World Indicators new].[sum:F: GDP (curr $):qk]> + + + + + + + +
+
+
+ + + + + <formatted-text> + <run fontalignment='0'>GDP per Capita</run> + </formatted-text> + + +