Skip to content

Commit

Permalink
Merge pull request #725 from tableau/development
Browse files Browse the repository at this point in the history
Syncing master with v0.14.0 changes from development.
  • Loading branch information
Chris Shin authored Nov 9, 2020
2 parents 8d51355 + 1e089b4 commit bcb881c
Show file tree
Hide file tree
Showing 37 changed files with 4,956 additions and 178 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ target/
# pyenv
.python-version

# poetry
poetry.lock
pyproject.toml

# celery beat schedule file
celerybeat-schedule

Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
## 0.14.0 (6 Nov 2020)
* Added django-style filtering and sorting (#615)
* Added encoding tag-name before deleting (#687)
* Added 'Execute' Capability to permissions (#700)
* Added support for publishing workbook using file objects (#704)
* Added new fields to datasource_item (#705)
* Added all fields for users.get to get email and fullname (#713)
* Added support publishing datasource using file objects (#714)
* Improved request options by removing manual query param generation (#686)
* Improved publish_workbook sample to take in site (#694)
* Improved schedules.update() by removing constraint that required an interval (#711)
* Fixed site update/create not checking booleans properly (#723)

## 0.13 (1 Sept 2020)
* Added notes field to JobItem (#571)
* Added webpage_url field to WorkbookItem (#661)
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ The following people have contributed to this project to make it possible, and w
* [Stephen Mitchell](https://github.com/scuml)
* [absentmoose](https://github.com/absentmoose)
* [Paul Vickers](https://github.com/paulvic)
* [Madhura Selvarajan](https://github.com/maddy-at-leisure)
* [Niklas Nevalainen](https://github.com/nnevalainen)

## Core Team

Expand Down
40 changes: 40 additions & 0 deletions tableauserverclient/filesys_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,43 @@ def make_download_path(filepath, filename):
download_path = filepath + os.path.splitext(filename)[1]

return download_path


def get_file_object_size(file):
# Returns the size of a file object
file.seek(0, os.SEEK_END)
file_size = file.tell()
file.seek(0)
return file_size


def get_file_type(file):
# Tableau workbooks (twb) and data sources (tds) are both stored as xml files.
# Packaged workbooks (twbx) and data sources (tdsx) are zip files
# containing original files accompanied with supporting local files.

# This reference lists magic file signatures: https://www.garykessler.net/library/file_sigs.html
MAGIC_BYTES = {
'zip': bytes.fromhex("504b0304"),
'tde': bytes.fromhex("20020162"),
'xml': bytes.fromhex("3c3f786d6c20"),
'hyper': bytes.fromhex("487970657208000001000000")
}

# Peek first bytes of a file
first_bytes = file.read(32)

file_type = None
for ft, signature in MAGIC_BYTES.items():
if first_bytes.startswith(signature):
file_type = ft
break

# Return pointer back to start
file.seek(0)

if file_type is None:
error = "Unknown file type!"
raise ValueError(error)

return file_type
148 changes: 111 additions & 37 deletions tableauserverclient/models/datasource_item.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,50 @@
import xml.etree.ElementTree as ET
from .exceptions import UnpopulatedPropertyError
from .property_decorators import property_not_nullable, property_is_boolean
from .property_decorators import property_not_nullable, property_is_boolean, property_is_enum
from .tag_item import TagItem
from ..datetime_helpers import parse_datetime
import copy


class DatasourceItem(object):
class AskDataEnablement:
Enabled = 'Enabled'
Disabled = 'Disabled'
SiteDefault = 'SiteDefault'

def __init__(self, project_id, name=None):
self._ask_data_enablement = None
self._certified = None
self._certification_note = None
self._connections = None
self._content_url = None
self._created_at = None
self._datasource_type = None
self._encrypt_extracts = None
self._has_extracts = None
self._id = None
self._initial_tags = set()
self._project_name = None
self._updated_at = None
self._certified = None
self._certification_note = None
self._use_remote_query_agent = None
self._webpage_url = None
self.description = None
self.name = name
self.owner_id = None
self.project_id = project_id
self.tags = set()

self._permissions = None

@property
def ask_data_enablement(self):
return self._ask_data_enablement

@ask_data_enablement.setter
@property_is_enum(AskDataEnablement)
def ask_data_enablement(self, value):
self._ask_data_enablement = value

@property
def connections(self):
if self._connections is None:
Expand Down Expand Up @@ -65,6 +85,19 @@ def certification_note(self):
def certification_note(self, value):
self._certification_note = value

@property
def encrypt_extracts(self):
return self._encrypt_extracts

@encrypt_extracts.setter
@property_is_boolean
def encrypt_extracts(self, value):
self._encrypt_extracts = value

@property
def has_extracts(self):
return self._has_extracts

@property
def id(self):
return self._id
Expand All @@ -90,6 +123,19 @@ def datasource_type(self):
def updated_at(self):
return self._updated_at

@property
def use_remote_query_agent(self):
return self._use_remote_query_agent

@use_remote_query_agent.setter
@property_is_boolean
def use_remote_query_agent(self, value):
self._use_remote_query_agent = value

@property
def webpage_url(self):
return self._webpage_url

def _set_connections(self, connections):
self._connections = connections

Expand All @@ -100,38 +146,53 @@ def _parse_common_elements(self, datasource_xml, ns):
if not isinstance(datasource_xml, ET.Element):
datasource_xml = ET.fromstring(datasource_xml).find('.//t:datasource', namespaces=ns)
if datasource_xml is not None:
(_, _, _, _, _, updated_at, _, project_id, project_name, owner_id,
certified, certification_note) = self._parse_element(datasource_xml, ns)
self._set_values(None, None, None, None, None, updated_at, None, project_id,
project_name, owner_id, certified, certification_note)
(ask_data_enablement, certified, certification_note, _, _, _, _, encrypt_extracts, has_extracts,
_, _, owner_id, project_id, project_name, _, updated_at, use_remote_query_agent,
webpage_url) = self._parse_element(datasource_xml, ns)
self._set_values(ask_data_enablement, certified, certification_note, None, None, None, None,
encrypt_extracts, has_extracts, None, None, owner_id, project_id, project_name, None,
updated_at, use_remote_query_agent, webpage_url)
return self

def _set_values(self, id, name, datasource_type, content_url, created_at,
updated_at, tags, project_id, project_name, owner_id, certified, certification_note):
if id is not None:
self._id = id
if name:
self.name = name
if datasource_type:
self._datasource_type = datasource_type
def _set_values(self, ask_data_enablement, certified, certification_note, content_url, created_at, datasource_type,
description, encrypt_extracts, has_extracts, id_, name, owner_id, project_id, project_name, tags,
updated_at, use_remote_query_agent, webpage_url):
if ask_data_enablement is not None:
self._ask_data_enablement = ask_data_enablement
if certification_note:
self.certification_note = certification_note
self.certified = certified # Always True/False, not conditional
if content_url:
self._content_url = content_url
if created_at:
self._created_at = created_at
if updated_at:
self._updated_at = updated_at
if tags:
self.tags = tags
self._initial_tags = copy.copy(tags)
if datasource_type:
self._datasource_type = datasource_type
if description:
self.description = description
if encrypt_extracts is not None:
self.encrypt_extracts = str(encrypt_extracts).lower() == 'true'
if has_extracts is not None:
self._has_extracts = str(has_extracts).lower() == 'true'
if id_ is not None:
self._id = id_
if name:
self.name = name
if owner_id:
self.owner_id = owner_id
if project_id:
self.project_id = project_id
if project_name:
self._project_name = project_name
if owner_id:
self.owner_id = owner_id
if certification_note:
self.certification_note = certification_note
self.certified = certified # Always True/False, not conditional
if tags:
self.tags = tags
self._initial_tags = copy.copy(tags)
if updated_at:
self._updated_at = updated_at
if use_remote_query_agent is not None:
self._use_remote_query_agent = str(use_remote_query_agent).lower() == 'true'
if webpage_url:
self._webpage_url = webpage_url

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

for datasource_xml in all_datasource_xml:
(id_, name, datasource_type, content_url, created_at, updated_at,
tags, project_id, project_name, owner_id,
certified, certification_note) = cls._parse_element(datasource_xml, ns)
(ask_data_enablement, certified, certification_note, content_url, created_at, datasource_type,
description, encrypt_extracts, has_extracts, id_, name, owner_id, project_id, project_name, tags,
updated_at, use_remote_query_agent, webpage_url) = cls._parse_element(datasource_xml, ns)
datasource_item = cls(project_id)
datasource_item._set_values(id_, name, datasource_type, content_url, created_at, updated_at,
tags, None, project_name, owner_id, certified, certification_note)
datasource_item._set_values(ask_data_enablement, certified, certification_note, content_url,
created_at, datasource_type, description, encrypt_extracts,
has_extracts, id_, name, owner_id, None, project_name, tags, updated_at,
use_remote_query_agent, webpage_url)
all_datasource_items.append(datasource_item)
return all_datasource_items

@staticmethod
def _parse_element(datasource_xml, ns):
id_ = datasource_xml.get('id', None)
name = datasource_xml.get('name', None)
datasource_type = datasource_xml.get('type', None)
certification_note = datasource_xml.get('certificationNote', None)
certified = str(datasource_xml.get('isCertified', None)).lower() == 'true'
content_url = datasource_xml.get('contentUrl', None)
created_at = parse_datetime(datasource_xml.get('createdAt', None))
datasource_type = datasource_xml.get('type', None)
description = datasource_xml.get('description', None)
encrypt_extracts = datasource_xml.get('encryptExtracts', None)
has_extracts = datasource_xml.get('hasExtracts', None)
id_ = datasource_xml.get('id', None)
name = datasource_xml.get('name', None)
updated_at = parse_datetime(datasource_xml.get('updatedAt', None))
certification_note = datasource_xml.get('certificationNote', None)
certified = str(datasource_xml.get('isCertified', None)).lower() == 'true'
use_remote_query_agent = datasource_xml.get('useRemoteQueryAgent', None)
webpage_url = datasource_xml.get('webpageUrl', None)

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

return (id_, name, datasource_type, content_url, created_at, updated_at, tags, project_id,
project_name, owner_id, certified, certification_note)
ask_data_enablement = None
ask_data_elem = datasource_xml.find('.//t:askData', namespaces=ns)
if ask_data_elem is not None:
ask_data_enablement = ask_data_elem.get('enablement', None)

return (ask_data_enablement, certified, certification_note, content_url, created_at,
datasource_type, description, encrypt_extracts, has_extracts, id_, name, owner_id,
project_id, project_name, tags, updated_at, use_remote_query_agent, webpage_url)
1 change: 1 addition & 0 deletions tableauserverclient/models/permissions_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Capability:
ChangePermissions = 'ChangePermissions'
Connect = 'Connect'
Delete = 'Delete'
Execute = 'Execute'
ExportData = 'ExportData'
ExportImage = 'ExportImage'
ExportXml = 'ExportXml'
Expand Down
3 changes: 2 additions & 1 deletion tableauserverclient/models/view_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from ..datetime_helpers import parse_datetime
from .exceptions import UnpopulatedPropertyError
from .tag_item import TagItem
import copy


class ViewItem(object):
Expand Down Expand Up @@ -158,7 +159,7 @@ def from_xml_element(cls, parsed_response, ns, workbook_id=''):
if tags_elem is not None:
tags = TagItem.from_xml_element(tags_elem, ns)
view_item.tags = tags
view_item._initial_tags = tags
view_item._initial_tags = copy.copy(tags)

all_view_items.append(view_item)
return all_view_items
6 changes: 6 additions & 0 deletions tableauserverclient/server/endpoint/auth_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,9 @@ def switch_site(self, site_item):
self.parent_srv._set_auth(site_id, user_id, auth_token)
logger.info('Signed into {0} as user with id {1}'.format(self.parent_srv.server_address, user_id))
return Auth.contextmgr(self.sign_out)

@api(version="3.10")
def revoke_all_server_admin_tokens(self):
url = "{0}/{1}".format(self.baseurl, 'revokeAllServerAdminTokens')
self.post_request(url, '')
logger.info('Revoked all tokens for all server admins')
Loading

0 comments on commit bcb881c

Please sign in to comment.