3
3
# Workbook - A class for writing Tableau workbook files
4
4
#
5
5
###############################################################################
6
+ import contextlib
6
7
import os
8
+ import shutil
9
+ import tempfile
10
+ import zipfile
11
+
7
12
import xml .etree .ElementTree as ET
13
+
8
14
from tableaudocumentapi import Datasource
9
15
16
+ ###########################################################################
17
+ #
18
+ # Utility Functions
19
+ #
20
+ ###########################################################################
21
+
22
+
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
+
10
57
11
58
class Workbook (object ):
12
59
"""
@@ -24,30 +71,18 @@ def __init__(self, filename):
24
71
Constructor.
25
72
26
73
"""
27
- # We have a valid type of input file
28
- if self ._is_valid_file (filename ):
29
- # set our filename, open .twb, initialize things
30
- self ._filename = filename
31
- self ._workbookTree = ET .parse (filename )
32
- self ._workbookRoot = self ._workbookTree .getroot ()
33
-
34
- # prepare our datasource objects
35
- self ._datasources = self ._prepare_datasources (
36
- self ._workbookRoot ) # self.workbookRoot.find('datasources')
37
- else :
38
- print ('Invalid file type. Must be .twb or .tds.' )
39
- raise Exception ()
40
-
41
- @classmethod
42
- def from_file (cls , filename ):
43
- "Initialize datasource from file (.tds)"
44
- if self ._is_valid_file (filename ):
45
- self ._filename = filename
46
- dsxml = ET .parse (filename ).getroot ()
47
- return cls (dsxml )
74
+ self ._filename = filename
75
+
76
+ # Determine if this is a twb or twbx and get the xml root
77
+ if zipfile .is_zipfile (self ._filename ):
78
+ self ._workbookTree = get_twb_xml_from_twbx (self ._filename )
48
79
else :
49
- print ('Invalid file type. Must be .twb or .tds.' )
50
- raise Exception ()
80
+ self ._workbookTree = ET .parse (self ._filename )
81
+
82
+ self ._workbookRoot = self ._workbookTree .getroot ()
83
+ # prepare our datasource objects
84
+ self ._datasources = self ._prepare_datasources (
85
+ self ._workbookRoot ) # self.workbookRoot.find('datasources')
51
86
52
87
###########
53
88
# datasources
@@ -76,7 +111,11 @@ def save(self):
76
111
"""
77
112
78
113
# save the file
79
- self ._workbookTree .write (self ._filename )
114
+
115
+ if zipfile .is_zipfile (self ._filename ):
116
+ self ._save_into_twbx (self ._filename )
117
+ else :
118
+ self ._workbookTree .write (self ._filename )
80
119
81
120
def save_as (self , new_filename ):
82
121
"""
@@ -89,8 +128,10 @@ def save_as(self, new_filename):
89
128
Nothing.
90
129
91
130
"""
92
-
93
- self ._workbookTree .write (new_filename )
131
+ if zipfile .is_zipfile (self ._filename ):
132
+ self ._save_into_twbx (new_filename )
133
+ else :
134
+ self ._workbookTree .write (new_filename )
94
135
95
136
###########################################################################
96
137
#
@@ -107,6 +148,28 @@ def _prepare_datasources(self, xmlRoot):
107
148
108
149
return datasources
109
150
151
+ def _save_into_twbx (self , filename = None ):
152
+ # Save reuses existing filename, 'save as' takes a new one
153
+ if filename is None :
154
+ filename = self ._filename
155
+
156
+ # Saving a twbx means extracting the contents into a temp folder,
157
+ # saving the changes over the twb in that folder, and then
158
+ # packaging it back up into a specifically formatted zip with the correct
159
+ # relative file paths
160
+
161
+ # Extract to temp directory
162
+ with temporary_directory () as temp_path :
163
+ with zipfile .ZipFile (self ._filename ) as zf :
164
+ twb_file = find_twb_in_zip (zf )
165
+ zf .extractall (temp_path )
166
+ # Write the new version of the twb to the temp directory
167
+ self ._workbookTree .write (os .path .join (temp_path , twb_file ))
168
+
169
+ # Write the new twbx with the contents of the temp folder
170
+ with zipfile .ZipFile (filename , "w" , compression = zipfile .ZIP_DEFLATED ) as new_twbx :
171
+ build_twbx_file (temp_path , new_twbx )
172
+
110
173
@staticmethod
111
174
def _is_valid_file (filename ):
112
175
fileExtension = os .path .splitext (filename )[- 1 ].lower ()
0 commit comments