8000 External Content Support (Databases and Tables) (#445) · SnarkyPapi/server-client-python@19c6322 · GitHub
[go: up one dir, main page]

Skip to content

Commit 19c6322

Browse files
authored
External Content Support (Databases and Tables) (tableau#445)
Add the ability to query and update external content managed by Tableau Catalog. These APIs are read-only unless the Data Management Add-On is enabled on the Server/Online Site. Add: - Databases - Tables - Columns And permissions support preemptively. Permissions APIs are disabled until a 2019.3 maintenance release (2019.3.1 or 2019.3.2)
1 parent 869ae40 commit 19c6322

19 files changed

+996
-8
lines changed

tableauserverclient/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from .namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE
22
from .models import ConnectionCredentials, ConnectionItem, DatasourceItem,\
3-
GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem, \
4-
SiteItem, TableauAuth, PersonalAccessTokenAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError, \
5-
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem, \
6-
SubscriptionItem, Target, PermissionsRule, Permission
3+
GroupItem, JobItem, BackgroundJobItem, PaginationItem, ProjectItem, ScheduleItem,\
4+
SiteItem, TableauAuth, PersonalAccessTokenAuth, UserItem, ViewItem, WorkbookItem, UnpopulatedPropertyError,\
5+
HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval, IntervalItem, TaskItem,\
6+
SubscriptionItem, Target, PermissionsRule, Permission, DatabaseItem, TableItem, ColumnItem
77
from .server import RequestOptions, CSVRequestOptions, ImageRequestOptions, PDFRequestOptions, Filter, Sort, \
88
Server, ServerResponseError, MissingRequiredFieldError, NotSignedInError, Pager
99
from ._version import get_versions

tableauserverclient/models/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from .connection_credentials import ConnectionCredentials
22
from .connection_item import ConnectionItem
3+
from .column_item import ColumnItem
34
from .datasource_item import DatasourceItem
5+
from .database_item import DatabaseItem
46
from .exceptions import UnpopulatedPropertyError
57
from .group_item import GroupItem
68
from .interval_item import IntervalItem, DailyInterval, WeeklyInterval, MonthlyInterval, HourlyInterval
@@ -13,6 +15,7 @@
1315
from .tableau_auth import TableauAuth
1416
from .personal_access_token_auth import PersonalAccessTokenAuth
1517
from .target import Target
18+
from .table_item import TableItem
1619
from .task_item import TaskItem
1720
from .user_item import UserItem
1821
from .view_item import ViewItem
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import xml.etree.ElementTree as ET
2+
3+
from .property_decorators import property_is_enum, property_not_empty
4+
from .exceptions import UnpopulatedPropertyError
5+
6+
7+
class ColumnItem(object):
8+
def __init__(self, name, description=None):
9+
self._id = None
10+
self.description = description
11+
self.name = name
12+
13+
@property
14+
def id(self):
15+
return self._id
16+
17+
@property
18+
def name(self):
19+
return self._name
20+
21+
@name.setter
22+
@property_not_empty
23+
def name(self, value):
24+
self._name = value
25+
26+
@property
27+
def description(self):
28+
return self._description
29+
30+
@description.setter
31+
def description(self, value):
32+
self._description = value
33+
34+
@property
35+
def remote_type(self):
36+
return self._remote_type
37+
38+
def _set_values(self, id, name, description, remote_type):
39+
if id is not None:
40+
self._id = id
41+
if name:
42+
self._name = name
43+
if description:
44+
self.description = description
45+
if remote_type:
46+
self._remote_type = remote_type
47+
48+
@classmethod
49+
def from_response(cls, resp, ns):
50+
all_column_items = list()
51+
parsed_response = ET.fromstring(resp)
52+
all_column_xml = parsed_response.findall('.//t:column', namespaces=ns)
53+
54+
for column_xml in all_column_xml:
55+
(id, name, description, remote_type) = cls._parse_element(column_xml, ns)
56+
column_item = cls(name)
57+
column_item._set_values(id, name, description, remote_type)
58+
all_column_items.append(column_item)
59+
60+
return all_column_items
61+
62+
@staticmethod
63+
def _parse_element(column_xml, ns):
64+
id = column_xml.get('id', None)
65+
name = column_xml.get('name', None)
66+
description = column_xml.get('description', None)
67+
remote_type = column_xml.get('remoteType', None)
68+
69+
return id, name, description, remote_type
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
import xml.etree.ElementTree as ET
2+
3+
from .permissions_item import Permission
4+
5+
from .property_decorators import property_is_enum, property_not_empty, property_is_boolean
6+
from .exceptions import UnpopulatedPropertyError
7+
8+
9+
class DatabaseItem(object):
10+
class ContentPermissions:
11+
LockedToProject = 'LockedToDatabase'
12+
ManagedByOwner = 'ManagedByOwner'
13+
14+
def __init__(self, name, description=None, content_permissions=None):
15+
self._id = None
16+
self.name = name
17+
self.description = description
18+
self.content_permissions = content_permissions
19+
self._certified = None
20+
self._certification_note = None
21+
self._contact_id = None
22+
23+
self._connector_url = None
24+
self._connection_type = None
25+
self._embedded = None
26+
self._file_extension = None
27+
self._file_id = None
28+
self._file_path = None
29+
self._host_name = None
30+
self._metadata_type = None
31+
self._mime_type = None
32+
self._port = None
33+
self._provider = None
34+
self._request_url = None
35+
36+
self._permissions = None
37+
self._default_table_permissions = None
38+
39+
self._tables = None # Not implemented yet
40+
41+
@property
42+
def content_permissions(self):
43+
return self._content_permissions
44+
45+
@property
46+
def permissions(self):
47+
if self._permissions is None:
48+
error = "Project item must be populated with permissions first."
49+
raise UnpopulatedPropertyError(error)
50+
return self._permissions()
51+
52+
@property
53+
def default_table_permissions(self):
54+
if self._default_table_permissions is None:
55+
error = "Project item must be populated with permissions first."
56+
raise UnpopulatedPropertyError(error)
57+
return self._default_table_permissions()
58+
59+
@content_permissions.setter
60+
@property_is_enum(ContentPermissions)
61+
def content_permissions(self, value):
62+
self._content_permissions = value
63+
64+
@property
65+
def id(self):
66+
return self._id
67+
68+
@property
69+
def name(self):
70+
return self._name
71+
72+
@name.setter
73+
@property_not_empty
74+
def name(self, value):
75+
self._name = value
76+
77+
@property
78+
def description(self):
79+
return self._description
80+
81+
@description.setter
82+
def description(self, value):
83+
self._description = value
84+
85+
@property
86+
def embedded(self):
87+
return self._embedded
88+
89+
@property
90+
def certified(self):
91+
return self._certified
92+
93+
@certified.setter
94+
@property_is_boolean
95+
def certified(self, value):
96+
self._certified = value
97+
98+
@property
99+
def certification_note(self):
100+
return self._certification_note
101+
102+
@certification_note.setter
103+
def certification_note(self, value):
104+
self._certification_note = value
105+
106+
@property
107+
def metadata_type(self):
108+
return self._metadata_type
109+
110+
@property
111+
def host_name(self):
112+
return self._host_name
113+
114+
@property
115+
def port(self):
116+
return self._port
117+
118+
@property
119+
def file_path(self):
120+
return self._file_path
121+
122+
@property
123+
def provider(self):
124+
return self._provider
125+
126+
@property
127+
def mime_type(self):
128+
return self._mime_type
129+
130+
@property
131+
def connector_url(self):
132+
return self._connector_url
133+
134+
@property
135+
def connection_type(self):
136+
return self._connection_type
137+
138+
@property
139+
def request_url(self):
140+
return self._request_url
141+
142+
@property
143+
def file_extension(self):
144+
return self._file_extension
145+
146+
@property
147+
def file_id(self):
148+
return self._file_id
149+
150+
@property
151+
def contact_id(self):
152+
return self._contact_id
153+
154+
@contact_id.setter
155+
def contact_id(self, value):
156+
self._contact_id = value
157+
158+
@property
159+
def tables(self):
160+
if self._tables is None:
161+
error = "Database must be populated with tables first."
162+
raise UnpopulatedPropertyError(error)
163+
# Each call to `.tables` should create a new pager, this just runs the callable
164+
return self._tables()
165+
166+
def _set_values(self, database_values):
167+
# ID & Settable
168+
if 'id' in database_values:
169+
self._id = database_values['id']
170+
171+
if 'contact' in database_values:
172+
self._contact_id = database_values['contact']['id']
173+
174+
if 'name' in database_values:
175+
self._name = database_values['name']
176+
177+
if 'description' in database_values:
178+
self._description = database_values['description']
179+
180+
if 'isCertified' in database_values:
181+
self._certified = string_to_bool(database_values['isCertified'])
182+
183+
if 'certificationNote' in database_values:
184+
self._certification_note = database_values['certificationNote']
185+
186+
# Not settable, alphabetical
187+
188+
if 'connectionType' in database_values:
189+
self._connection_type = database_values['connectionType']
190+
191+
if 'connectorUrl' in database_values:
192+
self._connector_url = database_values['connectorUrl']
193+
194+
if 'contentPermissions' in database_values:
195+
self._content_permissions = database_values['contentPermissions']
196+
197+
if 'embedded' in database_values:
198+
self._embedded = string_to_bool(database_values['embedded'])
199+
200+
if 'fileExtension' in database_values:
201+
self._file_extension = database_values['fileExtension']
202+
203+
if 'fileId' in database_values:
204+
self._file_id = database_values['fileId']
205+
206+
if 'filePath' in database_values:
207+
self._file_path = database_values['filePath']
208+
209+
if 'hostName' in database_values:
210+
self._host_name = database_values['hostName']
211+
212+
if 'mimeType' in database_values:
213+
self._mime_type = database_values['mimeType']
214+
215+
if 'port' in database_values:
216+
self._port = int(database_values['port'])
217+
218+
if 'provider' in database_values:
219+
self._provider = database_values['provider']
220+
221+
if 'requestUrl' in database_values:
222+
self._request_url = database_values['requestUrl']
223+
224+
if 'type' in database_values:
225+
self._metadata_type = database_values['type']
226+
227+
def _set_permissions(self, permissions):
228+
self._permissions = permissions
229+
230+
def _set_tables(self, tables):
231+
self._tables = tables
232+
233+
def _set_default_permissions(self, permissions, content_type):
234+
setattr(self, "_default_{content}_permissions".format(content=content_type), permissions)
235+
236+
@classmethod
237+
def from_response(cls, resp, ns):
238+
all_database_items = list()
239+
parsed_response = ET.fromstring(resp)
240+
all_database_xml = parsed_response.findall('.//t:database', namespaces=ns)
241+
242+
for database_xml in all_database_xml:
243+
parsed_database = cls._parse_element(database_xml, ns)
244+
database_item = cls(parsed_database['name'])
245+
database_item._set_values(parsed_database)
246+
all_database_items.append(database_item)
247+
return all_database_items
248+
249+
@staticmethod
250+
def _parse_element(database_xml, ns):
251+
database_values = database_xml.attrib.copy()
252+
contact = database_xml.find('.//t:contact', namespaces=ns)
253+
if contact is not None:
254+
database_values['contact'] = contact.attrib.copy()
255+
return database_values
256+
257+
258+
# Used to convert string represented boolean to a boolean type
259+
def string_to_bool(s):
260+
return s.lower() == 'true'

tableauserverclient/models/permissions_item.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class Resource:
3636
Workbook = 'workbook'
3737
Datasource = 'datasource'
3838
Flow = 'flow'
39+
Table = 'table'
40+
Database = 'database'
3941

4042

4143
class PermissionsRule(object):

0 commit comments

Comments
 (0)
0