diff --git a/README.md b/README.md index 16f9786..0e09f21 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Document API The Document API provides a supported way to programmatically make updates to Tableau workbook and data source files. If you've been making changes to these file types by directly updating the XML--that is, by XML hacking--this SDK is for you :) Features include: -- Support for 8.X, 9.X, and 10.X workbook and data source files +- Support for 9.X, and 10.X workbook and data source files - Including TDSX and TWBX files - Getting connection information from data sources and workbooks - Server Name diff --git a/tableaudocumentapi/datasource.py b/tableaudocumentapi/datasource.py index 7e3c2af..3f64145 100644 --- a/tableaudocumentapi/datasource.py +++ b/tableaudocumentapi/datasource.py @@ -7,11 +7,11 @@ import itertools import xml.etree.ElementTree as ET import xml.sax.saxutils as sax -import zipfile from tableaudocumentapi import Connection, xfile from tableaudocumentapi import Field from tableaudocumentapi.multilookup_dict import MultiLookupDict +from tableaudocumentapi.xfile import xml_open ######## # This is needed in order to determine if something is a string or not. It is necessary because @@ -113,10 +113,7 @@ def __init__(self, dsxml, filename=None): def from_file(cls, filename): """Initialize datasource from file (.tds)""" - if zipfile.is_zipfile(filename): - dsxml = xfile.get_xml_from_archive(filename).getroot() - else: - dsxml = ET.parse(filename).getroot() + dsxml = xml_open(filename).getroot() return cls(dsxml, filename) def save(self): diff --git a/tableaudocumentapi/workbook.py b/tableaudocumentapi/workbook.py index fd85b3c..28ddd03 100644 --- a/tableaudocumentapi/workbook.py +++ b/tableaudocumentapi/workbook.py @@ -10,6 +10,7 @@ import xml.etree.ElementTree as ET from tableaudocumentapi import Datasource, xfile +from tableaudocumentapi.xfile import xml_open class Workbook(object): @@ -31,12 +32,7 @@ def __init__(self, filename): self._filename = filename - # Determine if this is a twb or twbx and get the xml root - if zipfile.is_zipfile(self._filename): - self._workbookTree = xfile.get_xml_from_archive( - self._filename) - else: - self._workbookTree = ET.parse(self._filename) + self._workbookTree = xml_open(self._filename) self._workbookRoot = self._workbookTree.getroot() # prepare our datasource objects @@ -145,6 +141,7 @@ def _prepare_worksheets(xml_root, ds_index): datasource = ds_index[datasource_name] for column in dependency.findall('.//column'): column_name = column.attrib['name'] - datasource.fields[column_name].add_used_in(worksheet_name) + if column_name in datasource.fields: + datasource.fields[column_name].add_used_in(worksheet_name) return worksheets diff --git a/tableaudocumentapi/xfile.py b/tableaudocumentapi/xfile.py index 13e08c7..a0cd62e 100644 --- a/tableaudocumentapi/xfile.py +++ b/tableaudocumentapi/xfile.py @@ -3,9 +3,31 @@ import shutil import tempfile import zipfile - import xml.etree.ElementTree as ET +try: + from distutils2.version import NormalizedVersion as Version +except ImportError: + from distutils.version import LooseVersion as Version + +MIN_SUPPORTED_VERSION = Version("9.0") + + +class TableauVersionNotSupportedException(Exception): + pass + + +def xml_open(filename): + # Determine if this is a twb or twbx and get the xml root + if zipfile.is_zipfile(filename): + tree = get_xml_from_archive(filename) + else: + tree = ET.parse(filename) + file_version = Version(tree.getroot().attrib.get('version', '0.0')) + if file_version < MIN_SUPPORTED_VERSION: + raise TableauVersionNotSupportedException(file_version) + return tree + @contextlib.contextmanager def temporary_directory(*args, **kwargs): @@ -16,10 +38,10 @@ def temporary_directory(*args, **kwargs): shutil.rmtree(d) -def find_file_in_zip(zip): - for filename in zip.namelist(): +def find_file_in_zip(zip_file): + for filename in zip_file.namelist(): try: - with zip.open(filename) as xml_candidate: + with zip_file.open(filename) as xml_candidate: ET.parse(xml_candidate).getroot().tag in ( 'workbook', 'datasource') return filename @@ -36,14 +58,14 @@ def get_xml_from_archive(filename): return xml_tree -def build_archive_file(archive_contents, zip): +def build_archive_file(archive_contents, zip_file): for root_dir, _, files in os.walk(archive_contents): relative_dir = os.path.relpath(root_dir, archive_contents) for f in files: temp_file_full_path = os.path.join( archive_contents, relative_dir, f) zipname = os.path.join(relative_dir, f) - zip.write(temp_file_full_path, arcname=zipname) + zip_file.write(temp_file_full_path, arcname=zipname) def save_into_archive(xml_tree, filename, new_filename=None): diff --git a/test/assets/ephemeral_field.twb b/test/assets/ephemeral_field.twb new file mode 100644 index 0000000..030c433 --- /dev/null +++ b/test/assets/ephemeral_field.twb @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + a + 130 + [a] + [xy] + a + 1 + string + Count + 255 + true + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + x + 3 + [x] + [xy] + x + 2 + integer + Sum + 10 + true + + "SQL_INTEGER" + "SQL_C_SLONG" + + + + y + 3 + [y] + [xy] + y + 3 + integer + Sum + 10 + true + + "SQL_INTEGER" + "SQL_C_SLONG" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +