Skip to content

Commit bcb881c

Browse files
author
Chris Shin
authored
Merge pull request #725 from tableau/development
Syncing master with v0.14.0 changes from development.
2 parents 8d51355 + 1e089b4 commit bcb881c

37 files changed

+4956
-178
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ target/
7676
# pyenv
7777
.python-version
7878

79+
# poetry
80+
poetry.lock
81+
pyproject.toml
82+
7983
# celery beat schedule file
8084
celerybeat-schedule
8185

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
## 0.14.0 (6 Nov 2020)
2+
* Added django-style filtering and sorting (#615)
3+
* Added encoding tag-name before deleting (#687)
4+
* Added 'Execute' Capability to permissions (#700)
5+
* Added support for publishing workbook using file objects (#704)
6+
* Added new fields to datasource_item (#705)
7+
* Added all fields for users.get to get email and fullname (#713)
8+
* Added support publishing datasource using file objects (#714)
9+
* Improved request options by removing manual query param generation (#686)
10+
* Improved publish_workbook sample to take in site (#694)
11+
* Improved schedules.update() by removing constraint that required an interval (#711)
12+
* Fixed site update/create not checking booleans properly (#723)
13+
114
## 0.13 (1 Sept 2020)
215
* Added notes field to JobItem (#571)
316
* Added webpage_url field to WorkbookItem (#661)

CONTRIBUTORS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ The following people have contributed to this project to make it possible, and w
3939
* [Stephen Mitchell](https://github.com/scuml)
4040
* [absentmoose](https://github.com/absentmoose)
4141
* [Paul Vickers](https://github.com/paulvic)
42+
* [Madhura Selvarajan](https://github.com/maddy-at-leisure)
43+
* [Niklas Nevalainen](https://github.com/nnevalainen)
4244

4345
## Core Team
4446

tableauserverclient/filesys_helpers.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,43 @@ def make_download_path(filepath, filename):
2020
download_path = filepath + os.path.splitext(filename)[1]
2121

2222
return download_path
23+
24+
25+
def get_file_object_size(file):
26+
# Returns the size of a file object
27+
file.seek(0, os.SEEK_END)
28+
file_size = file.tell()
29+
file.seek(0)
30+
return file_size
31+
32+
33+
def get_file_type(file):
34+
# Tableau workbooks (twb) and data sources (tds) are both stored as xml files.
35+
# Packaged workbooks (twbx) and data sources (tdsx) are zip files
36+
# containing original files accompanied with supporting local files.
37+
38+
# This reference lists magic file signatures: https://www.garykessler.net/library/file_sigs.html
39+
MAGIC_BYTES = {
40+
'zip': bytes.fromhex("504b0304"),
41+
'tde': bytes.fromhex("20020162"),
42+
'xml': bytes.fromhex("3c3f786d6c20"),
43+
'hyper': bytes.fromhex("487970657208000001000000")
44+
}
45+
46+
# Peek first bytes of a file
47+
first_bytes = file.read(32)
48+
49+
file_type = None
50+
for ft, signature in MAGIC_BYTES.items():
51+
if first_bytes.startswith(signature):
52+
file_type = ft
53+
break
54+
55+
# Return pointer back to start
56+
file.seek(0)
57+
58+
if file_type is None:
59+
error = "Unknown file type!"
60+
raise ValueError(error)
61+
62+
return file_type

tableauserverclient/models/datasource_item.py

Lines changed: 111 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,50 @@
11
import xml.etree.ElementTree as ET
22
from .exceptions import UnpopulatedPropertyError
3-
from .property_decorators import property_not_nullable, property_is_boolean
3+
from .property_decorators import property_not_nullable, property_is_boolean, property_is_enum
44
from .tag_item import TagItem
55
from ..datetime_helpers import parse_datetime
66
import copy
77

88

99
class DatasourceItem(object):
10+
class AskDataEnablement:
11+
Enabled = 'Enabled'
12+
Disabled = 'Disabled'
13+
SiteDefault = 'SiteDefault'
14+
1015
def __init__(self, project_id, name=None):
16+
self._ask_data_enablement = None
17+
self._certified = None
18+
self._certification_note = None
1119
self._connections = None
1220
self._content_url = None
1321
self._created_at = None
1422
self._datasource_type = None
23+
self._encrypt_extracts = None
24+
self._has_extracts = None
1525
self._id = None
1626
self._initial_tags = set()
1727
self._project_name = None
1828
self._updated_at = None
19-
self._certified = None
20-
self._certification_note = None
29+
self._use_remote_query_agent = None
30+
self._webpage_url = None
31+
self.description = None
2132
self.name = name
2233
self.owner_id = None
2334
self.project_id = project_id
2435
self.tags = set()
2536

2637
self._permissions = None
2738

39+
@property
40+
def ask_data_enablement(self):
41+
return self._ask_data_enablement
42+
43+
@ask_data_enablement.setter
44+
@property_is_enum(AskDataEnablement)
45+
def ask_data_enablement(self, value):
46+
self._ask_data_enablement = value
47+
2848
@property
2949
def connections(self):
3050
if self._connections is None:
@@ -65,6 +85,19 @@ def certification_note(self):
6585
def certification_note(self, value):
6686
self._certification_note = value
6787

88+
@property
89+
def encrypt_extracts(self):
90+
return self._encrypt_extracts
91+
92+
@encrypt_extracts.setter
93+
@property_is_boolean
94+
def encrypt_extracts(self, value):
95+
self._encrypt_extracts = value
96+
97+
@property
98+
def has_extracts(self):
99+
return self._has_extracts
100+
68101
@property
69102
def id(self):
70103
return self._id
@@ -90,6 +123,19 @@ def datasource_type(self):
90123
def updated_at(self):
91124
return self._updated_at
92125

126+
@property
127+
def use_remote_query_agent(self):
128+
return self._use_remote_query_agent
129+
130+
@use_remote_query_agent.setter
131+
@property_is_boolean
132+
def use_remote_query_agent(self, value):
133+
self._use_remote_query_agent = value
134+
135+
@property
136+
def webpage_url(self):
137+
return self._webpage_url
138+
93139
def _set_connections(self, connections):
94140
self._connections = connections
95141

@@ -100,38 +146,53 @@ def _parse_common_elements(self, datasource_xml, ns):
100146
if not isinstance(datasource_xml, ET.Element):
101147
datasource_xml = ET.fromstring(datasource_xml).find('.//t:datasource', namespaces=ns)
102148
if datasource_xml is not None:
103-
(_, _, _, _, _, updated_at, _, project_id, project_name, owner_id,
104-
certified, certification_note) = self._parse_element(datasource_xml, ns)
105-
self._set_values(None, None, None, None, None, updated_at, None, project_id,
106-
project_name, owner_id, certified, certification_note)
149+
(ask_data_enablement, certified, certification_note, _, _, _, _, encrypt_extracts, has_extracts,
150+
_, _, owner_id, project_id, project_name, _, updated_at, use_remote_query_agent,
151+
webpage_url) = self._parse_element(datasource_xml, ns)
152+
self._set_values(ask_data_enablement, certified, certification_note, None, None, None, None,
153+
encrypt_extracts, has_extracts, None, None, owner_id, project_id, project_name, None,
154+
updated_at, use_remote_query_agent, webpage_url)
107155
return self
108156

109-
def _set_values(self, id, name, datasource_type, content_url, created_at,
110-
updated_at, tags, project_id, project_name, owner_id, certified, certification_note):
111-
if id is not None:
112-
self._id = id
113-
if name:
114-
self.name = name
115-
if datasource_type:
116-
self._datasource_type = datasource_type
157+
def _set_values(self, ask_data_enablement, certified, certification_note, content_url, created_at, datasource_type,
158+
description, encrypt_extracts, has_extracts, id_, name, owner_id, project_id, project_name, tags,
159+
updated_at, use_remote_query_agent, webpage_url):
160+
if ask_data_enablement is not None:
161+
self._ask_data_enablement = ask_data_enablement
162+
if certification_note:
163+
self.certification_note = certification_note
164+
self.certified = certified # Always True/False, not conditional
117165
if content_url:
118166
self._content_url = content_url
119167
if created_at:
120168
self._created_at = created_at
121-
if updated_at:
122-
self._updated_at = updated_at
123-
if tags:
124-
self.tags = tags
125-
self._initial_tags = copy.copy(tags)
169+
if datasource_type:
170+
self._datasource_type = datasource_type
171+
if description:
172+
self.description = description
173+
if encrypt_extracts is not None:
174+
self.encrypt_extracts = str(encrypt_extracts).lower() == 'true'
175+
if has_extracts is not None:
176+
self._has_extracts = str(has_extracts).lower() == 'true'
177+
if id_ is not None:
178+
self._id = id_
179+
if name:
180+
self.name = name
181+
if owner_id:
182+
self.owner_id = owner_id
126183
if project_id:
127184
self.project_id = project_id
128185
if project_name:
129186
self._project_name = project_name
130-
if owner_id:
131-
self.owner_id = owner_id
132-
if certification_note:
133-
self.certification_note = certification_note
134-
self.certified = certified # Always True/False, not conditional
187+
if tags:
188+
self.tags = tags
189+
self._initial_tags = copy.copy(tags)
190+
if updated_at:
191+
self._updated_at = updated_at
192+
if use_remote_query_agent is not None:
193+
self._use_remote_query_agent = str(use_remote_query_agent).lower() == 'true'
194+
if webpage_url:
195+
self._webpage_url = webpage_url
135196

136197
@classmethod
137198
def from_response(cls, resp, ns):
@@ -140,25 +201,32 @@ def from_response(cls, resp, ns):
140201
all_datasource_xml = parsed_response.findall('.//t:datasource', namespaces=ns)
141202

142203
for datasource_xml in all_datasource_xml:
143-
(id_, name, datasource_type, content_url, created_at, updated_at,
144-
tags, project_id, project_name, owner_id,
145-
certified, certification_note) = cls._parse_element(datasource_xml, ns)
204+
(ask_data_enablement, certified, certification_note, content_url, created_at, datasource_type,
205+
description, encrypt_extracts, has_extracts, id_, name, owner_id, project_id, project_name, tags,
206+
updated_at, use_remote_query_agent, webpage_url) = cls._parse_element(datasource_xml, ns)
146207
datasource_item = cls(project_id)
147-
datasource_item._set_values(id_, name, datasource_type, content_url, created_at, updated_at,
148-
tags, None, project_name, owner_id, certified, certification_note)
208+
datasource_item._set_values(ask_data_enablement, certified, certification_note, content_url,
209+
created_at, datasource_type, description, encrypt_extracts,
210+
has_extracts, id_, name, owner_id, None, project_name, tags, updated_at,
211+
use_remote_query_agent, webpage_url)
149212
all_datasource_items.append(datasource_item)
150213
return all_datasource_items
151214

152215
@staticmethod
153216
def _parse_element(datasource_xml, ns):
154-
id_ = datasource_xml.get('id', None)
155-
name = datasource_xml.get('name', None)
156-
datasource_type = datasource_xml.get('type', None)
217+
certification_note = datasource_xml.get('certificationNote', None)
218+
certified = str(datasource_xml.get('isCertified', None)).lower() == 'true'
157219
content_url = datasource_xml.get('contentUrl', None)
158220
created_at = parse_datetime(datasource_xml.get('createdAt', None))
221+
datasource_type = datasource_xml.get('type', None)
222+
description = datasource_xml.get('description', None)
223+
encrypt_extracts = datasource_xml.get('encryptExtracts', None)
224+
has_extracts = datasource_xml.get('hasExtracts', None)
225+
id_ = datasource_xml.get('id', None)
226+
name = datasource_xml.get('name', None)
159227
updated_at = parse_datetime(datasource_xml.get('updatedAt', None))
160-
certification_note = datasource_xml.get('certificationNote', None)
161-
certified = str(datasource_xml.get('isCertified', None)).lower() == 'true'
228+
use_remote_query_agent = datasource_xml.get('useRemoteQueryAgent', None)
229+
webpage_url = datasource_xml.get('webpageUrl', None)
162230

163231
tags = None
164232
tags_elem = datasource_xml.find('.//t:tags', namespaces=ns)
@@ -177,5 +245,11 @@ def _parse_element(datasource_xml, ns):
177245
if owner_elem is not None:
178246
owner_id = owner_elem.get('id', None)
179247

180-
return (id_, name, datasource_type, content_url, created_at, updated_at, tags, project_id,
181-
project_name, owner_id, certified, certification_note)
248+
ask_data_enablement = None
249+
ask_data_elem = datasource_xml.find('.//t:askData', namespaces=ns)
250+
if ask_data_elem is not None:
251+
ask_data_enablement = ask_data_elem.get('enablement', None)
252+
253+
return (ask_data_enablement, certified, certification_note, content_url, created_at,
254+
datasource_type, description, encrypt_extracts, has_extracts, id_, name, owner_id,
255+
project_id, project_name, tags, updated_at, use_remote_query_agent, webpage_url)

tableauserverclient/models/permissions_item.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Capability:
2020
ChangePermissions = 'ChangePermissions'
2121
Connect = 'Connect'
2222
Delete = 'Delete'
23+
Execute = 'Execute'
2324
ExportData = 'ExportData'
2425
ExportImage = 'ExportImage'
2526
ExportXml = 'ExportXml'

tableauserverclient/models/view_item.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from ..datetime_helpers import parse_datetime
33
from .exceptions import UnpopulatedPropertyError
44
from .tag_item import TagItem
5+
import copy
56

67

78
class ViewItem(object):
@@ -158,7 +159,7 @@ def from_xml_element(cls, parsed_response, ns, workbook_id=''):
158159
if tags_elem is not None:
159160
tags = TagItem.from_xml_element(tags_elem, ns)
160161
view_item.tags = tags
161-
view_item._initial_tags = tags
162+
view_item._initial_tags = copy.copy(tags)
162163

163164
all_view_items.append(view_item)
164165
return all_view_items

tableauserverclient/server/endpoint/auth_endpoint.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,9 @@ def switch_site(self, site_item):
7373
self.parent_srv._set_auth(site_id, user_id, auth_token)
7474
logger.info('Signed into {0} as user with id {1}'.format(self.parent_srv.server_address, user_id))
7575
return Auth.contextmgr(self.sign_out)
76+
77+
@api(version="3.10")
78+
def revoke_all_server_admin_tokens(self):
79+
url = "{0}/{1}".format(self.baseurl, 'revokeAllServerAdminTokens')
80+
self.post_request(url, '')
81+
logger.info('Revoked all tokens for all server admins')

0 commit comments

Comments
 (0)