Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 92 additions & 25 deletions tableaudocumentapi/workbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,57 @@
# Workbook - A class for writing Tableau workbook files
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets talk at work. A twbx is much more than just the workbook and needs its own representation. With just the twb ... it is not so interesting or useful. While I admit that we only have the ability to edit the xml for twb right now, you could see people using this api to potentially one day create an extract using our Extract API, create a workbook and then package them up together. In this example, the packaging also affects the path specified in the connection string itself. Lets spend a bit more time talking about this. Thanks for getting it started though!!!

#
###############################################################################
import contextlib
import os
import shutil
import tempfile
import zipfile

import xml.etree.ElementTree as ET

from tableaudocumentapi import Datasource

###########################################################################
#
# Utility Functions
#
###########################################################################


@contextlib.contextmanager
def temporary_directory(*args, **kwargs):
d = tempfile.mkdtemp(*args, **kwargs)
try:
yield d
finally:
shutil.rmtree(d)


def find_twb_in_zip(zip):
for filename in zip.namelist():
if os.path.splitext(filename)[-1].lower() == '.twb':
return filename


def get_twb_xml_from_twbx(filename):
with temporary_directory() as temp:
with zipfile.ZipFile(filename) as zf:
zf.extractall(temp)
twb_file = find_twb_in_zip(zf)
twb_xml = ET.parse(os.path.join(temp, twb_file))

return twb_xml


def build_twbx_file(twbx_contents, zip):
for root_dir, _, files in os.walk(twbx_contents):
relative_dir = os.path.relpath(root_dir, twbx_contents)
for f in files:
temp_file_full_path = os.path.join(
twbx_contents, relative_dir, f)
zipname = os.path.join(relative_dir, f)
zip.write(temp_file_full_path, arcname=zipname)


class Workbook(object):
"""
Expand All @@ -24,30 +71,18 @@ def __init__(self, filename):
Constructor.

"""
# We have a valid type of input file
if self._is_valid_file(filename):
# set our filename, open .twb, initialize things
self._filename = filename
self._workbookTree = ET.parse(filename)
self._workbookRoot = self._workbookTree.getroot()

# prepare our datasource objects
self._datasources = self._prepare_datasources(
self._workbookRoot) # self.workbookRoot.find('datasources')
else:
print('Invalid file type. Must be .twb or .tds.')
raise Exception()

@classmethod
def from_file(cls, filename):
"Initialize datasource from file (.tds)"
if self._is_valid_file(filename):
self._filename = filename
dsxml = ET.parse(filename).getroot()
return cls(dsxml)
self._filename = filename

# Determine if this is a twb or twbx and get the xml root
if zipfile.is_zipfile(self._filename):
self._workbookTree = get_twb_xml_from_twbx(self._filename)
else:
print('Invalid file type. Must be .twb or .tds.')
raise Exception()
self._workbookTree = ET.parse(self._filename)

self._workbookRoot = self._workbookTree.getroot()
# prepare our datasource objects
self._datasources = self._prepare_datasources(
self._workbookRoot) # self.workbookRoot.find('datasources')

###########
# datasources
Expand Down Expand Up @@ -76,7 +111,12 @@ def save(self):
"""

# save the file
self._workbookTree.write(self._filename, encoding="utf-8", xml_declaration=True)

if zipfile.is_zipfile(self._filename):
self._save_into_twbx(self._filename)
else:
self._workbookTree.write(
self._filename, encoding="utf-8", xml_declaration=True)

def save_as(self, new_filename):
"""
Expand All @@ -90,7 +130,11 @@ def save_as(self, new_filename):

"""

self._workbookTree.write(new_filename, encoding="utf-8", xml_declaration=True)
if zipfile.is_zipfile(self._filename):
self._save_into_twbx(new_filename)
else:
self._workbookTree.write(
new_filename, encoding="utf-8", xml_declaration=True)

###########################################################################
#
Expand All @@ -107,6 +151,29 @@ def _prepare_datasources(self, xmlRoot):

return datasources

def _save_into_twbx(self, filename=None):
# Save reuses existing filename, 'save as' takes a new one
if filename is None:
filename = self._filename

# Saving a twbx means extracting the contents into a temp folder,
# saving the changes over the twb in that folder, and then
# packaging it back up into a specifically formatted zip with the correct
# relative file paths

# Extract to temp directory
with temporary_directory() as temp_path:
with zipfile.ZipFile(self._filename) as zf:
twb_file = find_twb_in_zip(zf)
zf.extractall(temp_path)
# Write the new version of the twb to the temp directory
self._workbookTree.write(os.path.join(
temp_path, twb_file), encoding="utf-8", xml_declaration=True)

# Write the new twbx with the contents of the temp folder
with zipfile.ZipFile(filename, "w", compression=zipfile.ZIP_DEFLATED) as new_twbx:
build_twbx_file(temp_path, new_twbx)

@staticmethod
def _is_valid_file(filename):
fileExtension = os.path.splitext(filename)[-1].lower()
Expand Down
Loading