8000 Implemented ConnectionParser to handle Federated and Legacy connections by t8y8 · Pull Request #7 · tableau/document-api-python · GitHub
[go: up one dir, main page]

Skip to content

Implemented ConnectionParser to handle Federated and Legacy connections #7

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 3 commits into from
May 16, 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 tableaudocumentapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__version__ = '0.0.1'
__VERSION__ = __version__
from .connection import Connection
from .datasource import Datasource
from .datasource import Datasource, ConnectionParser
from .workbook import Workbook
22 changes: 21 additions & 1 deletion tableaudocumentapi/datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@
import xml.etree.ElementTree as ET
from tableaudocumentapi import Connection

class ConnectionParser(object):

def __init__(self, datasource_xml, version):
self._dsxml = datasource_xml
self._dsversion = version

def _extract_federated_connections(self):
return list(map(Connection,self._dsxml.findall('.//named-connections/named-connection/*')))

def _extract_legacy_connection(self):
return Connection(self._dsxml.find('connection'))

def get_connections(self):
if float(self._dsversion) < 10:
connections = self._extract_legacy_connection()
else:
connections = self._extract_federated_connections()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm beginning to wonder if we shouldn't have a different class for the NamedConnection that just mimics the old interface, but can handle things like the caption and other attributes that legacy connections don't have.

But that might be more than we need right now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I spoke to Sundeep, and he confirmed I can ignore the named-connection elements for Server purposes.

Copy link

Choose a reason for hiding this comment

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

Fine for now. In the end, we should have separate classes for parsing 10.0+ vs prior connections. This current format, I think, will lead to potentially a good number of if else cases. I think there can be inheritance. Just a note for going forward. For now ... I wouldn't bother.

return connections


