8000 Merge pull request #78 from tableau/development · tableau/document-api-python@9a098a7 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9a098a7

Browse files
author
Russell Hay
authored
Merge pull request #78 from tableau/development
Release 0.3
2 parents bcd73c1 + 7078177 commit 9a098a7

26 files changed

+355
-39
lines changed

.travis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ script:
1616
# pep8
1717
- pep8 .
1818
# Examples
19-
- (cd "Examples/Replicate Workbook" && python replicateWorkbook.py)
20-
- (cd "Examples/List TDS Info" && python listTDSInfo.py)
21-
- (cd "Examples/GetFields" && python show_fields.py)
19+
- (cd "samples/replicate-workbook" && python replicate_workbook.py)
20+
- (cd "samples/list-tds-info" && python list_tds_info.py)
21+
- (cd "samples/show-fields" && python show_fields.py)
2222

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.3 (31 August 2016)
2+
3+
* Added basic connection class retargeting (#65)
4+
* Added ability to create a new connection (#69)
5+
* Added description to the field object (#73)
6+
* Improved Test Coverage (#62, #67)
7+
18
## 0.2 (22 July 2016)
29

310
* Added support for loading twbx and tdsx files (#43, #44)

Examples/GetFields/World.tds

Lines changed: 0 additions & 1 deletion
This file was deleted.

Examples/List TDS Info/listTDSInfo.py renamed to samples/list-tds-info/list_tds_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
############################################################
77
# Step 2) Open the .tds we want to replicate
88
############################################################
9-
sourceTDS = Datasource.from_file('World.tds')
9+
sourceTDS = Datasource.from_file('world.tds')
1010

1111
############################################################
1212
# Step 3) List out info from the TDS
File renamed without changes.

Examples/Replicate Workbook/replicateWorkbook.py renamed to samples/replicate-workbook/replicate_workbook.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
############################################################
99
# Step 2) Open the .twb we want to replicate
1010
############################################################
11-
sourceWB = Workbook('Sample - Superstore.twb')
11+
sourceWB = Workbook('sample-superstore.twb')
1212

1313
############################################################
1414
# Step 3) Use a database list (in CSV), loop thru and

Examples/GetFields/show_fields.py renamed to samples/show-fields/show_fields.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
############################################################
77
# Step 2) Open the .tds we want to inspect
88
############################################################
9-
sourceTDS = Datasource.from_file('World.tds')
9+
sourceTDS = Datasource.from_file('world.tds')
1010

1111
############################################################
1212
# Step 3) Print out all of the fields and what type they are
@@ -23,6 +23,8 @@
2323
if field.default_aggregation:
2424
print(' the default aggregation is {}'.format(field.default_aggregation))
2525
blank_line = True
26+
if field.description:
27+
print(' the description is {}'.format(field.description))
2628

2729
if blank_line:
2830
print('')

samples/show-fields/world.tds

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../list-tds-info/world.tds

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name='tableaudocumentapi',
8-
version='0.2',
8+
version='0.3',
99
author='Tableau Software',
1010
author_email='github@tableau.com',
1111
url='https://github.com/tableau/document-api-python',

tableaudocumentapi/connection.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# Connection - A class for writing connections to Tableau files
44
#
55
###############################################################################
6+
import xml.etree.ElementTree as ET
7+
from tableaudocumentapi.dbclass import is_valid_dbclass
68

79

810
class Connection(object):
@@ -32,6 +34,17 @@ def __init__(self, connxml):
3234
def __repr__(self):
3335
return "'<Connection server='{}' dbname='{}' @ {}>'".format(self._server, self._dbname, hex(id(self)))
3436

37+
@classmethod
38+
def from_attributes(cls, server, dbname, username, dbclass, authentication=''):
39+
root = ET.Element('connection', authentication=authentication)
40+
xml = cls(root)
41+
xml.server = server
42+
xml.dbname = dbname
43+
xml.username = username
44+
xml.dbclass = dbclass
45+
46+
return xml
47+
3548
###########
3649
# dbname
3750
###########
@@ -111,3 +124,12 @@ def authentication(self):
111124
@property
112125
def dbclass(self):
113126
return self._class
127+
128+
@dbclass.setter
129+
def dbclass(self, value):
130+
131+
if not is_valid_dbclass(value):
132+
raise AttributeError("'{}' is not a valid database type".format(value))
133+
134+
self._class = value
135+
self._connectionXML.set('class', value)

tableaudocumentapi/datasource.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
###############################################################################
66
import collections
77
import itertools
8+
import random
89
import xml.etree.ElementTree as ET
910
import xml.sax.saxutils as sax
11+
from uuid import uuid4
1012

1113
from tableaudocumentapi import Connection, xfile
1214
from tableaudocumentapi import Field
@@ -19,7 +21,7 @@
1921
# dropped, remove this and change the basestring references below to str
2022
try:
2123
basestring
22-
except NameError:
24+
except NameError: # pragma: no cover
2325
basestring = str
2426
########
2527

@@ -38,6 +40,7 @@ def _is_used_by_worksheet(names, field):
3840

3941

4042
class FieldDictionary(MultiLookupDict):
43+
4144
def used_by_sheet(self, name):
4245
# If we pass in a string, no need to get complicated, just check to see if name is in
4346
# the field's list of worksheets
@@ -63,7 +66,36 @@ def _column_object_from_metadata_xml(metadata_xml):
6366
return _ColumnObjectReturnTuple(field_object.id, field_object)
6467

6568

69+
def base36encode(number):
70+
"""Converts an integer into a base36 string."""
71+
72+
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
73+
74+
base36 = ''
75+
sign = ''
76+
77+
if number < 0:
78+
sign = '-'
79+
number = -number
80+
81+
if 0 <= number < len(ALPHABET):
82+
return sign + ALPHABET[number]
83+
84+
while number != 0:
85+
number, i = divmod(number, len(ALPHABET))
86+
base36 = ALPHABET[i] + base36
87+
88+
return sign + base36
89+
90+
91+
def make_unique_name(dbclass):
92+
rand_part = base36encode(uuid4().int)
93+
name = dbclass + '.' + rand_part
94+
return name
95+
96+
6697
class ConnectionParser(object):
98+
6799
def __init__(self, datasource_xml, version):
68100
self._dsxml = datasource_xml
69101
self._dsversion = version
@@ -113,9 +145,23 @@ def __init__(self, dsxml, filename=None):
113145
def from_file(cls, filename):
114146
"""Initialize datasource from file (.tds)"""
115147

116-
dsxml = xml_open(filename).getroot()
148+
dsxml = xml_open(filename, cls.__name__.lower()).getroot()
117149
return cls(dsxml, filename)
118150

151+
@classmethod
152+
def from_connections(cls, caption, connections):
153+
root = ET.Element('datasource', caption=caption, version='10.0', inline='true')
154+
outer_connection = ET.SubElement(root, 'connection')
155+
outer_connection.set('class', 'federated')
156+
named_conns = ET.SubElement(outer_connection, 'named-connections')
157+
for conn in connections:
158+
nc = ET.SubElement(named_conns,
159+
'named-connection',
160+
name=make_unique_name(conn.dbclass),
161+
caption=conn.server)
162+
nc.append(conn._connectionXML)
163+
return cls(root)
164+
119165
def save(self):
120166
"""
121167
Call finalization code and save file.
@@ -143,6 +189,7 @@ def save_as(self, new_filename):
143189
Nothing.
144190
145191
"""
192+
146193
xfile._save_file(self._filename, self._datasourceTree, new_filename)
147194

148195
###########

tableaudocumentapi/dbclass.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
2+
3+
KNOWN_DB_CLASSES = ('msaccess',
4+
'msolap',
5+
'bigquery',
6+
'asterncluster',
7+
'bigsql',
8+
'aurora',
9+
'awshadoophive',
10+
'dataengine',
11+
'DataStax',
12+
'db2',
13+
'essbase',
14+
'exasolution',
15+
'excel',
16+
'excel-direct',
17+
'excel-reader',
18+
'firebird',
19+
'powerpivot',
20+
'genericodbc',
21+
'google-analytics',
22+
'googlecloudsql',
23+
'google-sheets',
24+
'greenplum',
25+
'saphana',
26+
'hadoophive',
27+
'hortonworkshadoophive',
28+
'maprhadoophive',
29+
'marklogic',
30+
'memsql',
31+
'mysql',
32+
'netezza',
33+
'oracle',
34+
'paraccel',
35+
'postgres',
36+
'progressopenedge',
37+
'redshift',
38+
'snowflake',
39+
'spark',
40+
'splunk',
41+
'kognitio',
42+
'sqlserver',
43+
'salesforce',
44+
'sapbw',
45+
'sybasease',
46+
'sybaseiq',
47+
'tbio',
48+
'teradata',
49+
'vectorwise',
50+
'vertica',
51+
'denormalized-cube',
52+
'csv',
53+
'textscan',
54+
'webdata',
55+
'webdata-direct',
56+
'cubeextract')
57+
58+
59+
def is_valid_dbclass(dbclass):
60+
return dbclass in KNOWN_DB_CLASSES

tableaudocumentapi/field.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import functools
2+
import xml.etree.ElementTree as ET
3+
24

35
_ATTRIBUTES = [
46
'id', # Name of the field as specified in the file, usually surrounded by [ ]
@@ -8,6 +10,7 @@
810
'type', # three possible values: quantitative, ordinal, or nominal
911
'alias', # Name of the field as displayed in Tableau if the default name isn't wanted
1012
'calculation', # If this field is a calculated field, this will be the formula
13+
'description', # If this field has a description, this will be the description (including formatting tags)
1114
]
1215

1316
_METADATA_ATTRIBUTES = [
@@ -42,8 +45,10 @@ def __init__(self, column_xml=None, metadata_xml=None):
4245

4346
if column_xml is not None:
4447
self._initialize_from_column_xml(column_xml)
45-
if metadata_xml is not None:
46-
self.apply_metadata(metadata_xml)
48+
# This isn't currently never called because of the way we get the data from the xml,
49+
# but during the refactor, we might need it. This is commented out as a reminder
50+
# if metadata_xml is not None:
51+
# self.apply_metadata(metadata_xml)
4752

4853
elif metadata_xml is not None:
4954
self._initialize_from_metadata_xml(metadata_xml)
@@ -162,6 +167,11 @@ def default_aggregation(self):
162167
""" The default type of aggregation on the field (e.g Sum, Avg)"""
163168
return self._aggregation
164169

170+
@property
171+
def description(self):
172+
""" The contents of the <desc> tag on a field """
173+
return self._description
174+
165175
@property
166176
def worksheets(self):
167177
return list(self._worksheets 10000 )
@@ -182,3 +192,11 @@ def _read_calculation(xmldata):
182192
return None
183193

184194
return calc.attrib.get('formula', None)
195+
196+
@staticmethod
197+
def _read_description(xmldata):
198+
description = xmldata.find('.//desc')
199+
if description is None:
200+
return None
201+
202+
return u'{}'.format(ET.tostring(description, encoding='utf-8')) # This is necessary for py3 support

tableaudocumentapi/multilookup_dict.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def _resolve_value(key, value):
1313
if retval is None:
1414
retval = getattr(value, key, None)
1515
except AttributeError:
16+
# We should never hit this.
1617
retval = None
1718
return retval
1819

@@ -39,15 +40,18 @@ def _populate_indexes(self):
3940
self._indexes['alias'] = _build_index('alias', self)
4041
self._indexes['caption'] = _build_index('caption', self)
4142

43+
def _get_real_key(self, key):
44+
if key in self._indexes['alias']:
45+
return self._indexes['alias'][key]
46+
if key in self._indexes['caption']:
47+
return self._indexes['caption'][key]
48+
49+
return key
50+
4251
def __setitem__(self, key, value):
43-
alias = _resolve_value('alias', value)
44-
caption = _resolve_value('caption', value)
45-
if alias is not None:
46-
self._indexes['alias'][alias] = key
47-
if caption is not None:
48-
self._indexes['caption'][caption] = key
52+
real_key = self._get_real_key(key)
4953

50-
dict.__setitem__(self, key, value)
54+
dict.__setitem__(self, real_key, value)
5155

5256
def get(self, key, default_value=_no_default_value):
5357
try:
@@ -58,9 +62,5 @@ def get(self, key, default_value=_no_default_value):
5862
raise
5963

6064
def __getitem__(self, key):
61-
if key in self._indexes['alias']:
62-
key = self._indexes['alias'][key]
63-
elif key in self._indexes['caption']:
64-
key = self._indexes['caption'][key]
65-
66-
return dict.__getitem__(self, key)
65+
real_key = self._get_real_key(key)
66+
return dict.__getitem__(self, real_key)

tableaudocumentapi/workbook.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self, filename):
3232

3333
self._filename = filename
3434

35-
self._workbookTree = xml_open(self._filename)
35+
self._workbookTree = xml_open(self._filename, self.__class__.__name__.lower())
3636

3737
self._workbookRoot = self._workbookTree.getroot()
3838
# prepare our datasource objects

0 commit comments

Comments
 (0)
0