1
- ###############################################################################
2
- #
3
- # Datasource - A class for writing datasources to Tableau files
4
- #
5
- ###############################################################################
6
1
import collections
7
2
import itertools
8
3
import xml .etree .ElementTree as ET
16
11
17
12
########
18
13
# This is needed in order to determine if something is a string or not. It is necessary because
19
- # of differences between python2 (basestring) and python3 (str). If python2 support is every
14
+ # of differences between python2 (basestring) and python3 (str). If python2 support is ever
20
15
# dropped, remove this and change the basestring references below to str
21
16
try :
22
17
basestring
@@ -35,7 +30,7 @@ def _get_metadata_xml_for_field(root_xml, field_name):
35
30
36
31
37
32
def _is_used_by_worksheet (names , field ):
38
- return any (( y for y in names if y in field .worksheets ) )
33
+ return any (y for y in names if y in field .worksheets )
39
34
40
35
41
36
class FieldDictionary (MultiLookupDict ):
@@ -87,27 +82,32 @@ def base36encode(number):
87
82
return sign + base36
88
83
89
84
90
- def make_unique_name (dbclass ):
85
+ def _make_unique_name (dbclass ):
91
86
rand_part = base36encode (uuid4 ().int )
92
87
name = dbclass + '.' + rand_part
93
88
return name
94
89
95
90
96
91
class ConnectionParser (object ):
92
+ """Parser for detecting and extracting connections from differing Tableau file formats."""
97
93
98
94
def __init__ (self , datasource_xml , version ):
99
95
self ._dsxml = datasource_xml
100
96
self ._dsversion = version
101
97
102
98
def _extract_federated_connections (self ):
103
99
connections = list (map (Connection , self ._dsxml .findall ('.//named-connections/named-connection/*' )))
100
+ # 'sqlproxy' connections (Tableau Server Connections) are not embedded into named-connection elements
101
+ # extract them manually for now
104
102
connections .extend (map (Connection , self ._dsxml .findall ("./connection[@class='sqlproxy']" )))
105
103
return connections
106
104
107
105
def _extract_legacy_connection (self ):
108
106
return list (map (Connection , self ._dsxml .findall ('connection' )))
109
107
110
108
def get_connections (self ):
109
+ """Find and return all connections based on file format version."""
110
+
111
111
if float (self ._dsversion ) < 10 :
112
112
connections = self ._extract_legacy_connection ()
113
113
else :
@@ -116,16 +116,11 @@ def get_connections(self):
116
116
117
117
118
118
class Datasource (object ):
119
- """
120
- A class for writing datasources to Tableau files.
119
+ """A class representing Tableau Data Sources, embedded in workbook files or
120
+ in TDS files.
121
121
122
122
"""
123
123
124
- ###########################################################################
125
- #
126
- # Public API.
127
- #
128
- ###########################################################################
129
124
def __init__ (self , dsxml , filename = None ):
130
125
"""
131
126
Constructor. Default is to create datasource from xml.
@@ -145,21 +140,23 @@ def __init__(self, dsxml, filename=None):
145
140
146
141
@classmethod
147
142
def from_file (cls , filename ):
148
- """Initialize datasource from file (.tds)"""
143
+ """Initialize datasource from file (.tds ot .tdsx )"""
149
144
150
- dsxml = xml_open (filename , cls . __name__ . lower () ).getroot ()
145
+ dsxml = xml_open (filename , 'datasource' ).getroot ()
151
146
return cls (dsxml , filename )
152
147
153
148
@classmethod
154
149
def from_connections (cls , caption , connections ):
150
+ """Create a new Data Source give a list of Connections."""
151
+
155
152
root = ET .Element ('datasource' , caption = caption , version = '10.0' , inline = 'true' )
156
153
outer_connection = ET .SubElement (root , 'connection' )
157
154
outer_connection .set ('class' , 'federated' )
158
155
named_conns = ET .SubElement (outer_connection , 'named-connections' )
159
156
for conn in connections :
160
157
nc = ET .SubElement (named_conns ,
161
158
'named-connection' ,
162
- name = make_unique_name (conn .dbclass ),
159
+ name = _make_unique_name (conn .dbclass ),
163
160
caption = conn .server )
164
161
nc .append (conn ._connectionXML )
165
162
return cls (root )
@@ -194,16 +191,10 @@ def save_as(self, new_filename):
194
191
195
192
xfile ._save_file (self ._filename , self ._datasourceTree , new_filename )
196
193
197
- ###########
198
- # name
199
- ###########
200
194
@property
201
195
def name (self ):
202
196
return self ._name
203
197
204
- ###########
205
- # version
206
- ###########
207
198
@property
208
199
def version (self ):
209
200
return self ._version
@@ -222,9 +213,6 @@ def caption(self):
222
213
del self ._datasourceXML .attrib ['caption' ]
223
214
self ._caption = ''
224
215
225
- ###########
226
- # connections
227
- ###########
228
216
@property
229
217
def connections (self ):
230
218
return self ._connections
@@ -234,16 +222,15 @@ def clear_repository_location(self):
234
222
if tag is not None :
235
223
self ._datasourceXML .remove (tag )
236
224
237
- ###########
238
- # fields
239
- ###########
240
225
@property
241
226
def fields (self ):
242
227
if not self ._fields :
243
228
self ._fields = self ._get_all_fields ()
244
229
return self ._fields
245
230
246
231
def _get_all_fields (se
10000
lf ):
232
+ # Some columns are represented by `column` tags and others as `metadata-record` tags
233
+ # Find them all and chain them into one dictionary
247
234
column_field_objects = self ._get_column_objects ()
248
235
existing_column_fields = [x .id for x in column_field_objects ]
249
236
metadata_only_field_objects = (x for x in self ._get_metadata_objects () if x .id not in existing_column_fields )
0 commit comments