class Datasource(object):
"""
Expand All @@ -28,7 +47,8 @@ def __init__(self, dsxml, filename=None):
self._datasourceTree = ET.ElementTree(self._datasourceXML)
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'))
self._connection_parser = ConnectionParser(self._datasourceXML, version=self._version)
self._connection = self._connection_parser.get_connections()
Copy link

Choose a reason for hiding this comment

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

If we are returning a list, connection needs to be plural (same for the accessor)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That breaks the existing API, but that's probably ok because only the sample needs to be updated :)

@benlower and I discussed always returning a list for connections, even in the legacy case, what do you think? I have that change shelved already and can add it to the PR.

Copy link

Choose a reason for hiding this comment

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

The "existing" api doesn't exist yet. :) I think we are okay for just a bit longer and then will have to worry about versioining and such but (a) we are still in BETA and (b) we haven't even released the BETA. :)

And I am good with returning a list even for older datasources. Consistent API. I don't want users having to put if cases for version numbers.


@classmethod
def from_file(cls, filename):
Expand Down
74 changes: 56 additions & 18 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,20 @@
import os
import xml.etree.ElementTree as ET

from tableaudocumentapi import Workbook, Datasource, Connection

TABLEAU_93_WORKBOOK = '''<?xml version='1.0' encoding='utf-8' ?>
<workbook source-build='9.3.1 (9300.16.0510.0100)' source-platform='mac' version='9.3' xmlns:user='http://www.tableausoftware.com/xml/user'>
<datasources>
<datasource caption='xy (TestV1)' inline='true' name='sqlserver.17u3bqc16tjtxn14e2hxh19tyvpo' version='9.3'>
<connection authentication='sspi' class='sqlserver' dbname='TestV1' odbc-native-protocol='yes' one-time-sql='' server='mssql2012.test.tsi.lan' username=''>
</connection>
</datasource>
</datasources>
</workbook>'''

TABLEAU_93_TDS = '''<?xml version='1.0' encoding='utf-8' ?>
<datasource formatted-name='sqlserver.17u3bqc16tjtxn14e2hxh19tyvpo' inline='true' source-platform='mac' version='9.3' xmlns:user='http://www.tableausoftware.com/xml/user'>
<connection authentication='sspi' class='sqlserver' dbname='TestV1' odbc-native-protocol='yes' one-time-sql='' server='mssql2012.test.tsi.lan' username=''>
</connection>
</datasource>'''
from tableaudocumentapi import Workbook, Datasource, Connection, ConnectionParser


TABLEAU_93_WORKBOOK = '''<?xml version='1.0' encoding='utf-8' ?><workbook source-build='9.3.1 (9300.16.0510.0100)' source-platform='mac' version='9.3' xmlns:user='http://www.tableausoftware.com/xml/user'><datasources><datasource caption='xy (TestV1)' inline='true' name='sqlserver.17u3bqc16tjtxn14e2hxh19tyvpo' version='9.3'><connection authentication='sspi' class='sqlserver' dbname='TestV1' odbc-native-protocol='yes' one-time-sql='' server='mssql2012.test.tsi.lan' username=''></connection></datasource></datasources></workbook>'''

TABLEAU_93_TDS = '''<?xml version='1.0' encoding='utf-8' ?><datasource formatted-name='sqlserver.17u3bqc16tjtxn14e2hxh19tyvpo' inline='true' source-platform='mac' version='9.3' xmlns:user='http://www.tableausoftware.com/xml/user'><connection authentication='sspi' class='sqlserver' dbname='TestV1' odbc-native-protocol='yes' one-time-sql='' server='mssql2012.test.tsi.lan' username=''></connection></datasource>'''

TABLEAU_10_TDS = '''<?xml version='1.0' encoding='utf-8' ?><datasources><datasource caption='xy+ (Multiple Connections)' inline='true' name='federated.1s4nxn20cywkdv13ql0yk0g1mpdx' version='10.0'><connection class='federated'><named-connections><named-connection caption='mysql55.test.tsi.lan' name='mysql.1ewmkrw0mtgsev1dnurma1blii4x'><connection class='mysql' dbname='testv1' odbc-native-protocol='yes' port='3306' server='mysql55.test.tsi.lan' source-charset='' username='test' /></named-connection><named-connection caption='mssql2012.test.tsi.lan' name='sqlserver.1erdwp01uqynlb14ul78p0haai2r'><connection authentication='sqlserver' class='sqlserver' dbname='TestV1' odbc-native-protocol='yes' one-time-sql='' server='mssql2012.test.tsi.lan' username='test' /></named-connection></named-connections></connection></datasource></datasources>'''

TABLEAU_10_WORKBOOK = '''<?xml version='1.0' encoding='utf-8' ?><workbook source-build='0.0.0 (0000.16.0510.1300)' source-platform='mac' version='10.0' xmlns:user='http://www.tableausoftware.com/xml/user'><datasources><datasource caption='xy+ (Multiple Connections)' inline='true' name='federated.1s4nxn20cywkdv13ql0yk0g1mpdx' version='10.0'><connection class='federated'><named-connections><named-connection caption='mysql55.test.tsi.lan' name='mysql.1ewmkrw0mtgsev1dnurma1blii4x'><connection class='mysql' dbname='testv1' odbc-native-protocol='yes' port='3306' server='mysql55.test.tsi.lan' source-charset='' username='test' /></named-connection><named-connection caption='mssql2012.test.tsi.lan' name='sqlserver.1erdwp01uqynlb14ul78p0haai2r'><connection authentication='sqlserver' class='sqlserver' dbname='TestV1' odbc-native-protocol='yes' one-time-sql='' server='mssql2012.test.tsi.lan' username='test' /></named-connection></named-connections></connection></datasource></datasources></workbook>'''

TABLEAU_CONNECTION_XML = ET.fromstring(
'''<connection authentication='sspi' class='sqlserver' dbname='TestV1' odbc-native-protocol='yes' one-time-sql='' server='mssql2012.test.tsi.lan' username=''></connection>''')


class HelperMethodTests(unittest.TestCase):

def test_is_valid_file_with_valid_inputs(self):
Expand All @@ -38,6 +30,23 @@ def test_is_valid_file_with_invalid_inputs(self):
self.assertFalse(Workbook._is_valid_file('file2.twb3'))


class ConnectionParserTests(unittest.TestCase):

def test_can_extract_legacy_connection(self):
parser = ConnectionParser(ET.fromstring(TABLEAU_93_TDS), '9.2')
connection = parser.get_connections()
self.assertIsInstance(connection, Connection)
self.assertEqual(connection.dbname, 'TestV1')


def test_can_extract_federated_connections(self):
parser = ConnectionParser(ET.fromstring(TABLEAU_10_TDS), '10.0')
connections = parser.get_connections()
self.assertIsInstance(connections, list)
self.assertIsInstance(connections[0], Connection)
self.assertEqual(connections[0].dbname, 'testv1')


class ConnectionModelTests(unittest.TestCase):

def setUp(self):
Expand Down Expand Up @@ -114,5 +123,34 @@ def test_can_update_datasource_connection_and_save(self):
self.assertEqual(new_wb.datasources[0].connection.dbname, 'newdb.test.tsi.lan')


class WorkbookModelV10Tests(unittest.TestCase):

def setUp(self):
self.workbook_file = io.FileIO('testv10.twb', 'w')
self.workbook_file.write(TABLEAU_10_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_datasourceV10(self):
wb = Workbook(self.workbook_file.name)
self.assertEqual(len(wb.datasources), 1)
self.assertEqual(len(wb.datasources[0].connection), 2)
self.assertIsInstance(wb.datasources[0].connection, list)
self.assertIsInstance(wb.datasources[0], Datasource)
self.assertEqual(wb.datasources[0].name,
'federated.1s4nxn20cywkdv13ql0yk0g1mpdx')

def test_can_update_datasource_connection_and_saveV10(self):
original_wb = Workbook(self.workbook_file.name)
original_wb.datasources[0].connection[0].dbname = 'newdb.test.tsi.lan'

original_wb.save()

new_wb = Workbook(self.workbook_file.name)
self.assertEqual(new_wb.datasources[0].connection[0].dbname, 'newdb.test.tsi.lan')

if __name__ == '__main__':
unittest.main()
0