Skip to content

Commit aba2a35

Browse files
authored
Merge pull request #44 from t8y8/tdsx-support
Added TDSX support and refactored Workbooks and Datasources to share zip-handling code
2 parents 23e897a + a885f15 commit aba2a35

File tree

5 files changed

+135
-98
lines changed

5 files changed

+135
-98
lines changed

tableaudocumentapi/datasource.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
# Datasource - A class for writing datasources to Tableau files
44
#
55
###############################################################################
6+
import os
7+
import zipfile
8+
69
import xml.etree.ElementTree as ET
7-
from tableaudocumentapi import Connection
10+
from tableaudocumentapi import Connection, xfile
811

912

1013
class ConnectionParser(object):
@@ -56,7 +59,11 @@ def __init__(self, dsxml, filename=None):
5659
@classmethod
5760
def from_file(cls, filename):
5861
"Initialize datasource from file (.tds)"
59-
dsxml = ET.parse(filename).getroot()
62+
63+
if zipfile.is_zipfile(filename):
64+
dsxml = xfile.get_xml_from_archive(filename).getroot()
65+
else:
66+
dsxml = ET.parse(filename).getroot()
6067
return cls(dsxml, filename)
6168

6269
def save(self):
@@ -72,7 +79,8 @@ def save(self):
7279
"""
7380

7481
# save the file
75-
self._datasourceTree.write(self._filename, encoding="utf-8", xml_declaration=True)
82+
83+
xfile._save_file(self._filename, self._datasourceTree)
7684

7785
def save_as(self, new_filename):
7886
"""
@@ -85,7 +93,7 @@ def save_as(self, new_filename):
8593
Nothing.
8694
8795
"""
88-
self._datasourceTree.write(new_filename, encoding="utf-8", xml_declaration=True)
96+
xfile._save_file(self._filename, self._datasourceTree, new_filename)
8997

9098
###########
9199
# name

tableaudocumentapi/workbook.py

Lines changed: 6 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,12 @@
33
# Workbook - A class for writing Tableau workbook files
44
#
55
###############################################################################
6-
import contextlib
76
import os
8-
import shutil
9-
import tempfile
107
import zipfile
118

129
import xml.etree.ElementTree as ET
1310

14-
from tableaudocumentapi import Datasource
11+
from tableaudocumentapi import Datasource, xfile
1512

1613
###########################################################################
1714
#
@@ -20,41 +17,6 @@
2017
###########################################################################
2118

2219

23-
@contextlib.contextmanager
24-
def temporary_directory(*args, **kwargs):
25-
d = tempfile.mkdtemp(*args, **kwargs)
26-
try:
27-
yield d
28-
finally:
29-
shutil.rmtree(d)
30-
31-
32-
def find_twb_in_zip(zip):
33-
for filename in zip.namelist():
34-
if os.path.splitext(filename)[-1].lower() == '.twb':
35-
return filename
36-
37-
38-
def get_twb_xml_from_twbx(filename):
39-
with temporary_directory() as temp:
40-
with zipfile.ZipFile(filename) as zf:
41-
zf.extractall(temp)
42-
twb_file = find_twb_in_zip(zf)
43-
twb_xml = ET.parse(os.path.join(temp, twb_file))
44-
45-
return twb_xml
46-
47-
48-
def build_twbx_file(twbx_contents, zip):
49-
for root_dir, _, files in os.walk(twbx_contents):
50-
relative_dir = os.path.relpath(root_dir, twbx_contents)
51-
for f in files:
52-
temp_file_full_path = os.path.join(
53-
twbx_contents, relative_dir, f)
54-
zipname = os.path.join(relative_dir, f)
55-
zip.write(temp_file_full_path, arcname=zipname)
56-
57-
5820
class Workbook(object):
5921
"""
6022
A class for writing Tableau workbook files.
@@ -75,7 +37,8 @@ def __init__(self, filename):
7537

7638
# Determine if this is a twb or twbx and get the xml root
7739
if zipfile.is_zipfile(self._filename):
78-
self._workbookTree = get_twb_xml_from_twbx(self._filename)
40+
self._workbookTree = xfile.get_xml_from_archive(
41+
self._filename)
7942
else:
8043
self._workbookTree = ET.parse(self._filename)
8144

