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