From c67766e14c711def954ebc78e5867e38af5677f1 Mon Sep 17 00:00:00 2001 From: Russell Hay Date: Fri, 8 Jul 2016 09:48:04 -0700 Subject: [PATCH 01/14] first stab at an API, not correct in retrospect --- tableaudocumentapi/datasource.py | 22 +++- tableaudocumentapi/field.py | 13 ++ tableaudocumentapi/multilookup_dict.py | 21 ++++ tableaudocumentapi/workbook.py | 2 +- test/assets/datasource_test.twb | 164 +++++++++++++++++++++++++ test/test_datasource.py | 30 ++++- test/test_predicate_mld.py | 36 ++++++ 7 files changed, 282 insertions(+), 6 deletions(-) create mode 100644 test/assets/datasource_test.twb create mode 100644 test/test_predicate_mld.py diff --git a/tableaudocumentapi/datasource.py b/tableaudocumentapi/datasource.py index 924575d..d0aaa07 100644 --- a/tableaudocumentapi/datasource.py +++ b/tableaudocumentapi/datasource.py @@ -10,7 +10,7 @@ from tableaudocumentapi import Connection, xfile from tableaudocumentapi import Field -from tableaudocumentapi.multilookup_dict import MultiLookupDict +from tableaudocumentapi.multilookup_dict import MultiLookupDict, PredicatedDictionary def _mapping_from_xml(root_xml, column_xml): @@ -55,7 +55,7 @@ class Datasource(object): # Public API. # ########################################################################### - def __init__(self, dsxml, filename=None): + def __init__(self, dsxml, filename=None, workbook_xml_root=None): """ Constructor. Default is to create datasource from xml. @@ -70,6 +70,9 @@ def __init__(self, dsxml, filename=None): self._datasourceXML, version=self._version) self._connections = self._connection_parser.get_connections() self._fields = None + self._usedFields = None + if workbook_xml_root is not None: + self._prepare_from_workbook(workbook_xml_root) @classmethod def from_file(cls, filename): @@ -140,7 +143,22 @@ def fields(self): self._fields = self._get_all_fields() return self._fields + @property + def usedFields(self): + if not self._usedFields: + raise RuntimeError('usedFields not initialized properly, most likely not loaded from a workbook') + return self._usedFields + def _get_all_fields(self): column_objects = (_mapping_from_xml(self._datasourceTree, xml) for xml in self._datasourceTree.findall('.//column')) return MultiLookupDict({k: v for k, v in column_objects}) + + def _prepare_from_workbook(self, workbook_xml): + self._fields = self._get_all_fields() + for element in workbook_xml.findall(".//datasource-dependencies[@datasource='{}']/column".format(self.name)): + column_name = element.attrib.get('name', None) + column = self._fields.get(column_name, None) + if column is not None: + column.set_in_use() + self._usedFields = PredicatedDictionary(lambda x: x.in_use, self._fields) diff --git a/tableaudocumentapi/field.py b/tableaudocumentapi/field.py index 8162cdb..610232d 100644 --- a/tableaudocumentapi/field.py +++ b/tableaudocumentapi/field.py @@ -33,10 +33,19 @@ def __init__(self, xmldata): for attrib in _METADATA_ATTRIBUTES: setattr(self, '_{}'.format(attrib), None) + self._in_use = False + + ######################################## + # Special Case methods for construction fields from various sources + # not intended for client use + ######################################## def apply_metadata(self, metadata_record): for attrib in _METADATA_ATTRIBUTES: self._apply_attribute(metadata_record, attrib, functools.partial(_find_metadata_record, metadata_record)) + def set_in_use(self): + self._in_use = True + @classmethod def from_xml(cls, xmldata): return cls(xmldata) @@ -121,6 +130,10 @@ def default_aggregation(self): """ The default type of aggregation on the field (e.g Sum, Avg)""" return self._aggregation + @property + def in_use(self): + return self._in_use + ###################################### # Special Case handling methods for reading the values from the XML ###################################### diff --git a/tableaudocumentapi/multilookup_dict.py b/tableaudocumentapi/multilookup_dict.py index 64b742a..9a2ed57 100644 --- a/tableaudocumentapi/multilookup_dict.py +++ b/tableaudocumentapi/multilookup_dict.py @@ -64,3 +64,24 @@ def __getitem__(self, key): key = self._indexes['caption'][key] return dict.__getitem__(self, key) + + +class PredicatedDictionary(object): + def __init__(self, predicate, dictionary): + self.predicate = predicate + self.dictionary = dictionary + + def __getitem__(self, key): + if self.predicate(self.dictionary[key]): + return self.dictionary[key] + else: + raise KeyError(key) + + def __setitem__(self, key, value): + self.dictionary[key] = value + + def get(self, key, default_value=None): + try: + return self[key] + except KeyError: + return default_value diff --git a/tableaudocumentapi/workbook.py b/tableaudocumentapi/workbook.py index 9e29973..c9e68db 100644 --- a/tableaudocumentapi/workbook.py +++ b/tableaudocumentapi/workbook.py @@ -100,7 +100,7 @@ def _prepare_datasources(self, xmlRoot): # loop through our datasources and append for datasource in xmlRoot.find('datasources'): - ds = Datasource(datasource) + ds = Datasource(datasource, workbook_xml_root=xmlRoot) datasources.append(ds) return datasources diff --git a/test/assets/datasource_test.twb b/test/assets/datasource_test.twb new file mode 100644 index 0000000..9ef54d8 --- /dev/null +++ b/test/assets/datasource_test.twb @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + a + 130 + [a] + [xy] + a + 1 + string + Count + 255 + true + + "SQL_WVARCHAR" + "SQL_C_WCHAR" + "true" + + + + x + 3 + [x] + [xy] + x + 2 + integer + Sum + 10 + true + + "SQL_INTEGER" + "SQL_C_SLONG" + + + + y + 3 + [y] + [xy] + y + 3 + integer + Sum + 10 + true + + "SQL_INTEGER" + "SQL_C_SLONG" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +