Skip to content

Add a version check to provide good errors on version incompatibility #59

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 26, 2016
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 2 additions & 5 deletions tableaudocumentapi/datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
11 changes: 4 additions & 7 deletions tableaudocumentapi/workbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import xml.etree.ElementTree as ET

from tableaudocumentapi import Datasource, xfile
from tableaudocumentapi.xfile import xml_open


class Workbook(object):
Expand All @@ -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
Expand Down Expand Up @@ -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
34 changes: 28 additions & 6 deletions tableaudocumentapi/xfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change is because zip is a built-in so we should not use that as a name of the argument.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's coo' w. me

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
Expand All @@ -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):
Expand Down
189 changes: 189 additions & 0 deletions test/assets/ephemeral_field.twb
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?xml version='1.0' encoding='utf-8' ?>

<!-- build 9300.16.0603.2240 -->
<workbook source-build='9.3.3 (9300.16.0603.2240)' source-platform='mac' version='9.3' xmlns:user='http://www.tableausoftware.com/xml/user'>
<preferences>
<preference name='ui.encoding.shelf.height' value='24' />
<preference name='ui.shelf.height' value='26' />
</preferences>
<datasources>
<datasource inline='true' name='datasource_test' version='9.3'>
<connection authentication='username-password' class='postgres' dbname='TestV1' odbc-native-protocol='yes' port='5432' server='postgres91.test.tsi.lan' username='test'>
<relation name='xy' table='[public].[xy]' type='table' />
<metadata-records>
<metadata-record class='column'>
<remote-name>a</remote-name>
<remote-type>130</remote-type>
<local-name>[a]</local-name>
<parent-name>[xy]</parent-name>
<remote-alias>a</remote-alias>
<ordinal>1</ordinal>
<local-type>string</local-type>
<aggregation>Count</aggregation>
<width>255</width>
<contains-null>true</contains-null>
<attributes>
<attribute datatype='string' name='DebugRemoteType'>&quot;SQL_WVARCHAR&quot;</attribute>
<attribute datatype='string' name='DebugWireType'>&quot;SQL_C_WCHAR&quot;</attribute>
<attribute datatype='string' name='TypeIsVarchar'>&quot;true&quot;</attribute>
</attributes>
</metadata-record>
<metadata-record class='column'>
<remote-name>x</remote-name>
<remote-type>3</remote-type>
<local-name>[x]</local-name>
<parent-name>[xy]</parent-name>
<remote-alias>x</remote-alias>
<ordinal>2</ordinal>
<local-type>integer</local-type>
<aggregation>Sum</aggregation>
<precision>10</precision>
<contains-null>true</contains-null>
<attributes>
<attribute datatype='string' name='DebugRemoteType'>&quot;SQL_INTEGER&quot;</attribute>
<attribute datatype='string' name='DebugWireType'>&quot;SQL_C_SLONG&quot;</attribute>
</attributes>
</metadata-record>
<metadata-record class='column'>
<remote-name>y</remote-name>
<remote-type>3</remote-type>
<local-name>[y]</local-name>
<parent-name>[xy]</parent-name>
<remote-alias>y</remote-alias>
<ordinal>3</ordinal>
<local-type>integer</local-type>
<aggregation>Sum</aggregation>
<precision>10</precision>
<contains-null>true</contains-null>
<attributes>
<attribute datatype='string' name='DebugRemoteType'>&quot;SQL_INTEGER&quot;</attribute>
<attribute datatype='string' name='DebugWireType'>&quot;SQL_C_SLONG&quot;</attribute>
</attributes>
</metadata-record>
</metadata-records>
</connection>
<aliases enabled='yes' />
<column datatype='integer' name='[Number of Records]' role='measure' type='quantitative' user:auto-column='numrec'>
<calculation class='tableau' formula='1' />
</column>
<column caption='A' datatype='string' name='[a]' role='dimension' type='nominal' />
<column caption='X' datatype='integer' name='[x]' role='measure' type='quantitative' />
<column caption='Y' datatype='integer' name='[y]' role='measure' type='quantitative' />
<layout dim-ordering='alphabetic' dim-percentage='0.48' measure-ordering='alphabetic' measure-percentage='0.52' show-structure='true' />
<semantic-values>
<semantic-value key='[Country].[Name]' value='&quot;United States&quot;' />
</semantic-values>
</datasource>
</datasources>
<worksheets>
<worksheet name='Sheet 1'>
<table>
<view>
<datasources>
<datasource name='datasource_test' />
</datasources>
<datasource-dependencies datasource='datasource_test'>
<column caption='A' datatype='string' name='[a]' role='dimension' type='nominal' />
<column-instance column='[a]' derivation='None' name='[none:a:nk]' pivot='key' type='nominal' />
</datasource-dependencies>
<aggregation value='true' />
</view>
<style />
<panes>
<pane>
<view>
<breakdown value='auto' />
</view>
<mark class='Automatic' />
</pane>
</panes>
<rows>[datasource_test].[none:a:nk]</rows>
<cols />
</table>
</worksheet>
<worksheet name='Sheet 2'>
<table>
<view>
<datasources>
<datasource name='datasource_test' />
</datasources>
<datasource-dependencies datasource='datasource_test'>
<column caption='SUM(X) + SUM(Y)' datatype='integer' name='[Calculation_3002775091869110273]' role='measure' type='quantitative' user:unnamed='Sheet 2'>
<calculation class='tableau' formula='SUM([x]) + SUM([y])' />
</column>
<column caption='A' datatype='string' name='[a]' role='dimension' type='nominal' />
<column-instance column='[a]' derivation='None' name='[none:a:nk]' pivot='key' type='nominal' />
<column-instance column='[x]' derivation='Sum' name='[sum:x:qk]' pivot='key' type='quantitative' />
<column-instance column='[Calculation_3002775091869110273]' derivation='User' name='[usr:Calculation_3002775091869110273:qk]' pivot='key' type='quantitative' />
<column caption='X' datatype='integer' name='[x]' role='measure' type='quantitative' />
<column caption='Y' datatype='integer' name='[y]' role='measure' type='quantitative' />
</datasource-dependencies>
<aggregation value='true' />
</view>
<style />
<panes>
<pane>
<view>
<breakdown value='auto' />
</view>
<mark class='Automatic' />
</pane>
<pane id='1' x-axis-name='[datasource_test].[sum:x:qk]'>
<view>
<breakdown value='auto' />
</view>
<mark class='Automatic' />
</pane>
<pane id='2' x-axis-name='[datasource_test].[usr:Calculation_3002775091869110273:qk]'>
<view>
<breakdown value='auto' />
</view>
<mark class='Automatic' />
</pane>
</panes>
<rows>[datasource_test].[none:a:nk]</rows>
<cols>([datasource_test].[sum:x:qk] + [datasource_test].[usr:Calculation_3002775091869110273:qk])</cols>
</table>
</worksheet>
</worksheets>
<windows source-height='28'>
<window class='worksheet' name='Sheet 1'>
<cards>
<edge name='left'>
<strip size='160'>
<card type='pages' />
<card type='filters' />
<card type='marks' />
</strip>
</edge>
<edge name='top'>
<strip size='2147483647'>
<card type='columns' />
</strip>
<strip size='2147483647'>
<card type='rows' />
</strip>
</edge>
</cards>
</window>
<window class='worksheet' maximized='true' name='Sheet 2'>
<cards>
<edge name='left'>
<strip size='160'>
<card type='pages' />
<card type='filters' />
<card type='marks' />
</strip>
</edge>
<edge name='top'>
<strip size='2147483647'>
<card type='columns' />
</strip>
<strip size='2147483647'>
<card type='rows' />
</strip>
</edge>
</cards>
</window>
</windows>
</workbook>
19 changes: 19 additions & 0 deletions test/test_workbook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import unittest
import os.path

from tableaudocumentapi import Datasource, Workbook

TEST_ASSET_DIR = os.path.join(
os.path.dirname(__file__),
'assets'
)
EPHEMERAL_FIELD_FILE = os.path.join(
TEST_ASSET_DIR,
'ephemeral_field.twb'
)


class EphemeralFields(unittest.TestCase):
def test_ephemeral_fields_do_not_cause_errors(self):
wb = Workbook(EPHEMERAL_FIELD_FILE)
self.assertIsNotNone(wb)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy