10000 Recreate MR from KernpunktAnalytics · randyoswald/document-api-python@965812b · GitHub
[go: up one dir, main page]

Skip to content

Commit 965812b

Browse files
committed
Recreate MR from KernpunktAnalytics
1 parent 593b63e commit 965812b

File tree

5 files changed

+480
-1
lines changed

5 files changed

+480
-1
lines changed

tableaudocumentapi/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .field import Field
2+
from .folder import Folder, FolderItem
23
from .connection import Connection
34
from .datasource import Datasource, ConnectionParser
45
from .workbook import Workbook

tableaudocumentapi/datasource.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from uuid import uuid4
66

77
from tableaudocumentapi import Connection, xfile
8-
from tableaudocumentapi import Field
8+
from tableaudocumentapi import Field, Folder
99
from tableaudocumentapi.multilookup_dict import MultiLookupDict
1010
from tableaudocumentapi.xfile import xml_open
1111

@@ -137,6 +137,7 @@ def __init__(self, dsxml, filename=None):
137137
self._datasourceXML, version=self._version)
138138
self._connections = self._connection_parser.get_connections()
139139
self._fields = None
140+
self._folders = None
140141

141142
@classmethod
142143
def from_file(cls, filename):

tableaudocumentapi/folder.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import xml.etree.ElementTree as ET
2+
from functools import wraps
3+
4+
from tableaudocumentapi import Field
5+
6+
########
7+
# This is needed in order to determine if something is a string or not. It is necessary because
8+
# of differences between python2 (basestring) and python3 (str). If python2 support is every
9+
# dropped, remove this and change the basestring references below to str
10+
try:
11+
basestring
12+
except NameError: # pragma: no cover
13+
basestring = str
14+
15+
16+
class AlreadyMemberOfThisFolderException(Exception):
17+
pass
18+
19+
20+
class MemberOfMultipleFoldersException(Exception):
21+
pass
22+
23+
24+
def argument_is_one_of(*allowed_values):
25+
def property_type_decorator(func):
26+
@wraps(func)
27+
def wrapper(self, value):
28+
if value not in allowed_values:
29+
error = "Invalid argument: {0}. {1} must be one of {2}."
30+
msg = error.format(value, func.__name__, allowed_values)
31+
raise ValueError(error)
32+
return func(self, value)
33+
return wrapper
34+
return property_type_decorator
35+
36+
37+
class FolderItem(object):
38+
""" FolderItems belong to Folders and describe the Field-Objects
39+
that belong to a folder
40+
"""
41+
42+
def __init__(self, name, _type):
43+
self.name = name
44+
self.type = _type
45+
46+
@classmethod
47+
def from_xml(cls, xml):
48+
return cls(xml.get('name', None), xml.get('type', None))
49+
50+
@classmethod
51+
def from_field(cls, field):
52+
return cls(field.id, 'field')
53+
54+
55+
class Folder(object):
56+
""" This class represents a folder in a Datasource.
57+
58+
Folders have a name, a role (dimensions or measures) and contain Items
59+
"""
60+
61+
def __init__(self, datasource, xml):
62+
self._datasource = datasource
63+
self._xml = xml
64+
self.name = self._xml.get('name', None)
65+
self.role = self._xml.get('role', None)
66+
folder_item_xml = self._xml.findall('folder-item')
67+
self._folder_items = [FolderItem.from_xml(xml) for xml in folder_item_xml]
68+
69+
# Alternative constructors
70+
71+
@classmethod
72+
def all_folders_from_datasource(cls, datasource):
73+
folders_xml = datasource._datasourceTree.findall('.//folder')
74+
return [cls(datasource, xml) for xml in folders_xml]
75+
76+
@classmethod
77+
def from_name_and_role(cls, name, role, parent_datasource):
78+
"""Creates a new folder with a given name and a given role.
79+
"""
80+
attributes = {
81+
'name': name,
82+
'role': role
83+
}
84+
xml = ET.Element('folder', attrib=attributes)
85+
return cls(parent_datasource, xml)
86+
87+
# Properties
88+
89+
@property
90+
def folder_items(self):
91+
return self._folder_items
92+
93+
@property
94+
def name(self):
95+
return self._name
96+
97+
@name.setter
98+
def name(self, name):
99+
self._name = name
100+
self._xml.set('name', name)
101+
102+
@property
103+
def role(self):
104+
return self._role
105+
106+
@property
107+
def xml(self):
108+
return self._xml
109+
110+
@role.setter
111+
@argument_is_one_of('dimensions', 'measures')
112+
def role(self, role):
113+
self._role = role
114+
self._xml.set('role', role)
115+
116+
# Functions that deal with folder-items
117+
118+
def add_field(self, field):
119+
""" Adds a field to this folder
120+
"""
121+
if not isinstance(field, Field):
122+
msg = 'Can only add Fields to Folders, not {}'
123+
raise ValueError(msg.format(type(field)))
124+
if self.has_item(field):
125+
raise AlreadyMemberOfThisFolderException(field)
126+
if any(f.has_item(field) for f in self._datasource.folders.values()):
127+
raise MemberOfMultipleFoldersException(field)
128+
129+
self._add_field(field)
130+
131+
def _add_field(self, field):
132+
""" Internal function to add a field
133+
"""
134+
folder_item = FolderItem.from_field(field)
135+
self._folder_items.append(folder_item)
136+
name, _type = folder_item.name, folder_item.type
137+
ET.SubElement(self._xml, 'folder-item', {'name': name, 'type': _type})
138+
139+
def remove_field(self, field):
140+
""" Removes a field from this folder
141+
"""
142+
if not isinstance(field, Field):
143+
msg = 'Can only remove Fields from Folders, not {}'
144+
raise ValueError(msg.format(type(field)))
145+
if not self.has_item(field):
146+
raise ValueError('This field is not a member of the folder')
147+
148+
self._remove_field(field)
149+
150+
def _remove_field(self, field):
151+
""" Internal function to remove field
152+
"""
153+
# remove from the data structure
154+
folder_items = filter(lambda f: f.name == field.id, self.folder_items)
155+
folder_item = list(folder_items)[0]
156+
self.folder_items.remove(folder_item)
157+
158+
# remove from xml
159+
xml_elem = self.xml.find("folder-item[@name='{}']".format(field.id))
160+
self.xml.remove(xml_elem)
161+
162+
# Utility functions
163+
164+
def has_item(self, item):
165+
""" Returns True if the given item is a FolderItem of this Folder.
166+
Item may be String, Field or FolderItem
167+
"""
168+
if isinstance(item, FolderItem):
169+
return item in self.folder_items
170+
elif isinstance(item, Field):
171+
return item.id in map(lambda fi: fi.name, self.folder_items)
172+
elif isinstance(item, basestring):
173+
return item in map(lambda fi: fi.name, self.folder_items)
174+
else:
175+
msg = 'Argument must be either String or FolderItem, not {}'
176+
raise ValueError(msg.format(type(item)))

test/assets/folder_test.tds

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?xml version='1.0' encoding='utf-8' ?>
2+
3+
<!-- build 10000.16.0812.0001 -->
4+
<datasource formatted-name='test_base' inline='true' source-platform='win' version='10.0' xmlns:user='http://www.tableausoftware.com/xml/user'>
5+
<connection class='federated'>
6+
<named-connections>
7+
<named-connection caption='my.server.com' name='postgres.0slpt5u1crlvd816iez320cauqo3'>
8+
<connection authentication='username-password' class='postgres' dbname='my_db' odbc-native-protocol='yes' port='5432' server='my.server.com' username='my_db_user'/>
9+
</named-connection>
10+
</named-connections>
11+
<relation connection='postgres.0slpt5u1crlvd816iez320cauqo3' name='my_data' table='[public].[my_data]' type='table'/>
12+
<metadata-records>
13+
<metadata-record class='column'>
14+
<remote-name>name</remote-name>
15+
<remote-type>130</remote-type>
16+
<local-name>[name]</local-name>
17+
<parent-name>[my_data]</parent-name>
18+
<remote-alias>name</remote-alias>
19+
<ordinal>1</ordinal>
20+
<local-type>string</local-type>
21+
<aggregation>Count</aggregation>
22+
<width>8190</width>
23+
<contains-null>true</contains-null>
24+
<cast-to-local-type>true</cast-to-local-type>
25+
<collation flag='0' name='LEN_RUS'/>
26+
<attributes>
27+
<attribute datatype='string' name='DebugRemoteType'>&quot;SQL_WLONGVARCHAR&quot;</attribute>
28+
<attribute datatype='string' name='DebugWireType'>&quot;SQL_C_WCHAR&quot;</attribute>
29+
</attributes>
30+
</metadata-record>
31+
<metadata-record class='column'>
32+
<remote-name>typ</remote-name>
33+
<remote-type>130</remote-type>
34+
<local-name>[typ]</local-name>
35+
<parent-name>[my_data]</parent-name>
36+
<remote-alias>typ</remote-alias>
37+
<ordinal>2</ordinal>
38+
<local-type>string</local-type>
39+
<aggregation>Count</aggregation>
40+
<width>8190</width>
41+
<contains-null>true</contains-null>
42+
<cast-to-local-type>true</cast-to-local-type>
43+
<collation flag='0' name='LEN_RUS'/>
44+
<attributes>
45+
<attribute datatype='string' name='DebugRemoteType'>&quot;SQL_WLONGVARCHAR&quot;</attribute>
46+
<attribute datatype='string' name='DebugWireType'>&quot;SQL_C_WCHAR&quot;</attribute>
47+
</attributes>
48+
</metadata-record>
49+
<metadata-record class='column'>
50+
<remote-name>amount</remote-name>
51+
<remote-type>3</remote-type>
52+
<local-name>[amount]</local-name>
53+
<parent-name>[my_data]</parent-name>
54+
<remote-alias>amount</remote-alias>
55+
<ordinal>3</ordinal>
56+
<local-type>integer</local-type>
57+
<aggregation>Sum</aggregation>
58+
<precision>10</precision>
59+
<contains-null>true</contains-null>
60+
<attributes>
61+
<attribute datatype='string' name='DebugRemoteType'>&quot;SQL_INTEGER&quot;</attribute>
62+
<attribute datatype='string' name='DebugWireType'>&quot;SQL_C_SLONG&quot;</attribute>
63+
</attributes>
64+
</metadata-record>
65+
<metadata-record class='column'>
66+
<remote-name>price</remote-name>
67+
<remote-type>5</remote-type>
68+
<local-name>[price]</local-name>
69+
<parent-name>[my_data]</parent-name>
70+
<remote-alias>price</remote-alias>
71+
<ordinal>4</ordinal>
72+
<local-type>real</local-type>
73+
<aggregation>Sum</aggregation>
74+
<precision>17</precision>
75+
<contains-null>true</contains-null>
76+
<attributes>
77+
<attribute datatype='string' name='DebugRemoteType'>&quot;SQL_FLOAT&quot;</attribute>
78+
<attribute datatype='string' name='DebugWireType'>&quot;SQL_C_DOUBLE&quot;</attribute>
79+
</attributes>
80+
</metadata-record>
81+
</metadata-records>
82+
</connection>
83+
<aliases enabled='yes'/>
84+
<column caption='Calculation One' datatype='integer' name='[Calculation_357754699576291328]' role='measure' type='quantitative'>
85+
<calculation class='tableau' formula='[amount] * 100'/>
86+
</column>
87+
<column datatype='integer' name='[Number of Records]' role='measure' type='quantitative' user:auto-column='numrec'>
88+
<calculation class='tableau' formula='1'/>
89+
</column>
90+
<column caption='The Amount' datatype='integer' name='[amount]' role='dimension' type='quantitative'/>
91+
<column caption='The Name' datatype='string' name='[name]' role='dimension' type='nominal'/>
92+
<column caption='The Price' datatype='real' name='[price]' role='measure' type='quantitative'/>
93+
<column caption='The Type' datatype='string' name='[typ]' role='dimension' type='nominal'>
94+
<aliases>
95+
<alias key='&quot;mein typ&quot;' value='mein typ alias'/>
96+
</aliases>
97+
</column>
98+
<folder name="MyTestFolder" role="dimensions">
99+
<folder-item name="[price]" type="field"/>
100+
</folder>
101+
<folder name="MySecondTestFolder" role="dimensions">
102+
<folder-item name="[name]" type="field"/>
103+
</folder>
104+
<layout dim-ordering='alphabetic' dim-percentage='0.48449' measure-ordering='alphabetic' measure-percentage='0.51551' show-aliased-fields='true' show-structure='true'/>
105+
<semantic-values>
106+
<semantic-value key='[Country].[Name]' value='&quot;Germany&quot;'/>
107+
</semantic-values>
108+
<date-options start-of-week='monday'/>
109+
</datasource>

0 commit comments

Comments
 (0)
0