From 70771ffd0df4aeb0343eb86f887ceb6bd6bfd6b9 Mon Sep 17 00:00:00 2001 From: T8y8 Date: Sat, 30 Jul 2016 15:15:18 +0800 Subject: [PATCH 1/5] First attempt --- tableaudocumentapi/connection.py | 15 ++++++++++++++- test/bvt.py | 10 ++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/tableaudocumentapi/connection.py b/tableaudocumentapi/connection.py index ab4dcbb..4de1565 100644 --- a/tableaudocumentapi/connection.py +++ b/tableaudocumentapi/connection.py @@ -3,6 +3,7 @@ # Connection - A class for writing connections to Tableau files # ############################################################################### +import xml.etree.ElementTree as ET from tableaudocumentapi.dbclass import is_valid_dbclass @@ -33,6 +34,18 @@ def __init__(self, connxml): def __repr__(self): return "''".format(self._server, self._dbname, hex(id(self))) + @classmethod + def from_attributes(cls, server, dbname, username, dbclass, authentication=''): + root = ET.Element('connection') + root.set('authentication', authentication) + xml = cls(root) + xml.server = server + xml.dbname = dbname + xml.username = username + xml.dbclass = dbclass + + return xml + ########### # dbname ########### @@ -120,4 +133,4 @@ def dbclass(self, value): raise AttributeError("'{}' is not a valid database type".format(value)) self._class = value - self._connectionXML.set('dbclass', value) + self._connectionXML.set('class', value) diff --git a/test/bvt.py b/test/bvt.py index ce96afe..bd2d7ad 100644 --- a/test/bvt.py +++ b/test/bvt.py @@ -74,6 +74,16 @@ def test_bad_dbclass_rasies_attribute_error(self): with self.assertRaises(AttributeError): conn.dbclass = 'NotReal' + def test_can_create_connection_from_scratch(self): + conn = Connection.from_attributes( + server='a', dbname='b', username='c', dbclass='mysql', authentication='d') + self.assertEqual(conn.server, 'a') + self.assertEqual(conn.dbname, 'b') + self.assertEqual(conn.username, 'c') + self.assertEqual(conn.dbclass, 'mysql') + self.assertEqual(conn.authentication, 'd') + + class DatasourceModelTests(unittest.TestCase): From 27dceeac7b8eca62a327d7735e9f32d682337ff0 Mon Sep 17 00:00:00 2001 From: T8y8 Date: Sat, 30 Jul 2016 15:20:49 +0800 Subject: [PATCH 2/5] pep8, I'll add a precommit hook for myself --- test/bvt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/bvt.py b/test/bvt.py index bd2d7ad..557f781 100644 --- a/test/bvt.py +++ b/test/bvt.py @@ -84,7 +84,6 @@ def test_can_create_connection_from_scratch(self): self.assertEqual(conn.authentication, 'd') - class DatasourceModelTests(unittest.TestCase): def setUp(self): From 557b9fe50511996fe3b6d2e075f709088ee3b662 Mon Sep 17 00:00:00 2001 From: T8y8 Date: Thu, 4 Aug 2016 15:29:28 -0700 Subject: [PATCH 3/5] Protoype new datasource --- tableaudocumentapi/connection.py | 3 +-- tableaudocumentapi/datasource.py | 25 +++++++++++++++++++++++++ test/bvt.py | 10 ++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/tableaudocumentapi/connection.py b/tableaudocumentapi/connection.py index 4de1565..8e9eb58 100644 --- a/tableaudocumentapi/connection.py +++ b/tableaudocumentapi/connection.py @@ -36,8 +36,7 @@ def __repr__(self): @classmethod def from_attributes(cls, server, dbname, username, dbclass, authentication=''): - root = ET.Element('connection') - root.set('authentication', authentication) + root = ET.Element('connection', authentication=authentication) xml = cls(root) xml.server = server xml.dbname = dbname diff --git a/tableaudocumentapi/datasource.py b/tableaudocumentapi/datasource.py index 09860b4..d2c2c9b 100644 --- a/tableaudocumentapi/datasource.py +++ b/tableaudocumentapi/datasource.py @@ -5,6 +5,8 @@ ############################################################################### import collections import itertools +import random +import string import xml.etree.ElementTree as ET import xml.sax.saxutils as sax @@ -38,6 +40,7 @@ def _is_used_by_worksheet(names, field): class FieldDictionary(MultiLookupDict): + def used_by_sheet(self, name): # If we pass in a string, no need to get complicated, just check to see if name is in # the field's list of worksheets @@ -63,7 +66,15 @@ def _column_object_from_metadata_xml(metadata_xml): return _ColumnObjectReturnTuple(field_object.id, field_object) +def make_unique_name(dbclass): + rand_part = ''.join(random.choice( + string.ascii_lowercase + string.digits) for _ in range(28)) + name = dbclass + '.' + rand_part + return name + + class ConnectionParser(object): + def __init__(self, datasource_xml, version): self._dsxml = datasource_xml self._dsversion = version @@ -116,6 +127,20 @@ def from_file(cls, filename): dsxml = xml_open(filename, cls.__name__.lower()).getroot() return cls(dsxml, filename) + @classmethod + def from_scratch(cls, caption, connections): + root = ET.Element('datasource', caption=caption, version='10.0') + outer_connection = ET.SubElement(root, 'connection') + outer_connection.set('class', 'federated') + named_conns = ET.SubElement(outer_connection, 'named-connections') + for conn in connections: + nc = ET.SubElement(named_conns, + 'named-connection', + name=make_unique_name(conn.dbclass), + caption=conn.server) + nc.append(conn._connectionXML) + return cls(root) + def save(self): """ Call finalization code and save file. diff --git a/test/bvt.py b/test/bvt.py index 557f781..45441bd 100644 --- a/test/bvt.py +++ b/test/bvt.py @@ -83,6 +83,16 @@ def test_can_create_connection_from_scratch(self): self.assertEqual(conn.dbclass, 'mysql') self.assertEqual(conn.authentication, 'd') + def test_can_create_datasource_from_scratch(self): + conn1 = Connection.from_attributes( + server='a', dbname='b', username='c', dbclass='mysql', authentication='d') + conn2 = Connection.from_attributes( + server='1', dbname='2', username='3', dbclass='mysql', authentication='7') + ds = Datasource.from_scratch('test', connections=[conn1, conn2]) + + self.assertEqual(ds.connections[0].server, 'a') + self.assertEqual(ds.connections[1].server, '1') + class DatasourceModelTests(unittest.TestCase): From 69cfda6265cb912e6708bc208824951fecbe9f6c Mon Sep 17 00:00:00 2001 From: T8y8 Date: Tue, 9 Aug 2016 10:35:09 -0700 Subject: [PATCH 4/5] Enable save_as for new datasources and add a required attribute --- tableaudocumentapi/datasource.py | 5 +++-- tableaudocumentapi/xfile.py | 4 ++++ test/bvt.py | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tableaudocumentapi/datasource.py b/tableaudocumentapi/datasource.py index d2c2c9b..12dfb95 100644 --- a/tableaudocumentapi/datasource.py +++ b/tableaudocumentapi/datasource.py @@ -128,8 +128,8 @@ def from_file(cls, filename): return cls(dsxml, filename) @classmethod - def from_scratch(cls, caption, connections): - root = ET.Element('datasource', caption=caption, version='10.0') + def from_connections(cls, caption, connections): + root = ET.Element('datasource', caption=caption, version='10.0', inline='true') outer_connection = ET.SubElement(root, 'connection') outer_connection.set('class', 'federated') named_conns = ET.SubElement(outer_connection, 'named-connections') @@ -168,6 +168,7 @@ def save_as(self, new_filename): Nothing. """ + xfile._save_file(self._filename, self._datasourceTree, new_filename) ########### diff --git a/tableaudocumentapi/xfile.py b/tableaudocumentapi/xfile.py index 8f9ffd1..66e5aac 100644 --- a/tableaudocumentapi/xfile.py +++ b/tableaudocumentapi/xfile.py @@ -104,6 +104,10 @@ def save_into_archive(xml_tree, filename, new_filename=None): def _save_file(container_file, xml_tree, new_filename=None): + + if container_file is None: + container_file = new_filename + if zipfile.is_zipfile(container_file): save_into_archive(xml_tree, container_file, new_filename) else: diff --git a/test/bvt.py b/test/bvt.py index 45441bd..463a087 100644 --- a/test/bvt.py +++ b/test/bvt.py @@ -83,12 +83,12 @@ def test_can_create_connection_from_scratch(self): self.assertEqual(conn.dbclass, 'mysql') self.assertEqual(conn.authentication, 'd') - def test_can_create_datasource_from_scratch(self): + def test_can_create_datasource_from_connections(self): conn1 = Connection.from_attributes( server='a', dbname='b', username='c', dbclass='mysql', authentication='d') conn2 = Connection.from_attributes( server='1', dbname='2', username='3', dbclass='mysql', authentication='7') - ds = Datasource.from_scratch('test', connections=[conn1, conn2]) + ds = Datasource.from_connections('test', connections=[conn1, conn2]) self.assertEqual(ds.connections[0].server, 'a') self.assertEqual(ds.connections[1].server, '1') From 81e2946d9e69b743e85d1aeaac2aff056fac24ea Mon Sep 17 00:00:00 2001 From: T8y8 Date: Wed, 10 Aug 2016 10:01:14 -0700 Subject: [PATCH 5/5] Use UUID encoded in base36 --- tableaudocumentapi/datasource.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/tableaudocumentapi/datasource.py b/tableaudocumentapi/datasource.py index 12dfb95..bae9159 100644 --- a/tableaudocumentapi/datasource.py +++ b/tableaudocumentapi/datasource.py @@ -6,9 +6,9 @@ import collections import itertools import random -import string import xml.etree.ElementTree as ET import xml.sax.saxutils as sax +from uuid import uuid4 from tableaudocumentapi import Connection, xfile from tableaudocumentapi import Field @@ -66,9 +66,30 @@ def _column_object_from_metadata_xml(metadata_xml): return _ColumnObjectReturnTuple(field_object.id, field_object) +def base36encode(number): + """Converts an integer into a base36 string.""" + + ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz" + + base36 = '' + sign = '' + + if number < 0: + sign = '-' + number = -number + + if 0 <= number < len(ALPHABET): + return sign + ALPHABET[number] + + while number != 0: + number, i = divmod(number, len(ALPHABET)) + base36 = ALPHABET[i] + base36 + + return sign + base36 + + def make_unique_name(dbclass): - rand_part = ''.join(random.choice( - string.ascii_lowercase + string.digits) for _ in range(28)) + rand_part = base36encode(uuid4().int) name = dbclass + '.' + rand_part return name