diff --git a/README.md b/README.md index 37ac284..cc28044 100644 --- a/README.md +++ b/README.md @@ -1061,9 +1061,8 @@ The Tableau REST API can publish both data sources and workbooks, either as TWB If a workbook references a published data source, that data source must be published first. Additionally, unlike Tableau Desktop, the REST API will not find linked files and upload them. A workbook with a "live connection" to an Excel file, for example, must be saved as a TWBX rather than a TWB for an upload to work correctly. The error messages if you do not follow this order are not very clear. #### 1.5.1 Publishing a Workbook or Datasource -The publish methods must upload directly from disk. If you are manipulating a workbook or datasource using the TableauFile / TableauDocument classes, please save the file prior to publishing. Also note that you specify a Project object rather than the LUID. +The publish methods must upload directly from disk. If you are manipulating a workbook or datasource using the TableauFile / TableauDocument classes, please save the file prior to publishing. Also note that you specify a Project object rather than the LUID. Publishing methods live under their respective sub-objects in TableauServeRest: .workbooks, .datasources or .flows -These live under `TableauServerRest.publishing` when using those classes. `TableauRestApiConnection.publish_workbook(workbook_filename, workbook_name, project_obj, overwrite=False, connection_username=None, connection_password=None, save_credentials=True, show_tabs=True, check_published_ds=True)` diff --git a/tableau_rest_api/methods/datasource.py b/tableau_rest_api/methods/datasource.py index cf28b92..9580536 100644 --- a/tableau_rest_api/methods/datasource.py +++ b/tableau_rest_api/methods/datasource.py @@ -164,6 +164,16 @@ def download_datasource(self, ds_name_or_luid: str, filename_no_extension: str, self.end_log_block() return save_filename + def publish_datasource(self, ds_filename: str, ds_name: str, project_obj: Project, + overwrite: Optional[bool] = False, connection_username: Optional[str] = None, + connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, + oauth_flag: Optional[bool] = False) -> str: + project_luid = project_obj.luid + xml = self._publish_content('datasource', ds_filename, ds_name, project_luid, {"overwrite": overwrite}, + connection_username, connection_password, save_credentials, oauth_flag=oauth_flag) + datasource = xml.findall('.//t:datasource', self.ns_map) + return datasource[0].get('id') + # # Tags # diff --git a/tableau_rest_api/methods/flow.py b/tableau_rest_api/methods/flow.py index c1d0df2..87492fb 100644 --- a/tableau_rest_api/methods/flow.py +++ b/tableau_rest_api/methods/flow.py @@ -256,6 +256,18 @@ def download_flow(self, flow_name_or_luid: str, filename_no_extension: str, self.end_log_block() return save_filename + def publish_flow(self, flow_filename: str, flow_name: str, project_obj: Project, + overwrite: Optional[bool] = False, connection_username: Optional[str] = None, + connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, + oauth_flag: Optional[bool] = False, description: Optional[str] = None) -> str: + project_luid = project_obj.luid + xml = self._publish_content(content_type='flow', content_filename=flow_filename, content_name=flow_name, + project_luid=project_luid, url_params={"overwrite": overwrite}, + connection_username=connection_username, connection_password=connection_password, + save_credentials=save_credentials, oauth_flag=oauth_flag, description=description) + flow = xml.findall('.//t:flow', self.ns_map) + return flow[0].get('id') + class FlowMethods34(FlowMethods33): def __init__(self, rest_api_base: TableauRestApiBase34): self.rest_api_base = rest_api_base diff --git a/tableau_rest_api/methods/publishing.py b/tableau_rest_api/methods/publishing.py deleted file mode 100644 index e3ea3e2..0000000 --- a/tableau_rest_api/methods/publishing.py +++ /dev/null @@ -1,350 +0,0 @@ -from .rest_api_base import * - -class PublishingMethods(): - def __init__(self, rest_api_base: TableauRestApiBase): - self.rest_api_base = rest_api_base - - def __getattr__(self, attr): - return getattr(self.rest_api_base, attr) - # - # Start Publish methods -- workbook, datasources, file upload - # - - ''' Publish process can go two way: - (1) Initiate File Upload (2) Publish workbook/datasource (less than 64MB) - (1) Initiate File Upload (2) Append to File Upload (3) Publish workbook to commit (over 64 MB) - ''' - - def publish_workbook(self, workbook_filename: str, workbook_name: str, project_obj: Project, - overwrite: Optional[bool] = False, connection_username: Optional[str] = None, - connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, - show_tabs: Optional[bool] = True, check_published_ds: Optional[bool] = True, - oauth_flag: Optional[bool] = False) -> str: - project_luid = project_obj.luid - xml = self.publish_content('workbook', workbook_filename, workbook_name, project_luid, - {"overwrite": overwrite}, connection_username, connection_password, - save_credentials, show_tabs=show_tabs, check_published_ds=check_published_ds, - oauth_flag=oauth_flag) - workbook = xml.findall('.//t:workbook', self.ns_map) - return workbook[0].get('id') - - def publish_datasource(self, ds_filename: str, ds_name: str, project_obj: Project, - overwrite: Optional[bool] = False, connection_username: Optional[str] = None, - connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, - oauth_flag: Optional[bool] = False) -> str: - project_luid = project_obj.luid - xml = self.publish_content('datasource', ds_filename, ds_name, project_luid, {"overwrite": overwrite}, - connection_username, connection_password, save_credentials, oauth_flag=oauth_flag) - datasource = xml.findall('.//t:datasource', self.ns_map) - return datasource[0].get('id') - - # Main method for publishing a workbook. Should intelligently decide to chunk up if necessary - # If a TableauDatasource or TableauWorkbook is passed, will upload from its content - def publish_content(self, content_type, content_filename, content_name, project_luid, url_params=None, - connection_username=None, connection_password=None, save_credentials=True, show_tabs=False, - check_published_ds=True, oauth_flag=False, generate_thumbnails_as_username_or_luid=None, - description=None, views_to_hide_list=None): - # Single upload limit in MB - single_upload_limit = 20 - - # If you need a temporary copy when fixing the published datasources - temp_wb_filename = None - - # Must be 'workbook' or 'datasource' or 'flow' - if content_type not in ['workbook', 'datasource', 'flow']: - raise InvalidOptionException("content_type must be 'workbook', 'datasource', or 'flow' ") - - file_extension = None - final_filename = None - cleanup_temp_file = False - - for ending in ['.twb', '.twbx', '.tde', '.tdsx', '.tds', '.tde', '.hyper', '.tfl', '.tflx']: - if content_filename.endswith(ending): - file_extension = ending[1:] - - # If twb or twbx, open up and check for any published data sources - if file_extension.lower() in ['twb', 'twbx'] and check_published_ds is True: - self.log("Adjusting any published datasources") - t_file = TableauFile(content_filename, self.logger) - dses = t_file.tableau_document.datasources - for ds in dses: - # Set to the correct site - if ds.published is True: - self.log("Published datasource found") - self.log("Setting publish datasource repository to {}".format(self.site_content_url)) - ds.published_ds_site = self.site_content_url - - temp_wb_filename = t_file.save_new_file('temp_wb') - content_filename = temp_wb_filename - # Open the file to be uploaded - try: - content_file = open(content_filename, 'rb') - file_size = os.path.getsize(content_filename) - file_size_mb = float(file_size) / float(1000000) - self.log("File {} is size {} MBs".format(content_filename, file_size_mb)) - final_filename = content_filename - - # Request type is mixed and require a boundary - boundary_string = self.generate_boundary_string() - - # Create the initial XML portion of the request - publish_request = bytes("--{}\r\n".format(boundary_string).encode('utf-8')) - publish_request += bytes('Content-Disposition: name="request_payload"\r\n'.encode('utf-8')) - publish_request += bytes('Content-Type: text/xml\r\n\r\n'.encode('utf-8')) - - # Build publish request in ElementTree then convert at publish - publish_request_xml = ET.Element('tsRequest') - # could be either workbook, datasource, or flow - t1 = ET.Element(content_type) - t1.set('name', content_name) - if show_tabs is not False: - t1.set('showTabs', str(show_tabs).lower()) - if generate_thumbnails_as_username_or_luid is not None: - if self.is_luid(generate_thumbnails_as_username_or_luid): - thumbnail_user_luid = generate_thumbnails_as_username_or_luid - else: - thumbnail_user_luid = self.query_user_luid(generate_thumbnails_as_username_or_luid) - t1.set('generateThumbnailsAsUser', thumbnail_user_luid) - - if connection_username is not None: - cc = ET.Element('connectionCredentials') - cc.set('name', connection_username) - if oauth_flag is True: - cc.set('oAuth', "True") - if connection_password is not None: - cc.set('password', connection_password) - cc.set('embed', str(save_credentials).lower()) - t1.append(cc) - - # Views to Hide in Workbooks from 3.2 - if views_to_hide_list is not None: - if len(views_to_hide_list) > 0: - vs = ET.Element('views') - for view_name in views_to_hide_list: - v = ET.Element('view') - v.set('name', view_name) - v.set('hidden', 'true') - t1.append(vs) - - # Description only allowed for Flows as of 3.3 - if description is not None: - t1.set('description', description) - p = ET.Element('project') - p.set('id', project_luid) - t1.append(p) - publish_request_xml.append(t1) - - encoded_request = ET.tostring(publish_request_xml, encoding='utf-8') - - publish_request += bytes(encoded_request) - publish_request += bytes("\r\n--{}".format(boundary_string).encode('utf-8')) - - # Upload as single if less than file_size_limit MB - if file_size_mb <= single_upload_limit: - # If part of a single upload, this if the next portion - self.log("Less than {} MB, uploading as a single call".format(str(single_upload_limit))) - publish_request += bytes('\r\n'.encode('utf-8')) - publish_request += bytes('Content-Disposition: name="tableau_{}"; filename="{}"\r\n'.format( - content_type, final_filename).encode('utf-8')) - publish_request += bytes('Content-Type: application/octet-stream\r\n\r\n'.encode('utf-8')) - - # Content needs to be read unencoded from the file - content = content_file.read() - - # Add to string as regular binary, no encoding - publish_request += content - - publish_request += bytes("\r\n--{}--".format(boundary_string).encode('utf-8')) - - url = self.build_api_url("{}s").format(content_type) - - # Allow additional parameters on the publish url - if len(url_params) > 0: - additional_params = '?' - i = 1 - for param in url_params: - if i > 1: - additional_params += "&" - additional_params += "{}={}".format(param, str(url_params[param]).lower()) - i += 1 - url += additional_params - - content_file.close() - if temp_wb_filename is not None: - os.remove(temp_wb_filename) - if cleanup_temp_file is True: - os.remove(final_filename) - return self.send_publish_request(url, None, publish_request, boundary_string) - # Break up into chunks for upload - else: - self.log("Greater than 10 MB, uploading in chunks") - upload_session_id = self.initiate_file_upload() - - # Upload each chunk - for piece in self.read_file_in_chunks(content_file): - self.log("Appending chunk to upload session {}".format(upload_session_id)) - self.append_to_file_upload(upload_session_id, piece, final_filename) - - # Finalize the publish - url = self.build_api_url("{}s").format(content_type) + "?uploadSessionId={}".format( - upload_session_id) + "&{}Type={}".format(content_type, file_extension) - - # Allow additional parameters on the publish url - if len(url_params) > 0: - additional_params = '&' - i = 1 - for param in url_params: - if i > 1: - additional_params += "&" - additional_params += "{}={}".format(param, str(url_params[param]).lower()) - i += 1 - url += additional_params - - publish_request += bytes("--".encode('utf-8')) # Need to finish off the last boundary - self.log("Finishing the upload with a publish request") - content_file.close() - if temp_wb_filename is not None: - os.remove(temp_wb_filename) - if cleanup_temp_file is True: - os.remove(final_filename) - return self.send_publish_request(url, None, publish_request, boundary_string) - - except IOError: - print("Error: File '{}' cannot be opened to upload".format(content_filename)) - raise - - if file_extension is None: - raise InvalidOptionException( - "File {} does not have an acceptable extension. Should be .twb,.twbx,.tde,.tdsx,.tds,.tde".format( - content_filename)) - - def initiate_file_upload(self): - url = self.build_api_url("fileUploads") - xml = self.send_post_request(url) - file_upload = xml.findall('.//t:fileUpload', self.ns_map) - return file_upload[0].get("uploadSessionId") - - # Uploads a chunk to an already started session - def append_to_file_upload(self, upload_session_id, content, filename): - boundary_string = self.generate_boundary_string() - publish_request = "--{}\r\n".format(boundary_string) - publish_request += 'Content-Disposition: name="request_payload"\r\n' - publish_request += 'Content-Type: text/xml\r\n\r\n' - publish_request += '\r\n' - publish_request += "--{}\r\n".format(boundary_string) - publish_request += 'Content-Disposition: name="tableau_file"; filename="{}"\r\n'.format( - filename) - publish_request += 'Content-Type: application/octet-stream\r\n\r\n' - - publish_request += content - - publish_request += "\r\n--{}--".format(boundary_string) - url = self.build_api_url("fileUploads/{}".format(upload_session_id)) - self.send_append_request(url, publish_request, boundary_string) - -class PublishingMethods27(PublishingMethods): - def __init__(self, rest_api_base: TableauRestApiBase27): - self.rest_api_base = rest_api_base - -class PublishingMethods28(PublishingMethods27): - def __init__(self, rest_api_base: TableauRestApiBase28): - self.rest_api_base = rest_api_base - -class PublishingMethods30(PublishingMethods28): - def __init__(self, rest_api_base: TableauRestApiBase30): - self.rest_api_base = rest_api_base - - def publish_workbook(self, workbook_filename: str, workbook_name: str, project_obj: Project, - overwrite: Optional[bool] = False, async_publish: Optional[bool] = False, - connection_username: Optional[str] = None, - connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, - show_tabs: Optional[bool] = True, check_published_ds: Optional[bool] = True, - oauth_flag: Optional[bool] = False) -> str: - project_luid = project_obj.luid - xml = self.publish_content('workbook', workbook_filename, workbook_name, project_luid, - {"overwrite": overwrite, "asJob": async_publish}, connection_username, - connection_password, save_credentials, show_tabs=show_tabs, - check_published_ds=check_published_ds, oauth_flag=oauth_flag) - if async_publish is True: - job = xml.findall('.//t:job', self.ns_map) - return job[0].get('id') - else: - workbook = xml.findall('.//t:workbook', self.ns_map) - return workbook[0].get('id') - -class PublishingMethods31(PublishingMethods30): - def __init__(self, rest_api_base: TableauRestApiBase31): - self.rest_api_base = rest_api_base - -class PublishingMethods32(PublishingMethods31): - def __init__(self, rest_api_base: TableauRestApiBase32): - self.rest_api_base = rest_api_base - - # In 3.2, you can hide views from publishing - def publish_workbook(self, workbook_filename: str, workbook_name: str, project_obj: Project, - overwrite: Optional[bool] = False, async_publish: Optional[bool] = False, - connection_username: Optional[str] = None, - connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, - show_tabs: Optional[bool] = True, check_published_ds: Optional[bool] = True, - oauth_flag: Optional[bool] = False, views_to_hide_list: Optional[List[str]] = None) -> str: - - project_luid = project_obj.luid - xml = self.publish_content('workbook', workbook_filename, workbook_name, project_luid, - {"overwrite": overwrite, "asJob": async_publish}, connection_username, - connection_password, save_credentials, show_tabs=show_tabs, - check_published_ds=check_published_ds, oauth_flag=oauth_flag, - views_to_hide_list=views_to_hide_list) - if async_publish is True: - job = xml.findall('.//t:job', self.ns_map) - return job[0].get('id') - else: - workbook = xml.findall('.//t:workbook', self.ns_map) - return workbook[0].get('id') - -class PublishingMethods33(PublishingMethods32): - def __init__(self, rest_api_base: TableauRestApiBase33): - self.rest_api_base = rest_api_base - - def publish_workbook(self, workbook_filename: str, workbook_name: str, project_obj: Project, - overwrite: Optional[bool] = False, async_publish: Optional[bool] = False, - connection_username: Optional[str] = None, - connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, - show_tabs: Optional[bool] = True, check_published_ds: Optional[bool] = True, - oauth_flag: Optional[bool] = False, views_to_hide_list: Optional[List[str]] = None, - generate_thumbnails_as_username_or_luid: Optional[str] = None): - project_luid = project_obj.luid - xml = self.publish_content('workbook', workbook_filename, workbook_name, project_luid, - {"overwrite": overwrite, "asJob": async_publish}, connection_username, - connection_password, save_credentials, show_tabs=show_tabs, - check_published_ds=check_published_ds, oauth_flag=oauth_flag, - views_to_hide_list=views_to_hide_list, - generate_thumbnails_as_username_or_luid=generate_thumbnails_as_username_or_luid) - if async_publish is True: - job = xml.findall('.//t:job', self.ns_map) - return job[0].get('id') - else: - workbook = xml.findall('.//t:workbook', self.ns_map) - return workbook[0].get('id') - - def publish_flow(self, flow_filename: str, flow_name: str, project_obj: Project, - overwrite: Optional[bool] = False, connection_username: Optional[str] = None, - connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, - oauth_flag: Optional[bool] = False, description: Optional[str] = None) -> str: - project_luid = project_obj.luid - xml = self.publish_content('flow', flow_filename, flow_name, project_luid, {"overwrite": overwrite}, - connection_username, connection_password, save_credentials, oauth_flag=oauth_flag, - description=description) - flow = xml.findall('.//t:flow', self.ns_map) - return flow[0].get('id') - -class PublishingMethods34(PublishingMethods33): - def __init__(self, rest_api_base: TableauRestApiBase34): - self.rest_api_base = rest_api_base - -class PublishingMethods35(PublishingMethods34): - def __init__(self, rest_api_base: TableauRestApiBase35): - self.rest_api_base = rest_api_base - -class PublishingMethods36(PublishingMethods35): - def __init__(self, rest_api_base: TableauRestApiBase36): - self.rest_api_base = rest_api_base \ No newline at end of file diff --git a/tableau_rest_api/methods/rest_api_base.py b/tableau_rest_api/methods/rest_api_base.py index 2a225e4..c6877d3 100644 --- a/tableau_rest_api/methods/rest_api_base.py +++ b/tableau_rest_api/methods/rest_api_base.py @@ -665,6 +665,212 @@ def send_binary_get_request(self, url: str) -> bytes: self.end_log_block() return self._request_obj.get_response() + # Generic implementation of all content publishing + def _publish_content(self, content_type: str, content_filename: str, content_name: str, project_luid: str, + url_params: Optional[Dict] = None, + connection_username: Optional[str] = None, connection_password: Optional[str] = None, + save_credentials: Optional[bool] = True, show_tabs: Optional[bool] = False, + check_published_ds: Optional[bool] = True, oauth_flag: Optional[bool] = False, + generate_thumbnails_as_username_or_luid: Optional[str] = None, + description: Optional[str] = None, views_to_hide_list: Optional[List[str]] = None): + # Single upload limit in MB + single_upload_limit = 20 + + # If you need a temporary copy when fixing the published datasources + temp_wb_filename = None + + # Must be 'workbook' or 'datasource' or 'flow' + if content_type not in ['workbook', 'datasource', 'flow']: + raise InvalidOptionException("content_type must be 'workbook', 'datasource', or 'flow' ") + + file_extension = None + final_filename = None + cleanup_temp_file = False + + for ending in ['.twb', '.twbx', '.tde', '.tdsx', '.tds', '.tde', '.hyper', '.tfl', '.tflx']: + if content_filename.endswith(ending): + file_extension = ending[1:] + + # If twb or twbx, open up and check for any published data sources + if file_extension.lower() in ['twb', 'twbx'] and check_published_ds is True: + self.log("Adjusting any published datasources") + t_file = TableauFile(content_filename, self.logger) + dses = t_file.tableau_document.datasources + for ds in dses: + # Set to the correct site + if ds.published is True: + self.log("Published datasource found") + self.log("Setting publish datasource repository to {}".format(self.site_content_url)) + ds.published_ds_site = self.site_content_url + + temp_wb_filename = t_file.save_new_file('temp_wb') + content_filename = temp_wb_filename + # Open the file to be uploaded + try: + content_file = open(content_filename, 'rb') + file_size = os.path.getsize(content_filename) + file_size_mb = float(file_size) / float(1000000) + self.log("File {} is size {} MBs".format(content_filename, file_size_mb)) + final_filename = content_filename + + # Request type is mixed and require a boundary + boundary_string = self.generate_boundary_string() + + # Create the initial XML portion of the request + publish_request = bytes("--{}\r\n".format(boundary_string).encode('utf-8')) + publish_request += bytes('Content-Disposition: name="request_payload"\r\n'.encode('utf-8')) + publish_request += bytes('Content-Type: text/xml\r\n\r\n'.encode('utf-8')) + + # Build publish request in ElementTree then convert at publish + publish_request_xml = ET.Element('tsRequest') + # could be either workbook, datasource, or flow + t1 = ET.Element(content_type) + t1.set('name', content_name) + if show_tabs is not False: + t1.set('showTabs', str(show_tabs).lower()) + if generate_thumbnails_as_username_or_luid is not None: + if self.is_luid(generate_thumbnails_as_username_or_luid): + thumbnail_user_luid = generate_thumbnails_as_username_or_luid + else: + thumbnail_user_luid = self.query_user_luid(generate_thumbnails_as_username_or_luid) + t1.set('generateThumbnailsAsUser', thumbnail_user_luid) + + if connection_username is not None: + cc = ET.Element('connectionCredentials') + cc.set('name', connection_username) + if oauth_flag is True: + cc.set('oAuth', "True") + if connection_password is not None: + cc.set('password', connection_password) + cc.set('embed', str(save_credentials).lower()) + t1.append(cc) + + # Views to Hide in Workbooks from 3.2 + if views_to_hide_list is not None: + if len(views_to_hide_list) > 0: + vs = ET.Element('views') + for view_name in views_to_hide_list: + v = ET.Element('view') + v.set('name', view_name) + v.set('hidden', 'true') + t1.append(vs) + + # Description only allowed for Flows as of 3.3 + if description is not None: + t1.set('description', description) + p = ET.Element('project') + p.set('id', project_luid) + t1.append(p) + publish_request_xml.append(t1) + + encoded_request = ET.tostring(publish_request_xml, encoding='utf-8') + + publish_request += bytes(encoded_request) + publish_request += bytes("\r\n--{}".format(boundary_string).encode('utf-8')) + + # Upload as single if less than file_size_limit MB + if file_size_mb <= single_upload_limit: + # If part of a single upload, this if the next portion + self.log("Less than {} MB, uploading as a single call".format(str(single_upload_limit))) + publish_request += bytes('\r\n'.encode('utf-8')) + publish_request += bytes('Content-Disposition: name="tableau_{}"; filename="{}"\r\n'.format( + content_type, final_filename).encode('utf-8')) + publish_request += bytes('Content-Type: application/octet-stream\r\n\r\n'.encode('utf-8')) + + # Content needs to be read unencoded from the file + content = content_file.read() + + # Add to string as regular binary, no encoding + publish_request += content + + publish_request += bytes("\r\n--{}--".format(boundary_string).encode('utf-8')) + + url = self.build_api_url("{}s").format(content_type) + + # Allow additional parameters on the publish url + if len(url_params) > 0: + additional_params = '?' + i = 1 + for param in url_params: + if i > 1: + additional_params += "&" + additional_params += "{}={}".format(param, str(url_params[param]).lower()) + i += 1 + url += additional_params + + content_file.close() + if temp_wb_filename is not None: + os.remove(temp_wb_filename) + if cleanup_temp_file is True: + os.remove(final_filename) + return self.send_publish_request(url, None, publish_request, boundary_string) + # Break up into chunks for upload + else: + self.log("Greater than 10 MB, uploading in chunks") + upload_session_id = self.initiate_file_upload() + + # Upload each chunk + for piece in self.read_file_in_chunks(content_file): + self.log("Appending chunk to upload session {}".format(upload_session_id)) + self.append_to_file_upload(upload_session_id, piece, final_filename) + + # Finalize the publish + url = self.build_api_url("{}s").format(content_type) + "?uploadSessionId={}".format( + upload_session_id) + "&{}Type={}".format(content_type, file_extension) + + # Allow additional parameters on the publish url + if len(url_params) > 0: + additional_params = '&' + i = 1 + for param in url_params: + if i > 1: + additional_params += "&" + additional_params += "{}={}".format(param, str(url_params[param]).lower()) + i += 1 + url += additional_params + + publish_request += bytes("--".encode('utf-8')) # Need to finish off the last boundary + self.log("Finishing the upload with a publish request") + content_file.close() + if temp_wb_filename is not None: + os.remove(temp_wb_filename) + if cleanup_temp_file is True: + os.remove(final_filename) + return self.send_publish_request(url, None, publish_request, boundary_string) + + except IOError: + print("Error: File '{}' cannot be opened to upload".format(content_filename)) + raise + + if file_extension is None: + raise InvalidOptionException( + "File {} does not have an acceptable extension. Should be .twb,.twbx,.tde,.tdsx,.tds,.tde".format( + content_filename)) + + def initiate_file_upload(self): + url = self.build_api_url("fileUploads") + xml = self.send_post_request(url) + file_upload = xml.findall('.//t:fileUpload', self.ns_map) + return file_upload[0].get("uploadSessionId") + + # Uploads a chunk to an already started session + def append_to_file_upload(self, upload_session_id, content, filename): + boundary_string = self.generate_boundary_string() + publish_request = "--{}\r\n".format(boundary_string) + publish_request += 'Content-Disposition: name="request_payload"\r\n' + publish_request += 'Content-Type: text/xml\r\n\r\n' + publish_request += '\r\n' + publish_request += "--{}\r\n".format(boundary_string) + publish_request += 'Content-Disposition: name="tableau_file"; filename="{}"\r\n'.format( + filename) + publish_request += 'Content-Type: application/octet-stream\r\n\r\n' + + publish_request += content + + publish_request += "\r\n--{}--".format(boundary_string) + url = self.build_api_url("fileUploads/{}".format(upload_session_id)) + self.send_append_request(url, publish_request, boundary_string) + # Generic implementation of all the CSV/PDF/PNG requests def _query_data_file(self, download_type: str, view_name_or_luid: str, high_resolution: Optional[bool] = None, view_filter_map: Optional[Dict] = None, diff --git a/tableau_rest_api/methods/subscription.py b/tableau_rest_api/methods/subscription.py index 08ba8b5..012aede 100644 --- a/tableau_rest_api/methods/subscription.py +++ b/tableau_rest_api/methods/subscription.py @@ -160,7 +160,7 @@ def delete_subscriptions(self, subscription_luid_s: Union[List[str], str]): self.end_log_block() class SubscriptionMethods27(SubscriptionMethods): - def __init__(self, rest_api_base: TableauRestApiBase28): + def __init__(self, rest_api_base: TableauRestApiBase27): self.rest_api_base = rest_api_base class SubscriptionMethods28(SubscriptionMethods27): diff --git a/tableau_rest_api/methods/workbook.py b/tableau_rest_api/methods/workbook.py index 0235ed4..d42d85c 100644 --- a/tableau_rest_api/methods/workbook.py +++ b/tableau_rest_api/methods/workbook.py @@ -355,6 +355,19 @@ def download_workbook(self, wb_name_or_luid: str, filename_no_extension: str, self.end_log_block() return save_filename + def publish_workbook(self, workbook_filename: str, workbook_name: str, project_obj: Project, + overwrite: Optional[bool] = False, connection_username: Optional[str] = None, + connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, + show_tabs: Optional[bool] = True, check_published_ds: Optional[bool] = True, + oauth_flag: Optional[bool] = False) -> str: + project_luid = project_obj.luid + xml = self.publish_content('workbook', workbook_filename, workbook_name, project_luid, + {"overwrite": overwrite}, connection_username, connection_password, + save_credentials, show_tabs=show_tabs, check_published_ds=check_published_ds, + oauth_flag=oauth_flag) + workbook = xml.findall('.//t:workbook', self.ns_map) + return workbook[0].get('id') + # # Image and PDF endpoints # @@ -586,6 +599,27 @@ class WorkbookMethods30(WorkbookMethods28): def __init__(self, rest_api_base: TableauRestApiBase30): self.rest_api_base = rest_api_base + def publish_workbook(self, workbook_filename: str, workbook_name: str, project_obj: Project, + overwrite: Optional[bool] = False, async_publish: Optional[bool] = False, + connection_username: Optional[str] = None, + connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, + show_tabs: Optional[bool] = True, check_published_ds: Optional[bool] = True, + oauth_flag: Optional[bool] = False) -> str: + project_luid = project_obj.luid + xml = self._publish_content(content_type='workbook', content_filename=workbook_filename, + content_name=workbook_name, project_luid=project_luid, + url_params={"overwrite": overwrite, "asJob": async_publish}, + connection_username=connection_username, + connection_password=connection_password, save_credentials=save_credentials, + show_tabs=show_tabs, + check_published_ds=check_published_ds, oauth_flag=oauth_flag) + if async_publish is True: + job = xml.findall('.//t:job', self.ns_map) + return job[0].get('id') + else: + workbook = xml.findall('.//t:workbook', self.ns_map) + return workbook[0].get('id') + class WorkbookMethods31(WorkbookMethods30): def __init__(self, rest_api_base: TableauRestApiBase31): self.rest_api_base = rest_api_base @@ -594,6 +628,30 @@ class WorkbookMethods32(WorkbookMethods31): def __init__(self, rest_api_base: TableauRestApiBase32): self.rest_api_base = rest_api_base + # In 3.2, you can hide views from publishing + def publish_workbook(self, workbook_filename: str, workbook_name: str, project_obj: Project, + overwrite: Optional[bool] = False, async_publish: Optional[bool] = False, + connection_username: Optional[str] = None, + connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, + show_tabs: Optional[bool] = True, check_published_ds: Optional[bool] = True, + oauth_flag: Optional[bool] = False, views_to_hide_list: Optional[List[str]] = None) -> str: + + project_luid = project_obj.luid + xml = self._publish_content(content_type='workbook', content_filename=workbook_filename, + content_name=workbook_name, project_luid=project_luid, + url_params={"overwrite": overwrite, "asJob": async_publish}, + connection_username=connection_username, + connection_password=connection_password, + save_credentials=save_credentials, show_tabs=show_tabs, + check_published_ds=check_published_ds, oauth_flag=oauth_flag, + views_to_hide_list=views_to_hide_list) + if async_publish is True: + job = xml.findall('.//t:job', self.ns_map) + return job[0].get('id') + else: + workbook = xml.findall('.//t:workbook', self.ns_map) + return workbook[0].get('id') + class WorkbookMethods33(WorkbookMethods32): def __init__(self, rest_api_base: TableauRestApiBase33): self.rest_api_base = rest_api_base @@ -612,6 +670,30 @@ def query_view_image(self, view_name_or_luid: Optional[str] = None, high_resolut self.end_log_block() return image + def publish_workbook(self, workbook_filename: str, workbook_name: str, project_obj: Project, + overwrite: Optional[bool] = False, async_publish: Optional[bool] = False, + connection_username: Optional[str] = None, + connection_password: Optional[str] = None, save_credentials: Optional[bool] = True, + show_tabs: Optional[bool] = True, check_published_ds: Optional[bool] = True, + oauth_flag: Optional[bool] = False, views_to_hide_list: Optional[List[str]] = None, + generate_thumbnails_as_username_or_luid: Optional[str] = None): + project_luid = project_obj.luid + xml = self._publish_content(content_type='workbook', content_filename=workbook_filename, + content_name=workbook_name, project_luid=project_luid, + url_params={"overwrite": overwrite, "asJob": async_publish}, + connection_username=connection_username, + connection_password=connection_password, + save_credentials=save_credentials, show_tabs=show_tabs, + check_published_ds=check_published_ds, oauth_flag=oauth_flag, + views_to_hide_list=views_to_hide_list, + generate_thumbnails_as_username_or_luid=generate_thumbnails_as_username_or_luid) + if async_publish is True: + job = xml.findall('.//t:job', self.ns_map) + return job[0].get('id') + else: + workbook = xml.findall('.//t:workbook', self.ns_map) + return workbook[0].get('id') + class WorkbookMethods35(WorkbookMethods34): def __init__(self, rest_api_base: TableauRestApiBase35): self.rest_api_base = rest_api_base diff --git a/tableau_rest_api/tableau_rest_api_connection.py b/tableau_rest_api/tableau_rest_api_connection.py index 2289894..e4471f6 100644 --- a/tableau_rest_api/tableau_rest_api_connection.py +++ b/tableau_rest_api/tableau_rest_api_connection.py @@ -13,58 +13,58 @@ def __init__(self, server: str, username: str, password: str, site_content_url: #self.user_methods = UserMethods(self.rest_api_base) class TableauRestApiConnection27(WorkbookMethods27, UserMethods27, SubscriptionMethods27, SiteMethods27, - ScheduleMethods27, RevisionMethods27, PublishingMethods27, ProjectMethods27, GroupMethods27, FavoritesMethods27, + ScheduleMethods27, RevisionMethods27, ProjectMethods27, GroupMethods27, FavoritesMethods27, ExtractMethods27, DatasourceMethods27, TableauRestApiBase27): pass class TableauRestApiConnection28(WorkbookMethods28, UserMethods28, SubscriptionMethods28, SiteMethods28, - ScheduleMethods28, RevisionMethods28, PublishingMethods28, ProjectMethods28, + ScheduleMethods28, RevisionMethods28, ProjectMethods28, GroupMethods28, FavoritesMethods28, ExtractMethods28, DatasourceMethods28, TableauRestApiBase28): pass class TableauRestApiConnection30(WorkbookMethods30, UserMethods30, SubscriptionMethods30, SiteMethods30, - ScheduleMethods30, RevisionMethods30, PublishingMethods30, ProjectMethods30, + ScheduleMethods30, RevisionMethods30, ProjectMethods30, GroupMethods30, FavoritesMethods30, ExtractMethods30, DatasourceMethods30, TableauRestApiBase30): pass class TableauRestApiConnection31(WorkbookMethods31, UserMethods31, SubscriptionMethods31, SiteMethods31, - ScheduleMethods31, RevisionMethods31, PublishingMethods31, ProjectMethods31, + ScheduleMethods31, RevisionMethods31, ProjectMethods31, GroupMethods31, FavoritesMethods31, ExtractMethods31, DatasourceMethods31, TableauRestApiBase31): pass class TableauRestApiConnection32(WorkbookMethods32, UserMethods32, SubscriptionMethods32, SiteMethods32, - ScheduleMethods32, RevisionMethods32, PublishingMethods32, + ScheduleMethods32, RevisionMethods32, ProjectMethods32, GroupMethods32, FavoritesMethods32, ExtractMethods32, DatasourceMethods32, AlertMethods32, TableauRestApiBase32): pass class TableauRestApiConnection33(WorkbookMethods33, UserMethods33, SubscriptionMethods33, SiteMethods33, - ScheduleMethods33, RevisionMethods33, PublishingMethods33, ProjectMethods33, + ScheduleMethods33, RevisionMethods33, ProjectMethods33, GroupMethods33, FlowMethods33, FavoritesMethods33, ExtractMethods33, DatasourceMethods33, AlertMethods33, TableauRestApiBase33): pass class TableauRestApiConnection34(WorkbookMethods34, UserMethods34, SubscriptionMethods34, SiteMethods34, - ScheduleMethods34, RevisionMethods34, PublishingMethods34, ProjectMethods34, + ScheduleMethods34, RevisionMethods34, ProjectMethods34, GroupMethods34, FlowMethods34, FavoritesMethods34, ExtractMethods34, DatasourceMethods34, AlertMethods34, TableauRestApiBase34): pass class TableauRestApiConnection35(WorkbookMethods35, UserMethods35, SubscriptionMethods35, SiteMethods35, - ScheduleMethods35, RevisionMethods35, PublishingMethods35, ProjectMethods35, + ScheduleMethods35, RevisionMethods35, ProjectMethods35, GroupMethods35, FlowMethods35, FavoritesMethods35, ExtractMethods35, DatasourceMethods35, AlertMethods35, TableauRestApiBase35): pass class TableauRestApiConnection36(WorkbookMethods36, UserMethods36, SubscriptionMethods36, SiteMethods36, - ScheduleMethods36, RevisionMethods36, PublishingMethods36, ProjectMethods36, + ScheduleMethods36, RevisionMethods36, ProjectMethods36, GroupMethods36, FlowMethods36, FavoritesMethods36, ExtractMethods36, DatasourceMethods36, AlertMethods36, TableauRestApiBase36): @@ -85,7 +85,6 @@ def __init__(self, server: str, username: str, password: str, self.favorites: FavoritesMethods = FavoritesMethods(self.rest_api_base) self.groups: GroupMethods = GroupMethods(self.rest_api_base) self.projects: ProjectMethods = ProjectMethods(self.rest_api_base) - self.publishing: PublishingMethods = PublishingMethods(self.rest_api_base) self.revisions: RevisionMethods = RevisionMethods(self.rest_api_base) self.schedules: ScheduleMethods = ScheduleMethods(self.rest_api_base) self.sites: SiteMethods = SiteMethods(self.rest_api_base) @@ -104,7 +103,6 @@ def __init__(self, server: str, username: str, password: str, self.favorites: FavoritesMethods27 = FavoritesMethods27(self.rest_api_base) self.groups: GroupMethods27 = GroupMethods27(self.rest_api_base) self.projects: ProjectMethods27 = ProjectMethods27(self.rest_api_base) - self.publishing: PublishingMethods27 = PublishingMethods27(self.rest_api_base) self.revisions: RevisionMethods27 = RevisionMethods27(self.rest_api_base) self.schedules: ScheduleMethods27 = ScheduleMethods27(self.rest_api_base) self.sites: SiteMethods = SiteMethods27(self.rest_api_base) @@ -123,7 +121,6 @@ def __init__(self, server: str, username: str, password: str, self.favorites: FavoritesMethods28 = FavoritesMethods28(self.rest_api_base) self.groups: GroupMethods28 = GroupMethods28(self.rest_api_base) self.projects: ProjectMethods28 = ProjectMethods28(self.rest_api_base) - self.publishing: PublishingMethods28 = PublishingMethods28(self.rest_api_base) self.revisions: RevisionMethods28 = RevisionMethods28(self.rest_api_base) self.schedules: ScheduleMethods28 = ScheduleMethods28(self.rest_api_base) self.sites: SiteMethods = SiteMethods28(self.rest_api_base) @@ -142,7 +139,6 @@ def __init__(self, server: str, username: str, password: str, self.favorites: FavoritesMethods30 = FavoritesMethods30(self.rest_api_base) self.groups: GroupMethods30 = GroupMethods30(self.rest_api_base) self.projects: ProjectMethods30 = ProjectMethods30(self.rest_api_base) - self.publishing: PublishingMethods30 = PublishingMethods30(self.rest_api_base) self.revisions: RevisionMethods30 = RevisionMethods30(self.rest_api_base) self.schedules: ScheduleMethods30 = ScheduleMethods30(self.rest_api_base) self.sites: SiteMethods = SiteMethods30(self.rest_api_base) @@ -161,7 +157,6 @@ def __init__(self, server: str, username: str, password: str, self.favorites: FavoritesMethods31 = FavoritesMethods31(self.rest_api_base) self.groups: GroupMethods31 = GroupMethods31(self.rest_api_base) self.projects: ProjectMethods31 = ProjectMethods31(self.rest_api_base) - self.publishing: PublishingMethods31 = PublishingMethods31(self.rest_api_base) self.revisions: RevisionMethods31 = RevisionMethods31(self.rest_api_base) self.schedules: ScheduleMethods31 = ScheduleMethods31(self.rest_api_base) self.sites: SiteMethods = SiteMethods31(self.rest_api_base) @@ -181,7 +176,6 @@ def __init__(self, server: str, username: str, password: str, self.favorites: FavoritesMethods32 = FavoritesMethods32(self.rest_api_base) self.groups: GroupMethods32 = GroupMethods32(self.rest_api_base) self.projects: ProjectMethods32 = ProjectMethods32(self.rest_api_base) - self.publishing: PublishingMethods32 = PublishingMethods32(self.rest_api_base) self.revisions: RevisionMethods32 = RevisionMethods32(self.rest_api_base) self.schedules: ScheduleMethods32 = ScheduleMethods32(self.rest_api_base) self.sites: SiteMethods = SiteMethods32(self.rest_api_base) @@ -202,7 +196,6 @@ def __init__(self, server: str, username: str, password: str, self.flows: FlowMethods33 = FlowMethods33(self.rest_api_base) self.groups: GroupMethods33 = GroupMethods33(self.rest_api_base) self.projects: ProjectMethods33 = ProjectMethods33(self.rest_api_base) - self.publishing: PublishingMethods33 = PublishingMethods33(self.rest_api_base) self.revisions: RevisionMethods33 = RevisionMethods33(self.rest_api_base) self.schedules: ScheduleMethods33 = ScheduleMethods33(self.rest_api_base) self.sites: SiteMethods = SiteMethods33(self.rest_api_base) @@ -223,7 +216,6 @@ def __init__(self, server: str, username: str, password: str, self.flows: FlowMethods34 = FlowMethods34(self.rest_api_base) self.groups: GroupMethods34 = GroupMethods34(self.rest_api_base) self.projects: ProjectMethods34 = ProjectMethods34(self.rest_api_base) - self.publishing: PublishingMethods34 = PublishingMethods34(self.rest_api_base) self.revisions: RevisionMethods34 = RevisionMethods34(self.rest_api_base) self.schedules: ScheduleMethods34 = ScheduleMethods34(self.rest_api_base) self.sites: SiteMethods = SiteMethods34(self.rest_api_base) @@ -244,7 +236,6 @@ def __init__(self, server: str, username: str, password: str, self.flows: FlowMethods35 = FlowMethods35(self.rest_api_base) self.groups: GroupMethods35 = GroupMethods35(self.rest_api_base) self.projects: ProjectMethods35 = ProjectMethods35(self.rest_api_base) - self.publishing: PublishingMethods35 = PublishingMethods35(self.rest_api_base) self.revisions: RevisionMethods35 = RevisionMethods35(self.rest_api_base) self.schedules: ScheduleMethods35 = ScheduleMethods35(self.rest_api_base) self.sites: SiteMethods = SiteMethods35(self.rest_api_base) @@ -267,7 +258,6 @@ def __init__(self, server: str, username: Optional[str] = None, password: Option self.flows: FlowMethods36 = FlowMethods36(self.rest_api_base) self.groups: GroupMethods36 = GroupMethods36(self.rest_api_base) self.projects: ProjectMethods36 = ProjectMethods36(self.rest_api_base) - self.publishing: PublishingMethods36 = PublishingMethods36(self.rest_api_base) self.revisions: RevisionMethods36 = RevisionMethods36(self.rest_api_base) self.schedules: ScheduleMethods36 = ScheduleMethods36(self.rest_api_base) self.sites: SiteMethods = SiteMethods36(self.rest_api_base)