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 ._fi
EDBE
lename = 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,12 @@ def save(self):
76
111
"""
77
112
78
113
# save the file
79
- self ._workbookTree .write (self ._filename , encoding = "utf-8" , xml_declaration = True )
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 )
80
120
81
121
def save_as (self , new_filename ):
82
122
"""
@@ -90,7 +130,11 @@ def save_as(self, new_filename):
90
130
91
131
"""
92
132
93
- self ._workbookTree .write (new_filename , encoding = "utf-8" , xml_declaration = True )
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 )
94
138
95
139
###########################################################################
96
140
#
@@ -107,6 +151,29 @@ def _prepare_datasources(self, xmlRoot):
107
151
108
152
return datasources
109
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
+
110
177
@staticmethod
111
178
def _is_valid_file (filename ):
112
179
fileExtension = os .path .splitext (filename )[- 1 ].lower ()
0 commit comments