@@ -111,12 +74,7 @@ def save(self):
11174
"""
11275

11376
# save the file
114-
115-
if zipfile.is_zipfile(self._filename):
116-
self._save_into_twbx(self._filename)
117-
else:
118-
self._workbookTree.write(
119-
self._filename, encoding="utf-8", xml_declaration=True)
77+
xfile._save_file(self._filename, self._workbookTree)
12078

12179
def save_as(self, new_filename):
12280
"""
@@ -129,12 +87,8 @@ def save_as(self, new_filename):
12987
Nothing.
13088
13189
"""
132-
133-
if zipfile.is_zipfile(self._filename):
134-
self._save_into_twbx(new_filename)
135-
else:
136-
self._workbookTree.write(
137-
new_filename, encoding="utf-8", xml_declaration=True)
90+
xfile._save_file(
91+
self._filename, self._workbookTree, new_filename)
13892

13993
###########################################################################
14094
#
@@ -150,31 +104,3 @@ def _prepare_datasources(self, xmlRoot):
150104
datasources.append(ds)
151105

152106
return datasources
153-
154-
def _save_into_twbx(self, filename=None):
155-
# Save reuses existing filename, 'save as' takes a new one
156-
if filename is None:
157-
filename = self._filename
158-
159-
# Saving a twbx means extracting the contents into a temp folder,
160-
# saving the changes over the twb in that folder, and then
161-
# packaging it back up into a specifically formatted zip with the correct
162-
# relative file paths
163-
164-
# Extract to temp directory
165-
with temporary_directory() as temp_path:
166-
with zipfile.ZipFile(self._filename) as zf:
167-
twb_file = find_twb_in_zip(zf)
168-
zf.extractall(temp_path)
169-
# Write the new version of the twb to the temp directory
170-
self._workbookTree.write(os.path.join(
171-
temp_path, twb_file), encoding="utf-8", xml_declaration=True)
172-
173-
# Write the new twbx with the contents of the temp folder
174-
with zipfile.ZipFile(filename, "w", compression=zipfile.ZIP_DEFLATED) as new_twbx:
175-
build_twbx_file(temp_path, new_twbx)
176-
177-
@staticmethod
178-
def _is_valid_file(filename):
179-
fileExtension = os.path.splitext(filename)[-1].lower()
180-
return fileExtension in ('.twb', '.tds')

tableaudocumentapi/xfile.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import contextlib
2+
import os
3+
import shutil
4+
import tempfile
5+
import zipfile
6+
7+
import xml.etree.ElementTree as ET
8+
9+
10+
@contextlib.contextmanager
11+
def temporary_directory(*args, **kwargs):
12+
d = tempfile.mkdtemp(*args, **kwargs)
13+
try:
14+
yield d
15+
finally:
16+
shutil.rmtree(d)
17+
18+
19+
def find_file_in_zip(zip):
20+
for filename in zip.namelist():
21+
try:
22+
with zip.open(filename) as xml_candidate:
23+
ET.parse(xml_candidate).getroot().tag in (
24+
'workbook', 'datasource')
25+
return filename
26+
except ET.ParseError:
27+
# That's not an XML file by gosh
28+
pass
29+
30+
31+
def get_xml_from_archive(filename):
32+
with zipfile.ZipFile(filename) as zf:
33+
with zf.open(find_file_in_zip(zf)) as xml_file:
34+
xml_tree = ET.parse(xml_file)
35+
36+
return xml_tree
37+
38+
39+
def build_archive_file(archive_contents, zip):
40+
for root_dir, _, files in os.walk(archive_contents):
41+
relative_dir = os.path.relpath(root_dir, archive_contents)
42+
for f in files:
43+
temp_file_full_path = os.path.join(
44+
archive_contents, relative_dir, f)
45+
zipname = os.path.join(relative_dir, f)
46+
zip.write(temp_file_full_path, arcname=zipname)
47+
48+
49+
def save_into_archive(xml_tree, filename, new_filename=None):
50+
# Saving a archive means extracting the contents into a temp folder,
51+
# saving the changes over the twb/tds in that folder, and then
52+
# packaging it back up into a specifically formatted zip with the correct
53+
# relative file paths
54+
55+
if new_filename is None:
56+
new_filename = filename
57+
58+
# Extract to temp directory
59+
with temporary_directory() as temp_path:
60+
with zipfile.ZipFile(filename) as zf:
61+
xml_file = find_file_in_zip(zf)
62+
zf.extractall(temp_path)
63+
# Write the new version of the file to the temp directory
64+
xml_tree.write(os.path.join(
65+
temp_path, xml_file), encoding="utf-8", xml_declaration=True)
66+
67+
# Write the new archive with the contents of the temp folder
68+
with zipfile.ZipFile(new_filename, "w", compression=zipfile.ZIP_DEFLATED) as new_archive:
69+
build_archive_file(temp_path, new_archive)
70+
71+
72+
def _save_file(container_file, xml_tree, new_filename=None):
73+
if zipfile.is_zipfile(container_file):
74+
save_into_archive(xml_tree, container_file, new_filename)
75+
else:
76+
xml_tree.write(container_file, encoding="utf-8", xml_declaration=True)

test/assets/TABLEAU_10_TDSX.tdsx

1.82 KB
Binary file not shown.

test/bvt.py

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,12 @@
1515

1616
TABLEAU_10_TWB = os.path.join(TEST_DIR, 'assets', 'TABLEAU_10_TWB.twb')
1717

18-
TABLEAU_CONNECTION_XML = ET.parse(os.path.join(TEST_DIR, 'assets', 'CONNECTION.xml')).getroot()
18+
TABLEAU_CONNECTION_XML = ET.parse(os.path.join(
19+
TEST_DIR, 'assets', 'CONNECTION.xml')).getroot()
1920

2021
TABLEAU_10_TWBX = os.path.join(TEST_DIR, 'assets', 'TABLEAU_10_TWBX.twbx')
2122

22-
23-
class HelperMethodTests(unittest.TestCase):
24-
25-
def test_is_valid_file_with_valid_inputs(self):
26-
self.assertTrue(Workbook._is_valid_file('file1.tds'))
27-
self.assertTrue(Workbook._is_valid_file('file2.twb'))
28-
self.assertTrue(Workbook._is_valid_file('tds.twb'))
29-
30-
def test_is_valid_file_with_invalid_inputs(self):
31-
self.assertFalse(Workbook._is_valid_file(''))
32-
self.assertFalse(Workbook._is_valid_file('file1.tds2'))
33-
self.assertFalse(Workbook._is_valid_file('file2.twb3'))
23+
TABLEAU_10_TDSX = os.path.join(TEST_DIR, 'assets', 'TABLEAU_10_TDSX.tdsx')
3424

3525

3626
class ConnectionParserTests(unittest.TestCase):
@@ -144,6 +134,43 @@ def test_can_save_tds(self):
144134
self.assertEqual(new_tds.connections[0].dbname, 'newdb.test.tsi.lan')
145135

146136

137+
class DatasourceModelV10TDSXTests(unittest.TestCase):
138+
139+
def setUp(self):
140+
with open(TABLEAU_10_TDSX, 'rb') as in_file, open('test.tdsx', 'wb') as out_file:
141+
out_file.write(in_file.read())
142+
self.tdsx_file = out_file
143+
144+
def tearDown(self):
145+
self.tdsx_file.close()
146+
os.unlink(self.tdsx_file.name)
147+
148+
def test_can_open_tdsx(self):
149+
ds = Datasource.from_file(self.tdsx_file.name)
150+
self.assertTrue(ds.connections)
151+
self.assertTrue(ds.name)
152+
153+
def test_can_open_tdsx_and_save_changes(self):
154+
original_tdsx = Datasource.from_file(self.tdsx_file.name)
155+
original_tdsx.connections[0].server = 'newdb.test.tsi.lan'
156+
original_tdsx.save()
157+
158+
new_tdsx = Datasource.from_file(self.tdsx_file.name)
159+
self.assertEqual(new_tdsx.connections[
160+
0].server, 'newdb.test.tsi.lan')
161+
162+
def test_can_open_tdsx_and_save_as_changes(self):
163+
new_tdsx_filename = 'newtdsx.tdsx'
164+
original_wb = Datasource.from_file(self.tdsx_file.name)
165+
original_wb.connections[0].server = 'newdb.test.tsi.lan'
166+
original_wb.save_as(new_tdsx_filename)
167+
168+
new_wb = Datasource.from_file(new_tdsx_filename)
169+
self.assertEqual(new_wb.connections[
170+
0].server, 'newdb.test.tsi.lan')
171+
os.unlink(new_tdsx_filename)
172+
173+
147174
class WorkbookModelTests(unittest.TestCase):
148175

149176
def setUp(self):
@@ -240,7 +267,7 @@ def test_can_open_twbx_and_save_changes(self):
240267
0].server, 'newdb.test.tsi.lan')
241268

242269
def test_can_open_twbx_and_save_as_changes(self):
243-
new_twbx_filename = self.workbook_file.name + "_TEST_SAVE_AS"
270+
new_twbx_filename = 'newtwbx.twbx'
244271
original_wb = Workbook(self.workbook_file.name)
245272
original_wb.datasources[0].connections[0].server = 'newdb.test.tsi.lan'
246273
original_wb.save_as(new_twbx_filename)

0 commit comments

Comments
 (0)
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