From fb36a3adc5689c4741dffa864720222a321b361d Mon Sep 17 00:00:00 2001 From: Tyler Doyle Date: Sun, 11 Dec 2016 18:46:28 -0800 Subject: [PATCH 1/6] Fix #117 by only attempting files with the right extension inside the archive (#118) --- tableaudocumentapi/xfile.py | 16 ++++++++++++---- test/assets/Cache.twbx | Bin 0 -> 36196 bytes test/test_xfile.py | 9 +++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 test/assets/Cache.twbx diff --git a/tableaudocumentapi/xfile.py b/tableaudocumentapi/xfile.py index 3067781..cac6c09 100644 --- a/tableaudocumentapi/xfile.py +++ b/tableaudocumentapi/xfile.py @@ -52,7 +52,15 @@ def temporary_directory(*args, **kwargs): def find_file_in_zip(zip_file): - for filename in zip_file.namelist(): + '''Returns the twb/tds file from a Tableau packaged file format. Packaged + files can contain cache entries which are also valid XML, so only look for + files with a .tds or .twb extension. + ''' + + candidate_files = filter(lambda x: x.split('.')[-1] in ('twb', 'tds'), + zip_file.namelist()) + + for filename in candidate_files: with zip_file.open(filename) as xml_candidate: try: ET.parse(xml_candidate) @@ -81,10 +89,10 @@ def build_archive_file(archive_contents, zip_file): def save_into_archive(xml_tree, filename, new_filename=None): - # Saving a archive means extracting the contents into a temp folder, + # Saving an archive means extracting the contents into a temp folder, # saving the changes over the twb/tds in that folder, and then - # packaging it back up into a specifically formatted zip with the correct - # relative file paths + # packaging it back up into a zip with a very specific format + # e.g. no empty files for directories, which Windows and Mac do by default if new_filename is None: new_filename = filename diff --git a/test/assets/Cache.twbx b/test/assets/Cache.twbx new file mode 100644 index 0000000000000000000000000000000000000000..b1df6b7bb6607ccbced8233e7c60aae281431d6c GIT binary patch literal 36196 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 literal 0 HcmV?d00001 diff --git a/test/test_xfile.py b/test/test_xfile.py index 6cbe67f..259c98b 100644 --- a/test/test_xfile.py +++ b/test/test_xfile.py @@ -12,8 +12,17 @@ 'BadZip.zip' ) +TWBX_WITH_CACHE_FILES = os.path.join( + TEST_ASSET_DIR, + 'Cache.twbx' +) + class XFileEdgeTests(unittest.TestCase): def test_find_file_in_zip_no_xml_file(self): badzip = zipfile.ZipFile(BAD_ZIP_FILE) self.assertIsNone(find_file_in_zip(badzip)) + + def test_only_find_twbs(self): + twb_from_twbx_with_cache = zipfile.ZipFile(TWBX_WITH_CACHE_FILES) + self.assertEqual(find_file_in_zip(twb_from_twbx_with_cache), 'Superstore.twb') From fe16c0cba47806fb41f5240276597850de7576f4 Mon Sep 17 00:00:00 2001 From: Tyler Doyle Date: Tue, 13 Dec 2016 16:38:22 -0800 Subject: [PATCH 2/6] Commenting and Docstring cleanup. A few very small code cleanups (#120) Add docstrings and remove clutter. I also made some very tiny tweaks to some code for clarity. --- tableaudocumentapi/connection.py | 65 +++++++++++++++----------------- tableaudocumentapi/datasource.py | 47 +++++++++-------------- tableaudocumentapi/workbook.py | 36 +++--------------- tableaudocumentapi/xfile.py | 10 ++++- 4 files changed, 61 insertions(+), 97 deletions(-) diff --git a/tableaudocumentapi/connection.py b/tableaudocumentapi/connection.py index f71daa8..7bdc1a9 100644 --- a/tableaudocumentapi/connection.py +++ b/tableaudocumentapi/connection.py @@ -1,27 +1,14 @@ -############################################################################### -# -# Connection - A class for writing connections to Tableau files -# -############################################################################### import xml.etree.ElementTree as ET from tableaudocumentapi.dbclass import is_valid_dbclass class Connection(object): - """ - A class for writing connections to Tableau files. - - """ - - ########################################################################### - # - # Public API. - # - ########################################################################### + """A class representing connections inside Data Sources.""" def __init__(self, connxml): - """ - Constructor. + """Connection is usually instantiated by passing in connection elements + in a Data Source. If creating a connection from scratch you can call + `from_attributes` passing in the connection attributes. """ self._connectionXML = connxml @@ -37,6 +24,9 @@ def __repr__(self): @classmethod def from_attributes(cls, server, dbname, username, dbclass, port=None, authentication=''): + """Creates a new connection that can be added into a Data Source. + defaults to `''` which will be treated as 'prompt' by Tableau.""" + root = ET.Element('connection', authentication=authentication) xml = cls(root) xml.server = server @@ -47,11 +37,9 @@ def from_attributes(cls, server, dbname, username, dbclass, port=None, authentic return xml - ########### - # dbname - ########### @property def dbname(self): + """Database name for the connection. Not the table name.""" return self._dbname @dbname.setter @@ -69,11 +57,9 @@ def dbname(self, value): self._dbname = value self._connectionXML.set('dbname', value) - ########### - # server - ########### @property def server(self): + """Hostname or IP address of the database server. May also be a URL in some connection types.""" return self._server @server.setter @@ -91,11 +77,9 @@ def server(self, value): self._server = value self._connectionXML.set('server', value) - ########### - # username - ########### @property def username(self): + """Username used to authenticate to the database.""" return self._username @username.setter @@ -113,22 +97,26 @@ def username(self, value): self._username = value self._connectionXML.set('username', value) - ########### - # authentication - ########### @property def authentication(self): return self._authentication - ########### - # dbclass - ########### @property def dbclass(self): + """The type of connection (e.g. 'MySQL', 'Postgresql'). A complete list + can be found in dbclass.py""" return self._class @dbclass.setter def dbclass(self, value): + """Set the connection's dbclass property. + + Args: + value: New dbclass value. String. + + Returns: + Nothing. + """ if not is_valid_dbclass(value): raise AttributeError("'{}' is not a valid database type".format(value)) @@ -136,15 +124,22 @@ def dbclass(self, value): self._class = value self._connectionXML.set('class', value) - ########### - # port - ########### @property def port(self): + """Port used to connect to the database.""" return self._port @port.setter def port(self, value): + """Set the connection's port property. + + Args: + value: New port value. String. + + Returns: + Nothing. + """ + self._port = value # If port is None we remove the element and don't write it to XML if value is None: diff --git a/tableaudocumentapi/datasource.py b/tableaudocumentapi/datasource.py index a34cba5..418dc53 100644 --- a/tableaudocumentapi/datasource.py +++ b/tableaudocumentapi/datasource.py @@ -1,8 +1,3 @@ -############################################################################### -# -# Datasource - A class for writing datasources to Tableau files -# -############################################################################### import collections import itertools import xml.etree.ElementTree as ET @@ -16,7 +11,7 @@ ######## # This is needed in order to determine if something is a string or not. It is necessary because -# of differences between python2 (basestring) and python3 (str). If python2 support is every +# of differences between python2 (basestring) and python3 (str). If python2 support is ever # dropped, remove this and change the basestring references below to str try: basestring @@ -35,7 +30,7 @@ def _get_metadata_xml_for_field(root_xml, field_name): def _is_used_by_worksheet(names, field): - return any((y for y in names if y in field.worksheets)) + return any(y for y in names if y in field.worksheets) class FieldDictionary(MultiLookupDict): @@ -87,13 +82,14 @@ def base36encode(number): return sign + base36 -def make_unique_name(dbclass): +def _make_unique_name(dbclass): rand_part = base36encode(uuid4().int) name = dbclass + '.' + rand_part return name class ConnectionParser(object): + """Parser for detecting and extracting connections from differing Tableau file formats.""" def __init__(self, datasource_xml, version): self._dsxml = datasource_xml @@ -101,6 +97,8 @@ def __init__(self, datasource_xml, version): def _extract_federated_connections(self): connections = list(map(Connection, self._dsxml.findall('.//named-connections/named-connection/*'))) + # 'sqlproxy' connections (Tableau Server Connections) are not embedded into named-connection elements + # extract them manually for now connections.extend(map(Connection, self._dsxml.findall("./connection[@class='sqlproxy']"))) return connections @@ -108,6 +106,8 @@ def _extract_legacy_connection(self): return list(map(Connection, self._dsxml.findall('connection'))) def get_connections(self): + """Find and return all connections based on file format version.""" + if float(self._dsversion) < 10: connections = self._extract_legacy_connection() else: @@ -116,16 +116,11 @@ def get_connections(self): class Datasource(object): - """ - A class for writing datasources to Tableau files. + """A class representing Tableau Data Sources, embedded in workbook files or + in TDS files. """ - ########################################################################### - # - # Public API. - # - ########################################################################### def __init__(self, dsxml, filename=None): """ Constructor. Default is to create datasource from xml. @@ -145,13 +140,15 @@ def __init__(self, dsxml, filename=None): @classmethod def from_file(cls, filename): - """Initialize datasource from file (.tds)""" + """Initialize datasource from file (.tds ot .tdsx)""" - dsxml = xml_open(filename, cls.__name__.lower()).getroot() + dsxml = xml_open(filename, 'datasource').getroot() return cls(dsxml, filename) @classmethod def from_connections(cls, caption, connections): + """Create a new Data Source give a list of Connections.""" + root = ET.Element('datasource', caption=caption, version='10.0', inline='true') outer_connection = ET.SubElement(root, 'connection') outer_connection.set('class', 'federated') @@ -159,7 +156,7 @@ def from_connections(cls, caption, connections): for conn in connections: nc = ET.SubElement(named_conns, 'named-connection', - name=make_unique_name(conn.dbclass), + name=_make_unique_name(conn.dbclass), caption=conn.server) nc.append(conn._connectionXML) return cls(root) @@ -194,16 +191,10 @@ def save_as(self, new_filename): xfile._save_file(self._filename, self._datasourceTree, new_filename) - ########### - # name - ########### @property def name(self): return self._name - ########### - # version - ########### @property def version(self): return self._version @@ -222,9 +213,6 @@ def caption(self): del self._datasourceXML.attrib['caption'] self._caption = '' - ########### - # connections - ########### @property def connections(self): return self._connections @@ -234,9 +222,6 @@ def clear_repository_location(self): if tag is not None: self._datasourceXML.remove(tag) - ########### - # fields - ########### @property def fields(self): if not self._fields: @@ -244,6 +229,8 @@ def fields(self): return self._fields def _get_all_fields(self): + # Some columns are represented by `column` tags and others as `metadata-record` tags + # Find them all and chain them into one dictionary column_field_objects = self._get_column_objects() existing_column_fields = [x.id for x in column_field_objects] metadata_only_field_objects = (x for x in self._get_metadata_objects() if x.id not in existing_column_fields) diff --git a/tableaudocumentapi/workbook.py b/tableaudocumentapi/workbook.py index 4b4ce59..70b280c 100644 --- a/tableaudocumentapi/workbook.py +++ b/tableaudocumentapi/workbook.py @@ -1,8 +1,3 @@ -############################################################################### -# -# Workbook - A class for writing Tableau workbook files -# -############################################################################### import weakref @@ -11,25 +6,18 @@ class Workbook(object): - """ - A class for writing Tableau workbook files. + """A class for writing Tableau workbook files.""" - """ - - ########################################################################### - # - # Public API. - # - ########################################################################### def __init__(self, filename): - """ - Constructor. + """Open the workbook at `filename`. This will handle packaged and unpacked + workbook files automatically. This will also parse Data Sources and Worksheets + for access. """ self._filename = filename - self._workbookTree = xml_open(self._filename, self.__class__.__name__.lower()) + self._workbookTree = xml_open(self._filename, 'workbook') self._workbookRoot = self._workbookTree.getroot() # prepare our datasource objects @@ -42,23 +30,14 @@ def __init__(self, filename): self._workbookRoot, self._datasource_index ) - ########### - # datasources - ########### @property def datasources(self): return self._datasources - ########### - # worksheets - ########### @property def worksheets(self): return self._worksheets - ########### - # filename - ########### @property def filename(self): return self._filename @@ -92,11 +71,6 @@ def save_as(self, new_filename): xfile._save_file( self._filename, self._workbookTree, new_filename) - ########################################################################### - # - # Private API. - # - ########################################################################### @staticmethod def _prepare_datasource_index(datasources): retval = weakref.WeakValueDictionary() diff --git a/tableaudocumentapi/xfile.py b/tableaudocumentapi/xfile.py index cac6c09..8e213ab 100644 --- a/tableaudocumentapi/xfile.py +++ b/tableaudocumentapi/xfile.py @@ -22,19 +22,23 @@ class TableauInvalidFileException(Exception): def xml_open(filename, expected_root=None): + """Opens the provided 'filename'. Handles detecting if the file is an archive, + detecting the document version, and validating the root tag.""" + # Is the file a zip (.twbx or .tdsx) if zipfile.is_zipfile(filename): tree = get_xml_from_archive(filename) else: tree = ET.parse(filename) + # Is the file a supported version tree_root = tree.getroot() - file_version = Version(tree_root.attrib.get('version', '0.0')) if file_version < MIN_SUPPORTED_VERSION: raise TableauVersionNotSupportedException(file_version) + # Does the root tag match the object type (workbook or data source) if expected_root and (expected_root != tree_root.tag): raise TableauInvalidFileException( "'{}'' is not a valid '{}' file".format(filename, expected_root)) @@ -79,6 +83,10 @@ def get_xml_from_archive(filename): def build_archive_file(archive_contents, zip_file): + """Build a Tableau-compatible archive file.""" + + # This is tested against Desktop and Server, and reverse engineered by lots + # of trial and error. Do not change this logic. for root_dir, _, files in os.walk(archive_contents): relative_dir = os.path.relpath(root_dir, archive_contents) for f in files: From cdccd2f06cb7e130af8a39ce39a9fdee24868ed8 Mon Sep 17 00:00:00 2001 From: Tyler Doyle Date: Wed, 14 Dec 2016 18:10:05 -0800 Subject: [PATCH 3/6] Small cleanups for various editors. Play nice with built in test-runners (#121) --- .gitignore | 6 ++++++ test/__init__.py | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 0b50f47..581cedb 100644 --- a/.gitignore +++ b/.gitignore @@ -65,5 +65,11 @@ target/ .DS_Store .idea +#Editor things +*.sublime-project +*.sublime-workspace +settings.json +tasks.json + #Jekyll docs/_site diff --git a/test/__init__.py b/test/__init__.py index c715da8..e69de29 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,2 +0,0 @@ -from . import bvt -from . import test_datasource From 3ab99f4a2a0c457b7e16c14c075798514be3b1d3 Mon Sep 17 00:00:00 2001 From: Tyler Doyle Date: Thu, 5 Jan 2017 22:03:33 -0800 Subject: [PATCH 4/6] Add Py36, update travis to use pycodestyle (#124) --- .travis.yml | 7 ++++--- test/bvt.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 32f39d0..454322d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,17 +6,18 @@ python: - "3.3" - "3.4" - "3.5" + - "3.6" - "pypy" # command to install dependencies install: - "pip install -e ." - - "pip install pep8" + - "pip install pycodestyle" # command to run tests script: # Tests - python setup.py test - # pep8 - - pep8 . + # pycodestyle + - pycodestyle tableaudocumentapi test samples # Examples - (cd "samples/replicate-workbook" && python replicate_workbook.py) - (cd "samples/list-tds-info" && python list_tds_info.py) diff --git a/test/bvt.py b/test/bvt.py index e09ec55..8698f7f 100644 --- a/test/bvt.py +++ b/test/bvt.py @@ -393,5 +393,6 @@ def test_82_workbook_throws_exception(self): with self.assertRaises(TableauVersionNotSupportedException): wb = Workbook(TABLEAU_82_TWB) + if __name__ == '__main__': unittest.main() From cc97fc45429031372a082c10d2a6ccf561c55558 Mon Sep 17 00:00:00 2001 From: r-richmond Date: Mon, 9 Jan 2017 12:03:51 -0800 Subject: [PATCH 5/6] Add `initial sql` and `query band` support (#123) Addresses #109 and #110 --- docs/docs/api-ref.md | 24 +++++++++++++ tableaudocumentapi/connection.py | 59 +++++++++++++++++++++++++++++++- test/assets/CONNECTION.xml | 2 +- test/bvt.py | 18 ++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/docs/docs/api-ref.md b/docs/docs/api-ref.md index 3819b70..cf6bc96 100644 --- a/docs/docs/api-ref.md +++ b/docs/docs/api-ref.md @@ -48,6 +48,30 @@ class Datasource(dsxml, filename=None) class Connection(connxml) ``` +The Connection class represents a tableau data connection. It can be from any type of connection found in `dbclass.py` via `is_valid_dbclass` + +**Params:** + +**Raises:** + +**Methods:** + +**Properities:** + +`self.server:` Returns a string containing the server. + +`self.dbname:` Returns a string containing the database name. + +`self.username:` Returns a string containing the username. + +`self.dbclass:` Returns a string containing the database class. + +`self.port:` Returns a string containing the port. + +`self.query_band:` Returns a string containing the query band. + +`self.initial_sql:` Returns a string containing the initial sql. + ## Fields ```python class Workbook(column_xml=None, metadata_xml=None) diff --git a/tableaudocumentapi/connection.py b/tableaudocumentapi/connection.py index 7bdc1a9..30343b5 100644 --- a/tableaudocumentapi/connection.py +++ b/tableaudocumentapi/connection.py @@ -18,12 +18,15 @@ def __init__(self, connxml): self._authentication = connxml.get('authentication') self._class = connxml.get('class') self._port = connxml.get('port', None) + self._query_band = connxml.get('query-band-spec', None) + self._initial_sql = connxml.get('one-time-sql', None) def __repr__(self): return "''".format(self._server, self._dbname, hex(id(self))) @classmethod - def from_attributes(cls, server, dbname, username, dbclass, port=None, authentication=''): + def from_attributes(cls, server, dbname, username, dbclass, port=None, query_band=None, + initial_sql=None, authentication=''): """Creates a new connection that can be added into a Data Source. defaults to `''` which will be treated as 'prompt' by Tableau.""" @@ -34,6 +37,8 @@ def from_attributes(cls, server, dbname, username, dbclass, port=None, authentic xml.username = username xml.dbclass = dbclass xml.port = port + xml.query_band = query_band + xml.initial_sql = initial_sql return xml @@ -149,3 +154,55 @@ def port(self, value): pass else: self._connectionXML.set('port', value) + + @property + def query_band(self): + """Query band passed on connection to database.""" + return self._query_band + + @query_band.setter + def query_band(self, value): + """Set the connection's query_band property. + + Args: + value: New query_band value. String. + + Returns: + Nothing. + """ + + self._query_band = value + # If query band is None we remove the element and don't write it to XML + if value is None: + try: + del self._connectionXML.attrib['query-band-spec'] + except KeyError: + pass + else: + self._connectionXML.set('query-band-spec', value) + + @property + def initial_sql(self): + """Initial SQL to be run.""" + return self._initial_sql + + @initial_sql.setter + def initial_sql(self, value): + """Set the connection's initial_sql property. + + Args: + value: New initial_sql value. String. + + Returns: + Nothing. + """ + + self._initial_sql = value + # If initial_sql is None we remove the element and don't write it to XML + if value is None: + try: + del self._connectionXML.attrib['one-time-sql'] + except KeyError: + pass + else: + self._connectionXML.set('one-time-sql', value) diff --git a/test/assets/CONNECTION.xml b/test/assets/CONNECTION.xml index beb606f..56d17d5 100644 --- a/test/assets/CONNECTION.xml +++ b/test/assets/CONNECTION.xml @@ -1 +1 @@ - + diff --git a/test/bvt.py b/test/bvt.py index 8698f7f..b2fb4af 100644 --- a/test/bvt.py +++ b/test/bvt.py @@ -60,6 +60,8 @@ def test_can_read_attributes_from_connection(self): self.assertEqual(conn.dbclass, 'sqlserver') self.assertEqual(conn.authentication, 'sspi') self.assertEqual(conn.port, '1433') + self.assertEqual(conn.initial_sql, '') + self.assertEqual(conn.query_band, '') def test_can_write_attributes_to_connection(self): conn = Connection(self.connection) @@ -67,10 +69,14 @@ def test_can_write_attributes_to_connection(self): conn.server = 'mssql2014' conn.username = 'bob' conn.port = '1337' + conn.initial_sql = "insert values (1, 'winning') into schema.table" + conn.query_band = 'TableauReport=' self.assertEqual(conn.dbname, 'BubblesInMyDrink') self.assertEqual(conn.username, 'bob') self.assertEqual(conn.server, 'mssql2014') self.assertEqual(conn.port, '1337') + self.assertEqual(conn.initial_sql, "insert values (1, 'winning') into schema.table") + self.assertEqual(conn.query_band, 'TableauReport=') def test_can_delete_port_from_connection(self): conn = Connection(self.connection) @@ -78,6 +84,18 @@ def test_can_delete_port_from_connection(self): self.assertEqual(conn.port, None) self.assertIsNone(conn._connectionXML.get('port')) + def test_can_delete_initial_sql_from_connection(self): + conn = Connection(self.connection) + conn.initial_sql = None + self.assertEqual(conn.initial_sql, None) + self.assertIsNone(conn._connectionXML.get('initial_sql')) + + def test_can_delete_query_band_from_connection(self): + conn = Connection(self.connection) + conn.query_band = None + self.assertEqual(conn.query_band, None) + self.assertIsNone(conn._connectionXML.get('query_band')) + def test_bad_dbclass_rasies_attribute_error(self): conn = Connection(self.connection) conn.dbclass = 'sqlserver' From 3d8021d8a68987242aa1df7d14051eeacc5fa882 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Wed, 11 Jan 2017 15:41:45 -0800 Subject: [PATCH 6/6] Prep for release of 0.6 (#125) * Prep for release of 0.6 * wordsmithing the changelog --- CHANGELOG.md | 8 ++++++++ CONTRIBUTORS.md | 1 + setup.py | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49783e1..3c5976a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 06 (11 January 2017) + +* Initial SQL and query banding support (#123) +* Fixed bug in xfiles to allow opening workbooks with external file caches (#117, #118) +* Code Cleanup (#120, #121) +* Added Py36 support (#124) +* Switched to pycodestyle from pip8 on travis runs (#124) + ## 05 (01 November 2016) * Added ability to set the port for connections (#97) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d79e152..903e57b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -6,6 +6,7 @@ The following people have contributed to this project to make it possible, and w * [Charley Peng](https://github.com/chid) * [Miguel Sánchez](https://github.com/MiguelSR) +* [Ryan Richmond](https://github.com/r-richmond) ## Core Team diff --git a/setup.py b/setup.py index 82e9517..59270ed 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='tableaudocumentapi', - version='0.5', + version='0.6', author='Tableau', author_email='github@tableau.com', url='https://github.com/tableau/document-api-python', 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