From ac4ee7244b99e1ccef72640096050f15c5b9c007 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Fri, 22 Jul 2016 15:46:35 -0700 Subject: [PATCH 1/4] add an explicit version check to be explicit about what we support --- README.md | 2 +- tableaudocumentapi/datasource.py | 7 ++----- tableaudocumentapi/workbook.py | 8 ++------ tableaudocumentapi/xml_open.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 tableaudocumentapi/xml_open.py 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..defae26 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.xml_open 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..e885aa6 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.xml_open 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 diff --git a/tableaudocumentapi/xml_open.py b/tableaudocumentapi/xml_open.py new file mode 100644 index 0000000..e1afce6 --- /dev/null +++ b/tableaudocumentapi/xml_open.py @@ -0,0 +1,28 @@ +try: + from distutils2.version import NormalizedVersion as Version +except ImportError: + from distutils.version import LooseVersion as Version + +import xml.etree.ElementTree as ET +import zipfile + +from tableaudocumentapi import xfile + +MIN_SUPPORTED_VERSION = Version("9.0") + + +class VersionNotSupportedException(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 = xfile.get_xml_from_archive(filename) + else: + tree = ET.parse(filename) + file_version = Version(tree.getroot().attrib.get('version', '0.0')) + print(file_version, MIN_SUPPORTED_VERSION) + if file_version < MIN_SUPPORTED_VERSION: + raise VersionNotSupportedException(file_version) + return tree From 8cf7228cc07ddfde57d4bcf9905951a96d2ab036 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Mon, 25 Jul 2016 08:17:08 -0700 Subject: [PATCH 2/4] Move xml_open to xfile --- tableaudocumentapi/datasource.py | 2 +- tableaudocumentapi/workbook.py | 2 +- tableaudocumentapi/xfile.py | 35 ++++++++++++++++++++++++++------ tableaudocumentapi/xml_open.py | 28 ------------------------- 4 files changed, 31 insertions(+), 36 deletions(-) delete mode 100644 tableaudocumentapi/xml_open.py diff --git a/tableaudocumentapi/datasource.py b/tableaudocumentapi/datasource.py index defae26..3f64145 100644 --- a/tableaudocumentapi/datasource.py +++ b/tableaudocumentapi/datasource.py @@ -11,7 +11,7 @@ from tableaudocumentapi import Connection, xfile from tableaudocumentapi import Field from tableaudocumentapi.multilookup_dict import MultiLookupDict -from tableaudocumentapi.xml_open import xml_open +from tableaudocumentapi.xfile import xml_open ######## # This is needed in order to determine if something is a string or not. It is necessary because diff --git a/tableaudocumentapi/workbook.py b/tableaudocumentapi/workbook.py index e885aa6..db4ee7d 100644 --- a/tableaudocumentapi/workbook.py +++ b/tableaudocumentapi/workbook.py @@ -10,7 +10,7 @@ import xml.etree.ElementTree as ET from tableaudocumentapi import Datasource, xfile -from tableaudocumentapi.xml_open import xml_open +from tableaudocumentapi.xfile import xml_open class Workbook(object): diff --git a/tableaudocumentapi/xfile.py b/tableaudocumentapi/xfile.py index 13e08c7..a8b3c91 100644 --- a/tableaudocumentapi/xfile.py +++ b/tableaudocumentapi/xfile.py @@ -3,9 +3,32 @@ 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')) + print(file_version, MIN_SUPPORTED_VERSION) + if file_version < MIN_SUPPORTED_VERSION: + raise TableauVersionNotSupportedException(file_version) + return tree + @contextlib.contextmanager def temporary_directory(*args, **kwargs): @@ -16,10 +39,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 +59,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/tableaudocumentapi/xml_open.py b/tableaudocumentapi/xml_open.py deleted file mode 100644 index e1afce6..0000000 --- a/tableaudocumentapi/xml_open.py +++ /dev/null @@ -1,28 +0,0 @@ -try: - from distutils2.version import NormalizedVersion as Version -except ImportError: - from distutils.version import LooseVersion as Version - -import xml.etree.ElementTree as ET -import zipfile - -from tableaudocumentapi import xfile - -MIN_SUPPORTED_VERSION = Version("9.0") - - -class VersionNotSupportedException(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 = xfile.get_xml_from_archive(filename) - else: - tree = ET.parse(filename) - file_version = Version(tree.getroot().attrib.get('version', '0.0')) - print(file_version, MIN_SUPPORTED_VERSION) - if file_version < MIN_SUPPORTED_VERSION: - raise VersionNotSupportedException(file_version) - return tree From ab1235eae2d8e00dbd06ab72cf23e0e571590736 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Mon, 25 Jul 2016 08:49:08 -0700 Subject: [PATCH 3/4] Updating to ignore ephemeral fields --- tableaudocumentapi/workbook.py | 3 +- tableaudocumentapi/xfile.py | 1 - test/assets/ephemeral_field.twb | 222 ++++++++++++++++++++++++++++++++ test/test_workbook.py | 19 +++ 4 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 test/assets/ephemeral_field.twb create mode 100644 test/test_workbook.py diff --git a/tableaudocumentapi/workbook.py b/tableaudocumentapi/workbook.py index db4ee7d..28ddd03 100644 --- a/tableaudocumentapi/workbook.py +++ b/tableaudocumentapi/workbook.py @@ -141,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 a8b3c91..a0cd62e 100644 --- a/tableaudocumentapi/xfile.py +++ b/tableaudocumentapi/xfile.py @@ -24,7 +24,6 @@ def xml_open(filename): else: tree = ET.parse(filename) file_version = Version(tree.getroot().attrib.get('version', '0.0')) - print(file_version, MIN_SUPPORTED_VERSION) if file_version < MIN_SUPPORTED_VERSION: raise TableauVersionNotSupportedException(file_version) return tree diff --git a/test/assets/ephemeral_field.twb b/test/assets/ephemeral_field.twb new file mode 100644 index 0000000..14ac5e3 --- /dev/null +++ b/test/assets/ephemeral_field.twb @@ -0,0 +1,222 @@ + + + + + + + + + + + + + + + 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" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +