diff --git a/.travis.yml b/.travis.yml
index b0d0b8b7b..33e133203 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,7 @@ python:
- "3.3"
- "3.4"
- "3.5"
+ - "3.6"
- "pypy"
# command to install dependencies
install:
@@ -14,4 +15,4 @@ script:
# Tests
- python setup.py test
# pep8 - disabled for now until we can scrub the files to make sure we pass before turning it on
- - pycodestyle .
+ - pycodestyle tableauserverclient test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9aa404ed..8505d4c90 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,17 @@
+## 0.3 (11 January 2017)
+
+* Return DateTime objects instead of strings (#102)
+* UserItem now is compatible with Pager (#107, #109)
+* Deprecated site in favor of site_id (#97)
+* Improved handling of large downloads (#105, #111)
+* Added support for oAuth when publishing (#117)
+* Added Testing against Py36 (#122, #123)
+* Added Version Checking to use highest supported REST api version (#100)
+* Added Infrastructure for throwing error if trying to do something that is not supported by REST api version (#124)
+* Various Code Cleanup
+* Added Documentation (#98)
+* Improved Test Infrastructure (#91)
+
## 0.2 (02 November 2016)
* Added Initial Schedules Support (#48)
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index c97e9301d..553a3c2b9 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -5,6 +5,7 @@ The following people have contributed to this project to make it possible, and w
## Contributors
* [geordielad](https://github.com/geordielad)
+* [Hugo Stijns)(https://github.com/hugoboos)
* [kovner](https://github.com/kovner)
@@ -14,3 +15,5 @@ The following people have contributed to this project to make it possible, and w
* [lgraber](https://github.com/lgraber)
* [t8y8](https://github.com/t8y8)
* [RussTheAerialist](https://github.com/RussTheAerialist)
+* [Ben Lower](https://github.com/benlower)
+* [Jared Dominguez](https://github.com/jdomingu)
diff --git a/README.md b/README.md
index 099f4ba7d..f1f8f462a 100644
--- a/README.md
+++ b/README.md
@@ -11,3 +11,4 @@ This repository contains Python source code and sample files.
For more information on installing and using TSC, see the documentation:
+
diff --git a/samples/initialize_server.py b/samples/initialize_server.py
new file mode 100644
index 000000000..e37317c0e
--- /dev/null
+++ b/samples/initialize_server.py
@@ -0,0 +1,107 @@
+####
+# This script sets up a server. It uploads datasources and workbooks from the local filesystem.
+#
+# By default, all content is published to the Default project on the Default site.
+####
+
+import tableauserverclient as TSC
+import argparse
+import getpass
+import logging
+import glob
+
+
+def main():
+ parser = argparse.ArgumentParser(description='Initialize a server with content.')
+ parser.add_argument('--server', '-s', required=True, help='server address')
+ parser.add_argument('--datasources-folder', '-df', required=True, help='folder containing datasources')
+ parser.add_argument('--workbooks-folder', '-wf', required=True, help='folder containing workbooks')
+ parser.add_argument('--site', '-si', required=False, default='Default', help='site to use')
+ parser.add_argument('--project', '-p', required=False, default='Default', help='project to use')
+ parser.add_argument('--username', '-u', required=True, help='username to sign into server')
+ parser.add_argument('--logging-level', '-l', choices=['debug', 'info', 'error'], default='error',
+ help='desired logging level (set to error by default)')
+ args = parser.parse_args()
+
+ password = getpass.getpass("Password: ")
+
+ # Set logging level based on user input, or error by default
+ logging_level = getattr(logging, args.logging_level.upper())
+ logging.basicConfig(level=logging_level)
+
+ ################################################################################
+ # Step 1: Sign in to server.
+ ################################################################################
+ tableau_auth = TSC.TableauAuth(args.username, password)
+ server = TSC.Server(args.server)
+
+ with server.auth.sign_in(tableau_auth):
+
+ ################################################################################
+ # Step 2: Create the site we need only if it doesn't exist
+ ################################################################################
+ print("Checking to see if we need to create the site...")
+
+ all_sites, _ = server.sites.get()
+ existing_site = next((s for s in all_sites if s.name == args.site), None)
+
+ # Create the site if it doesn't exist
+ if existing_site is None:
+ print("Site not found: {0} Creating it...").format(args.site)
+ new_site = TSC.SiteItem(name=args.site, content_url=args.site.replace(" ", ""),
+ admin_mode=TSC.SiteItem.AdminMode.ContentAndUsers)
+ server.sites.create(new_site)
+ else:
+ print("Site {0} exists. Moving on...").format(args.site)
+
+ ################################################################################
+ # Step 3: Sign-in to our target site
+ ################################################################################
+ print("Starting our content upload...")
+ server_upload = TSC.Server(args.server)
+ tableau_auth.site = args.site
+
+ with server_upload.auth.sign_in(tableau_auth):
+
+ ################################################################################
+ # Step 4: Create the project we need only if it doesn't exist
+ ################################################################################
+ all_projects, _ = server_upload.projects.get()
+ project = next((p for p in all_projects if p.name == args.project), None)
+
+ # Create our project if it doesn't exist
+ if project is None:
+ print("Project not found: {0} Creating it...").format(args.project)
+ new_project = TSC.ProjectItem(name=args.project)
+ project = server_upload.projects.create(new_project)
+
+ ################################################################################
+ # Step 5: Set up our content
+ # Publish datasources to our site and project
+ # Publish workbooks to our site and project
+ ################################################################################
+ publish_datasources_to_site(server_upload, project, args.datasources_folder)
+ publish_workbooks_to_site(server_upload, project, args.workbooks_folder)
+
+
+def publish_datasources_to_site(server_object, project, folder):
+ path = folder + '/*.tds*'
+
+ for fname in glob.glob(path):
+ new_ds = TSC.DatasourceItem(project.id)
+ new_ds = server_object.datasources.publish(new_ds, fname, server_object.PublishMode.Overwrite)
+ print("Datasource published. ID: {0}".format(new_ds.id))
+
+
+def publish_workbooks_to_site(server_object, project, folder):
+ path = folder + '/*.twb*'
+
+ for fname in glob.glob(path):
+ new_workbook = TSC.WorkbookItem(project.id)
+ new_workbook.show_tabs = True
+ new_workbook = server_object.workbooks.publish(new_workbook, fname, server_object.PublishMode.Overwrite)
+ print("Workbook published. ID: {0}".format(new_workbook.id))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/samples/pagination_sample.py b/samples/pagination_sample.py
index 882fc85ad..25effd7b2 100644
--- a/samples/pagination_sample.py
+++ b/samples/pagination_sample.py
@@ -66,5 +66,6 @@ def main():
# >>> request_options = TSC.RequestOptions(pagesize=1000)
# >>> all_workbooks = list(TSC.Pager(server.workbooks, request_options))
+
if __name__ == '__main__':
main()
diff --git a/setup.py b/setup.py
index e4214aa70..ac932390d 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@
setup(
name='tableauserverclient',
- version='0.2',
+ version='0.3',
author='Tableau',
author_email='github@tableau.com',
url='https://github.com/tableau/server-client-python',
diff --git a/tableauserverclient/datetime_helpers.py b/tableauserverclient/datetime_helpers.py
new file mode 100644
index 000000000..af88d5c71
--- /dev/null
+++ b/tableauserverclient/datetime_helpers.py
@@ -0,0 +1,37 @@
+import datetime
+
+
+# This code below is from the python documentation for tzinfo: https://docs.python.org/2.3/lib/datetime-tzinfo.html
+ZERO = datetime.timedelta(0)
+HOUR = datetime.timedelta(hours=1)
+
+# A UTC class.
+
+
+class UTC(datetime.tzinfo):
+ """UTC"""
+
+ def utcoffset(self, dt):
+ return ZERO
+
+ def tzname(self, dt):
+ return "UTC"
+
+ def dst(self, dt):
+ return ZERO
+
+
+utc = UTC()
+
+TABLEAU_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
+
+
+def parse_datetime(date):
+ if date is None:
+ return None
+
+ return datetime.datetime.strptime(date, TABLEAU_DATE_FORMAT).replace(tzinfo=utc)
+
+
+def format_datetime(date):
+ return date.astimezone(tz=utc).strftime(TABLEAU_DATE_FORMAT)
diff --git a/tableauserverclient/models/connection_credentials.py b/tableauserverclient/models/connection_credentials.py
index d823b0b7f..8c3a77925 100644
--- a/tableauserverclient/models/connection_credentials.py
+++ b/tableauserverclient/models/connection_credentials.py
@@ -9,10 +9,11 @@ class ConnectionCredentials(object):
"""
- def __init__(self, name, password, embed=True):
+ def __init__(self, name, password, embed=True, oauth=False):
self.name = name
self.password = password
self.embed = embed
+ self.oauth = oauth
@property
def embed(self):
@@ -22,3 +23,12 @@ def embed(self):
@property_is_boolean
def embed(self, value):
self._embed = value
+
+ @property
+ def oauth(self):
+ return self._oauth
+
+ @oauth.setter
+ @property_is_boolean
+ def oauth(self, value):
+ self._oauth = value
diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py
index 3ae4c5743..2ae469674 100644
--- a/tableauserverclient/models/datasource_item.py
+++ b/tableauserverclient/models/datasource_item.py
@@ -3,6 +3,7 @@
from .property_decorators import property_not_nullable
from .tag_item import TagItem
from .. import NAMESPACE
+from ..datetime_helpers import parse_datetime
class DatasourceItem(object):
@@ -118,8 +119,8 @@ def _parse_element(datasource_xml):
name = datasource_xml.get('name', None)
datasource_type = datasource_xml.get('type', None)
content_url = datasource_xml.get('contentUrl', None)
- created_at = datasource_xml.get('createdAt', None)
- updated_at = datasource_xml.get('updatedAt', None)
+ created_at = parse_datetime(datasource_xml.get('createdAt', None))
+ updated_at = parse_datetime(datasource_xml.get('updatedAt', None))
tags = None
tags_elem = datasource_xml.find('.//t:tags', namespaces=NAMESPACE)
diff --git a/tableauserverclient/models/property_decorators.py b/tableauserverclient/models/property_decorators.py
index de8fe8d8c..77612b172 100644
--- a/tableauserverclient/models/property_decorators.py
+++ b/tableauserverclient/models/property_decorators.py
@@ -1,5 +1,12 @@
+import datetime
import re
from functools import wraps
+from ..datetime_helpers import parse_datetime
+try:
+ basestring
+except NameError:
+ # In case we are in python 3 the string check is different
+ basestring = str
def property_is_enum(enum_type):
@@ -99,3 +106,25 @@ def validate_regex_decorator(self, value):
return func(self, value)
return validate_regex_decorator
return wrapper
+
+
+def property_is_datetime(func):
+ """ Takes the following datetime format and turns it into a datetime object:
+
+ 2016-08-18T18:25:36Z
+
+ Because we return everything with Z as the timezone, we assume everything is in UTC and create
+ a timezone aware datetime.
+ """
+
+ @wraps(func)
+ def wrapper(self, value):
+ if isinstance(value, datetime.datetime):
+ return func(self, value)
+ if not isinstance(value, basestring):
+ raise ValueError("Cannot convert {} into a datetime, cannot update {}".format(value.__class__.__name__,
+ func.__name__))
+
+ dt = parse_datetime(value)
+ return func(self, dt)
+ return wrapper
diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py
index b0f7d1edb..84b070044 100644
--- a/tableauserverclient/models/schedule_item.py
+++ b/tableauserverclient/models/schedule_item.py
@@ -4,6 +4,7 @@
from .interval_item import IntervalItem, HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval
from .property_decorators import property_is_enum, property_not_nullable, property_is_int
from .. import NAMESPACE
+from ..datetime_helpers import parse_datetime
class ScheduleItem(object):
@@ -208,12 +209,12 @@ def _parse_element(schedule_xml):
id = schedule_xml.get('id', None)
name = schedule_xml.get('name', None)
state = schedule_xml.get('state', None)
- created_at = schedule_xml.get('createdAt', None)
- updated_at = schedule_xml.get('updatedAt', None)
+ created_at = parse_datetime(schedule_xml.get('createdAt', None))
+ updated_at = parse_datetime(schedule_xml.get('updatedAt', None))
schedule_type = schedule_xml.get('type', None)
frequency = schedule_xml.get('frequency', None)
- next_run_at = schedule_xml.get('nextRunAt', None)
- end_schedule_at = schedule_xml.get('endScheduleAt', None)
+ next_run_at = parse_datetime(schedule_xml.get('nextRunAt', None))
+ end_schedule_at = parse_datetime(schedule_xml.get('endScheduleAt', None))
execution_order = schedule_xml.get('executionOrder', None)
priority = schedule_xml.get('priority', None)
diff --git a/tableauserverclient/models/tableau_auth.py b/tableauserverclient/models/tableau_auth.py
index 7670e2812..3b60741d6 100644
--- a/tableauserverclient/models/tableau_auth.py
+++ b/tableauserverclient/models/tableau_auth.py
@@ -18,3 +18,10 @@ def site(self):
warnings.warn('TableauAuth.site is deprecated, use TableauAuth.site_id instead.',
DeprecationWarning)
return self.site_id
+
+ @site.setter
+ def site(self, value):
+ import warnings
+ warnings.warn('TableauAuth.site is deprecated, use TableauAuth.site_id instead.',
+ DeprecationWarning)
+ self.site_id = value
diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py
index 49a048f69..2df6764d9 100644
--- a/tableauserverclient/models/user_item.py
+++ b/tableauserverclient/models/user_item.py
@@ -2,6 +2,7 @@
from .exceptions import UnpopulatedPropertyError
from .property_decorators import property_is_enum, property_not_empty, property_not_nullable
from .. import NAMESPACE
+from ..datetime_helpers import parse_datetime
class UserItem(object):
@@ -118,7 +119,7 @@ def _set_values(self, id, name, site_role, last_login,
@classmethod
def from_response(cls, resp):
- all_user_items = set()
+ all_user_items = []
parsed_response = ET.fromstring(resp)
all_user_xml = parsed_response.findall('.//t:user', namespaces=NAMESPACE)
for user_xml in all_user_xml:
@@ -127,7 +128,7 @@ def from_response(cls, resp):
user_item = cls(name, site_role)
user_item._set_values(id, name, site_role, last_login, external_auth_user_id,
fullname, email, auth_setting, domain_name)
- all_user_items.add(user_item)
+ all_user_items.append(user_item)
return all_user_items
@staticmethod
@@ -135,7 +136,7 @@ def _parse_element(user_xml):
id = user_xml.get('id', None)
name = user_xml.get('name', None)
site_role = user_xml.get('siteRole', None)
- last_login = user_xml.get('lastLogin', None)
+ last_login = parse_datetime(user_xml.get('lastLogin', None))
external_auth_user_id = user_xml.get('externalAuthUserId', None)
fullname = user_xml.get('fullName', None)
email = user_xml.get('email', None)
diff --git a/tableauserverclient/models/workbook_item.py b/tableauserverclient/models/workbook_item.py
index 9ccde5606..26a3a00c3 100644
--- a/tableauserverclient/models/workbook_item.py
+++ b/tableauserverclient/models/workbook_item.py
@@ -4,6 +4,7 @@
from .tag_item import TagItem
from .view_item import ViewItem
from .. import NAMESPACE
+from ..datetime_helpers import parse_datetime
import copy
@@ -163,8 +164,8 @@ def _parse_element(workbook_xml):
id = workbook_xml.get('id', None)
name = workbook_xml.get('name', None)
content_url = workbook_xml.get('contentUrl', None)
- created_at = workbook_xml.get('createdAt', None)
- updated_at = workbook_xml.get('updatedAt', None)
+ created_at = parse_datetime(workbook_xml.get('createdAt', None))
+ updated_at = parse_datetime(workbook_xml.get('updatedAt', None))
size = workbook_xml.get('size', None)
if size:
diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py
index 63d69510c..d9dca0f42 100644
--- a/tableauserverclient/server/endpoint/__init__.py
+++ b/tableauserverclient/server/endpoint/__init__.py
@@ -1,7 +1,7 @@
from .auth_endpoint import Auth
from .datasources_endpoint import Datasources
from .endpoint import Endpoint
-from .exceptions import ServerResponseError, MissingRequiredFieldError
+from .exceptions import ServerResponseError, MissingRequiredFieldError, ServerInfoEndpointNotFoundError
from .groups_endpoint import Groups
from .projects_endpoint import Projects
from .schedules_endpoint import Schedules
diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py
index e8e4e4bf6..af8efcd13 100644
--- a/tableauserverclient/server/endpoint/datasources_endpoint.py
+++ b/tableauserverclient/server/endpoint/datasources_endpoint.py
@@ -6,6 +6,7 @@
import logging
import copy
import cgi
+from contextlib import closing
# The maximum size of a file that can be published in a single request is 64MB
FILESIZE_LIMIT = 1024 * 1024 * 64 # 64MB
@@ -64,16 +65,18 @@ def download(self, datasource_id, filepath=None):
error = "Datasource ID undefined."
raise ValueError(error)
url = "{0}/{1}/content".format(self.baseurl, datasource_id)
- server_response = self.get_request(url)
- _, params = cgi.parse_header(server_response.headers['Content-Disposition'])
- filename = os.path.basename(params['filename'])
- if filepath is None:
- filepath = filename
- elif os.path.isdir(filepath):
- filepath = os.path.join(filepath, filename)
-
- with open(filepath, 'wb') as f:
- f.write(server_response.content)
+ with closing(self.get_request(url, parameters={'stream': True})) as server_response:
+ _, params = cgi.parse_header(server_response.headers['Content-Disposition'])
+ filename = os.path.basename(params['filename'])
+ if filepath is None:
+ filepath = filename
+ elif os.path.isdir(filepath):
+ filepath = os.path.join(filepath, filename)
+
+ with open(filepath, 'wb') as f:
+ for chunk in server_response.iter_content(1024): # 1KB
+ f.write(chunk)
+
logger.info('Downloaded datasource to {0} (ID: {1})'.format(filepath, datasource_id))
return os.path.abspath(filepath)
diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py
index c90b91004..9f8a6dc3a 100644
--- a/tableauserverclient/server/endpoint/endpoint.py
+++ b/tableauserverclient/server/endpoint/endpoint.py
@@ -1,6 +1,12 @@
-from .exceptions import ServerResponseError
+from .exceptions import ServerResponseError, EndpointUnavailableError
+from functools import wraps
+
import logging
+try:
+ from distutils2.version import NormalizedVersion as Version
+except ImportError:
+ from distutils.version import LooseVersion as Version
logger = logging.getLogger('tableau.endpoint')
@@ -21,10 +27,11 @@ def _make_common_headers(auth_token, content_type):
return headers
- def _make_request(self, method, url, content=None, request_object=None, auth_token=None, content_type=None):
+ def _make_request(self, method, url, content=None, request_object=None,
+ auth_token=None, content_type=None, parameters=None):
if request_object is not None:
url = request_object.apply_query_params(url)
- parameters = {}
+ parameters = parameters or {}
parameters.update(self.parent_srv.http_options)
parameters['headers'] = Endpoint._make_common_headers(auth_token, content_type)
@@ -49,9 +56,9 @@ def _check_status(server_response):
def get_unauthenticated_request(self, url, request_object=None):
return self._make_request(self.parent_srv.session.get, url, request_object=request_object)
- def get_request(self, url, request_object=None):
+ def get_request(self, url, request_object=None, parameters=None):
return self._make_request(self.parent_srv.session.get, url, auth_token=self.parent_srv.auth_token,
- request_object=request_object)
+ request_object=request_object, parameters=parameters)
def delete_request(self, url):
# We don't return anything for a delete
@@ -68,3 +75,35 @@ def post_request(self, url, xml_request, content_type='text/xml'):
content=xml_request,
auth_token=self.parent_srv.auth_token,
content_type=content_type)
+
+
+def api(version):
+ '''Annotate the minimum supported version for an endpoint.
+
+ Checks the version on the server object and compares normalized versions.
+ It will raise an exception if the server version is > the version specified.
+
+ Args:
+ `version` minimum version that supports the endpoint. String.
+ Raises:
+ EndpointUnavailableError
+ Returns:
+ None
+
+ Example:
+ >>> @api(version="2.3")
+ >>> def get(self, req_options=None):
+ >>> ...
+ '''
+ def _decorator(func):
+ @wraps(func)
+ def wrapper(self, *args, **kwargs):
+ server_version = Version(self.parent_srv.version)
+ minimum_supported = Version(version)
+ if server_version < minimum_supported:
+ error = "This endpoint is not available in API version {}. Requires {}".format(
+ server_version, minimum_supported)
+ raise EndpointUnavailableError(error)
+ return func(self, *args, **kwargs)
+ return wrapper
+ return _decorator
diff --git a/tableauserverclient/server/endpoint/exceptions.py b/tableauserverclient/server/endpoint/exceptions.py
index 7907a6dab..5cb6a06d7 100644
--- a/tableauserverclient/server/endpoint/exceptions.py
+++ b/tableauserverclient/server/endpoint/exceptions.py
@@ -24,3 +24,11 @@ def from_response(cls, resp):
class MissingRequiredFieldError(Exception):
pass
+
+
+class ServerInfoEndpointNotFoundError(Exception):
+ pass
+
+
+class EndpointUnavailableError(Exception):
+ pass
diff --git a/tableauserverclient/server/endpoint/server_info_endpoint.py b/tableauserverclient/server/endpoint/server_info_endpoint.py
index 1fb17f26f..d6b2b7d96 100644
--- a/tableauserverclient/server/endpoint/server_info_endpoint.py
+++ b/tableauserverclient/server/endpoint/server_info_endpoint.py
@@ -1,4 +1,5 @@
from .endpoint import Endpoint
+from .exceptions import ServerResponseError, ServerInfoEndpointNotFoundError
from ...models import ServerInfoItem
import logging
@@ -12,6 +13,11 @@ def baseurl(self):
def get(self):
""" Retrieve the server info for the server. This is an unauthenticated call """
- server_response = self.get_unauthenticated_request(self.baseurl)
+ try:
+ server_response = self.get_unauthenticated_request(self.baseurl)
+ except ServerResponseError as e:
+ if e.code == "404003":
+ raise ServerInfoEndpointNotFoundError
+
server_info = ServerInfoItem.from_response(server_response.content)
return server_info
diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py
index 6aabc6029..eb185476e 100644
--- a/tableauserverclient/server/endpoint/workbooks_endpoint.py
+++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py
@@ -7,6 +7,7 @@
import logging
import copy
import cgi
+from contextlib import closing
# The maximum size of a file that can be published in a single request is 64MB
FILESIZE_LIMIT = 1024 * 1024 * 64 # 64MB
@@ -92,16 +93,18 @@ def download(self, workbook_id, filepath=None):
error = "Workbook ID undefined."
raise ValueError(error)
url = "{0}/{1}/content".format(self.baseurl, workbook_id)
- server_response = self.get_request(url)
- _, params = cgi.parse_header(server_response.headers['Content-Disposition'])
- filename = os.path.basename(params['filename'])
- if filepath is None:
- filepath = filename
- elif os.path.isdir(filepath):
- filepath = os.path.join(filepath, filename)
-
- with open(filepath, 'wb') as f:
- f.write(server_response.content)
+
+ with closing(self.get_request(url, parameters={"stream": True})) as server_response:
+ _, params = cgi.parse_header(server_response.headers['Content-Disposition'])
+ filename = os.path.basename(params['filename'])
+ if filepath is None:
+ filepath = filename
+ elif os.path.isdir(filepath):
+ filepath = os.path.join(filepath, filename)
+
+ with open(filepath, 'wb') as f:
+ for chunk in server_response.iter_content(1024): # 1KB
+ f.write(chunk)
logger.info('Downloaded workbook to {0} (ID: {1})'.format(filepath, workbook_id))
return os.path.abspath(filepath)
diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py
index 9a9bf53e1..db82b52aa 100644
--- a/tableauserverclient/server/request_factory.py
+++ b/tableauserverclient/server/request_factory.py
@@ -1,3 +1,4 @@
+from ..datetime_helpers import format_datetime
import xml.etree.ElementTree as ET
from requests.packages.urllib3.fields import RequestField
@@ -41,6 +42,9 @@ def _generate_xml(self, datasource_item, connection_credentials=None):
credentials_element.attrib['name'] = connection_credentials.name
credentials_element.attrib['password'] = connection_credentials.password
credentials_element.attrib['embed'] = 'true' if connection_credentials.embed else 'false'
+
+ if connection_credentials.oauth:
+ credentials_element.attrib['oAuth'] = 'true'
return ET.tostring(xml_request)
def update_req(self, datasource_item):
@@ -278,6 +282,9 @@ def _generate_xml(self, workbook_item, connection_credentials=None):
credentials_element.attrib['name'] = connection_credentials.name
credentials_element.attrib['password'] = connection_credentials.password
credentials_element.attrib['embed'] = 'true' if connection_credentials.embed else 'false'
+
+ if connection_credentials.oauth:
+ credentials_element.attrib['oAuth'] = 'true'
return ET.tostring(xml_request)
def update_req(self, workbook_item):
diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py
index 2cb08a892..b233377fe 100644
--- a/tableauserverclient/server/server.py
+++ b/tableauserverclient/server/server.py
@@ -1,8 +1,19 @@
+import xml.etree.ElementTree as ET
+
from .exceptions import NotSignedInError
-from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, Schedules, ServerInfo
+from .endpoint import Sites, Views, Users, Groups, Workbooks, Datasources, Projects, Auth, \
+ Schedules, ServerInfo, ServerInfoEndpointNotFoundError
import requests
+_PRODUCT_TO_REST_VERSION = {
+ '10.0': '2.3',
+ '9.3': '2.2',
+ '9.2': '2.1',
+ '9.1': '2.0',
+ '9.0': '2.0'
+}
+
class Server(object):
class PublishMode:
@@ -47,6 +58,29 @@ def _set_auth(self, site_id, user_id, auth_token):
self._user_id = user_id
self._auth_token = auth_token
+ def _get_legacy_version(self):
+ response = self._session.get(self.server_address + "/auth?format=xml")
+ info_xml = ET.fromstring(response.content)
+ prod_version = info_xml.find('.//product_version').text
+ version = _PRODUCT_TO_REST_VERSION.get(prod_version, '2.1') # 2.1
+ return version
+
+ def _determine_highest_version(self):
+ try:
+ old_version = self.version
+ self.version = "2.4"
+ version = self.server_info.get().rest_api_version
+ except ServerInfoEndpointNotFoundError:
+ version = self._get_legacy_version()
+
+ finally:
+ self.version = old_version
+
+ return version
+
+ def use_highest_version(self):
+ self.version = self._determine_highest_version()
+
@property
def baseurl(self):
return "{0}/api/{1}".format(self._server_address, str(self.version))
diff --git a/test/assets/server_info_404.xml b/test/assets/server_info_404.xml
new file mode 100644
index 000000000..a23abf9ae
--- /dev/null
+++ b/test/assets/server_info_404.xml
@@ -0,0 +1,7 @@
+
+
+
+ Resource Not Found
+ Unknown resource '/2.4/serverInfo' specified in URI.
+
+
diff --git a/test/assets/server_info_auth_info.xml b/test/assets/server_info_auth_info.xml
new file mode 100644
index 000000000..58d9c5baf
--- /dev/null
+++ b/test/assets/server_info_auth_info.xml
@@ -0,0 +1,12 @@
+
+
+0.31
+0.31
+9.2
+9.3
+9.3.4
+hello.16.1106.2025
+unrestricted
+2.6
+
+
diff --git a/test/test_datasource.py b/test/test_datasource.py
index d01f3cb0f..9a1e07a24 100644
--- a/test/test_datasource.py
+++ b/test/test_datasource.py
@@ -2,6 +2,7 @@
import os
import requests_mock
import tableauserverclient as TSC
+from tableauserverclient.datetime_helpers import format_datetime
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
@@ -33,8 +34,8 @@ def test_get(self):
self.assertEqual('e76a1461-3b1d-4588-bf1b-17551a879ad9', all_datasources[0].id)
self.assertEqual('dataengine', all_datasources[0].datasource_type)
self.assertEqual('SampleDS', all_datasources[0].content_url)
- self.assertEqual('2016-08-11T21:22:40Z', all_datasources[0].created_at)
- self.assertEqual('2016-08-11T21:34:17Z', all_datasources[0].updated_at)
+ self.assertEqual('2016-08-11T21:22:40Z', format_datetime(all_datasources[0].created_at))
+ self.assertEqual('2016-08-11T21:34:17Z', format_datetime(all_datasources[0].updated_at))
self.assertEqual('default', all_datasources[0].project_name)
self.assertEqual('SampleDS', all_datasources[0].name)
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', all_datasources[0].project_id)
@@ -43,8 +44,8 @@ def test_get(self):
self.assertEqual('9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', all_datasources[1].id)
self.assertEqual('dataengine', all_datasources[1].datasource_type)
self.assertEqual('Sampledatasource', all_datasources[1].content_url)
- self.assertEqual('2016-08-04T21:31:55Z', all_datasources[1].created_at)
- self.assertEqual('2016-08-04T21:31:55Z', all_datasources[1].updated_at)
+ self.assertEqual('2016-08-04T21:31:55Z', format_datetime(all_datasources[1].created_at))
+ self.assertEqual('2016-08-04T21:31:55Z', format_datetime(all_datasources[1].updated_at))
self.assertEqual('default', all_datasources[1].project_name)
self.assertEqual('Sample datasource', all_datasources[1].name)
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', all_datasources[1].project_id)
@@ -75,8 +76,8 @@ def test_get_by_id(self):
self.assertEqual('9dbd2263-16b5-46e1-9c43-a76bb8ab65fb', single_datasource.id)
self.assertEqual('dataengine', single_datasource.datasource_type)
self.assertEqual('Sampledatasource', single_datasource.content_url)
- self.assertEqual('2016-08-04T21:31:55Z', single_datasource.created_at)
- self.assertEqual('2016-08-04T21:31:55Z', single_datasource.updated_at)
+ self.assertEqual('2016-08-04T21:31:55Z', format_datetime(single_datasource.created_at))
+ self.assertEqual('2016-08-04T21:31:55Z', format_datetime(single_datasource.updated_at))
self.assertEqual('default', single_datasource.project_name)
self.assertEqual('Sample datasource', single_datasource.name)
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', single_datasource.project_id)
@@ -125,8 +126,8 @@ def test_publish(self):
self.assertEqual('SampleDS', new_datasource.name)
self.assertEqual('SampleDS', new_datasource.content_url)
self.assertEqual('dataengine', new_datasource.datasource_type)
- self.assertEqual('2016-08-11T21:22:40Z', new_datasource.created_at)
- self.assertEqual('2016-08-17T23:37:08Z', new_datasource.updated_at)
+ self.assertEqual('2016-08-11T21:22:40Z', format_datetime(new_datasource.created_at))
+ self.assertEqual('2016-08-17T23:37:08Z', format_datetime(new_datasource.updated_at))
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', new_datasource.project_id)
self.assertEqual('default', new_datasource.project_name)
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', new_datasource.owner_id)
diff --git a/test/test_datasource_model.py b/test/test_datasource_model.py
index b43cc3f3d..600587801 100644
--- a/test/test_datasource_model.py
+++ b/test/test_datasource_model.py
@@ -1,3 +1,4 @@
+import datetime
import unittest
import tableauserverclient as TSC
diff --git a/test/test_group.py b/test/test_group.py
index ff928bf17..2f7f22701 100644
--- a/test/test_group.py
+++ b/test/test_group.py
@@ -3,6 +3,7 @@
import os
import requests_mock
import tableauserverclient as TSC
+from tableauserverclient.datetime_helpers import format_datetime
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
@@ -61,7 +62,7 @@ def test_populate_users(self):
self.assertEqual('dd2239f6-ddf1-4107-981a-4cf94e415794', user.id)
self.assertEqual('alice', user.name)
self.assertEqual('Publisher', user.site_role)
- self.assertEqual('2016-08-16T23:17:06Z', user.last_login)
+ self.assertEqual('2016-08-16T23:17:06Z', format_datetime(user.last_login))
def test_delete(self):
with requests_mock.mock() as m:
diff --git a/test/test_requests.py b/test/test_requests.py
new file mode 100644
index 000000000..686a4bbb4
--- /dev/null
+++ b/test/test_requests.py
@@ -0,0 +1,47 @@
+import unittest
+
+import requests
+import requests_mock
+
+import tableauserverclient as TSC
+
+
+class RequestTests(unittest.TestCase):
+ def setUp(self):
+ self.server = TSC.Server('http://test')
+
+ # Fake sign in
+ self.server._site_id = 'dad65087-b08b-4603-af4e-2887b8aafc67'
+ self.server._auth_token = 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM'
+
+ self.baseurl = self.server.workbooks.baseurl
+
+ def test_make_get_request(self):
+ with requests_mock.mock() as m:
+ m.get(requests_mock.ANY)
+ url = "http://test/api/2.3/sites/dad65087-b08b-4603-af4e-2887b8aafc67/workbooks"
+ opts = TSC.RequestOptions(pagesize=13, pagenumber=13)
+ resp = self.server.workbooks._make_request(requests.get,
+ url,
+ content=None,
+ request_object=opts,
+ auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM',
+ content_type='text/xml')
+
+ self.assertEqual(resp.request.query, 'pagenumber=13&pagesize=13')
+ self.assertEqual(resp.request.headers['x-tableau-auth'], 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM')
+ self.assertEqual(resp.request.headers['content-type'], 'text/xml')
+
+ def test_make_post_request(self):
+ with requests_mock.mock() as m:
+ m.post(requests_mock.ANY)
+ url = "http://test/api/2.3/sites/dad65087-b08b-4603-af4e-2887b8aafc67/workbooks"
+ resp = self.server.workbooks._make_request(requests.post,
+ url,
+ content=b'1337',
+ request_object=None,
+ auth_token='j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM',
+ content_type='multipart/mixed')
+ self.assertEqual(resp.request.headers['x-tableau-auth'], 'j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM')
+ self.assertEqual(resp.request.headers['content-type'], 'multipart/mixed')
+ self.assertEqual(resp.request.body, b'1337')
diff --git a/test/test_schedule.py b/test/test_schedule.py
index 710bfe2a2..965e414a8 100644
--- a/test/test_schedule.py
+++ b/test/test_schedule.py
@@ -1,8 +1,9 @@
+from datetime import time
import unittest
import os
import requests_mock
import tableauserverclient as TSC
-from datetime import time
+from tableauserverclient.datetime_helpers import format_datetime
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets")
@@ -37,19 +38,19 @@ def test_get(self):
self.assertEqual("Weekday early mornings", all_schedules[0].name)
self.assertEqual("Active", all_schedules[0].state)
self.assertEqual(50, all_schedules[0].priority)
- self.assertEqual("2016-07-06T20:19:00Z", all_schedules[0].created_at)
- self.assertEqual("2016-09-13T11:00:32Z", all_schedules[0].updated_at)
+ self.assertEqual("2016-07-06T20:19:00Z", format_datetime(all_schedules[0].created_at))
+ self.assertEqual("2016-09-13T11:00:32Z", format_datetime(all_schedules[0].updated_at))
self.assertEqual("Extract", all_schedules[0].schedule_type)
- self.assertEqual("2016-09-14T11:00:00Z", all_schedules[0].next_run_at)
+ self.assertEqual("2016-09-14T11:00:00Z", format_datetime(all_schedules[0].next_run_at))
self.assertEqual("bcb79d07-6e47-472f-8a65-d7f51f40c36c", all_schedules[1].id)
self.assertEqual("Saturday night", all_schedules[1].name)
self.assertEqual("Active", all_schedules[1].state)
self.assertEqual(80, all_schedules[1].priority)
- self.assertEqual("2016-07-07T20:19:00Z", all_schedules[1].created_at)
- self.assertEqual("2016-09-12T16:39:38Z", all_schedules[1].updated_at)
+ self.assertEqual("2016-07-07T20:19:00Z", format_datetime(all_schedules[1].created_at))
+ self.assertEqual("2016-09-12T16:39:38Z", format_datetime(all_schedules[1].updated_at))
self.assertEqual("Subscription", all_schedules[1].schedule_type)
- self.assertEqual("2016-09-18T06:00:00Z", all_schedules[1].next_run_at)
+ self.assertEqual("2016-09-18T06:00:00Z", format_datetime(all_schedules[1].next_run_at))
def test_get_empty(self):
with open(GET_EMPTY_XML, "rb") as f:
@@ -82,10 +83,10 @@ def test_create_hourly(self):
self.assertEqual("hourly-schedule-1", new_schedule.name)
self.assertEqual("Active", new_schedule.state)
self.assertEqual(50, new_schedule.priority)
- self.assertEqual("2016-09-15T20:47:33Z", new_schedule.created_at)
- self.assertEqual("2016-09-15T20:47:33Z", new_schedule.updated_at)
+ self.assertEqual("2016-09-15T20:47:33Z", format_datetime(new_schedule.created_at))
+ self.assertEqual("2016-09-15T20:47:33Z", format_datetime(new_schedule.updated_at))
self.assertEqual(TSC.ScheduleItem.Type.Extract, new_schedule.schedule_type)
- self.assertEqual("2016-09-16T01:30:00Z", new_schedule.next_run_at)
+ self.assertEqual("2016-09-16T01:30:00Z", format_datetime(new_schedule.next_run_at))
self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Parallel, new_schedule.execution_order)
self.assertEqual(time(2, 30), new_schedule.interval_item.start_time)
self.assertEqual(time(23), new_schedule.interval_item.end_time)
@@ -105,10 +106,10 @@ def test_create_daily(self):
self.assertEqual("daily-schedule-1", new_schedule.name)
self.assertEqual("Active", new_schedule.state)
self.assertEqual(90, new_schedule.priority)
- self.assertEqual("2016-09-15T21:01:09Z", new_schedule.created_at)
- self.assertEqual("2016-09-15T21:01:09Z", new_schedule.updated_at)
+ self.assertEqual("2016-09-15T21:01:09Z", format_datetime(new_schedule.created_at))
+ self.assertEqual("2016-09-15T21:01:09Z", format_datetime(new_schedule.updated_at))
self.assertEqual(TSC.ScheduleItem.Type.Subscription, new_schedule.schedule_type)
- self.assertEqual("2016-09-16T11:45:00Z", new_schedule.next_run_at)
+ self.assertEqual("2016-09-16T11:45:00Z", format_datetime(new_schedule.next_run_at))
self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Serial, new_schedule.execution_order)
self.assertEqual(time(4, 45), new_schedule.interval_item.start_time)
@@ -128,10 +129,10 @@ def test_create_weekly(self):
self.assertEqual("weekly-schedule-1", new_schedule.name)
self.assertEqual("Active", new_schedule.state)
self.assertEqual(80, new_schedule.priority)
- self.assertEqual("2016-09-15T21:12:50Z", new_schedule.created_at)
- self.assertEqual("2016-09-15T21:12:50Z", new_schedule.updated_at)
+ self.assertEqual("2016-09-15T21:12:50Z", format_datetime(new_schedule.created_at))
+ self.assertEqual("2016-09-15T21:12:50Z", format_datetime(new_schedule.updated_at))
self.assertEqual(TSC.ScheduleItem.Type.Extract, new_schedule.schedule_type)
- self.assertEqual("2016-09-16T16:15:00Z", new_schedule.next_run_at)
+ self.assertEqual("2016-09-16T16:15:00Z", format_datetime(new_schedule.next_run_at))
self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Parallel, new_schedule.execution_order)
self.assertEqual(time(9, 15), new_schedule.interval_item.start_time)
self.assertEqual(("Monday", "Wednesday", "Friday"),
@@ -151,10 +152,10 @@ def test_create_monthly(self):
self.assertEqual("monthly-schedule-1", new_schedule.name)
self.assertEqual("Active", new_schedule.state)
self.assertEqual(20, new_schedule.priority)
- self.assertEqual("2016-09-15T21:16:56Z", new_schedule.created_at)
- self.assertEqual("2016-09-15T21:16:56Z", new_schedule.updated_at)
+ self.assertEqual("2016-09-15T21:16:56Z", format_datetime(new_schedule.created_at))
+ self.assertEqual("2016-09-15T21:16:56Z", format_datetime(new_schedule.updated_at))
self.assertEqual(TSC.ScheduleItem.Type.Extract, new_schedule.schedule_type)
- self.assertEqual("2016-10-12T14:00:00Z", new_schedule.next_run_at)
+ self.assertEqual("2016-10-12T14:00:00Z", format_datetime(new_schedule.next_run_at))
self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Serial, new_schedule.execution_order)
self.assertEqual(time(7), new_schedule.interval_item.start_time)
self.assertEqual("12", new_schedule.interval_item.interval)
@@ -174,9 +175,9 @@ def test_update(self):
self.assertEqual("7bea1766-1543-4052-9753-9d224bc069b5", single_schedule.id)
self.assertEqual("weekly-schedule-1", single_schedule.name)
self.assertEqual(90, single_schedule.priority)
- self.assertEqual("2016-09-15T23:50:02Z", single_schedule.updated_at)
+ self.assertEqual("2016-09-15T23:50:02Z", format_datetime(single_schedule.updated_at))
self.assertEqual(TSC.ScheduleItem.Type.Extract, single_schedule.schedule_type)
- self.assertEqual("2016-09-16T14:00:00Z", single_schedule.next_run_at)
+ self.assertEqual("2016-09-16T14:00:00Z", format_datetime(single_schedule.next_run_at))
self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Parallel, single_schedule.execution_order)
self.assertEqual(time(7), single_schedule.interval_item.start_time)
self.assertEqual(("Monday", "Friday"),
diff --git a/test/test_server_info.py b/test/test_server_info.py
index 03e39210f..084e6c91f 100644
--- a/test/test_server_info.py
+++ b/test/test_server_info.py
@@ -6,21 +6,48 @@
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
SERVER_INFO_GET_XML = os.path.join(TEST_ASSET_DIR, 'server_info_get.xml')
+SERVER_INFO_404 = os.path.join(TEST_ASSET_DIR, 'server_info_404.xml')
+SERVER_INFO_AUTH_INFO_XML = os.path.join(TEST_ASSET_DIR, 'server_info_auth_info.xml')
class ServerInfoTests(unittest.TestCase):
def setUp(self):
self.server = TSC.Server('http://test')
- self.server.version = '2.4'
self.baseurl = self.server.server_info.baseurl
def test_server_info_get(self):
with open(SERVER_INFO_GET_XML, 'rb') as f:
response_xml = f.read().decode('utf-8')
with requests_mock.mock() as m:
- m.get(self.baseurl, text=response_xml)
+ self.server.version = '2.4'
+ m.get(self.server.server_info.baseurl, text=response_xml)
actual = self.server.server_info.get()
self.assertEqual('10.1.0', actual.product_version)
self.assertEqual('10100.16.1024.2100', actual.build_number)
self.assertEqual('2.4', actual.rest_api_version)
+
+ def test_server_info_use_highest_version_downgrades(self):
+ with open(SERVER_INFO_AUTH_INFO_XML, 'rb') as f:
+ # This is the auth.xml endpoint present back to 9.0 Servers
+ auth_response_xml = f.read().decode('utf-8')
+ with open(SERVER_INFO_404, 'rb') as f:
+ # 10.1 serverInfo response
+ si_response_xml = f.read().decode('utf-8')
+ with requests_mock.mock() as m:
+ # Return a 404 for serverInfo so we can pretend this is an old Server
+ m.get(self.server.server_address + "/api/2.4/serverInfo", text=si_response_xml, status_code=404)
+ m.get(self.server.server_address + "/auth?format=xml", text=auth_response_xml)
+ self.server.use_highest_version()
+ self.assertEqual(self.server.version, '2.2')
+
+ def test_server_info_use_highest_version_upgrades(self):
+ with open(SERVER_INFO_GET_XML, 'rb') as f:
+ si_response_xml = f.read().decode('utf-8')
+ with requests_mock.mock() as m:
+ m.get(self.server.server_address + "/api/2.4/serverInfo", text=si_response_xml)
+ # Pretend we're old
+ self.server.version = '2.0'
+ self.server.use_highest_version()
+ # Did we upgrade to 2.4?
+ self.assertEqual(self.server.version, '2.4')
diff --git a/test/test_user.py b/test/test_user.py
index 71ec30207..fa8344371 100644
--- a/test/test_user.py
+++ b/test/test_user.py
@@ -2,6 +2,7 @@
import os
import requests_mock
import tableauserverclient as TSC
+from tableauserverclient.datetime_helpers import format_datetime
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
@@ -38,7 +39,7 @@ def test_get(self):
single_user = next(user for user in all_users if user.id == 'dd2239f6-ddf1-4107-981a-4cf94e415794')
self.assertEqual('alice', single_user.name)
self.assertEqual('Publisher', single_user.site_role)
- self.assertEqual('2016-08-16T23:17:06Z', single_user.last_login)
+ self.assertEqual('2016-08-16T23:17:06Z', format_datetime(single_user.last_login))
self.assertTrue(any(user.id == '2a47bbf8-8900-4ebb-b0a4-2723bd7c46c3' for user in all_users))
single_user = next(user for user in all_users if user.id == '2a47bbf8-8900-4ebb-b0a4-2723bd7c46c3')
@@ -53,7 +54,7 @@ def test_get_empty(self):
all_users, pagination_item = self.server.users.get()
self.assertEqual(0, pagination_item.total_available)
- self.assertEqual(set(), all_users)
+ self.assertEqual([], all_users)
def test_get_before_signin(self):
self.server._auth_token = None
@@ -71,7 +72,7 @@ def test_get_by_id(self):
self.assertEqual('Alice', single_user.fullname)
self.assertEqual('Publisher', single_user.site_role)
self.assertEqual('ServerDefault', single_user.auth_setting)
- self.assertEqual('2016-08-16T23:17:06Z', single_user.last_login)
+ self.assertEqual('2016-08-16T23:17:06Z', format_datetime(single_user.last_login))
self.assertEqual('local', single_user.domain_name)
def test_get_by_id_missing_id(self):
@@ -136,8 +137,8 @@ def test_populate_workbooks(self):
self.assertEqual('SafariSample', workbook_list[0].content_url)
self.assertEqual(False, workbook_list[0].show_tabs)
self.assertEqual(26, workbook_list[0].size)
- self.assertEqual('2016-07-26T20:34:56Z', workbook_list[0].created_at)
- self.assertEqual('2016-07-26T20:35:05Z', workbook_list[0].updated_at)
+ self.assertEqual('2016-07-26T20:34:56Z', format_datetime(workbook_list[0].created_at))
+ self.assertEqual('2016-07-26T20:35:05Z', format_datetime(workbook_list[0].updated_at))
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', workbook_list[0].project_id)
self.assertEqual('default', workbook_list[0].project_name)
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', workbook_list[0].owner_id)
diff --git a/test/test_workbook.py b/test/test_workbook.py
index e99d07f81..4ad38b17d 100644
--- a/test/test_workbook.py
+++ b/test/test_workbook.py
@@ -2,6 +2,7 @@
import os
import requests_mock
import tableauserverclient as TSC
+from tableauserverclient.datetime_helpers import format_datetime
TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), 'assets')
@@ -39,8 +40,8 @@ def test_get(self):
self.assertEqual('Superstore', all_workbooks[0].content_url)
self.assertEqual(False, all_workbooks[0].show_tabs)
self.assertEqual(1, all_workbooks[0].size)
- self.assertEqual('2016-08-03T20:34:04Z', all_workbooks[0].created_at)
- self.assertEqual('2016-08-04T17:56:41Z', all_workbooks[0].updated_at)
+ self.assertEqual('2016-08-03T20:34:04Z', format_datetime(all_workbooks[0].created_at))
+ self.assertEqual('2016-08-04T17:56:41Z', format_datetime(all_workbooks[0].updated_at))
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', all_workbooks[0].project_id)
self.assertEqual('default', all_workbooks[0].project_name)
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', all_workbooks[0].owner_id)
@@ -50,8 +51,8 @@ def test_get(self):
self.assertEqual('SafariSample', all_workbooks[1].content_url)
self.assertEqual(False, all_workbooks[1].show_tabs)
self.assertEqual(26, all_workbooks[1].size)
- self.assertEqual('2016-07-26T20:34:56Z', all_workbooks[1].created_at)
- self.assertEqual('2016-07-26T20:35:05Z', all_workbooks[1].updated_at)
+ self.assertEqual('2016-07-26T20:34:56Z', format_datetime(all_workbooks[1].created_at))
+ self.assertEqual('2016-07-26T20:35:05Z', format_datetime(all_workbooks[1].updated_at))
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', all_workbooks[1].project_id)
self.assertEqual('default', all_workbooks[1].project_name)
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', all_workbooks[1].owner_id)
@@ -83,8 +84,8 @@ def test_get_by_id(self):
self.assertEqual('SafariSample', single_workbook.content_url)
self.assertEqual(False, single_workbook.show_tabs)
self.assertEqual(26, single_workbook.size)
- self.assertEqual('2016-07-26T20:34:56Z', single_workbook.created_at)
- self.assertEqual('2016-07-26T20:35:05Z', single_workbook.updated_at)
+ self.assertEqual('2016-07-26T20:34:56Z', format_datetime(single_workbook.created_at))
+ self.assertEqual('2016-07-26T20:35:05Z', format_datetime(single_workbook.updated_at))
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', single_workbook.project_id)
self.assertEqual('default', single_workbook.project_name)
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', single_workbook.owner_id)
@@ -250,8 +251,8 @@ def test_publish(self):
self.assertEqual('RESTAPISample_0', new_workbook.content_url)
self.assertEqual(False, new_workbook.show_tabs)
self.assertEqual(1, new_workbook.size)
- self.assertEqual('2016-08-18T18:33:24Z', new_workbook.created_at)
- self.assertEqual('2016-08-18T20:31:34Z', new_workbook.updated_at)
+ self.assertEqual('2016-08-18T18:33:24Z', format_datetime(new_workbook.created_at))
+ self.assertEqual('2016-08-18T20:31:34Z', format_datetime(new_workbook.updated_at))
self.assertEqual('ee8c6e70-43b6-11e6-af4f-f7b0d8e20760', new_workbook.project_id)
self.assertEqual('default', new_workbook.project_name)
self.assertEqual('5de011f8-5aa9-4d5b-b991-f462c8dd6bb7', new_workbook.owner_id)