33# Datasource - A class for writing datasources to Tableau files
44#
55###############################################################################
6+ import contextlib
7+ import os
8+ import shutil
9+ import tempfile
10+ import zipfile
11+
612import xml .etree .ElementTree as ET
713from tableaudocumentapi import Connection
814
915
16+ @contextlib .contextmanager
17+ def temporary_directory (* args , ** kwargs ):
18+ d = tempfile .mkdtemp (* args , ** kwargs )
19+ try :
20+ yield d
21+ finally :
22+ shutil .rmtree (d )
23+
24+
25+ def find_tds_in_zip (zip ):
26+ for filename in zip .namelist ():
27+ if os .path .splitext (filename )[- 1 ].lower () == '.tds' :
28+ return filename
29+
30+
31+ def get_tds_xml_from_tdsx (filename ):
32+ with temporary_directory () as temp :
33+ with zipfile .ZipFile (filename ) as zf :
34+ zf .extractall (temp )
35+ tds_file = find_tds_in_zip (zf )
36+ tds_xml = ET .parse (os .path .join (temp , tds_file ))
37+
38+ return tds_xml
39+
40+
41+ def build_tdsx_file (tdsx_contents , zip ):
42+ for root_dir , _ , files in os .walk (tdsx_contents ):
43+ relative_dir = os .path .relpath (root_dir , tdsx_contents )
44+ for f in files :
45+ temp_file_full_path = os .path .join (
46+ tdsx_contents , relative_dir , f )
47+ zipname = os .path .join (relative_dir , f )
48+ zip .write (temp_file_full_path , arcname = zipname )
49+
50+
1051class ConnectionParser (object ):
1152
1253 def __init__ (self , datasource_xml , version ):
@@ -56,9 +97,36 @@ def __init__(self, dsxml, filename=None):
5697 @classmethod
5798 def from_file (cls , filename ):
5899 "Initialize datasource from file (.tds)"
59- dsxml = ET .parse (filename ).getroot ()
100+
101+ if zipfile .is_zipfile (filename ):
102+ dsxml = get_tds_xml_from_tdsx (filename ).getroot ()
103+ else :
104+ dsxml = ET .parse (filename ).getroot ()
60105 return cls (dsxml , filename )
61106
107+ def _save_into_tdsx (self , filename = None ):
108+ # Save reuses existing filename, 'save as' takes a new one
109+ if filename is None :
110+ filename = self ._filename
111+
112+ # Saving a tdsx means extracting the contents into a temp folder,
113+ # saving the changes over the tds in that folder, and then
114+ # packaging it back up into a specifically formatted zip with the correct
115+ # relative file paths
116+
117+ # Extract to temp directory
118+ with temporary_directory () as temp_path :
119+ with zipfile .ZipFile (self ._filename ) as zf :
120+ tds_file = find_tds_in_zip (zf )
121+ zf .extractall (temp_path )
122+ # Write the new version of the tds to the temp directory
123+ self ._datasourceTree .write (os .path .join (
124+ temp_path , tds_file ), encoding = "utf-8" , xml_declaration = True )
125+
126+ # Write the new tdsx with the contents of the temp folder
127+ with zipfile .ZipFile (filename , "w" , compression = zipfile .ZIP_DEFLATED ) as new_tdsx :
128+ build_tdsx_file (temp_path , new_tdsx )
129+
62130 def save (self ):
63131 """
64132 Call finalization code and save file.
@@ -72,7 +140,12 @@ def save(self):
72140 """
73141
74142 # save the file
75- self ._datasourceTree .write (self ._filename , encoding = "utf-8" , xml_declaration = True )
143+
144+ if zipfile .is_zipfile (self ._filename ):
145+ self ._save_into_tdsx (self ._filename )
146+ else :
147+ self ._datasourceTree .write (
148+ self ._filename , encoding = "utf-8" , xml_declaration = True )
76149
77150 def save_as (self , new_filename ):
78151 """
@@ -85,7 +158,11 @@ def save_as(self, new_filename):
85158 Nothing.
86159
87160 """
88- self ._datasourceTree .write (new_filename , encoding = "utf-8" , xml_declaration = True )
161+ if zipfile .is_zipfile (self ._filename ):
162+ self ._save_into_tdsx (new_filename )
163+ else :
164+ self ._datasourceTree .write (
165+ new_filename , encoding = "utf-8" , xml_declaration = True )
89166
90167 ###########
91168 # name
0 commit comments