Skip to content

Commit

Permalink
Merge pull request #78 from tableau/development
Browse files Browse the repository at this point in the history
Release 0.3
  • Loading branch information
Russell Hay authored Aug 31, 2016
2 parents bcd73c1 + 7078177 commit 9a098a7
Show file tree
Hide file tree
Showing 26 changed files with 355 additions and 39 deletions.
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ script:
# pep8
- pep8 .
# Examples
- (cd "Examples/Replicate Workbook" && python replicateWorkbook.py)
- (cd "Examples/List TDS Info" && python listTDSInfo.py)
- (cd "Examples/GetFields" && python show_fields.py)
- (cd "samples/replicate-workbook" && python replicate_workbook.py)
- (cd "samples/list-tds-info" && python list_tds_info.py)
- (cd "samples/show-fields" && python show_fields.py)

7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.3 (31 August 2016)

* Added basic connection class retargeting (#65)
* Added ability to create a new connection (#69)
* Added description to the field object (#73)
* Improved Test Coverage (#62, #67)

## 0.2 (22 July 2016)

* Added support for loading twbx and tdsx files (#43, #44)
Expand Down
1 change: 0 additions & 1 deletion Examples/GetFields/World.tds

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
############################################################
# Step 2) Open the .tds we want to replicate
############################################################
sourceTDS = Datasource.from_file('World.tds')
sourceTDS = Datasource.from_file('world.tds')

############################################################
# Step 3) List out info from the TDS
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
############################################################
# Step 2) Open the .twb we want to replicate
############################################################
sourceWB = Workbook('Sample - Superstore.twb')
sourceWB = Workbook('sample-superstore.twb')

############################################################
# Step 3) Use a database list (in CSV), loop thru and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
############################################################
# Step 2) Open the .tds we want to inspect
############################################################
sourceTDS = Datasource.from_file('World.tds')
sourceTDS = Datasource.from_file('world.tds')

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

if blank_line:
print('')
Expand Down
1 change: 1 addition & 0 deletions samples/show-fields/world.tds
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name='tableaudocumentapi',
version='0.2',
version='0.3',
author='Tableau Software',
author_email='[email protected]',
url='https://github.com/tableau/document-api-python',
Expand Down
22 changes: 22 additions & 0 deletions tableaudocumentapi/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Connection - A class for writing connections to Tableau files
#
###############################################################################
import xml.etree.ElementTree as ET
from tableaudocumentapi.dbclass import is_valid_dbclass


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

@classmethod
def from_attributes(cls, server, dbname, username, dbclass, authentication=''):
root = ET.Element('connection', authentication=authentication)
xml = cls(root)
xml.server = server
xml.dbname = dbname
xml.username = username
xml.dbclass = dbclass

return xml

###########
# dbname
###########
Expand Down Expand Up @@ -111,3 +124,12 @@ def authentication(self):
@property
def dbclass(self):
return self._class

@dbclass.setter
def dbclass(self, value):

if not is_valid_dbclass(value):
raise AttributeError("'{}' is not a valid database type".format(value))

self._class = value
self._connectionXML.set('class', value)
51 changes: 49 additions & 2 deletions tableaudocumentapi/datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
###############################################################################
import collections
import itertools
import random
import xml.etree.ElementTree as ET
import xml.sax.saxutils as sax
from uuid import uuid4

from tableaudocumentapi import Connection, xfile
from tableaudocumentapi import Field
Expand All @@ -19,7 +21,7 @@
# dropped, remove this and change the basestring references below to str
try:
basestring
except NameError:
except NameError: # pragma: no cover
basestring = str
########

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


class FieldDictionary(MultiLookupDict):

def used_by_sheet(self, name):
# If we pass in a string, no need to get complicated, just check to see if name is in
# the field's list of worksheets
Expand All @@ -63,7 +66,36 @@ def _column_object_from_metadata_xml(metadata_xml):
return _ColumnObjectReturnTuple(field_object.id, field_object)


def base36encode(number):
"""Converts an integer into a base36 string."""

ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"

base36 = ''
sign = ''

if number < 0:
sign = '-'
number = -number

if 0 <= number < len(ALPHABET):
return sign + ALPHABET[number]

while number != 0:
number, i = divmod(number, len(ALPHABET))
base36 = ALPHABET[i] + base36

return sign + base36


def make_unique_name(dbclass):
rand_part = base36encode(uuid4().int)
name = dbclass + '.' + rand_part
return name


class ConnectionParser(object):

def __init__(self, datasource_xml, version):
self._dsxml = datasource_xml
self._dsversion = version
Expand Down Expand Up @@ -113,9 +145,23 @@ def __init__(self, dsxml, filename=None):
def from_file(cls, filename):
"""Initialize datasource from file (.tds)"""

dsxml = xml_open(filename).getroot()
dsxml = xml_open(filename, cls.__name__.lower()).getroot()
return cls(dsxml, filename)

@classmethod
def from_connections(cls, caption, connections):
root = ET.Element('datasource', caption=caption, version='10.0', inline='true')
outer_connection = ET.SubElement(root, 'connection')
outer_connection.set('class', 'federated')
named_conns = ET.SubElement(outer_connection, 'named-connections')
for conn in connections:
nc = ET.SubElement(named_conns,
'named-connection',
name=make_unique_name(conn.dbclass),
caption=conn.server)
nc.append(conn._connectionXML)
return cls(root)

def save(self):
"""
Call finalization code and save file.
Expand Down Expand Up @@ -143,6 +189,7 @@ def save_as(self, new_filename):
Nothing.
"""

xfile._save_file(self._filename, self._datasourceTree, new_filename)

###########
Expand Down
60 changes: 60 additions & 0 deletions tableaudocumentapi/dbclass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@


KNOWN_DB_CLASSES = ('msaccess',
'msolap',
'bigquery',
'asterncluster',
'bigsql',
'aurora',
'awshadoophive',
'dataengine',
'DataStax',
'db2',
'essbase',
'exasolution',
'excel',
'excel-direct',
'excel-reader',
'firebird',
'powerpivot',
'genericodbc',
'google-analytics',
'googlecloudsql',
'google-sheets',
'greenplum',
'saphana',
'hadoophive',
'hortonworkshadoophive',
'maprhadoophive',
'marklogic',
'memsql',
'mysql',
'netezza',
'oracle',
'paraccel',
'postgres',
'progressopenedge',
'redshift',
'snowflake',
'spark',
'splunk',
'kognitio',
'sqlserver',
'salesforce',
'sapbw',
'sybasease',
'sybaseiq',
'tbio',
'teradata',
'vectorwise',
'vertica',
'denormalized-cube',
'csv',
'textscan',
'webdata',
'webdata-direct',
'cubeextract')


def is_valid_dbclass(dbclass):
return dbclass in KNOWN_DB_CLASSES
22 changes: 20 additions & 2 deletions tableaudocumentapi/field.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import functools
import xml.etree.ElementTree as ET


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

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

if column_xml is not None:
self._initialize_from_column_xml(column_xml)
if metadata_xml is not None:
self.apply_metadata(metadata_xml)
# This isn't currently never called because of the way we get the data from the xml,
# but during the refactor, we might need it. This is commented out as a reminder
# if metadata_xml is not None:
# self.apply_metadata(metadata_xml)

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

@property
def description(self):
""" The contents of the <desc> tag on a field """
return self._description

@property
def worksheets(self):
return list(self._worksheets)
Expand All @@ -182,3 +192,11 @@ def _read_calculation(xmldata):
return None

return calc.attrib.get('formula', None)

@staticmethod
def _read_description(xmldata):
description = xmldata.find('.//desc')
if description is None:
return None

return u'{}'.format(ET.tostring(description, encoding='utf-8')) # This is necessary for py3 support
26 changes: 13 additions & 13 deletions tableaudocumentapi/multilookup_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def _resolve_value(key, value):
if retval is None:
retval = getattr(value, key, None)
except AttributeError:
# We should never hit this.
retval = None
return retval

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

def _get_real_key(self, key):
if key in self._indexes['alias']:
return self._indexes['alias'][key]
if key in self._indexes['caption']:
return self._indexes['caption'][key]

return key

def __setitem__(self, key, value):
alias = _resolve_value('alias', value)
caption = _resolve_value('caption', value)
if alias is not None:
self._indexes['alias'][alias] = key
if caption is not None:
self._indexes['caption'][caption] = key
real_key = self._get_real_key(key)

dict.__setitem__(self, key, value)
dict.__setitem__(self, real_key, value)

def get(self, key, default_value=_no_default_value):
try:
Expand All @@ -58,9 +62,5 @@ def get(self, key, default_value=_no_default_value):
raise

def __getitem__(self, key):
if key in self._indexes['alias']:
key = self._indexes['alias'][key]
elif key in self._indexes['caption']:
key = self._indexes['caption'][key]

return dict.__getitem__(self, key)
real_key = self._get_real_key(key)
return dict.__getitem__(self, real_key)
2 changes: 1 addition & 1 deletion tableaudocumentapi/workbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self, filename):

self._filename = filename

self._workbookTree = xml_open(self._filename)
self._workbookTree = xml_open(self._filename, self.__class__.__name__.lower())

self._workbookRoot = self._workbookTree.getroot()
# prepare our datasource objects
Expand Down
Loading

0 comments on commit 9a098a7

Please sign in to comment.