From 2c605e58e6502414f258f5eda7749b58513f2a16 Mon Sep 17 00:00:00 2001 From: T8y8 Date: Tue, 10 May 2016 16:17:57 -0700 Subject: [PATCH 1/7] PEP8-ify some files so future editors will be happy. Also add a test file and add some test cases for a help method I plan to update in the next commit. I made _is_valid_file a static method since it's just a helper and it makes it easier to test --- Document API/tableaudocumentapi/connection.py | 22 +++++++------- Document API/tableaudocumentapi/datasource.py | 7 +++-- Document API/tableaudocumentapi/workbook.py | 29 ++++++++++--------- Document API/test.py | 16 ++++++++++ 4 files changed, 47 insertions(+), 27 deletions(-) create mode 100644 Document API/test.py diff --git a/Document API/tableaudocumentapi/connection.py b/Document API/tableaudocumentapi/connection.py index 4f703ee..10096dd 100644 --- a/Document API/tableaudocumentapi/connection.py +++ b/Document API/tableaudocumentapi/connection.py @@ -3,6 +3,8 @@ # Connection - A class for writing connections to Tableau files # ############################################################################### + + class Connection(object): """ A class for writing connections to Tableau files. @@ -36,13 +38,13 @@ def dbname(self): def dbname(self, value): """ Set the connection's database name property. - + Args: value: New name of the database. String. - + Returns: Nothing. - + """ self._dbname = value self._connectionXML.set('dbname', value) @@ -58,17 +60,17 @@ def server(self): def server(self, value): """ Set the connection's server property. - + Args: value: New server. String. - + Returns: Nothing. - + """ self._server = value self._connectionXML.set('server', value) - + ########### # username ########### @@ -80,13 +82,13 @@ def username(self): def username(self, value): """ Set the connection's username property. - + Args: value: New username value. String. - + Returns: Nothing. - + """ self._username = value self._connectionXML.set('username', value) diff --git a/Document API/tableaudocumentapi/datasource.py b/Document API/tableaudocumentapi/datasource.py index 60dc2c0..cb17412 100644 --- a/Document API/tableaudocumentapi/datasource.py +++ b/Document API/tableaudocumentapi/datasource.py @@ -6,6 +6,7 @@ import xml.etree.ElementTree as ET from tableaudocumentapi import Connection + class Datasource(object): """ A class for writing datasources to Tableau files. @@ -26,20 +27,20 @@ def __init__(self, dsxml): self._name = self._datasourceXML.get('name') self._version = self._datasourceXML.get('version') self._connection = Connection(self._datasourceXML.find('connection')) - + @classmethod def from_file(cls, filename): "Initialize datasource from file (.tds)" dsxml = ET.parse(filename).getroot() return cls(dsxml) - + ########### # name ########### @property def name(self): return self._name - + ########### # version ########### diff --git a/Document API/tableaudocumentapi/workbook.py b/Document API/tableaudocumentapi/workbook.py index 55bd8ee..83ed17a 100644 --- a/Document API/tableaudocumentapi/workbook.py +++ b/Document API/tableaudocumentapi/workbook.py @@ -7,6 +7,7 @@ import xml.etree.ElementTree as ET from tableaudocumentapi import Datasource + class Workbook(object): """ A class for writing Tableau workbook files. @@ -29,13 +30,14 @@ def __init__(self, filename): 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') + 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)" @@ -46,14 +48,14 @@ def from_file(cls, filename): else: print('Invalid file type. Must be .twb or .tds.') raise Exception() - + ########### # datasources ########### @property def datasources(self): return self._datasources - + ########### # filename ########### @@ -72,10 +74,10 @@ def save(self): Nothing. """ - + # save the file self._workbookTree.write(self._filename) - + def save_as(self, value): """ Save our file with the name provided. @@ -87,7 +89,7 @@ def save_as(self, value): Nothing. """ - + # We have a valid type of input file if self._is_valid_file(value): # save the file @@ -103,21 +105,20 @@ def save_as(self, value): ########################################################################### def _prepare_datasources(self, xmlRoot): datasources = [] - + # loop through our datasources and append for datasource in xmlRoot.find('datasources'): ds = Datasource(datasource) datasources.append(ds) - + return datasources - - def _is_valid_file(self, filename): + + @staticmethod + def _is_valid_file(filename): valid = 0 fileExtension = os.path.splitext(filename)[-1].lower() - if fileExtension == ".twb": valid = 1 elif fileExtension == ".tds": valid = 1 - return valid diff --git a/Document API/test.py b/Document API/test.py new file mode 100644 index 0000000..cb1d044 --- /dev/null +++ b/Document API/test.py @@ -0,0 +1,16 @@ +import unittest + +from tableaudocumentapi import Workbook + +class HelperMethodTests(unittest.TestCase): + + def test_valid_file_with_valid_inputs(self): + self.assertTrue(Workbook._is_valid_file('file1.tds')) + self.assertTrue(Workbook._is_valid_file('file2.twb')) + + def test_valid_file_with_invalid_inputs(self): + self.assertFalse(Workbook._is_valid_file('file1.tds2')) + self.assertFalse(Workbook._is_valid_file('file2.twb3')) + +if __name__ == '__main__': + unittest.main() From 0f5f757909ba4456018d5f5c20e3f1441b939d34 Mon Sep 17 00:00:00 2001 From: T8y8 Date: Tue, 10 May 2016 16:19:34 -0700 Subject: [PATCH 2/7] Refactor _is_valid_file to be mroe pythonic and extensible if we add more filetypes in the future. No more C++ code in the python :) --- Document API/tableaudocumentapi/workbook.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Document API/tableaudocumentapi/workbook.py b/Document API/tableaudocumentapi/workbook.py index 83ed17a..e2f931f 100644 --- a/Document API/tableaudocumentapi/workbook.py +++ b/Document API/tableaudocumentapi/workbook.py @@ -115,10 +115,6 @@ def _prepare_datasources(self, xmlRoot): @staticmethod def _is_valid_file(filename): - valid = 0 fileExtension = os.path.splitext(filename)[-1].lower() - if fileExtension == ".twb": - valid = 1 - elif fileExtension == ".tds": - valid = 1 - return valid + return fileExtension in ('.twb', '.tds') + From c5e53ecad4793c0ab68ae5ebb4bbff97c54f2050 Mon Sep 17 00:00:00 2001 From: T8y8 Date: Tue, 10 May 2016 17:51:12 -0700 Subject: [PATCH 3/7] Added new test cases --- Document API/test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Document API/test.py b/Document API/test.py index cb1d044..b919815 100644 --- a/Document API/test.py +++ b/Document API/test.py @@ -4,11 +4,13 @@ class HelperMethodTests(unittest.TestCase): - def test_valid_file_with_valid_inputs(self): + def test_is_valid_file_with_valid_inputs(self): self.assertTrue(Workbook._is_valid_file('file1.tds')) self.assertTrue(Workbook._is_valid_file('file2.twb')) + self.assertTrue(Workbook._is_valid_file('tds.twb')) - def test_valid_file_with_invalid_inputs(self): + def test_is_valid_file_with_invalid_inputs(self): + self.assertFalse(Workbook._is_valid_file('')) self.assertFalse(Workbook._is_valid_file('file1.tds2')) self.assertFalse(Workbook._is_valid_file('file2.twb3')) From a36337fca1a23e617e27c52daa40e681043fbcba Mon Sep 17 00:00:00 2001 From: T8y8 Date: Tue, 10 May 2016 18:45:03 -0700 Subject: [PATCH 4/7] Update Replicate Workbook example to using DictReader which makes the code more readable. Also remove the unnecessary copy.copy call and import. Small refactor of parameter name for Workbook.save_as method to be more descriptive than 'value'. Manual testing on python2 and pyhon3 for example. Verified still works --- .../Replicate Workbook/replicateWorkbook.py | 15 ++++++--------- Document API/tableaudocumentapi/workbook.py | 8 ++++---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Document API/Examples/Replicate Workbook/replicateWorkbook.py b/Document API/Examples/Replicate Workbook/replicateWorkbook.py index 6e60e95..5fc5227 100644 --- a/Document API/Examples/Replicate Workbook/replicateWorkbook.py +++ b/Document API/Examples/Replicate Workbook/replicateWorkbook.py @@ -1,5 +1,4 @@ import csv # so we can work with our database list (in a CSV file) -import copy # to make copies ############################################################ # Step 1) Use Workbook object from the Document API @@ -16,13 +15,11 @@ # create new .twb's with their settings ############################################################ with open('databases.csv') as csvfile: - next(csvfile) # Skip the first line which is our CSV header row - databases = csv.reader(csvfile, delimiter=',', quotechar='"') + databases = csv.DictReader(csvfile, delimiter=',', quotechar='"') for row in databases: - newWB = copy.copy(sourceWB) - # Set our unique values for this database - newWB.datasources[0].connection.server = row[1] # Server - newWB.datasources[0].connection.dbname = row[2] # Database - newWB.datasources[0].connection.username = row[3] # User - newWB.save_as(row[0] + ' - Superstore' + '.twb') # Save our newly created .twb with the new file name + sourceWB.datasources[0].connection.server = row['Server'] + sourceWB.datasources[0].connection.dbname = row['Database'] + sourceWB.datasources[0].connection.username = row['User'] + # Save our newly created .twb with the new file name + sourceWB.save_as(row['DBFriendlyName'] + ' - Superstore' + '.twb') diff --git a/Document API/tableaudocumentapi/workbook.py b/Document API/tableaudocumentapi/workbook.py index e2f931f..e2e0c75 100644 --- a/Document API/tableaudocumentapi/workbook.py +++ b/Document API/tableaudocumentapi/workbook.py @@ -78,12 +78,12 @@ def save(self): # save the file self._workbookTree.write(self._filename) - def save_as(self, value): + def save_as(self, new_filename): """ Save our file with the name provided. Args: - value: New name for the workbook file. String. + new_filename: New name for the workbook file. String. Returns: Nothing. @@ -91,9 +91,9 @@ def save_as(self, value): """ # We have a valid type of input file - if self._is_valid_file(value): + if self._is_valid_file(new_filename): # save the file - self._workbookTree.write(value) + self._workbookTree.write(new_filename) else: print('Invalid file type. Must be .twb or .tds.') raise Exception() From d3cb1be279a52021d61eadf159f4744a791cb54f Mon Sep 17 00:00:00 2001 From: T8y8 Date: Tue, 10 May 2016 23:36:46 -0700 Subject: [PATCH 5/7] Added tests to cover basic cases for Workbook, Datasource, and Connection classes. The temp-file setUp isn't the greatest but it's functional for now and will help with confidance if we decide to move anything around. Already uncovered a bug with them, fixed in this commit -- TDS files don't seem to have a 'name' attribute but instead call it 'formatted-name' I added a simple OR into the _name logic of the Datasource class and that resolves the bug. --- Document API/tableaudocumentapi/datasource.py | 2 +- Document API/test.py | 74 ++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/Document API/tableaudocumentapi/datasource.py b/Document API/tableaudocumentapi/datasource.py index cb17412..28f735f 100644 --- a/Document API/tableaudocumentapi/datasource.py +++ b/Document API/tableaudocumentapi/datasource.py @@ -24,7 +24,7 @@ def __init__(self, dsxml): """ self._datasourceXML = dsxml - self._name = self._datasourceXML.get('name') + self._name = self._datasourceXML.get('name') or self._datasourceXML.get('formatted-name') # TDS files don't have a name attribute self._version = self._datasourceXML.get('version') self._connection = Connection(self._datasourceXML.find('connection')) diff --git a/Document API/test.py b/Document API/test.py index b919815..cf11b2d 100644 --- a/Document API/test.py +++ b/Document API/test.py @@ -1,6 +1,27 @@ import unittest +import tempfile -from tableaudocumentapi import Workbook +import xml.etree.ElementTree as ET + +from tableaudocumentapi import Workbook, Datasource, Connection + +TABLEAU_93_WORKBOOK = ''' + + + + + + + +''' + +TABLEAU_93_TDS = ''' + + + +''' + +TABLEAU_CONNECTION_XML = ET.fromstring('''''') class HelperMethodTests(unittest.TestCase): @@ -14,5 +35,56 @@ def test_is_valid_file_with_invalid_inputs(self): self.assertFalse(Workbook._is_valid_file('file1.tds2')) self.assertFalse(Workbook._is_valid_file('file2.twb3')) +class WorkbookModelTests(unittest.TestCase): + + def setUp(self): + self.workbook_file = tempfile.NamedTemporaryFile(suffix='.twb') + self.workbook_file.write(TABLEAU_93_WORKBOOK.encode('utf8')) + self.workbook_file.seek(0) + + def test_can_extract_datasource(self): + wb = Workbook(self.workbook_file.name) + self.assertEqual(len(wb.datasources), 1) + self.assertIsInstance(wb.datasources[0], Datasource) + self.assertEqual(wb.datasources[0].name, 'sqlserver.17u3bqc16tjtxn14e2hxh19tyvpo') + +class DatasourceModelTests(unittest.TestCase): + + def setUp(self): + self.tds_file = tempfile.NamedTemporaryFile(suffix='.tds') + self.tds_file.write(TABLEAU_93_TDS.encode('utf8')) + self.tds_file.seek(0) + + def test_can_extract_datasource_from_file(self): + ds = Datasource.from_file(self.tds_file.name) + self.assertEqual(ds.name, 'sqlserver.17u3bqc16tjtxn14e2hxh19tyvpo') + self.assertEqual(ds.version, '9.3') + + def test_can_extract_connection(self): + ds = Datasource.from_file(self.tds_file.name) + self.assertIsInstance(ds.connection, Connection) + self.assertEqual(ds.connection.dbname, 'TestV1') + self.assertEqual(ds.connection.server, 'mssql2012.test.tsi.lan') + self.assertEqual(ds.connection.username, '') + +class ConnectionModelTests(unittest.TestCase): + + def setUp(self): + self.connection = TABLEAU_CONNECTION_XML + + def test_can_read_attributes_from_connection(self): + conn = Connection(self.connection) + self.assertEqual(conn.dbname, 'TestV1') + self.assertEqual(conn.username, '') + self.assertEqual(conn.server, 'mssql2012.test.tsi.lan') + + def test_can_write_attributes_to_connection(self): + conn = Connection(self.connection) + conn.dbname = 'BubblesInMyDrink' + conn.server = 'mssql2014.test.tsi.lan' + self.assertEqual(conn.dbname, 'BubblesInMyDrink') + self.assertEqual(conn.username, '') + self.assertEqual(conn.server, 'mssql2014.test.tsi.lan') + if __name__ == '__main__': unittest.main() From 564c172d6fff0e31a8a76050e2804ddbd968ebe2 Mon Sep 17 00:00:00 2001 From: T8y8 Date: Wed, 11 May 2016 00:12:41 -0700 Subject: [PATCH 6/7] One more test --- Document API/test.py | 66 ++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/Document API/test.py b/Document API/test.py index cf11b2d..376b094 100644 --- a/Document API/test.py +++ b/Document API/test.py @@ -21,7 +21,9 @@ ''' -TABLEAU_CONNECTION_XML = ET.fromstring('''''') +TABLEAU_CONNECTION_XML = ET.fromstring( + '''''') + class HelperMethodTests(unittest.TestCase): @@ -35,18 +37,26 @@ def test_is_valid_file_with_invalid_inputs(self): self.assertFalse(Workbook._is_valid_file('file1.tds2')) self.assertFalse(Workbook._is_valid_file('file2.twb3')) -class WorkbookModelTests(unittest.TestCase): + +class ConnectionModelTests(unittest.TestCase): def setUp(self): - self.workbook_file = tempfile.NamedTemporaryFile(suffix='.twb') - self.workbook_file.write(TABLEAU_93_WORKBOOK.encode('utf8')) - self.workbook_file.seek(0) + self.connection = TABLEAU_CONNECTION_XML + + def test_can_read_attributes_from_connection(self): + conn = Connection(self.connection) + self.assertEqual(conn.dbname, 'TestV1') + self.assertEqual(conn.username, '') + self.assertEqual(conn.server, 'mssql2012.test.tsi.lan') + + def test_can_write_attributes_to_connection(self): + conn = Connection(self.connection) + conn.dbname = 'BubblesInMyDrink' + conn.server = 'mssql2014.test.tsi.lan' + self.assertEqual(conn.dbname, 'BubblesInMyDrink') + self.assertEqual(conn.username, '') + self.assertEqual(conn.server, 'mssql2014.test.tsi.lan') - def test_can_extract_datasource(self): - wb = Workbook(self.workbook_file.name) - self.assertEqual(len(wb.datasources), 1) - self.assertIsInstance(wb.datasources[0], Datasource) - self.assertEqual(wb.datasources[0].name, 'sqlserver.17u3bqc16tjtxn14e2hxh19tyvpo') class DatasourceModelTests(unittest.TestCase): @@ -63,28 +73,30 @@ def test_can_extract_datasource_from_file(self): def test_can_extract_connection(self): ds = Datasource.from_file(self.tds_file.name) self.assertIsInstance(ds.connection, Connection) - self.assertEqual(ds.connection.dbname, 'TestV1') - self.assertEqual(ds.connection.server, 'mssql2012.test.tsi.lan') - self.assertEqual(ds.connection.username, '') -class ConnectionModelTests(unittest.TestCase): + +class WorkbookModelTests(unittest.TestCase): def setUp(self): - self.connection = TABLEAU_CONNECTION_XML + self.workbook_file = tempfile.NamedTemporaryFile(suffix='.twb') + self.workbook_file.write(TABLEAU_93_WORKBOOK.encode('utf8')) + self.workbook_file.seek(0) - def test_can_read_attributes_from_connection(self): - conn = Connection(self.connection) - self.assertEqual(conn.dbname, 'TestV1') - self.assertEqual(conn.username, '') - self.assertEqual(conn.server, 'mssql2012.test.tsi.lan') + def test_can_extract_datasource(self): + wb = Workbook(self.workbook_file.name) + self.assertEqual(len(wb.datasources), 1) + self.assertIsInstance(wb.datasources[0], Datasource) + self.assertEqual(wb.datasources[0].name, + 'sqlserver.17u3bqc16tjtxn14e2hxh19tyvpo') + + def test_can_update_datasource_connection_and_save(self): + original_wb = Workbook(self.workbook_file.name) + original_wb.datasources[0].connection.dbname = 'newdb.test.tsi.lan' + original_wb.save() + + new_wb = Workbook(self.workbook_file.name) + self.assertEqual(new_wb.datasources[0].connection.dbname, 'newdb.test.tsi.lan') - def test_can_write_attributes_to_connection(self): - conn = Connection(self.connection) - conn.dbname = 'BubblesInMyDrink' - conn.server = 'mssql2014.test.tsi.lan' - self.assertEqual(conn.dbname, 'BubblesInMyDrink') - self.assertEqual(conn.username, '') - self.assertEqual(conn.server, 'mssql2014.test.tsi.lan') if __name__ == '__main__': unittest.main() From cc9437130e904c363dded9bdf943fc7f4f40fb04 Mon Sep 17 00:00:00 2001 From: T8y8 Date: Wed, 11 May 2016 16:24:12 -0700 Subject: [PATCH 7/7] Updating tests to work on Windows and Mac, Py2 and Py3. I'm manually managing temp files right now, but we may want to modify our Workbook and Datasource classes to accept all File-Like objects in the future so I don't have to do this filename sillyness. Still, this works for now and is cross platform --- Document API/test.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Document API/test.py b/Document API/test.py index 376b094..baf95c3 100644 --- a/Document API/test.py +++ b/Document API/test.py @@ -1,6 +1,6 @@ import unittest -import tempfile - +import io +import os import xml.etree.ElementTree as ET from tableaudocumentapi import Workbook, Datasource, Connection @@ -61,10 +61,14 @@ def test_can_write_attributes_to_connection(self): class DatasourceModelTests(unittest.TestCase): def setUp(self): - self.tds_file = tempfile.NamedTemporaryFile(suffix='.tds') + self.tds_file = io.FileIO('test.tds', 'w') self.tds_file.write(TABLEAU_93_TDS.encode('utf8')) self.tds_file.seek(0) + def tearDown(self): + self.tds_file.close() + os.unlink(self.tds_file.name) + def test_can_extract_datasource_from_file(self): ds = Datasource.from_file(self.tds_file.name) self.assertEqual(ds.name, 'sqlserver.17u3bqc16tjtxn14e2hxh19tyvpo') @@ -78,10 +82,14 @@ def test_can_extract_connection(self): class WorkbookModelTests(unittest.TestCase): def setUp(self): - self.workbook_file = tempfile.NamedTemporaryFile(suffix='.twb') + self.workbook_file = io.FileIO('test.twb', 'w') self.workbook_file.write(TABLEAU_93_WORKBOOK.encode('utf8')) self.workbook_file.seek(0) + def tearDown(self): + self.workbook_file.close() + os.unlink(self.workbook_file.name) + def test_can_extract_datasource(self): wb = Workbook(self.workbook_file.name) self.assertEqual(len(wb.datasources), 1)