3
3
# Datasource - A class for writing datasources to Tableau files
4
4
#
5
5
###############################################################################
6
+ import contextlib
7
+ import os
8
+ import shutil
9
+ import tempfile
10
+ import zipfile
11
+
6
12
import xml .etree .ElementTree as ET
7
13
from tableaudocumentapi import Connection
8
14
9
15
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
+
10
51
class ConnectionParser (object ):
11
52
12
53
def __init__ (self , datasource_xml , version ):
@@ -56,9 +97,36 @@ def __init__(self, dsxml, filename=None):
56
97
@classmethod
57
98
def from_file (cls , filename ):
58
99
"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 ()
60
105
return cls (dsxml , filename )
61
106
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
+
62
130
def save (self ):
63
131
"""
64
132
Call finalization code and save file.
@@ -72,7 +140,12 @@ def save(self):
72
140
"""
73
141
74
142
# 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 )
76
149
77
150
def save_as (self , new_filename ):
78
151
"""
@@ -85,7 +158,11 @@ def save_as(self, new_filename):
85
158
Nothing.
86
159
87
160
"""
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 )
B428
td>
163
+ else :
164
+ self ._datasourceTree .write (
165
+ new_filename , encoding = "utf-8" , xml_declaration = True )
89
166
90
167
###########
91
168
# name
0 commit comments