Skip to content

Commit

Permalink
Merge pull request #52 from bryantbhowell/4.9.0
Browse files Browse the repository at this point in the history
4.9.0
Bryant Howell authored Apr 16, 2019
2 parents ba4ffbf + 1c8e99f commit 4fc5f78
Showing 13 changed files with 790 additions and 190 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@ The TableauDatasource class uses the `TDEFileGenerator` and/or the `HyperFileGen
* 4.5.0: 2018.1 (API 3.0) compatibility. All requests for a given connection using a single HTTP session, and other improvements to the RestXmlRequest class.
* 4.7.0: Dropping API 2.0 (Tableau 9.0) compatibility. Any method that is overwritten in a later version will not be updated in the TableauRestApiConnection class going forward. Also implemented a direct_xml_request parameter for Add and Update methods, allowing direct submission of an ElementTree.Element request to the endpoints, particularly for replication.
* 4.8.0 Introduces the RestJsonRequest object and _json plural querying methods for passing JSON responses to other systems
* 4.9.0 API 3.3 (2019.1) compatibility, as well as ability to swap in static files using TableauDocument and other bug fixes.
## --- Table(au) of Contents ---
------

@@ -1121,6 +1122,23 @@ ex.
print(file_2)
# u'A Workbook (2).twb'

#### 2.2.1 Replacing Static Data Files
`TableauFile` has an optional argument on the save_new_file method to allow swapping in new data files (CSV, XLS or Hyper) into an existing TWBX or TDSX.

TableauFile.save_new_file(new_filename_no_extension, data_file_replacement_map=None) # returns new filename

data_file_replacement_map accepts a dict in format { 'TableauFileFilename' : 'FilenameOfNewFileOnDisk' }. To find out the TableauFileFilename, print out the `other_files` property of the TableauFile object:

t_file = TableauFile('My AmazingWorkbook.twbx')
for file in t_file.other_files:
print(file)
You should be able to find the exact naming of the data file you want to replace. Copy that exactly and use it as the key in your dictionary. For the value, use a fully qualified filename on your machine:

t_file = TableauFile('My AmazingWorkbook.twbx')
file_map = { 'Data/en_US-US/Sample - Superstore.xls' : '/Users/bhowell/Documents/My Tableau Repository/Datasources/2018.3/en_US-EU/Sample - EU Superstore.xls'}
t_file.save_new_file('My AmazingWorkbook - Updated', data_file_replacement_map=file_map)

### 2.3 TableauDocument Class
The TableauDocument class helps map the differences between `TableauWorkbook` and `TableauDatasource`. It only implements two properties:

2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

setup(
name='tableau_tools',
version='4.8.3',
version='4.9.0',
packages=['tableau_tools', 'tableau_tools.tableau_rest_api', 'tableau_tools.tableau_documents', 'tableau_tools.examples'],
url='https://github.com/bryantbhowell/tableau_tools',
license='',
53 changes: 47 additions & 6 deletions tableau_base.py
Original file line number Diff line number Diff line change
@@ -10,13 +10,13 @@
class TableauBase(object):
def __init__(self):
# In reverse order to work down until the acceptable version is found on the server, through login process
self.supported_versions = (u'2018.3', u'2018.2', u'2018.1', u"10.5", u"10.4", u"10.3", u"10.2", u"10.1", u"10.0", u"9.3", u"9.2", u"9.1", u"9.0")
self.supported_versions = (u'2019.1', u'2018.3', u'2018.2', u'2018.1', u"10.5", u"10.4", u"10.3", u"10.2", u"10.1", u"10.0", u"9.3", u"9.2", u"9.1", u"9.0")
self.logger = None
self.luid_pattern = r"[0-9a-fA-F]*-[0-9a-fA-F]*-[0-9a-fA-F]*-[0-9a-fA-F]*-[0-9a-fA-F]*"

# Defaults, will get updated with each update. Overwritten by set_tableau_server_version
self.version = u"10.5"
self.api_version = u"2.8"
self.version = u"2018.2"
self.api_version = u"3.1"
self.tableau_namespace = u'http://tableau.com/api'
self.ns_map = {'t': 'http://tableau.com/api'}
self.ns_prefix = '{' + self.ns_map['t'] + '}'
@@ -89,7 +89,8 @@ def __init__(self):
u"2.8": server_content_roles_2_1,
u'3.0': server_content_roles_2_1,
u'3.1': server_content_roles_2_1,
u'3.2': server_content_roles_2_1
u'3.2': server_content_roles_2_1,
u'3.3': server_content_roles_2_1
}

self.server_to_rest_capability_map = {
@@ -220,6 +221,43 @@ def __init__(self):
)
}

capabilities_3_3 = {
u"project": (u"Read", u"Write", u'ProjectLeader', u'InheritedProjectLeader'),
u"workbook": (
u'Read',
u'ExportImage',
u'ExportData',
u'ViewComments',
u'AddComment',
u'Filter',
u'ViewUnderlyingData',
u'ShareView',
u'WebAuthoring',
u'Write',
u'ExportXml',
u'ChangeHierarchy',
u'Delete',
u'ChangePermissions',

),
u"datasource": (
u'Read',
u'Connect',
u'Write',
u'ExportXml',
u'Delete',
u'ChangePermissions'
),
u'flow': (
u'ChangeHierarchy',
u'ChangePermissions',
u'Delete',
u'ExportXml',
u'Read',
u'Write'
)
}

self.available_capabilities = {
u"2.0": capabilities_2_0,
u"2.1": capabilities_2_1,
@@ -232,7 +270,8 @@ def __init__(self):
u'2.8': capabilities_2_8,
u'3.0': capabilities_2_8,
u'3.1': capabilities_2_8,
u'3.2': capabilities_2_8
u'3.2': capabilities_2_8,
u'3.3': capabilities_3_3

}

@@ -283,7 +322,7 @@ def __init__(self):
u"Hyper": u'hyper'
}

self.permissionable_objects = (u'datasource', u'project', u'workbook')
self.permissionable_objects = (u'datasource', u'project', u'workbook', u'flow')

def set_tableau_server_version(self, tableau_server_version):
"""
@@ -314,6 +353,8 @@ def set_tableau_server_version(self, tableau_server_version):
self.api_version = u'3.1'
elif unicode(tableau_server_version) == u'2018.3':
self.api_version = u'3.2'
elif unicode(tableau_server_version) == u'2019.1':
self.api_version = u'3.3'
self.tableau_namespace = u'http://tableau.com/api'
self.ns_map = {'t': 'http://tableau.com/api'}
self.version = tableau_server_version
22 changes: 17 additions & 5 deletions tableau_documents/tableau_file.py
Original file line number Diff line number Diff line change
@@ -118,9 +118,10 @@ def tableau_document(self):
return self._tableau_document

# Appropriate extension added if needed
def save_new_file(self, new_filename_no_extension):
def save_new_file(self, new_filename_no_extension, data_file_replacement_map=None):
"""
:type new_filename_no_extension: unicode
:type data_file_replacement_map: dict
:rtype: unicode
"""
self.start_log_block()
@@ -194,9 +195,16 @@ def save_new_file(self, new_filename_no_extension):
self.log(u'File {} is from an extract that has been replaced, skipping'.format(filename))
continue

o_zf.extract(filename)
new_zf.write(filename)
os.remove(filename)
# If file is listed in the data_file_replacement_map, write data from the mapped in file
if filename in data_file_replacement_map:
#data_file_obj = open(filename, mode='wb')
#data_file_obj.write(data_file_replacement_map[filename])
#data_file_obj.close()
new_zf.write(data_file_replacement_map[filename], u"/" + filename)
else:
o_zf.extract(filename)
new_zf.write(filename)
os.remove(filename)
self.log(u'Removed file {}'.format(filename))
lowest_level = filename.split('/')
temp_directories_to_remove[lowest_level[0]] = True
@@ -211,7 +219,11 @@ def save_new_file(self, new_filename_no_extension):
# Cleanup all the temporary directories
for directory in temp_directories_to_remove:
self.log(u'Removing directory {}'.format(directory))
shutil.rmtree(directory)
try:
shutil.rmtree(directory)
except OSError as e:
# Just means that directory didn't exist for some reason, probably a swap occurred
pass
new_zf.close()

return save_filename
49 changes: 0 additions & 49 deletions tableau_repository.py
Original file line number Diff line number Diff line change
@@ -268,52 +268,3 @@ def query_site_id_from_datasource_luid(self, datasource_luid):
datasource_id = row[0]
return datasource_id

def set_workbook_on_schedule(self, workbook_luid, schedule_name):
if TableauBase.is_luid(workbook_luid) is False:
raise InvalidOptionException(u'Workbook luid must be a luid. You passed in {}'.format(workbook_luid))
wb_id = self.query_workbook_id_from_luid(workbook_luid)
site_id = self.query_site_id_from_workbook_luid(workbook_luid)
schedule_id = self.get_extract_schedule_id_by_name(schedule_name)

insert_query = """
INSERT INTO tasks
VALUES(
DEFAULT -- id will auto-increment if you pass DEFAULT
, %s --schedule_id from _schedules
, 'RefreshExtractTask' -- or 'IncrementExtractTask' for incremental
,1 --priority, can be lower if you want. Workbooks seem to default to 50
,%s --obj_id from _datasources or _workbooks
,NOW() --created_at
,NOW() --created_at
,%s --site_id
,'Workbook' -- 'Datasource' or 'Workbook'
,NULL --luid will autogenerate correctly when NULL, based on trigger function
, 0 -- this starts as 0
)
"""
self.query(insert_query, [schedule_id, wb_id, site_id])

def set_datasource_on_schedule(self, datasource_luid, schedule_name):
if TableauBase.is_luid(datasource_luid) is False:
raise InvalidOptionException(u'Workbook luid must be a luid. You passed in {}'.format(datasource_luid))
ds_id = self.query_datasource_id_from_luid(datasource_luid)
site_id = self.query_site_id_from_datasource_luid(datasource_luid)
schedule_id = self.get_extract_schedule_id_by_name(schedule_name)

insert_query = """
INSERT INTO tasks
VALUES(
DEFAULT -- id will auto-increment if you pass DEFAULT
, %s --schedule_id from _schedules
, 'RefreshExtractTask' -- or 'IncrementExtractTask' for incremental
,1 --priority, can be lower if you want. Workbooks seem to default to 50
,%s --obj_id from _datasources or _workbooks
,NOW() --created_at
,NOW() --created_at
,%s --site_id
,'Datasource' -- 'Datasource' or 'Workbook'
,NULL --luid will autogenerate correctly when NULL, based on trigger function
, 0 -- this starts as 0
)
"""
self.query(insert_query, [schedule_id, ds_id, site_id])
10 changes: 10 additions & 0 deletions tableau_rest_api/permissions.py
Original file line number Diff line number Diff line change
@@ -386,3 +386,13 @@ def __init__(self, group_or_user, group_or_user_luid):
u'all': u'Allow'
}
}


class FlowPermissions33(Permissions):
def __init__(self, group_or_user, group_or_user_luid):
Permissions.__init__(self, group_or_user, group_or_user_luid, u'flow')
for cap in self.available_capabilities[u'3.3'][u'flow']:
if cap != u'all':
self.capabilities[cap] = None
# Unclear that there are any defined roles for Prep Conductor flows
self.role_set = {}
11 changes: 10 additions & 1 deletion tableau_rest_api/published_content.py
Original file line number Diff line number Diff line change
@@ -1190,4 +1190,13 @@ def convert_capabilities_xml_into_obj_list(self, xml_obj):
obj_list.append(perms_obj)
self.log(u'Permissions object list has {} items'.format(unicode(len(obj_list))))
self.end_log_block()
return obj_list
return obj_list


class Flow(PublishedContent):
def __init__(self, luid, tableau_rest_api_obj, tableau_server_version, default=False, logger_obj=None,
content_xml_obj=None):
PublishedContent.__init__(self, luid, u"flow", tableau_rest_api_obj, tableau_server_version,
default=default, logger_obj=logger_obj, content_xml_obj=content_xml_obj)
self.__available_capabilities = self.available_capabilities[self.api_version][u"flow"]
self.log(u"Flow object initiating")
122 changes: 97 additions & 25 deletions tableau_rest_api/tableau_rest_api_connection.py
Original file line number Diff line number Diff line change
@@ -1219,16 +1219,17 @@ def query_job(self, job_luid):
# Start of download / save methods
#

# Do not include file extension
def save_workbook_view_preview_image(self, wb_name_or_luid, view_name_or_luid, filename_no_extension,
# You must pass in the wb name because the endpoint needs it (although, you could potentially look up the
# workbook LUID from the view LUID
def query_view_preview_image(self, wb_name_or_luid, view_name_or_luid,
proj_name_or_luid=None):
"""
:type wb_name_or_luid: unicode
:type view_name_or_luid: unicode
:type proj_name_or_luid: unicode
:type filename_no_extension: unicode
:rtype:
"""
:rtype: bytes
"""
self.start_log_block()
if self.is_luid(wb_name_or_luid):
wb_luid = wb_name_or_luid
@@ -1241,54 +1242,105 @@ def save_workbook_view_preview_image(self, wb_name_or_luid, view_name_or_luid, f
view_luid = self.query_workbook_view_luid(wb_name_or_luid, view_name=view_name_or_luid,
proj_name_or_luid=proj_name_or_luid)
try:
if filename_no_extension.find('.png') == -1:
filename_no_extension += '.png'
save_file = open(filename_no_extension, 'wb')

url = self.build_api_url(u"workbooks/{}/views/{}/previewImage".format(wb_luid, view_luid))
image = self.send_binary_get_request(url)
save_file.write(image)
save_file.close()

self.end_log_block()
return image

# You might be requesting something that doesn't exist
except RecoverableHTTPException as e:
self.log(u"Attempt to request preview image results in HTTP error {}, Tableau Code {}".format(e.http_code, e.tableau_error_code))
self.log(u"Attempt to request preview image results in HTTP error {}, Tableau Code {}".format(e.http_code,
e.tableau_error_code))
self.end_log_block()
raise


# Do not include file extension

# Just an alias but it matches the naming of the current reference guide (2019.1)
def save_view_preview_image(self, wb_name_or_luid, view_name_or_luid, filename_no_extension,
proj_name_or_luid=None):
"""
:type wb_name_or_luid: unicode
:type view_name_or_luid: unicode
:type proj_name_or_luid: unicode
:type filename_no_extension: unicode
:rtype:
"""
self.save_workbook_view_preview_image(wb_name_or_luid, view_name_or_luid, filename_no_extension,
proj_name_or_luid)

def save_workbook_view_preview_image(self, wb_name_or_luid, view_name_or_luid, filename_no_extension,
proj_name_or_luid=None):
"""
:type wb_name_or_luid: unicode
:type view_name_or_luid: unicode
:type proj_name_or_luid: unicode
:type filename_no_extension: unicode
:rtype:
"""
self.start_log_block()
image = self.query_view_preview_image(wb_name_or_luid=wb_name_or_luid, view_name_or_luid=view_name_or_luid,
proj_name_or_luid=proj_name_or_luid)
if filename_no_extension.find('.png') == -1:
filename_no_extension += '.png'
try:
save_file = open(filename_no_extension, 'wb')
save_file.write(image)
save_file.close()
self.end_log_block()

except IOError:
self.log(u"Error: File '{}' cannot be opened to save to".format(filename_no_extension))
self.end_log_block()
raise

# Do not include file extension
def save_workbook_preview_image(self, wb_name_or_luid, filename_no_extension, proj_name_or_luid=None):
def query_workbook_preview_image(self, wb_name_or_luid, proj_name_or_luid=None):
"""
:type wb_name_or_luid: unicode
:param filename_no_extension: Correct extension will be added automatically
:type filename_no_extension: unicode
:type proj_name_or_luid: unicode
:rtype:
:rtype: bytes
"""
self.start_log_block()
if self.is_luid(wb_name_or_luid):
wb_luid = wb_name_or_luid
else:
wb_luid = self.query_workbook_luid(wb_name_or_luid, proj_name_or_luid)
try:
if filename_no_extension.find('.png') == -1:
filename_no_extension += '.png'
save_file = open(filename_no_extension, 'wb')

url = self.build_api_url(u"workbooks/{}/previewImage".format(wb_luid))
image = self.send_binary_get_request(url)
save_file.write(image)
save_file.close()
self.end_log_block()
return image

# You might be requesting something that doesn't exist, but unlikely
except RecoverableHTTPException as e:
self.log(u"Attempt to request preview image results in HTTP error {}, Tableau Code {}".format(e.http_code, e.tableau_error_code))
self.end_log_block()
raise


# Do not include file extension
def save_workbook_preview_image(self, wb_name_or_luid, filename_no_extension, proj_name_or_luid=None):
"""
:type wb_name_or_luid: unicode
:param filename_no_extension: Correct extension will be added automatically
:type filename_no_extension: unicode
:type proj_name_or_luid: unicode
:rtype:
"""
self.start_log_block()
image = self.query_workbook_preview_image(wb_name_or_luid=wb_name_or_luid, proj_name_or_luid=proj_name_or_luid)
if filename_no_extension.find('.png') == -1:
filename_no_extension += '.png'
try:
save_file = open(filename_no_extension, 'wb')
save_file.write(image)
save_file.close()
self.end_log_block()

except IOError:
self.log(u"Error: File '{}' cannot be opened to save to".format(filename_no_extension))
self.end_log_block()
@@ -2288,22 +2340,23 @@ def publish_datasource(self, ds_filename, ds_name, project_obj, overwrite=False,
# 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):
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'
if content_type not in [u'workbook', u'datasource']:
raise InvalidOptionException(u"content_type must be 'workbook' or 'datasource'")
if content_type not in [u'workbook', u'datasource', u'flow']:
raise InvalidOptionException(u"content_type must be 'workbook', 'datasource', or 'flow' ")

file_extension = None
final_filename = None
cleanup_temp_file = False

for ending in [u'.twb', u'.twbx', u'.tde', u'.tdsx', u'.tds', u'.tde', u'.hyper']:
for ending in [u'.twb', u'.twbx', u'.tde', u'.tdsx', u'.tds', u'.tde', u'.hyper', u'.tfl', u'.tflx']:
if content_filename.endswith(ending):
file_extension = ending[1:]

@@ -2339,11 +2392,17 @@ def publish_content(self, content_type, content_filename, content_name, project_

# Build publish request in ElementTree then convert at publish
publish_request_xml = etree.Element(u'tsRequest')
# could be either workbook or datasource
# could be either workbook, datasource, or flow
t1 = etree.Element(content_type)
t1.set(u'name', content_name)
if show_tabs is not False:
t1.set(u'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(u'generateThumbnailsAsUser', thumbnail_user_luid)

if connection_username is not None:
cc = etree.Element(u'connectionCredentials')
@@ -2355,6 +2414,19 @@ def publish_content(self, content_type, content_filename, content_name, project_
cc.set(u'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 = etree.Element(u'views')
for view_name in views_to_hide_list:
v = etree.Element(u'view')
v.set(u'name', view_name)
v.set(u'hidden', u'true')
t1.append(vs)

# Description only allowed for Flows as of 3.3
if description is not None:
t1.set(u'description', description)
p = etree.Element(u'project')
p.set(u'id', project_luid)
t1.append(p)
85 changes: 82 additions & 3 deletions tableau_rest_api/tableau_rest_api_connection_25.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from tableau_rest_api_connection_24 import *

import urllib

class TableauRestApiConnection25(TableauRestApiConnection24):
def __init__(self, server, username, password, site_content_url=u""):
@@ -120,12 +120,13 @@ def update_project(self, name_or_luid, new_project_name=None, new_project_descri
self.end_log_block()
return self.get_published_project_object(project_luid, response)

def query_view_image(self, view_name_or_luid, save_filename_no_extension, high_resolution=False,
# Generic implementation of all the CSV/PDF/PNG requests
def _query_data_file(self, download_type, view_name_or_luid, high_resolution=None, view_filter_map=None,
wb_name_or_luid=None, proj_name_or_luid=None):
"""
:type view_name_or_luid: unicode
:type save_filename_no_extension: unicode
:type high_resolution: bool
:type view_filter_map: dict
:type wb_name_or_luid: unicode
:type proj_name_or_luid
:rtype:
@@ -137,7 +138,85 @@ def query_view_image(self, view_name_or_luid, save_filename_no_extension, high_r
view_luid = self.query_workbook_view_luid(wb_name_or_luid, view_name=view_name_or_luid,
proj_name_or_luid=proj_name_or_luid)

if view_filter_map is not None:
final_filter_map = {}
for key in view_filter_map:
new_key = u"vf_{}".format(key)
# Check if this just a string
if isinstance(view_filter_map[key], basestring):
value = view_filter_map[key]
else:
value = ",".join(map(unicode,view_filter_map[key]))
final_filter_map[new_key] = value

additional_url_params = u"?" + urllib.urlencode(final_filter_map)
if high_resolution is True:
additional_url_params += u"&resolution=high"

else:
additional_url_params = u""
if high_resolution is True:
additional_url_params += u"?resolution=high"
try:

url = self.build_api_url(u"views/{}/{}{}".format(view_luid, download_type, additional_url_params))
binary_result = self.send_binary_get_request(url)

self.end_log_block()
return binary_result
except RecoverableHTTPException as e:
self.log(u"Attempt to request results in HTTP error {}, Tableau Code {}".format(e.http_code, e.tableau_error_code))
self.end_log_block()
raise

def query_view_image(self, view_name_or_luid, high_resolution=False, view_filter_map=None,
wb_name_or_luid=None, proj_name_or_luid=None):
"""
:type view_name_or_luid: unicode
:type high_resolution: bool
:type view_filter_map: dict
:type wb_name_or_luid: unicode
:type proj_name_or_luid
:rtype:
"""
self.start_log_block()
image = self._query_data_file(u'image', view_name_or_luid=view_name_or_luid, high_resolution=high_resolution,
view_filter_map=view_filter_map, wb_name_or_luid=wb_name_or_luid,
proj_name_or_luid=proj_name_or_luid)
self.end_log_block()
return image

def save_view_image(self, wb_name_or_luid=None, view_name_or_luid=None, filename_no_extension=None,
proj_name_or_luid=None, view_filter_map=None):
"""
:type wb_name_or_luid: unicode
:type view_name_or_luid: unicode
:type proj_name_or_luid: unicode
:type filename_no_extension: unicode
:type view_filter_map: dict
:rtype:
"""
self.start_log_block()
data = self.query_view_image(wb_name_or_luid=wb_name_or_luid, view_name_or_luid=view_name_or_luid,
proj_name_or_luid=proj_name_or_luid, view_filter_map=view_filter_map)

if filename_no_extension is not None:
if filename_no_extension.find('.png') == -1:
filename_no_extension += '.png'
try:
save_file = open(filename_no_extension, 'wb')
save_file.write(data)
save_file.close()
self.end_log_block()
return
except IOError:
self.log(u"Error: File '{}' cannot be opened to save to".format(filename_no_extension))
self.end_log_block()
raise
else:
raise InvalidOptionException(
u'This method is for saving response to file. Must include filename_no_extension parameter')


###
### Fields can be used to limit or expand details can be brought in
140 changes: 40 additions & 100 deletions tableau_rest_api/tableau_rest_api_connection_28.py
Original file line number Diff line number Diff line change
@@ -208,6 +208,14 @@ def add_datasource_to_schedule(self, ds_name_or_luid, schedule_name_or_luid, pro

self.end_log_block()

def query_view_pdf(self, wb_name_or_luid, view_name_or_luid, proj_name_or_luid=None,
view_filter_map=None):
self.start_log_block()
pdf = self._query_data_file(u'pdf', view_name_or_luid=view_name_or_luid, wb_name_or_luid=wb_name_or_luid,
proj_name_or_luid=proj_name_or_luid, view_filter_map=view_filter_map)
self.end_log_block()
return pdf

# Do not include file extension
def save_view_pdf(self, wb_name_or_luid, view_name_or_luid, filename_no_extension,
proj_name_or_luid=None, view_filter_map=None):
@@ -220,134 +228,66 @@ def save_view_pdf(self, wb_name_or_luid, view_name_or_luid, filename_no_extensio
:rtype:
"""
self.start_log_block()
pdf = self.query_view_pdf(view_name_or_luid=view_name_or_luid, wb_name_or_luid=wb_name_or_luid,
proj_name_or_luid=proj_name_or_luid, view_filter_map=view_filter_map)

if self.is_luid(view_name_or_luid):
view_luid = view_name_or_luid
else:
if wb_name_or_luid is None:
raise InvalidOptionException(u'If looking up view by name, must include workbook')
view_luid = self.query_workbook_view_luid(wb_name_or_luid, view_name=view_name_or_luid,
proj_name_or_luid=proj_name_or_luid)
if filename_no_extension.find(u'.pdf') == -1:
filename_no_extension += u'.pdf'
try:
if filename_no_extension.find(u'.pdf') == -1:
filename_no_extension += u'.pdf'
save_file = open(filename_no_extension, 'wb')
if view_filter_map is not None:
final_filter_map = {}
for key in view_filter_map:
new_key = u"vf_{}".format(key)
final_filter_map[new_key] = view_filter_map[key]

additional_url_params = u"?" + urllib.urlencode(final_filter_map)
else:
additional_url_params = u""
url = self.build_api_url(u"views/{}/pdf{}".format(view_luid, additional_url_params))
image = self.send_binary_get_request(url)
save_file.write(image)
save_file.write(pdf)
save_file.close()
self.end_log_block()

# You might be requesting something that doesn't exist
except RecoverableHTTPException as e:
self.log(u"Attempt to request preview image results in HTTP error {}, Tableau Code {}".format(e.http_code, e.tableau_error_code))
self.end_log_block()
raise
except IOError:
self.log(u"Error: File '{}' cannot be opened to save to".format(filename_no_extension))
self.end_log_block()
raise

def save_view_data_as_csv(self, wb_name_or_luid=None, view_name_or_luid=None, filename_no_extension=None,
proj_name_or_luid=None, view_filter_map=None):
def query_view_data(self, wb_name_or_luid=None, view_name_or_luid=None, proj_name_or_luid=None,
view_filter_map=None):
"""
:type wb_name_or_luid: unicode
:type view_name_or_luid: unicode
:type proj_name_or_luid: unicode
:type filename_no_extension: unicode
:type view_filter_map: dict
:rtype:
"""
self.start_log_block()
csv = self._query_data_file(u'data', view_name_or_luid=view_name_or_luid, wb_name_or_luid=wb_name_or_luid,
proj_name_or_luid=proj_name_or_luid, view_filter_map=view_filter_map)
self.end_log_block()
return csv

if self.is_luid(view_name_or_luid):
view_luid = view_name_or_luid
else:
if wb_name_or_luid is None:
raise InvalidOptionException(u'If looking up view by name, must include workbook')
view_luid = self.query_workbook_view_luid(wb_name_or_luid, view_name=view_name_or_luid,
proj_name_or_luid=proj_name_or_luid)
try:
if view_filter_map is not None:
final_filter_map = {}
for key in view_filter_map:
new_key = u"vf_{}".format(key)
final_filter_map[new_key] = view_filter_map[key]

additional_url_params = u"?" + urllib.urlencode(final_filter_map)
else:
additional_url_params = u""
url = self.build_api_url(u"views/{}/data{}".format(view_luid, additional_url_params))
data = self.send_binary_get_request(url)
if filename_no_extension is not None:
if filename_no_extension.find('.csv') == -1:
filename_no_extension += '.csv'
save_file = open(filename_no_extension, 'wb')
save_file.write(data)
save_file.close()
self.end_log_block()
return
else:
raise InvalidOptionException(u'This method is for saving response to file. Must include filename_no_extension parameter')

# You might be requesting something that doesn't exist
except RecoverableHTTPException as e:
self.log(u"Attempt to request data results in HTTP error {}, Tableau Code {}".format(e.http_code, e.tableau_error_code))
self.end_log_block()
raise
except IOError:
self.log(u"Error: File '{}' cannot be opened to save to".format(filename_no_extension))
self.end_log_block()
raise

def query_view_data(self, wb_name_or_luid=None, view_name_or_luid=None, proj_name_or_luid=None,
view_filter_map=None):
def save_view_data_as_csv(self, wb_name_or_luid=None, view_name_or_luid=None, filename_no_extension=None,
proj_name_or_luid=None, view_filter_map=None):
"""
:type wb_name_or_luid: unicode
:type view_name_or_luid: unicode
:type proj_name_or_luid: unicode
:type filename_no_extension: unicode
:type view_filter_map: dict
:rtype: csv
:rtype:
"""
self.start_log_block()
data = self.query_view_data(wb_name_or_luid=wb_name_or_luid, view_name_or_luid=view_name_or_luid,
proj_name_or_luid=proj_name_or_luid, view_filter_map=view_filter_map)

if self.is_luid(view_name_or_luid):
view_luid = view_name_or_luid
if filename_no_extension is not None:
if filename_no_extension.find('.csv') == -1:
filename_no_extension += '.csv'
try:
save_file = open(filename_no_extension, 'wb')
save_file.write(data)
save_file.close()
self.end_log_block()
return
except IOError:
self.log(u"Error: File '{}' cannot be opened to save to".format(filename_no_extension))
self.end_log_block()
raise
else:
if wb_name_or_luid is None:
raise InvalidOptionException(u'If looking up view by name, must include workbook')
view_luid = self.query_workbook_view_luid(wb_name_or_luid, view_name=view_name_or_luid,
proj_name_or_luid=proj_name_or_luid)
try:
if view_filter_map is not None:
final_filter_map = {}
for key in view_filter_map:
new_key = u"vf_{}".format(key)
final_filter_map[new_key] = view_filter_map[key]

additional_url_params = u"?" + urllib.urlencode(final_filter_map)
else:
additional_url_params = u""
url = self.build_api_url(u"views/{}/data{}".format(view_luid, additional_url_params))
# Raw response should be UTF-8 encoded plain text CSV
data = self.send_binary_get_request(url)
# Convert to CSV object?
return data

# You might be requesting something that doesn't exist
except RecoverableHTTPException as e:
self.log(u"Attempt to request data results in HTTP error {}, Tableau Code {}".format(e.http_code, e.tableau_error_code))
self.end_log_block()
raise
raise InvalidOptionException(
u'This method is for saving response to file. Must include filename_no_extension parameter')

def update_datasource_now(self, ds_name_or_luid, project_name_or_luid=False):
"""
33 changes: 33 additions & 0 deletions tableau_rest_api/tableau_rest_api_connection_32.py
Original file line number Diff line number Diff line change
@@ -97,3 +97,36 @@ def delete_user_from_data_driven_alert(self, data_alert_luid, username_or_luid):
self.send_delete_request(url)
self.end_log_block()

# In 3.2, you can hide views from publishing
def publish_workbook(self, workbook_filename, workbook_name, project_obj, overwrite=False, async_publish=False, connection_username=None,
connection_password=None, save_credentials=True, show_tabs=True, check_published_ds=True,
oauth_flag=False, views_to_hide_list=None):
"""
:type workbook_filename: unicode
:type workbook_name: unicode
:type project_obj: Project20 or Project21
:type overwrite: bool
:type connection_username: unicode
:type connection_password: unicode
:type save_credentials: bool
:type show_tabs: bool
:param check_published_ds: Set to False to improve publish speed if you KNOW there are no published data sources
:type check_published_ds: bool
:type oauth_flag: bool:
:type views_to_hide_list: list[unicode]
:
:rtype: unicode
"""

project_luid = project_obj.luid
xml = self.publish_content(u'workbook', workbook_filename, workbook_name, project_luid,
{u"overwrite": overwrite, u"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(u'.//t:job', self.ns_map)
return job[0].get(u'id')
else:
workbook = xml.findall(u'.//t:workbook', self.ns_map)
return workbook[0].get(u'id')
423 changes: 423 additions & 0 deletions tableau_rest_api/tableau_rest_api_connection_33.py

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions tableau_rest_api/url_filter.py
Original file line number Diff line number Diff line change
@@ -429,3 +429,15 @@ def create_subtitle_has_filter(subtitle):
:rtype: UrlFilter
"""
return UrlFilter(u'subtitle', u'has', [subtitle, ])

class UrlFilter33(UrlFilter31):
def __init__(self, field, operator, values):
UrlFilter31.__init__(self, field, operator, values)

@staticmethod
def create_project_name_equals_filter(project_name):
"""
:type subtitle: unicode
:rtype: UrlFilter
"""
return UrlFilter(u'projectName', u'eq', [project_name, ])

0 comments on commit 4fc5f78

Please sign in to comment.